feat(desktop): show context compaction in transcript - #3651
Conversation
5548e5c to
2332d67
Compare
|
wait #3653 |
jackwener
left a comment
There was a problem hiding this comment.
Independent review of 2332d67d0dff4eb03b37761b27f62451568e4414.
GO. No P0–P2. MERGEABLE but BLOCKED. Hosted test on this head is terminal failure; I am not approving. The failing compile is packages/storage/src/__tests__/codex-session-adapter.test.ts using decodeStoredMessage without importing it — that file is not in this diff.
What this solves
Explicit context compaction was a toast-only side effect. This PR makes that dedicated compact Turn durable in the transcript: rootExecutionKind: 'context_compact' plus a typed contextCompactionOutcome, projected through persistence, repair, Host wire (epoch 44→45), and one localized system row.
That is a defined external contract, not a leak of mid-turn internal compaction. Automatic in-turn compact still uses the existing system_note (context_compacted / context_compaction_failed_open). The two paths are different executions and keep different copy, which is the right cut.
Boundary / predicate (the questions for this PR)
- Running / terminal: label is
turn.statusfor running and aborted; otherwiseoutcome.kind(compacted/unchanged/failed). Internalreason/checkpointIdstay off the UI. - Failure: a failed compact still writes the legacy
context_compaction_failed_opennote, then suppresses it when the sameturnIdis a compact Turn, so the user sees one row. - Multiple: compact is refused while another Turn is running. Repeated compact is separate Turns; each
turnIdemits one row, in storage order. - History: sessions from before this field was persisted do not grow a new row (no
rootExecutionKind). Old visiblesystem_notes still render. No reorder/hole introduced by the new projector. - Predicate sufficiency: the UI keys off the compact-Turn identity, not “a compact-shaped event happened.” Missing outcome on a completed compact Turn fail-closes to the failed label (same as Host
context-coordinator). That is conservative, not a premature unlock.
No inline findings. No merge from me.
2332d67 to
d31caba
Compare
|
#3656 resolved the build error |
|
Hi, @Astro-Han, could you help take a look when free, thanks! |
0579cb6 to
9e525b2
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for this — the core contract work is solid. The strict isContextCompactionOutcome validator, the epoch bump, and the projector/snapshot reuse all line up, and CI is green on this head. I have four things I would like resolved before this goes in, three of which are about paths where the new identity does not survive.
[P2] Repair can never enrich the history that needs it most.
This PR threads rootExecutionKind and contextCompactionOutcome all the way into #appendTurnStateIfMissing, but that method's early return is unchanged: it bails when a terminal turn_state row already exists with the same status. For a session whose compaction completed before this upgrade, exactly that row exists — terminal, same status, missing both new fields. So the enrichment is unreachable for the population it was written for. Combined with terminal transcript reads being storage-only (packages/runtime-host/src/server/session-transcript-reader.ts:52-57 returns [] from readActiveOverlay once the root turn is terminal), the canonical facts are present — the Run header already carries rootExecutionKind: context_compact, the terminal RuntimeEvent already carries the outcome — but nothing renders. Reopen a session with a completed pre-upgrade compaction and no inline row appears. I think repair should append an enriched terminal projection when the semantic fields are missing, not only when the status differs. Inline note on the guard.
[P2] A copied conversation's terminal outcome points at a checkpoint that does not exist in the target.
packages/runtime/src/conversation-copy.ts:679-700 rebuilds the checkpoint under a fresh target ID and records the mapping in checkpointIds. But RuntimeEvents are cloned earlier at :330-350, and rewriteRuntimeEventActions (:1052-1059) only rewrites toolDispatch/toolRecovery operation IDs — everything else passes through the ...actions spread untouched, including stateDelta.contextCompactionOutcome. Since { kind: 'compacted', checkpointId } is introduced by this PR, the rewriter has no way to know about it yet. The result is a target terminal Turn whose outcome names the source checkpoint, while the target's own history_compact_checkpoint_recorded event names a different one; the read model and wire then publish the stale reference. Branch through a successful explicit compaction and compare the two IDs. Rewriting the outcome once the checkpoint map is known would close this.
[P2] Branch and edit-and-resend copies lose the compaction row entirely.
packages/runtime/src/conversation-copy.ts:174-180 drops every turn_state message from a copy slice, and cloneConversationRuntimeLedger returns only that stripped set (:393-395, :452-454). Because terminal reads take no RuntimeEvent overlay, a copied conversation has no stored rootExecutionKind row for packages/ui/src/materialize.ts:171-174 to render. A state-only compaction is affected more sharply: retained Run selection is recomputed from the stripped messages at :227-233, so those Runs are not carried over at all.
To be clear about attribution: that strip predates this PR — conversation-copy.ts is untouched here, and the same line exists at the merge-base. I am raising it because this PR is what makes stored turn_state load-bearing for rendering, so the gap only becomes visible now. It is a coverage hole in the new feature rather than a regression, and if you would rather handle it in a follow-up I am fine with that as long as it is written down somewhere.
[P3] The new epoch test cannot detect removal of the bump it guards.
Inline note.
Two smaller things I did not count as findings: the compaction-row projection is duplicated between materializeChat and materializeTurns, and rootExecutionKind + contextCompactionOutcome travel as an optional pair that permits combinations the domain does not have. Both are judgment calls and I am happy to leave them.
Because these are open items rather than a green light, I am leaving this as a comment rather than an approval. This is also a feature rather than a fix, so the merge call is a human's either way.
简体中文
感谢这个 PR,核心契约部分做得很扎实:严格的 isContextCompactionOutcome 校验、epoch 提升、以及 projector/snapshot 复用同一个校验器,都对得上,这个 head 上 CI 也是绿的。合并前我有四点想解决,其中三点都是"新身份在某条路径上没能存活下来"。
[P2] repair 恰恰无法补全最需要补全的那部分历史。
这个 PR 把 rootExecutionKind 和 contextCompactionOutcome 一路传进了 #appendTurnStateIfMissing,但该方法的提前返回没有改:当已存在一条同状态的终态 turn_state 行时它就直接返回。而对于升级前就完成了压缩的会话,存在的正是这样一行——终态、同状态、缺少这两个新字段。于是这段补全逻辑对它本该服务的人群是不可达的。再加上终态 transcript 读取是纯存储的(packages/runtime-host/src/server/session-transcript-reader.ts:52-57,root turn 一旦终态,readActiveOverlay 就返回 []),权威事实其实都在——Run header 已经记了 rootExecutionKind: context_compact,终态 RuntimeEvent 也已经记了 outcome——但什么都渲染不出来。打开一个升级前已完成压缩的会话,行内那条记录不会出现。我认为 repair 应该在语义字段缺失时就追加一条补全后的终态投影,而不是只在状态不同时才追加。已在那个守卫处留了行内注记。
[P2] 复制出来的会话,其终态 outcome 指向一个在目标会话中并不存在的 checkpoint。
packages/runtime/src/conversation-copy.ts:679-700 会用新的目标 ID 重建 checkpoint,并把映射记进 checkpointIds。但 RuntimeEvent 在更早的 :330-350 就已经克隆完了,而 rewriteRuntimeEventActions(:1052-1059)只改写 toolDispatch/toolRecovery 的 operation ID,其余内容都经由 ...actions 原样透传,其中就包括 stateDelta.contextCompactionOutcome。由于 { kind: 'compacted', checkpointId } 是本 PR 新引入的,改写器还无从知晓它。结果是:目标会话的终态 Turn,其 outcome 指向源 checkpoint,而目标自己的 history_compact_checkpoint_recorded 事件指向另一个 ID;read model 和 wire 随后发布的是过期的那个。从一次成功的显式压缩处 branch 出去,对比这两个 ID 即可复现。在拿到 checkpoint 映射之后再改写 outcome 就能解决。
[P2] branch 和 edit-and-resend 复制会完全丢失压缩记录行。
packages/runtime/src/conversation-copy.ts:174-180 会从复制切片中剔除所有 turn_state 消息,而 cloneConversationRuntimeLedger 返回的正是这个被剔除后的集合(:393-395, :452-454)。由于终态读取不叠加 RuntimeEvent,复制出的会话里没有任何带 rootExecutionKind 的存储行供 packages/ui/src/materialize.ts:171-174 渲染。仅有状态、没有普通消息的压缩 Turn 受影响更重:保留 Run 的选择在 :227-233 处是基于剔除后的消息重算的,因此那些 Run 根本不会被复制过去。
归因上我说清楚:这个剔除早于本 PR——conversation-copy.ts 在本 PR 中未被改动,merge-base 上就有同一行。我提出来,是因为本 PR 让存储的 turn_state 成为渲染的承重结构,这个缺口到现在才显现。它属于新功能的覆盖空缺,而不是回归;如果你更愿意放到后续 PR 处理,我没有意见,只要有个地方把它记下来。
[P3] 新增的 epoch 测试无法发现它所守护的那次提升被移除。
已留行内注记。
另外两点我没有计为 finding:压缩行的投影在 materializeChat 和 materializeTurns 之间存在重复;rootExecutionKind 与 contextCompactionOutcome 作为一对可选字段一起传递,允许出现领域中并不存在的组合。这两点都是取舍问题,保持现状我也接受。
由于这些是待办事项而不是放行,我这次留的是评论而非 approve。另外这是新功能而不是修复,合并与否本来也要由人类决定。
|
Update on the epoch: this branch now conflicts with #3573 merged a few minutes ago as I should be clear that I caused this. When I rebased #3573 I picked 47 without first checking whether another open PR had already claimed it, and yours had. Nothing in your branch was wrong; it just lost a race it was never told it was in. What this needs when you next touch the branch:
I scanned all open PRs that declare an epoch: after this merge, no other one claims 48, so 48 is free. Every other open branch sits at 45 or below and will renumber whenever it rebases. Happy to push the rebase to your branch if that is easier — just say the word. Otherwise the three findings in my earlier review are the substantive part; this is only bookkeeping. 简体中文epoch 的进展同步一下:这个分支现在和 #3573 在几分钟前以 我要说清楚:这是我造成的。我在 rebase #3573 时选了 47,事先没有检查是否已有别的开放 PR 占用了这个号,而你的 PR 正好占了。你的分支没有任何地方写错,它只是输掉了一场从没被告知存在的竞速。 等你下次动这个分支时需要做的:
我扫过所有声明了 epoch 的开放 PR:本次合并之后没有别的 PR 占用 48,所以 48 是空的。其余开放分支都停在 45 及以下,各自 rebase 时再重新编号即可。 如果由我把 rebase 推到你的分支更省事,说一声就行。除此之外,我先前那条 review 里的三个问题才是实质部分,这一条只是记账。 |
9e525b2 to
ade23fb
Compare
|
Correction to my previous comment: do not take 48. #3669 already claims it, and I did not check before suggesting a number. That is the same mistake that produced this conflict in the first place, so let me give you the rule instead of another number. Current state: So the reliable procedure, at the moment you rebase:
for n in $(gh pr list --state open --limit 300 --json number -q '.[].number'); do
H=$(gh pr view $n --json headRefOid -q .headRefOid)
v=$(git show $H:packages/runtime-host/src/protocol/index.ts 2>/dev/null \
| grep -oP 'COMPATIBILITY_EPOCH = \K[0-9]+')
[ -n "$v" ] && echo "#$n = $v"
done | sort -t= -k2 -n | tail
As of right now that gives you 49, but please re-run step 2 rather than trusting that figure — #3669 may land before you rebase, which would move Sorry for the churn. The three findings in my earlier review are unaffected by any of this — #3573 only touched the epoch declaration, so those still stand at whatever head you land on. 简体中文更正我上一条评论:不要用 48。 #3669 已经占了 48,而我上次建议这个数字之前没有检查——这正是最初造成这次冲突的同一个错误。所以这次我给你规则,而不是再给一个数字。 当前状态: 因此,在你 rebase 的那一刻,可靠的做法是:
for n in $(gh pr list --state open --limit 300 --json number -q '.[].number'); do
H=$(gh pr view $n --json headRefOid -q .headRefOid)
v=$(git show $H:packages/runtime-host/src/protocol/index.ts 2>/dev/null \
| grep -oP 'COMPATIBILITY_EPOCH = \K[0-9]+')
[ -n "$v" ] && echo "#$n = $v"
done | sort -t= -k2 -n | tail
按现在的情况这会得到 49,但请你重新跑一遍第 2 步,不要直接采信这个数字——#3669 可能在你 rebase 之前合入,那会把 为这些反复致歉。我先前 review 里的三个问题不受此影响——#3573 只改动了 epoch 声明,因此无论你最终落在哪个 head,那三条依然成立。 |
c7623ff to
a793027
Compare
|
Addressed all four findings in a793027:
Local verification: targeted Runtime tests 18/18, Runtime Host protocol tests 49/49, full typecheck, lint, format check, and |
|
The epoch is now clean: this branch declares The remaining One thing to watch after the rebase: 简体中文epoch 现在是干净的:这个分支声明 剩下的 rebase 后有一点需要留意: |
0654222 to
9537219
Compare
|
Correction to my earlier comment: I said 49 was not claimed by any other open pull request. That is no longer true, and it was already becoming untrue when I wrote it. #3299 also declares Whichever of the two merges first takes 49; the other will have to move to 50 and change its assertion to Note the Sorry for the noise. The rest of my earlier comment stands: the 简体中文更正我之前那条评论:我说 49 没有被任何其它开放 PR 占用。这句话现在不成立,而且在我写下它的时候就已经在失效了。 #3299 同样声明了 两者中先合并的那个拿到 49;另一个需要改成 50,并把断言改为 注意最后那个模式里的 抱歉打扰。之前那条评论的其余部分仍然成立: |
23b21f3 to
2304285
Compare
Make context-compaction turns durable across runtime projection, recovery, conversation copies, the Runtime Host protocol, and transcript UI surfaces. Closes apache#3587 Generated-by: Codex
a662967 to
30d6e1b
Compare
Summary
Fixes #3587
Verification
npm --workspace @maka/core run typechecknpm --workspace @maka/runtime run typechecknpm --workspace @maka/runtime-host run typechecknpm --workspace @maka/ui run typecheck@maka/core,@maka/runtime,@maka/runtime-host, and@maka/ui.docs/contribution/.zhwork/manifest.jsonneeds formatting; it is not part of this PR.maincurrently has an unrelated@maka/storagetest compilation error:codex-session-adapter.test.tscallsdecodeStoredMessagewithout importing it.正在压缩上下文…to上下文已压缩。.AI use
Select exactly one:
Tool(s) and scope: Codex implemented the Runtime, wire, and UI projection changes, added tests, addressed review findings, and assisted with verification.
Checklist
Does this PR entail a change in behavior?