Uh oh!
There was an error while loading. Please reload this page.
feat(runtime-host): establish interaction authority - #1359
Conversation
c26f508 to
8dad88cCompare
Astro-Han
left a comment
There was a problem hiding this comment.
No P0 or P1 found in the current Interaction slice. Two P2 issues remain:
- [P2]
projectToolReview()inpackages/core/src/interaction-permission-review.tsaccepts only a fixed set of tool names. Runtime already has valid promptable tools outside that set, includingagent_spawn,agent_swarm, and MCP proxy tools. When hosted execution routes one of those permission requests, projection throws andHostInteractionCoordinatorreturnsinvalid_request, so the user cannot approve the tool. Give unknown registered tools a bounded, redacted review representation, or let tool metadata supply the projector. Add coverage for at least one subagent tool and one MCP tool. - [P2]
InteractionStore.listPending(filter)enumerates every interaction directory and reads every record before applying the filter.CanonicalSessionProjectioncalls it with{ sessionId }, so producing one session snapshot becomes proportional to all retained interactions, including settled interactions from other sessions. Add an index or another bounded lookup path, plus a large-history test that catches the full scan.
The Runtime Host is still non-serving, so these do not block approval of this draft slice. They should be resolved before production cutover.
7f3946a to
cc3f943Compare
Astro-Han
left a comment
There was a problem hiding this comment.
Review of cc3f943. I found one merge-blocking P1 and seven P2 follow-ups. I did not find a concrete P3.
[P1] Preserve the legacy
waiting_permissionvalue on reads.AGENT_RUN_STATUSESreplaces the persistedwaiting_permissionvalue withwaiting_for_user, whiledecodeAgentRunHeader()accepts only the new set. Existing runs written by released versions are therefore dropped bylistSessionRuns()and make strict recovery fail. Accept and normalize the legacy value on read, keep writing only the new value, and add an upgrade/recovery test with an old header.[P2] Redact AWS Secret Access Key field names. The
keysuffix logic checks only the immediately preceding segment, soawsSecretAccessKey,secretAccessKey, andAWS_SECRET_ACCESS_KEYretain their values. Generic tool review then persists and exposes those values. Handle the standardsecret_access_keyshape at the redaction owner and cover JSON, assignment, Bash, and generic MCP projections.[P2] Always stop or abort the backend when Interaction close fails. The stop path awaits
interactionRun.close()before callingbackend.stop(), and the abandoned-consumer cleanup awaits it beforeabortController.abort(). A Store, continuity, or settlement failure therefore leaves the backend running until Host shutdown. Attempt durable closure first, but guarantee best-effort stop/abort in cleanup and report the closure failure afterward.[P2] Escape the missing bidi controls before review admission.
UNSAFE_REVIEW_CHARACTERomits U+061C, U+200E, and U+200F. These invisible marks survive projection, so visually identical Question labels remain distinct strings and bypass the collision check. Cover the complete bidi-control set and add option-label collision tests for the omitted marks.[P2] Do not follow symlinks in canonical Interaction locators.
publish()recursively createsinteractions/<locator>and then opens and links files through that path without binding the directory identity; reads also follow the path. A pre-existing locator symlink can redirectrequest.jsonoroutcome.jsonoutside the storage root, and recovery skips that locator. Apply the same no-follow/inode-binding discipline used by the pending index and test that the external directory stays untouched.[P2] Preflight Interaction admission against the queue's worst-case state. Interaction preflight merges the candidate with the current queue, while message admission explicitly converts queued steering entries to their larger
in_flightrepresentation. A snapshot near the byte limit can pass Interaction admission and exceed the limit during the normal queue transition, causing Host drain. Use one conservative snapshot bound for both owners and test the boundary transition.[P2] Reject publication after a continuation has started closing or settling.
assertPendingAdmission()checks the historical pending admission andpublishedflag, but not Run closure or settlement state. A stop/terminal race can therefore publish a request after its canonical Interaction has already closed. Require the Run and continuation to remain open at publication and add a close-versus-publication race test.[P2] Keep the permission answer authoritative in one durable store. Question answers use an identity-only Run audit fact, but permission answers are still persisted in full in both Interaction Store and the Runtime ledger, and the Runtime read model consumes the second copy. That leaves two durable facts that can diverge after a partial write. Align permission with the identity-only audit contract and project answer details from the canonical Interaction outcome.
The earlier findings about generic registered-tool admission and Session-scoped pending lookup are fixed in this head. The locator reservation ordering change also holds.
English@Astro-Han Thanks for the detailed review. We reproduced all eight findings and addressed them in the current head. The changes and their remaining boundaries are below.
Verification remains covered by the affected suites and independent review. 简体中文@Astro-Han 感谢细致审查。八项问题都已复现,并在当前 head 中完成处理。下面分别说明触发场景、处理方式和仍然保留的边界。
最终验证:Core 1,194/1,194;Storage 537 项通过,另有 1 项既有平台 skip;Runtime 2,588 项通过,另有 7 项既有 skip;Runtime Host 182/182。Workspace typecheck、production build、Biome format/lint、 |
e052386 to
f2ce184CompareAstro-Han
commented
Jul 26, 2026
Reviewing current head P1
P2
P3
The P1 is the only blocker in this list. Once the legacy subagent transcript migration is covered, I would not hold the merge for the P2 or P3 follow-ups. |
f2ce184 to
91d9085CompareEnglish@Astro-Han Thanks for the follow-up review. The current head addresses the blocking compatibility issue and closes the Interaction-owned lifecycle gaps that are reachable from this slice. Addressed in this slice
Kept in follow-up slices
The final implementation has passed the affected full suites, GitHub checks, Biome, and independent correctness review. The PR is ready for review again. 简体中文@Astro-Han 感谢继续审查。当前 head 已处理阻塞合并的兼容性问题,并收口本 slice 能真实触发的 Interaction 生命周期缺口。 本 slice 已处理
保留在后续 slice
最终实现已通过受影响的完整测试、GitHub checks、Biome 与独立正确性复审。PR 现已重新进入 Ready for review。 |
Astro-Han
left a comment
There was a problem hiding this comment.
English
Review of 29b17c9. I found two P1 blockers and seven P2s.
P1
Shell review can omit commands that still execute.
I can reproduce this with:
# " review note password=dummy-value python deploy.py --target productionredactSecrets()returns:# " review note password=[redacted]Bash ignores the quote in the comment and executes the third line. The review scanner treats it as an opening quote, so the assignment replacement consumes the rest of the command.
categorizeBash()still returnsshell_unsafebefore and after redaction, so the category check accepts the incomplete review.A permission review cannot omit non-secret commands that will execute. If the replacement span is uncertain, projection should fail closed.
Malformed shell-like text makes
redactSecrets()quadratic."token[".repeat(n)causes every indexed-assignment candidate to scan the rest of the input. On my machine:Input main29b17c912KB 0.5ms 39ms 24KB 0.1ms 161ms 48KB 0.2ms 648ms 96KB 0.4ms 2.6s This affects the existing Runtime, not only the dormant Host path.
PipeTailCollector.snapshot()synchronously redacts retained stdout and stderr, with a limit of 1MiB per stream. A tool can stall the Runtime event loop by producing this shape of output.The shared redactor needs a linear scan or a hard work budget.
P2
Generic reviews drop an own
__proto__property. Redacting{"__proto__":{"operation":"delete"},"token":"dummy-value"}produces{"token":"[redacted]"}. Rebuilding the object with{}and assignment changes its prototype instead of creating an own property. The tool still receives the original argument, but the approval review does not show it.Stop can finish before a reserved Run binds its Interaction owner. A stop between Run reservation and
run.begin()sees no binding and skips the close. The late binding is closed during cleanup, after the stop caller has already received success. Any close failure is then lost to that caller.A successful required resume write does not recover terminal status writes. After one best-effort RunStore failure, required resume can persist
running, but the old failure latch still suppresses the later terminal header. The RuntimeEvent is terminal while the Run header remainsrunning.Hosted questions still have a legacy settlement path below
SessionManager.RuntimeKernel,AiSdkBackend,FakeBackend, andToolRuntimecan settle a hosted pending question without using the captured Host continuation. The acknowledgement is produced, then the Interaction binding fails because its continuation never settled.Embedded Runs resume after the first of several pending interactions. With two
AskUserQuestioncalls in one Turn, answering the first leaves one question pending but projects the Run asrunning. The embedded path needs the same aggregate settlement check used by the hosted path.Interaction Store open can return a facade after its lease has closed. When owner close overlaps writer recovery, open may report success and return a facade whose first operation fails with
invalid_lease.A branch clone failure leaves a partial child Session visible. The child is published before RuntimeEvent ledgers are cloned. If a later write fails, the API rejects but the partial branch remains active and visible.
Design note
I am not sure the two redaction failures are isolated cases. They look like signs that the shell parsing boundary is still moving. Fixing comments and indexed assignments may lead to the same discussion again for heredocs, substitutions, or malformed recovery.
Since #1359 is mainly about Interaction authority and Runtime Host is not serving yet, it may be worth keeping the current bounded, best-effort redaction boundary here. Stronger shell-aware durable review could be handled separately, with a smaller contract and a focused set of invariants.
I do not have a strong preference on the implementation. I would mainly avoid another round of case-by-case parser fixes in this PR.
The current test coverage is substantial and GitHub checks are green. After the lifecycle changes, a short repeated run of the Runtime Host execution tests would still be useful because several recent intermediate heads failed during shutdown. A real two-client UDS test with conflicting answers would also close the remaining wire-level arbitration gap; I see that as test follow-up rather than another code finding.
简体中文
对当前 29b17c9 的审查发现两项 P1 blocker 和七项 P2。
P1
Shell review 会漏掉实际仍会执行的命令。
可以用下面的输入复现:
# " review note password=dummy-value python deploy.py --target productionredactSecrets()返回:# " review note password=[redacted]Bash 会忽略注释里的引号并执行第三行。Review scanner 却把这个引号当成了起始引号,导致 assignment replacement 一直吞到命令末尾。脱敏前后的
categorizeBash()都返回shell_unsafe,所以分类检查不会拒绝这份不完整的 review。权限 review 不能漏掉实际会执行的非敏感命令。无法确定替换 span 时,应当 fail closed。
畸形 shell-like 文本会让
redactSecrets()退化到 O(n²)。"token[".repeat(n)会让每个 indexed-assignment candidate 重新扫描剩余输入。本机结果:输入 main29b17c912KB 0.5ms 39ms 24KB 0.1ms 161ms 48KB 0.2ms 648ms 96KB 0.4ms 2.6s 这会影响现有 Runtime,并不只存在于尚未 serving 的 Host 路径。
PipeTailCollector.snapshot()会同步脱敏 stdout 和 stderr,每个 stream 最多保留 1MiB。工具只要输出这种形态的文本,就可能长时间阻塞 Runtime event loop。共享脱敏器需要线性扫描或明确的工作量上限。
P2
Generic review 会丢掉 own
__proto__属性。 对{"__proto__":{"operation":"delete"},"token":"dummy-value"}脱敏后只剩{"token":"[redacted]"}。使用{}和属性赋值重建对象时,__proto__改变的是 prototype,不会成为 own property。工具仍收到原始参数,但审批 review 中没有它。Stop 可能在 Run 绑定 Interaction owner 前结束。 如果 stop 发生在 Run reservation 和
run.begin()之间,它看不到 binding,会跳过 close。迟到的 binding 要等 cleanup 才关闭,此时 stop caller 已经收到成功,后续 close failure 也无法再反馈。Required resume 写入成功后,terminal status 仍可能被跳过。 一次 best-effort RunStore failure 会留下 failure latch。Required resume 可以绕过它并写入
running,但最终 terminal header 仍被抑制,形成 RuntimeEvent 已终止、Run header 仍是running的状态。Hosted Question 在
SessionManager下层仍保留 legacy settlement 路径。RuntimeKernel、AiSdkBackend、FakeBackend和ToolRuntime可以绕过捕获的 Host continuation,直接 settle hosted pending question。Answer acknowledgement 会正常产生,随后 Interaction binding 因 continuation 未 settle 而失败。Embedded Run 会在多个 pending Interaction 中的第一个完成后提前恢复。 同一 Turn 中有两个
AskUserQuestion时,只回答第一个,另一个仍然 pending,但 Run 已投影为running。Embedded 路径也需要聚合检查所有 Interaction 是否已经 settle。Interaction Store open 可能在 lease 已关闭后返回 facade。 Owner close 与 writer recovery 重叠时,open 可能返回成功,但 facade 的第一次操作立即报
invalid_lease。Branch clone 失败后会留下可见的部分 child Session。 Child 会先发布,再 clone RuntimeEvent ledger。后续写入失败时 API 虽然报错,部分 branch 仍保持 active 和可见。
设计上的一点想法
我不太确定前面两个 redaction 问题是否真的只是独立 case。看起来更像是 shell parsing 的边界还在变化。补完 comment 和 indexed assignment 后,heredoc、substitution 或畸形输入恢复可能还会遇到类似问题。
#1359 的主体是 Interaction authority,而且 Runtime Host 目前还没有 serving。这里或许可以先维持现有有界、best-effort 的脱敏边界。更强的 shell-aware durable review 可以单独处理,把契约和需要保证的 invariant 收得更小一些。
我对具体实现没有强偏好,主要是不太建议在这个 PR 里继续逐个补 parser case。
现有测试覆盖已经不少,GitHub checks 也全部通过。不过近期有几个中间 head 曾在 shutdown 阶段失败,所以 lifecycle 修改完成后,最好再重复跑几轮 Runtime Host execution tests。另外可以补一条真实双 UDS Client 提交冲突答案的测试,验证 wire 层的 first-winner;这更像测试补充,不算新的代码 finding。
M4n5ter
commented
Jul 27, 2026
English@Astro-Han Thanks for the design direction. The current head removes the small custom shell scanner and returns review redaction to a bounded, best-effort layer. It preserves the executable tail in the reported repro, avoids quadratic work on malformed indexed-looking text, redacts JSON string leaves before serialization, and preserves an own The reachable authority/lifecycle findings in this slice are also addressed: stop now spans Run reservation through Interaction owner binding, successful required writes recover terminal persistence, hosted questions settle only through their captured continuations, and Interaction Store open revalidates the lease before publishing a writer facade. We intentionally did not keep expanding redaction into a shell parser. A stronger shell-aware durable review/redactor should be focused future work; it is not the main purpose of #1359. The embedded multi-pending and branch-clone transactional issues likewise remain with their owning follow-up slices. The affected full package suites and workspace typecheck/build are green. 简体中文@Astro-Han 感谢这里给出的设计方向。当前 head 已撤销这套小型自制 shell scanner,让 review 脱敏回到有界、best-effort 的边界。维护者给出的复现中,实际会执行的后续命令会被完整保留;畸形 indexed-looking 文本不再产生二次复杂度;JSON string leaf 会在序列化前脱敏,own 本 slice 内可触发的 authority/lifecycle 问题也已处理:stop 会覆盖 Run reservation 到 Interaction owner binding 的窗口;成功的 required write 会恢复 terminal persistence;hosted Question 只能通过捕获的 continuation 完成;Interaction Store open 会在发布 writer facade 前重新验证 lease。 我们没有继续把 redactor 扩张成 shell parser。更强的 shell-aware durable review/redactor 应作为后续独立工作认真设计,它不是 #1359 的主要目的。Embedded multi-pending 与 branch clone transaction 问题也仍留在各自负责的后续 slice 中。 受影响 package 的完整测试以及 workspace typecheck/build 均已通过。 |
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for the update — I rechecked the latest head. The earlier P1s are addressed, and keeping redaction bounded and best-effort here looks like a reasonable scope for this PR. CI is green as well.
The two remaining P2 items — aggregate status for multiple pending embedded interactions, and cleanup/atomicity when a branch clone fails — look suitable for follow-up work and don’t need to hold this PR. Approving from my side.
Uh oh!
There was an error while loading. Please reload this page.
`main` has been red since 17362dc (#1515); 0e80fe1 (#1359) inherited it. All three jobs failed, none of it visible in that PR's own checks because the earliest failure short-circuits the rest of the job. typecheck job - `format:check`: scripts/build-cursor-overlay.mjs was committed unformatted. Reapplied `biome format`; purely mechanical. - `knip`: #1515 added a second overlay entry point pair (permission-overlay{,-preload}.ts) but registered only the cursor overlay in knip.json, so both files read as unused. Registered them beside their cursor-overlay twins — they are loaded by path at runtime (loadFile / preload), never imported. - `knip`: asOsPermissionId was exported and referenced nowhere in the repo. It is an identity function (`return id`), so nothing is lost by dropping it — git history holds it if Stage 2 wants it back. Its sole import (OsPermissionId) went with it. test job - `check-console`: three console.warn sites in permission-overlay-main.ts (locale fallback, missing .app bundle, the controller log sink). These are main-process diagnostics that expose paths, not secrets — the same category as the daily-review and settings-runtime entries — so they are allow-listed with a reason per the script's own documented convention. e2e job - `settings.spec.ts:141` asserted a permission row draws exactly two grant buttons. #1515 added a third (拖拽授权) to precisely the row the fixture aims at — screen_recording is requestable, openable AND drag-grantable — so the `=== 2` filter matched nothing and the guard silently measured no rows. Relaxed to `>= 2`, which is what the contract was always about: a row whose actions track competes with the body. A wider actions track only makes the squeeze worse, so the three-button row is strictly the better subject. Verified locally: lint, format:check, build, typecheck, knip (both workspaces), check-console, and the full test:dist suite.
…ues (#1520) * fix(design-system): stop the radius scanner from skipping wrapped values The CSS governance scanner matched `border-radius` values with the class `[^;}\n]+`. Excluding `\n` meant a declaration whose VALUE wrapped onto a second line matched nothing at all — the scanner skipped it in silence rather than reporting it, so any radius could escape the token contract just by being formatted across two lines: border-radius: 10px 12px; /* never scanned */ Neither prettier nor biome reflows such a value back onto one line, so a long four-corner shorthand or calc() reaches this state on its own. (A newline directly after the colon was always fine — the `\s*` there already spans it. Only a newline *inside* the value was affected, which is narrower than it first looks.) Widening the class to `[^;}]+` cannot make a match outrun its own declaration: `;` and `}` are still excluded, so it stops at the first declaration terminator or the end of the rule body either way. The regression test covering that is asserted alongside the fix. Found while reviewing #1514, which fixed a separate bug in the same contract (it accepted only invalid unspaced calc()). This is the remaining scanner gap. * fix(ci): restore green main after #1515 `main` has been red since 17362dc (#1515); 0e80fe1 (#1359) inherited it. All three jobs failed, none of it visible in that PR's own checks because the earliest failure short-circuits the rest of the job. typecheck job - `format:check`: scripts/build-cursor-overlay.mjs was committed unformatted. Reapplied `biome format`; purely mechanical. - `knip`: #1515 added a second overlay entry point pair (permission-overlay{,-preload}.ts) but registered only the cursor overlay in knip.json, so both files read as unused. Registered them beside their cursor-overlay twins — they are loaded by path at runtime (loadFile / preload), never imported. - `knip`: asOsPermissionId was exported and referenced nowhere in the repo. It is an identity function (`return id`), so nothing is lost by dropping it — git history holds it if Stage 2 wants it back. Its sole import (OsPermissionId) went with it. test job - `check-console`: three console.warn sites in permission-overlay-main.ts (locale fallback, missing .app bundle, the controller log sink). These are main-process diagnostics that expose paths, not secrets — the same category as the daily-review and settings-runtime entries — so they are allow-listed with a reason per the script's own documented convention. e2e job - `settings.spec.ts:141` asserted a permission row draws exactly two grant buttons. #1515 added a third (拖拽授权) to precisely the row the fixture aims at — screen_recording is requestable, openable AND drag-grantable — so the `=== 2` filter matched nothing and the guard silently measured no rows. Relaxed to `>= 2`, which is what the contract was always about: a row whose actions track competes with the body. A wider actions track only makes the squeeze worse, so the three-button row is strictly the better subject. Verified locally: lint, format:check, build, typecheck, knip (both workspaces), check-console, and the full test:dist suite.
… on CI The "drain requested before factory completion begins drain before recovery exactly once" test failed the test job on main after #1658 landed (run 30544963454), even though #1658 only touches packages/headless. The flaky test was introduced in #1359. The flake came from asserting on the registration file mid-flight. requestDrain() flips in-memory #state to 'draining' synchronously, but persisting that to the registration file is the async writeHostRegistration I/O in #closeResources, while #start() had already persisted 'recovering' before entering the factory. The fixed sleep(50) bet that I/O landed in time, which lost on loaded CI runners and read back stale 'recovering'. That assertion tested a non-contract. No production caller reads HostRegistration.state; clients learn draining from the handshake (#admitHandshake returns kind:'draining' from #shutdownRequested), and connectResolvedRuntimeHost reads the registration only for rootId, hostEpoch, and the endpoint. The invariant the test name claims, "begins drain before recovery exactly once", is already proven by the final lifecycle array ['factory-return', 'begin-drain', 'recover', 'close'] with begin-drain counted once. The in-memory draining state is covered elsewhere (candidate.host.state === 'draining'). Drop the sleep, the readHostRegistration call, and the state assertion. The test still verifies, while the factory is suspended, that startup has not settled, that no lifecycle event has fired, and that the host still holds the interactive root owner lock, then checks the full lifecycle ordering after release. A handshake-based replacement was tried first but also raced: #closeResources calls server.close() right after the registration write, so on loaded CI the socket stops accepting before connectRuntimeHost connects, returning 'unavailable' instead of 'draining' (the first push of #1660). Refs #1658 (main CI failure, not a regression from that PR).
… on CI (#1660) The "drain requested before factory completion begins drain before recovery exactly once" test failed the test job on main after #1658 landed (run 30544963454), even though #1658 only touches packages/headless. The flaky test was introduced in #1359. The flake came from asserting on the registration file mid-flight. requestDrain() flips in-memory #state to 'draining' synchronously, but persisting that to the registration file is the async writeHostRegistration I/O in #closeResources, while #start() had already persisted 'recovering' before entering the factory. The fixed sleep(50) bet that I/O landed in time, which lost on loaded CI runners and read back stale 'recovering'. That assertion tested a non-contract. No production caller reads HostRegistration.state; clients learn draining from the handshake (#admitHandshake returns kind:'draining' from #shutdownRequested), and connectResolvedRuntimeHost reads the registration only for rootId, hostEpoch, and the endpoint. The invariant the test name claims, "begins drain before recovery exactly once", is already proven by the final lifecycle array ['factory-return', 'begin-drain', 'recover', 'close'] with begin-drain counted once. The in-memory draining state is covered elsewhere (candidate.host.state === 'draining'). Drop the sleep, the readHostRegistration call, and the state assertion. The test still verifies, while the factory is suspended, that startup has not settled, that no lifecycle event has fired, and that the host still holds the interactive root owner lock, then checks the full lifecycle ordering after release. A handshake-based replacement was tried first but also raced: #closeResources calls server.close() right after the registration write, so on loaded CI the socket stops accepting before connectRuntimeHost connects, returning 'unavailable' instead of 'draining' (the first push of #1660). Refs #1658 (main CI failure, not a regression from that PR).
… non-terminal prior runs (#2078) * fix(runtime): replay prior runs that never reached a terminal fact A run stopped while parked on an interaction keeps a non-terminal header and a ledger with no terminal fact. buildPriorRuntimeContext treated that as skippable and dropped the whole run, so the turn's user message, assistant text, and tool activity disappeared from every later turn's model context — silently, and permanently, because the header never becomes terminal on its own. Read the ledger it does have instead: attempt the same repair the terminal-status branch already performs, and replay the events either way. Only the effective run header still depends on a terminal fact. Refs #2074 * fix(runtime): land the terminal fact a stop already claimed stop() takes the Run's terminal claim, but only finalize() — reached when the backend's event stream ends — ever cashed it. A turn parked on an unanswered interaction never ends that stream, so the stop projected an aborted turn into the Session while the Run stayed non-terminal in the ledger for good, and the backend generation it was waiting on could never wake it. Cash the claim where the stop completes. The claim itself keeps this idempotent: a stream that later produces its own terminal event finds the claim taken and writes nothing. Refs #2074 * fix(runtime): keep the stop terminal fact inside the embedded owner The ledger may already hold a terminal fact this Run never wrote — a Host recovery or a resumed continuation sealed it — and a sealed run rejects further appends. Read before landing one. Scope the settlement to embedded owners too. A Hosted Run's terminal fact belongs to the Host's terminal authority (#1359, #1996), which parks provider-indeterminate Runs that a stop must not resolve on their behalf. Refs #2074 * fix(runtime): drop the unreachable repair from the non-terminal prior run repairRunTerminalFact returns false for a non-terminal header before it reads anything, so the call could only ever answer "no". It read as a recovery attempt that never happens. Refs #2074 * fix(runtime): commit a run's terminal fact at most once under a racing stop commitTerminalRun checked terminalRunHeaderCommitted only on entry. A stop settling the claim and the stream's own finalize both pass that guard, then queue behind the same in-flight write. The claim slot dedupes the RuntimeEvent, but the run-store projection appended a second terminal AgentRunEvent for the one run. Re-check after the await. Before this, the new stop settlement made the duplicate reachable on the ordinary path: stopping mid-generation overlaps the stream's unwind. Refs #2074 * fix(runtime): fail a stop whose terminal settlement never landed * test(runtime): pin the stop settlement's hosted and sealed boundaries * style(runtime): apply Biome formatting to the stop settlement guard
English
Context
This PR establishes the Interaction authority slice in the Runtime Host Session-core migration tracked by #1167.
#1358 has merged, and this branch is rebased onto the latest main with only the Interaction slice remaining in the diff. Runtime Host remains non-serving in production; this PR does not switch Desktop, TUI, CLI, or Headless entrypoints.
What changes
Canonical Interaction facts
Runtime continuation and status boundary
waiting_for_useracross AgentRun, Turn, child-result, Task Ledger, CLI, and UI projections. Permission-specificwaiting_permissionremains only on tool activity where that narrower meaning is true.Host-owned arbitration and continuity
interaction.queryandinteraction.answeroperations with exact input/output decoding and typed failures.HostInteractionCoordinatorthat owns durable admission, concurrent Client answer arbitration, remembered-permission siblings, Run terminal fences, recovery, continuity refresh, and Interaction residency.Design decisions
Evidence
Coverage exercises real file-backed first-winner races and recovery, exact acknowledgement ownership, concurrent pending settlements, required Run-status durability, remembered permission siblings, stop/terminal fences, combined snapshot preflight, and real multi-Client UDS handoff after the originating Client disconnects. After rebasing onto current main and addressing the review, Core passes 1,194 tests; Storage passes 551 with one existing platform skip; Runtime passes 2,588 with seven existing skips; Runtime Host passes all 190 tests. Workspace typecheck, production build, Biome checks, and independent correctness and maintainability reviews also pass.
Scope
This slice intentionally does not add production surface wiring, complete hosted root-Turn convergence, archive/remove control, Automation/Goal ownership, or the M4/M5 production cutover.
Builds on #1358. Part of #1167. Related to #853.
简体中文
背景
本 PR 建立 Runtime Host Session core 迁移中的 Interaction authority slice,整体由 #1167 跟踪。
#1358 已合并,本分支也已 rebase 到最新 main;当前 diff 只包含 Interaction slice。Runtime Host 仍未进入 production serving,本 PR 不切换 Desktop、TUI、CLI 或 Headless 的任何入口。
改动内容
Canonical Interaction fact
Runtime continuation 与状态边界
waiting_for_user。更窄的waiting_permission只保留在确实表示工具权限等待的 tool activity 边界。Host-owned arbitration 与 continuity
interaction.query与interaction.answeroperation,具有精确 input/output 解码与 typed failure。HostInteractionCoordinator统一持有 durable admission、并发 Client answer arbitration、remembered-permission sibling、Run terminal fence、recovery、continuity refresh 与 Interaction residency。设计决策
验证证据
覆盖范围包括真实文件 Store 上的 first-winner 竞态与 recovery、acknowledgement 精确 ownership、并发 pending settlement、必要 Run-status durability、remembered permission sibling、stop/terminal fence、组合 snapshot preflight,以及发起 Client 断开后的真实多 Client UDS 接管。Rebase 到当前 main 并处理完本轮审查后,Core 1,194 项全部通过;Storage 551 项通过,另有 1 项既有平台 skip;Runtime 2,588 项通过,另有 7 项既有 skip;Runtime Host 190 项全部通过。Workspace typecheck、production build、Biome check,以及独立的正确性和可维护性审查也均通过。
范围
本 slice 明确不加入 production surface wiring、完整 hosted root-Turn convergence、archive/remove control、Automation/Goal ownership,或 M4/M5 production cutover。
构建于 #1358;属于 #1167;关联 #853。