Skip to content

fix(runtime-host): unify durable message lifecycle - #3633

Draft
Astro-Han wants to merge 33 commits into
mainfrom
fix/steering-durable-admission
Draft

fix(runtime-host): unify durable message lifecycle#3633
Astro-Han wants to merge 33 commits into
mainfrom
fix/steering-durable-admission

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Queued messages were split across transcript rows, an in-memory Host queue, Client bindings, and recovery-time inference. A Host restart could therefore lose an accepted message, replay a discarded one, or change the capabilities under which it ran.

This change gives the lifecycle four explicit authorities:

  • durable message admissions own accepted identity, current placement, and queue order
  • the Runtime Host in-memory queue owns only live delivery and leases
  • durable provider/root/Stop/retraction facts own settlement
  • the transcript owns presentation only

Admission and transcript materialization now share one SQLite transaction. Promotion, reorder, and edit are durable-first. Recovery reads unresolved admissions and starts them through the normal successor-root path; it no longer scans the transcript or waits for the original Client. Retracted identities retain only their original placement and canonical content digest, so they cannot be resurrected while message contents are removed.

A queued successor executes under the durable Session contract. It does not inherit session-, turn-, call-, or initiating-Client bindings, and an explicit durable capability marker prevents online call-affine providers from being discovered later. Editing uses the connection performing that edit only for transient content preparation; the queue does not retain that Client identity.

Stop is settled only by the immutable terminal Stop fact. If the Host dies before that fact, Stop was not confirmed and accepted messages recover; after the fact, they do not.

The parallel embedded RuntimeKernel queue and its dead admission path/tests are removed. The CLI and Desktop continue to render from canonical transcript/Host projections rather than owning fallback delivery state.

Architecture

flowchart TD
  C[Client message intent] --> U[Immediate transient row<br/>keyed by messageId]
  C --> A

  subgraph H[Runtime Host - sole execution authority]
    A[1. Durable admission<br/>identity, content proof, placement, order]
    Q[2. Live delivery<br/>steering queue, follow-up queue, in-flight leases]
    D{Delivery cut}
    R[Root admission and Run]
    S[3. Durable settlement<br/>provider proof, Stop, or retraction]

    A --> Q --> D
    D -->|consumed in current Turn| S
    D -->|fold into successor| R --> S
    A -->|Host restart: unresolved only| R
  end

  A -. materialize .-> T[4. Transcript<br/>presentation only]
  R --> T
  S --> T
  T -->|canonical reconciliation| U
Loading

The diagram intentionally shows responsibilities rather than every internal phase. The invariants are:

  1. messageId is the end-to-end identity; UI immediacy never creates a second lifecycle owner.
  2. Durable admission happens before a message becomes deliverable.
  3. Only durable execution, Stop, or retraction facts settle an admission.
  4. Restart recovers unresolved admissions; it never infers delivery from transcript appearance.

Schema migration

  • core execution schema v6 adds durable message admissions, queue order, and minimal retraction identity proofs
  • the unmerged v5 pending-steering shape is discarded because its old two-write protocol cannot prove that a row was accepted
  • Session retirement removes both pending admissions and settlements

Verification

After rebasing onto current main:

  • Storage and Runtime Host builds pass
  • Storage core execution: 19 passed
  • Message coordinator: 57 passed
  • edited steering fold/restart integration: 1 passed
  • focused Biome check passes
  • git diff --check

Repository-wide tests and the full Desktop Playwright suite were not run locally; CI owns full coverage.

Screenshots are not included because this revision changes lifecycle and projection ownership without changing visual styling or layout. The observable transcript/queue behavior is covered by focused CLI, Desktop contract, and Host integration tests.

AI use

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Maka and Codex contributed implementation, tests, architecture analysis, and delegated read-only reviews. Material commits include Generated-by trailers.

Checklist

  • Tests cover the change and fail without it
  • Format, typecheck, affected builds, and focused suites pass locally

Does this PR entail a change in behavior?

  • Yes - described under Summary above
  • No

Astro-Han

This comment was marked as duplicate.

@Astro-Han

Copy link
Copy Markdown
Contributor Author

Follow-up on the CI caveat in my review: the hosted run at 946f4268282146af26f4ab2502fbcecba8fdfc35 has since finished and test is completed/success. Correcting the note that it was still in progress — the two findings above are unchanged, since neither depended on that result.

@Astro-Han
Astro-Han force-pushed the fix/steering-durable-admission branch from bf1b2f6 to 07a11c7 Compare August 23, 2026 16:36

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed on behalf of WAWQAQ under the standing Kabi review authorization, scoped to exact head 220bdebbae3d42c6e2c01f18f81fe80655fbadf5.

NO-GO: 2×P1. The old CLI visibility P2 is closed: subscription.transcript_advanced now reloads canonical stored messages and publishes a transcript replacement rather than a live-only echo. The large deletion is also a real simplification: it removes CLI fallback/live-steering ownership and leaves Host queue + durable transcript as the authority.

Exact-head hosted test is completed/success; the PR is OPEN, non-draft, and mergeable against current main. Local focused verification passed: message coordinator 40/40, Host message integration 4/4, Runtime steering recovery 8/8, CLI durable/transcript 23/23, Desktop contract 1/1. The two inline P1s still block approval. No merge performed.

Comment thread packages/runtime-host/src/server/message-coordinator.ts Outdated
Comment thread packages/runtime-host/src/server/message-coordinator.ts Outdated
Comment thread packages/runtime-host/src/server/message-coordinator.ts
@Astro-Han
Astro-Han force-pushed the fix/steering-durable-admission branch from 220bdeb to d526e8b Compare August 23, 2026 18:53

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Representative review action on behalf of @WAWQAQ; scope: fix-head re-review of the two previously reported Runtime Host steering-recovery findings; exact head d526e8bfd01762561bc80a45e979efbd0c5aeccf.

Result: NO-GO — 1×P1 remains.

The previous promote → Stop content-loss finding is closed: promotion now durably commits the steering admission before removing the follow-up entry, and a failed durability write leaves the entry retractable. Stop can still discard the live steering queue, but it no longer deletes the durable transcript row or fabricates provider delivery.

The automatic-recovery finding remains: the helper exists, but its only production call is behind a new-root admission path that the target crash state cannot reach. A production Host restart reproduction retains the durable user message but never delivers it to the model (durable row = 1, provider steering proof = 0, root admission chain = 1, where recovery requires a successor/root chain of 2). See the two inline comments for the wiring and mutation evidence.

Required repair boundary: do not fix this by replaying every durable steering row that lacks a provider proof. Crash-before-queue-commit and an intentional Stop-discard currently leave the same durable-row shape. Blind replay would resend a message after the user explicitly pressed Stop. The repair must durably distinguish recovery-eligible queue state from intentional discard (or provide an equivalent fence) while preserving Stop’s “no successor” contract.

Local verification on this head: focused coordinator tests 41/41, Runtime Host full suite green, and root build green. The PR is OPEN/non-draft/MERGEABLE; exact-head hosted test is completed/success. This review is COMMENT only; no approval or merge.

Comment thread packages/runtime-host/src/server/root-turn-coordinator.ts Outdated
Comment thread packages/runtime-host/src/server/message-coordinator.ts Outdated

@ARE404 ARE404 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head re-review at 064157d3f09039e7f9842be39d447db8f18a59c4. NO-GO: 3×P1, 1×P3.

The previously reported disconnected restart recovery is now wired at Host startup, and the normal promote path correctly waits for durable transcript admission before exposing steering. Successful Stop also now preserves the transcript row while suppressing delivery. The three inline P1s are new crash/recovery boundary defects in that replacement design.

[P3 — documented standard] This diff changes renderer/UI behavior and stories, but the PR description has no before/after screenshots or recording as required by CONTRIBUTING.md:85.

No other documented-standard violations. The admission-time transcript model also semantically supersedes #3316's synthetic in-flight projection; textual overlap there is not an additional correctness finding. git diff --check is clean, and the only expected hosted workflow for these paths is CI test, which is terminal SUCCESS (run 32668976841, job 97266724286).

Comment thread packages/runtime-host/src/server/message-coordinator.ts Outdated
Comment thread packages/runtime-host/src/server/message-coordinator.ts Outdated
Comment thread packages/runtime-host/src/server/message-coordinator.ts Outdated
@Astro-Han
Astro-Han force-pushed the fix/steering-durable-admission branch from ca08e9a to 7d6a5fb Compare August 24, 2026 04:44
@Astro-Han Astro-Han changed the title fix: persist steering at message admission fix(runtime-host): unify durable message lifecycle Aug 24, 2026
@Astro-Han
Astro-Han force-pushed the fix/steering-durable-admission branch from 7d6a5fb to 1343cee Compare August 24, 2026 04:49

@ARE404 ARE404 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head re-review at 1343cee25bddb40b84456d7f15f08d74d8ffc0f7. NO-GO: 3×P2, 1×P3.

The three P1 crash/recovery defects from my prior review are closed: promotion is now atomic with transcript materialization, Stop suppression waits for immutable terminal Stop proof, and queued successors execute under the explicit durable Session capability contract. The replacement design still has three independent product regressions, documented inline.

[P3 — documented standard] CONTRIBUTING.md requires before/after screenshots or a recording for UI changes. This PR changes observable CLI pending/transcript rendering and Desktop existing-session submission behavior, but the description explicitly omits media; focused tests do not satisfy that evidence rule.

git diff --check is clean. Exact-head dependency audit is SUCCESS; hosted test is still IN_PROGRESS, so I do not claim a terminal CI result.

简体中文

本次复审锁定 1343cee2,结论为 NO-GO:3×P2、1×P3。旧 review 的三条 P1 均已关闭:promotion 与 transcript 写入已原子化,Stop 丢弃已由不可变终态事实约束,durable successor 也不再继承临时 client capability。

当前三条 P2 分别是:CLI 同一 steering 同时走 durable transcript 与 steering_message 两条渲染路径,导致重复显示;Quote Companion 在发送竞态中会把任意旧 turn 的首个事件误认成本次 turn,随后丢失真正 successor 的回复;follow-up promotion 未重新执行 queue/canonical projection 容量预检,边界大小输入可把 Host 推入 fail-stop。另有一条 P3:可观察 UI 行为已变化,但缺少 CONTRIBUTING 要求的前后截图或录屏。

当前 audit 已通过,test 仍在运行中。

Comment thread packages/cli/src/pi-tui-runner.ts
Comment thread apps/desktop/src/renderer/features/workbar/tools/side-chat/use-quote-companion.ts Outdated
Comment thread packages/runtime-host/src/server/message-coordinator.ts Outdated

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

Review result: NO-GO (3 × P1)

The architectural direction is sound: this change genuinely removes the RuntimeKernel/CLI fallback queue authorities instead of merely moving them. However, the new unified durable authority still has three reachable crash cuts that violate the PR's no-loss/no-resurrection/restart guarantees. I reproduced each with a focused probe on this exact head.

Validation completed: Core, Storage, MCP, Runtime, and Runtime Host builds; 107 focused Runtime Host tests; 18 SQLite core-execution tests; and 5 focused AiSdk steering tests. All existing tests pass. The three added diagnostic probes fail as described in the inline comments; they were removed afterward and the worktree is clean. Hosted audit is green; hosted test was still in progress at the final freshness check.

Simplify audit: production-like changes are +1299/-1085 and test-like changes are +1443/-1929. Across the relevant RuntimeKernel and CLI files, +179/-1009 removes pendingFallback (16 → 0 references), takePendingFollowup (5 → 0), and confirmLiveTurn (21 → 0). Please preserve that single-authority direction while closing these crash cuts; reintroducing a fallback owner would be the wrong fix.

中文

审查结论:NO-GO(3 × P1)

架构方向成立:这次确实删除了 RuntimeKernel / CLI 的 fallback queue authority,不是简单搬目录。但新的统一 durable authority 仍有三个可达崩溃切点,分别破坏本 PR 承诺的“不丢消息 / 不复活 / 可重启恢复”。我在这个 exact head 上分别做了最小探针复现。

已完成验证:Core、Storage、MCP、Runtime、Runtime Host 构建;Runtime Host 相关 107 个测试;SQLite core-execution 18 个测试;AiSdk steering 定向 5 个测试,现有测试全部通过。三个临时诊断探针均按内联评论描述失败,随后已移除,worktree clean。最终刷新时 hosted audit 已绿,hosted test 仍在运行。

简化审计:production-like +1299/-1085,test-like +1443/-1929;相关 RuntimeKernel 与 CLI 文件合计 +179/-1009,并把 pendingFallback(16 → 0)、takePendingFollowup(5 → 0)、confirmLiveTurn(21 → 0)完全删掉。修复以下崩溃切点时应保留单一 authority 方向,不要重新引入 fallback owner。

Comment thread packages/runtime-host/src/server/message-coordinator.ts Outdated
Comment thread packages/runtime-host/src/server/message-coordinator.ts
Comment thread packages/runtime-host/src/server/root-turn-coordinator.ts

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kabi exact-head review under the standing WAWQAQ review authorization, scoped to 1343cee25bddb40b84456d7f15f08d74d8ffc0f7.

NO-GO — Kabi grading: 2×P1, 2×P2, 1×P3. Four defects were independently reproduced after a sealed provisional judgment. To avoid opening duplicate threads, their current-head anchors are: provider-echo recovery loss (P1), materialized-source/no-Run startup failure (P1), Stop-recovery identity resurrection (P2 in this line), and double CLI rendering of one steering identity (P2). The non-duplicate P3 is inline.

The architectural simplification itself is real: durable Host/Storage state is now the message authority, while the old CLI fallback/live-steering ownership has been removed. The repair should preserve that direction. A post-seal promotion-capacity candidate was withdrawn after boundary probing: submit preflight reserves the worst-case revision width and rejects larger candidates before promotion. The Quote Companion race was not included in this line's independent count because the final hook-level reproduction was not completed.

Verification on this head: Runtime Host 1134/1134, CLI 423/423, UI 223/223, Desktop 1339/1339; hosted test and audit are completed/success. Fresh gate: OPEN, non-draft, MERGEABLE/BLOCKED, six unresolved P0–P2 threads, and zero current or stale APPROVED reviews. COMMENT only; no approval or merge.

Comment thread packages/runtime-host/src/server/message-coordinator.ts Outdated

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supplement to review 5005153696 on the unchanged exact head 1343cee25bddb40b84456d7f15f08d74d8ffc0f7: the Quote Companion hook-level probe completed before the stop instruction arrived and independently reproduced the existing P2 thread. The correct Kabi count is therefore NO-GO — 2×P1, 3×P2, 1×P3. The earlier sentence excluding this race from Kabi independent reproduction is superseded; all other findings and the promotion-capacity withdrawal remain unchanged.\n\nThe exact hook sequence was: settle turn-old; begin a second send and execute onBeforeSend while the Host result is pending (activeTurnId=null, in-flight true); deliver a late terminal event for turn-old, which claims the active ref; return turn-new, whose ID is then not installed because the ref is non-null; deliver turn-new text, which remains absent from the live projection; release the old settlement, after which the hook reports streaming false while the real Host turn remains active. The React hook plus fake Workbar production service seam reproduced this in 30.5 ms; temporary probe removed and worktree clean. COMMENT only; no approval or merge.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at head b919db88e02d5c093dfeae10d61a4ee2d0cb1489. Conclusion: NO-GO, 2 × P1. Neither finding comes from reading thread resolution state; both were traced through the code and reproduced with an executable probe.

The 15 historical threads were re-judged against the code and are genuinely fixed, and the simplification does land: RuntimeKernel's queue authority, the CLI fallback/mirror, the Host synthetic transcript and the renderer's unconfirmed state are all really gone.

The problem is in what this rebase newly pulled in. Main's queue.entry.update (#3635) was integrated, but it was not folded into the durable admission transaction this PR itself introduces.

Both P1s are described in the inline comments below. The shared fix boundary is: an edit and a fold must both land inside the same durable admission transaction, rather than mutating memory or writing a receipt only.

Two test gaps to close as well:

  • an integration test for late steering to successor across a restart
  • a precise test for queue, edit, SIGKILL, recovery

The existing edit test is in-memory and never restarts, so it cannot catch the second P1.

简体中文

在当前 head b919db88e02d5c093dfeae10d61a4ee2d0cb1489 上复审,结论是 NO-GO,2 个 P1。两条都不是靠读 thread 的 resolved 状态得出的,是按代码链路复核并用可执行脚本复现的。

历史 15 条线程逐条按代码重新判过,都是真修复;简化方向也确实达成了(RuntimeKernel 的 queue authority、CLI fallback/mirror、Host synthetic transcript、renderer 的 unconfirmed 都真正消失了)。问题出在这次 rebase 新并进来的部分——把 main 的 queue.entry.update#3635)接进来时,没有同时接进这个 PR 自己新建的 durable admission 事务。

两条 P1 详见下面的行内评论。共同的修复边界是:编辑和折叠都必须落到同一个 durable admission 事务里,而不是只改内存或只写 receipt。

另外补两条测试缺口:late steering 到 successor 再重启的集成测试;queue 编辑后 SIGKILL 再恢复的精确测试。现有的 edit 测试是纯内存、不重启的,所以覆盖不到第二条 P1。

Comment thread packages/runtime/src/runtime-kernel.ts Outdated
Comment thread packages/storage/src/message-receipt-store.ts

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked at 07a3adb9c. Both blocking issues from my earlier review are resolved. I have no remaining P0-P2.

Queued message edits now survive a restart. updatePendingMessage was added to the receipt-store interface and implemented in the SQLite store, and queue.entry.update calls it before mutating the in-memory entry and before committing the receipt — so a failure to persist leaves the in-memory state untouched rather than diverging from it. The new test in message-coordinator.test.ts closes the loop end to end: edit, restart under a fresh host epoch, then assert the recovered sources carry the edited content. That is exactly the path that previously lost the edit.

Source materialization no longer skips silently. materializeRootSourceMessages now resolves each existing message and validates it — type, content, and turn binding — instead of testing membership in a set of ids and moving on. A message already bound to a different turn raises Queued root source … conflicts with its transcript rather than being quietly left where it was. The one legitimate exception is stated explicitly: a steering source may sit under previousRootTurnId.

hosted-execution-recovery.ts was reworked to match, which is what makes the pair coherent. Verification is now by message identity rather than positional index, it permits the same steering-under-predecessor case, and it still rejects identity reuse and holes — a missing source followed by a present one raises incomplete source materialization, so prefix semantics hold.

Checks on my side: CI is terminal green on this exact head (run 32714419591). I also merged this into current main8801edc32, six commits ahead of this branch — which merges cleanly, and ran the affected runtime-host suites on the merged tree. Six tests fail there, but all six fail with the identical error execution Host exited before readiness: 1, and five of them fail the same way on a clean main checkout in the same environment. That is a local toolchain problem — @maka/runtime will not build here because @slack/web-api, @slack/socket-mode, and a compatible https-proxy-agent are missing — so the execution Host child cannot boot. Nothing is attributable to this change.

This still needs an approving review from an account other than the one that authored it.

简体中文

07a3adb9c 上重新核对:我先前 review 里的两个阻塞问题都已解决,没有遗留的 P0-P2。

队列消息的编辑现在能挺过重启。 receipt-store 接口新增了 updatePendingMessage 并在 SQLite 存储中实现,queue.entry.update 会在改动内存条目之前、也在提交 receipt 之前调用它——因此持久化失败时内存状态保持不变,不会与存储分叉。message-coordinator.test.ts 里新增的用例把整条链路闭环了:编辑、在新的 host epoch 下重启、然后断言恢复出来的 sources 带的是编辑后的内容。这正是之前会丢失编辑的那条路径。

源消息物化不再静默跳过。 materializeRootSourceMessages 现在会取出每条已存在的消息并校验它——类型、内容、以及 turn 绑定——而不是在一个 id 集合里判断存在与否然后跳过。已经绑定到别的 turn 的消息会抛出 Queued root source … conflicts with its transcript,而不是被悄悄留在原处。唯一合法的例外被显式写了出来:steering 类型的源可以位于 previousRootTurnId 之下。

hosted-execution-recovery.ts 做了对应改造,这是让两边自洽的关键。校验现在按消息 identity 而不是位置下标进行,允许同样的「steering 挂在前一个 root turn 下」的情形,并且仍然拒绝 identity 复用和空洞——先缺失、后又出现的源会抛出 incomplete source materialization,因此前缀语义得以保持。

我这边的检查:这个 exact head 的 CI 终态绿(运行记录 32714419591)。我还把它合进了当前 main8801edc32,比本分支新 6 个提交),合并干净,并在合并后的树上跑了受影响的 runtime-host 测试。那里有 6 个用例失败,但 6 个的报错完全相同,都是 execution Host exited before readiness: 1,其中 5 个在同一环境下的干净 main 检出上也以同样方式失败。这是本地工具链问题——@maka/runtime 在这里构建不起来,因为缺少 @slack/web-api@slack/socket-mode 和版本匹配的 https-proxy-agent——导致 execution Host 子进程无法启动。没有可归因于本次改动的问题。

这个 PR 仍然需要一条来自非作者账号的 approve。

@Astro-Han
Astro-Han force-pushed the fix/steering-durable-admission branch from 07a3adb to eecd4d9 Compare August 24, 2026 11:12
@Astro-Han

Copy link
Copy Markdown
Contributor Author

Note on freshness: my earlier review is bound to 07a3adb9c, which this branch no longer contains. Please do not read it as a statement about the current code.

The branch was rewritten rather than extended — between 07a3adb9c and the current eecd4d93d there is a new commit chain, and the two commits I examined now correspond to different objects. The change is also considerably larger than what I looked at: 82 files and +3989/-3158, against 37 files previously.

I am not restating a verdict here. A fresh review is underway at eecd4d93d, and I will publish conclusions bound to whatever head is current when they are ready.

One environment note that affects how CI is read right now, unrelated to this PR: main is failing its Check ASF source headers step because five packages/runtime/plugin-* files added by #3250 are missing the license header. Any ASF header failure seen while validating against main comes from there, not from this branch.

简体中文

关于时效性的说明:我先前那条 review 绑定在 07a3adb9c 上,而这个分支已经不包含该提交。请不要把它当作对当前代码的判断。

这个分支是被重写而不是被追加的——07a3adb9c 到当前 eecd4d93d 之间是一条新的提交链,我当时看的那两个提交现在对应的是不同的对象。改动规模也比我看过的大得多:82 个文件、+3989/-3158,而此前是 37 个文件。

我在这里不重述任何结论。针对 eecd4d93d 的新一轮复审正在进行,结论会绑定到届时的当前 head 再发布。

另有一条与本 PR 无关、但会影响当下 CI 判读的环境事实:mainCheck ASF source headers 这一步正在失败,原因是 #3250 新增的五个 packages/runtime/plugin-* 文件缺少版权头。因此在对 main 做验证时看到的 ASF 头失败来自那里,不是来自这个分支。

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the rework — the two problems I raised earlier are genuinely closed. Folded steering now survives a Host restart with source identity preserved on the successor admission, and a queued edit comes back as the edited content after a hard kill under a new Host epoch. Those were the hard ones.

Five things remain. The first three block merge.


[P1] Crash recovery and normal cleanup classify a pending admission by different proofs

prepareTerminalTransition() in packages/runtime-host/src/server/message-coordinator.ts:507 settles a pending admission when readImmutableSteeringMessageProof() returns a proof, falling back to an explicit Stop proof. The restart path at :431-440 does not consult the steering proof at all — it only asks readRootTurnSourceMessageReceipt() and then readExplicitStopProof(), and treats anything else as still pending.

That asymmetry is the bug. Take a steering message whose RuntimeEvent is already durable and already went into the next model request, with a durable provider-origin tool result after it. If the process dies before prepareTerminalTransition() collects the admission, the normal path would have settled it, but recovery does not: it finds no root-turn source receipt and no Stop proof, so it classifies the admission as pending and starts the same steering again as a successor root. With a side-effecting tool, the user's instruction runs twice.

A steering echo on its own must still recover, so the existing echo-only test is correct and should stay. What is missing is a settlement signal derived from durable downstream execution — either the same steering proof the cleanup path already trusts, or a strictly downstream causal fact. Please also add the crash cut where a provider-origin tool result is durable and the process dies before terminal cleanup.

[P1] The durable-Session marker is not reconstructed when an admitted root is replayed

bindDurableSession() in packages/runtime-host/src/server/client-capability-coordinator.ts:238 writes only to the in-memory #sessions map, and its only production caller is admitQueuedMessageRoot() in root-turn-coordinator.ts:2384. The restart replay path never goes through that caller.

So if the process dies after the root admission commits but before the Run is created, recovery replays the root without ever setting the marker. Once the Host is ready and that replay settles, a later supervisor wake on the same Session builds a fresh backend through neither binding path, and because state?.durable is absent, the if (!state?.durable) branch at :499 auto-discovers a newly registered call-affine Client offer. The identical wake cannot discover that offer without the crash, which inverts the durable-Session contract this PR states. Please persist or rebuild the durable mode before replay, and cover restart, replay, post-ready provider registration, and a later autonomous root in one test.

[P1] A regression test added by this PR contradicts the new atomic admission contract

packages/runtime-host/src/__tests__/execution-composition.test.ts:316-335 commits the current-turn steering through commitMessageAdmission, which now materializes the canonical user transcript inside that transaction (packages/storage/src/sqlite-session-metadata-store.ts:1575-1604). The fixture then appends the same identity and payload again through the direct path. The exact-once assertion consequently reads ['initial-message', 'admitted-steering', 'admitted-steering'] where it expects two entries, and production startup recovers steering admitted before an interrupted Run lost its queue fails 0/1 every run.

Removing only the redundant append makes the isolated test pass, which locates this in the fixture rather than the implementation — the runtime materializer does check message identity, so this is not a user-visible duplicate. Please delete the redundant append or route the fixture through the production materializer, and keep the exactly-one assertion as it is. Please do not make the duplicate append silently idempotent; that would remove the very guarantee this test exists to hold.

This did not show up in the checks here because test stopped at the ASF header step inherited from the base and skipped everything after it. That is fixed on main now, so a rebase should let the run reach this failure.

[P2] A 40,000-byte queue edit is accepted by every guard and then fails as an internal error

The wire limit is 48 KiB (packages/runtime-host/src/protocol/message.ts:335-345), but the durable admission envelope is 64 KiB and serializes content and modelContent together with identity metadata (packages/storage/src/message-receipt-store.ts:36,520-546). One wire-valid text does not guarantee both durable representations fit.

Queue a short followup, then edit it to 40,000 ASCII bytes. The queue projection, root admission, and full snapshot preflights at message-coordinator.ts:1373-1389 all pass, because none of them measures the envelope that normalizePendingMessageAdmission will actually build. The durable update then throws Pending message admission exceeds size limit past the guards at :1403-1414, and the client sees only Runtime Host operation failed. The transaction keeps the original text, so nothing is lost, but a legal input fails unpredictably with an internal error. Please reuse the same admission-size preflight before the durable boundary and return the declared bounded error, or make the contract guarantee that every wire-valid edit holds both representations. A test at the largest successful size would pin whichever you choose.

[P3] Please add the before/after evidence for the UI changes

This changes how queued and steering messages appear and when Side Chat goes live. CONTRIBUTING.md asks for before/after screenshots or a recording for UI changes and does not exempt behavior-only changes that add no styling. Please attach the Desktop and TUI states this lifecycle change affects.


One more thing outside the code: main has moved and this branch is now conflicting. It needs a rebase before it can merge regardless of the above, and I will re-review against the new head once you push.

简体中文

感谢返工——我之前提的两个问题确实解决了。folded steering 现在能跨 Host 重启存活,source identity 保留在 successor admission 上;queue edit 在硬杀并换新 Host epoch 后也能恢复为编辑后的内容。这两个是难的部分。

还剩五点,前三点阻塞合并。

[P1] 崩溃恢复与正常清理用了不同的证据来判定未决 admission

packages/runtime-host/src/server/message-coordinator.ts:507prepareTerminalTransition()readImmutableSteeringMessageProof() 返回证据时结算未决 admission,退化情况才看显式 Stop 证据。但 :431-440 的重启路径完全不查 steering 证据,只问 readRootTurnSourceMessageReceipt(),再问 readExplicitStopProof(),其余一律当作仍然未决。

这个不对称就是 bug。设想一条 steering,它的 RuntimeEvent 已持久化、已进入下一次模型请求,其后还有一个已持久化的 provider 侧工具结果。若进程在 prepareTerminalTransition() 回收该 admission 之前崩溃,正常路径本会结算它,恢复路径却不会:它找不到 root-turn source receipt,也找不到 Stop 证据,于是判为未决,把同一条 steering 当作 successor root 重新启动。若工具有副作用,用户的指令会被执行两次。

单独的 steering echo 仍然必须能恢复,所以现有的 echo-only 测试是对的,应当保留。缺的是一个来自持久化下游执行的结算信号——要么就用清理路径已经信任的那份 steering 证据,要么用严格的下游因果事实。另请补上"provider 侧工具结果已持久化、随后在 terminal cleanup 之前崩溃"这个切面的测试。

[P1] 重放已提交的 root 时没有重建 durable Session 标记

packages/runtime-host/src/server/client-capability-coordinator.ts:238bindDurableSession() 只写内存中的 #sessions map,而它在生产代码里唯一的调用者是 root-turn-coordinator.ts:2384admitQueuedMessageRoot()。重启重放路径根本不经过这个调用者。

于是,如果进程在 root admission 提交之后、Run 创建之前崩溃,恢复会重放这个 root 而始终不设置该标记。等 Host ready、这次重放结算之后,同一 Session 上后续的 supervisor wake 会经由两条 binding 路径之外的方式建立新 backend;由于 state?.durable 缺失,:499if (!state?.durable) 分支会自动发现一个新注册的 call-affine Client offer。没有崩溃时,同一个 wake 发现不了这个 offer——这与本 PR 声明的 durable Session 契约正好相反。请在重放前持久化或重建 durable 模式,并用一条测试覆盖重启、重放、ready 后注册 provider、以及之后的 autonomous root。

[P1] 本 PR 新增的回归测试与新的原子 admission 契约自相矛盾

packages/runtime-host/src/__tests__/execution-composition.test.ts:316-335 通过 commitMessageAdmission 提交当前 turn 的 steering,而该事务现在会在内部落地 canonical user transcript(packages/storage/src/sqlite-session-metadata-store.ts:1575-1604)。紧接着 fixture 又用直接 append 路径把同一 identity 和 payload 再写了一遍。于是 exact-once 断言实际读到 ['initial-message', 'admitted-steering', 'admitted-steering'],而它期望两条;production startup recovers steering admitted before an interrupted Run lost its queue 每次都是 0/1 失败。

只删掉那段冗余 append,隔离运行该测试即可通过——这说明问题在 fixture 而不在实现:runtime 的 materializer 确实做了 message identity 检查,所以这不是用户可见的重复。请删掉冗余 append,或让 fixture 走生产 materializer,并原样保留 exactly-one 断言。请不要把重复 append 改成静默幂等,那会把这条测试本来要守住的保证一并抹掉。

这个问题没有在本 PR 的检查里暴露,是因为 test 先停在了从 base 继承的 ASF header 步骤,之后全部跳过。main 上已修复,rebase 后运行就能跑到这个失败。

[P2] 40,000 字节的 queue edit 通过了全部守卫,然后以内部错误失败

wire 上限是 48 KiB(packages/runtime-host/src/protocol/message.ts:335-345),但持久化 admission envelope 是 64 KiB,并且要把 contentmodelContent 连同身份元数据一起序列化(packages/storage/src/message-receipt-store.ts:36,520-546)。单份文本合法,不等于两份持久化表示装得下。

先 queue 一条短的 followup,再把它编辑为 40,000 个 ASCII 字节。message-coordinator.ts:1373-1389 处的 queue projection、root admission、full snapshot 三道预检全部通过,因为它们都没有测量 normalizePendingMessageAdmission 真正会构造的那个 envelope。随后持久化更新在 :1403-1414 的守卫之外抛出 Pending message admission exceeds size limit,客户端只看到 Runtime Host operation failed。事务保留了原文,所以没有数据丢失,但一个合法输入以内部错误不可预期地失败了。请在持久化边界之前复用同一套 admission 尺寸预检并返回已声明的有界错误,或者让契约保证任何 wire 合法的编辑都能容纳两份表示。无论选哪种,都建议补一条"最大可成功尺寸"的测试来钉住边界。

[P3] 请补充 UI 变更的前后证据

本 PR 改变了 queued 与 steering 消息的呈现方式,以及 Side Chat 进入 live 状态的时机。CONTRIBUTING.md 要求 UI 变更提供前后截图或录屏,并没有为"只改行为、不加样式"设置例外。请附上受这次生命周期改动影响的 Desktop 与 TUI 状态。

最后一点在代码之外:main 已经前进,这个分支现在处于冲突状态。无论上述问题如何,它都需要先 rebase 才能合并;你推送之后我会针对新的 head 重新审阅。

Persist a Host-owned pending steering admission before exposing it to the active Run. Recover unresolved admissions after interrupted Runs are terminalized, and settle them when provider consumption, successor root admission, or an intentional stop makes replay unnecessary.

Add a production startup regression for the crash between durable admission and the in-memory queue commit.

Generated-by: Maka
Route ordinary messages through the Runtime Host admission API, stop creating renderer-owned empty live turns, and keep transcript publication within the active subscription. Preserve visible live tails across sends, return sparse transcripts to latest before submission, and remove redundant live/durable filtering and unconfirmed-arm bookkeeping.

Generated-by: Codex
Update the real-framing execution test to cover the ordinary-message operation now used by Desktop instead of retaining the removed direct turn-start contract.

Generated-by: Codex
Keep revision and Skill inputs on the turn-start control path while ordinary messages use Host admission. Rebind Side Chat to the Host-owned Turn identity from either the first event or admission response, and assert remount continuity without requiring an unobservable React intermediate paint.

Generated-by: Codex
@Astro-Han
Astro-Han force-pushed the fix/steering-durable-admission branch from eecd4d9 to 7128a97 Compare August 24, 2026 12:47
@Astro-Han

Astro-Han commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

After several exact-head review rounds, it is clear that the remaining findings share structural causes rather than isolated implementation mistakes. Settlement and recovery still classify the same admission differently, the durable Session execution contract is partly represented by memory-only state, and some tests bypass the new atomic admission boundary. The repeated fixes have also expanded this PR to 80 files and 33 commits, making both review and rebasing unnecessarily difficult.

I am pausing this implementation and will split it into three end-to-end changes:

  1. A complete durable message lifecycle, including admission, mutation, settlement, recovery, and removal of the old Runtime queue authority.
  2. Canonical CLI and Desktop conversation projection, preserving immediate send UX while reconciling by stable messageId.
  3. Side Conversation admission and event ownership, built cleanly on the current Side Conversation architecture.

I will convert this PR back to draft and stop updating its implementation. Once the replacement PRs are available, I will close this PR and link them here. The existing review threads remain useful evidence for the replacement design, but should not be treated as another local patch queue.

This restructuring plan was developed with Codex assistance and reviewed by me.

@Astro-Han
Astro-Han marked this pull request as draft August 24, 2026 13:07
@Astro-Han

Copy link
Copy Markdown
Contributor Author

The branch moved to 7128a97207 after my review, which was bound to eecd4d93d. I re-checked all three blocking findings against the new head rather than assuming either way, and all three are unchanged:

  • bindDurableSession() still writes only the in-memory map, and root-turn-coordinator.ts:2384 is still its only production caller, so the replay path still never sets the marker.
  • message-coordinator.ts:432-438 still classifies a pending admission from the root-turn receipt and the explicit Stop proof alone, while :516 still consults the immutable steering proof. The asymmetry between the two paths is what the finding is about, and it is still there.
  • execution-composition.test.ts still calls commitMessageAdmission and then appends the same admitted-steering identity through the direct path, so the exact-once assertion still reads three entries.

So the earlier review still applies in full; only its commit binding is stale. Nothing here asks you to re-explain the rebase — I mention it only so the moved head is not mistaken for a response to the findings.

简体中文

分支在我评审之后移到了 7128a97207,而那次评审绑定的是 eecd4d93d。我没有凭任何一边的假设下结论,而是对新 head 重新核了三条阻塞项,三条都没有变化

  • bindDurableSession() 仍然只写内存中的那个 map,root-turn-coordinator.ts:2384 仍是它在生产代码里唯一的调用者,因此重放路径依旧不会设置该标记。
  • message-coordinator.ts:432-438 仍然只凭 root-turn 收据和显式 Stop 证据来判定未决 admission,而 :516 仍然会查 immutable steering proof。这条 finding 说的正是两条路径之间的不对称,它依然存在。
  • execution-composition.test.ts 仍然在 commitMessageAdmission 之后,又用直接路径追加了同一个 admitted-steering identity,所以 exact-once 断言读到的仍然是三条。

因此之前那条评审整体仍然成立,过期的只是它的 commit 绑定。这里不需要你解释这次 rebase——我说明它,只是为了避免把 head 的移动误当成对上述问题的回应。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants