Skip to content

feat(runtime-host): establish interaction authority - #1359

Merged
Astro-Han merged 19 commits into
apache:mainfrom
M4n5ter:feat/runtime-host-interaction-authority
Jul 27, 2026
Merged

feat(runtime-host): establish interaction authority#1359
Astro-Han merged 19 commits into
apache:mainfrom
M4n5ter:feat/runtime-host-interaction-authority

Conversation

@M4n5ter

@M4n5terM4n5ter commented Jul 23, 2026

Copy link
Copy Markdown
Member
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

  • Add one closed Core contract for Permission and Question requests, answers, durable outcomes, and lifecycle closure reasons.
  • Project human-review text into a canonical safe form before durable admission: secret-like material is redacted, control and bidi characters are escaped, byte limits are rechecked, and Question option-label collisions fail closed.
  • Keep that projection at the review/persistence boundary. Original tool input still reaches execution unchanged.
  • Add a dedicated file-backed Interaction Store with lease-bound access, immutable request identity, atomic first-winner outcomes, exact retry comparison, session-indexed pending lookup, and startup validation/recovery.

Runtime continuation and status boundary

  • Add a Run-scoped Runtime authority binding for hosted Permission and Question continuations. Producer settlements do not carry Host-owned identity; Runtime derives it from the bound Run and request.
  • Keep answers authoritative only in Interaction Store. The Run ledger records a bounded answer-accepted audit fact containing the request identity, not a second copy of the answer.
  • Require every hosted answer acknowledgement to match the tracked request kind, Turn, and tool call. A Run resumes only after that exact settlement has started and every other tracked continuation has settled.
  • Treat canonical user waits generically as waiting_for_user across AgentRun, Turn, child-result, Task Ledger, CLI, and UI projections. Permission-specific waiting_permission remains only on tool activity where that narrower meaning is true.
  • On resume, persist the canonical Run header before publishing the Session as running. Best-effort trace/audit failure cannot suppress this required status write or advertise false readiness.

Host-owned arbitration and continuity

  • Add closed interaction.query and interaction.answer operations with exact input/output decoding and typed failures.
  • Add one HostInteractionCoordinator that owns durable admission, concurrent Client answer arbitration, remembered-permission siblings, Run terminal fences, recovery, continuity refresh, and Interaction residency.
  • Return the canonical first winner to equivalent retries and reject conflicting retries. Remembered permission siblings commit durably before one ordered Runtime apply.
  • Include pending Interactions in the canonical Session snapshot. Queue and Interaction candidates share one preflighted snapshot byte bound, so admission cannot publish state that continuity cannot encode.
  • Refresh canonical continuity synchronously after accepted Permission and Question answers, so subscribers observe the settled Interaction together with the resumed Run/Session state.

Design decisions

  • Durable facts are authoritative. Client disconnect and Host restart never make an in-memory callback the source of truth.
  • Host and Runtime own different things. Host owns the Interaction lifecycle and durable winner; Runtime owns only the live execution continuation.
  • Review safety does not mutate execution. Redaction and escaping apply to persisted/projected review text, never to actual tool arguments.
  • Resume is an exact durability boundary. Unknown or mismatched acknowledgements fail closed; canonical Run state advances before Session readiness is published.
  • User wait is generic above the tool boundary. A question must not be relabeled as a permission wait merely because both require human input.
  • The wire remains domain-specific and bounded. This adds no generic Store RPC, callback serialization, durable event replay, or unbounded Interaction stream.

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

  • 为 Permission 与 Question request、answer、durable outcome 和 lifecycle closure reason 建立唯一的封闭 Core 契约。
  • 在 durable admission 前,将 human-review text 投影为 canonical safe form:脱敏疑似 secret,转义控制字符与 bidi 字符,重新检查字节上限,并在 Question option label 投影后发生冲突时 fail closed。
  • 该投影只作用于 review/persistence 边界;原始 tool input 仍会不经修改地进入执行。
  • 增加专用的文件型 Interaction Store,提供 lease-bound access、不可变 request identity、原子 first-winner outcome、精确 retry 比较、按 Session 索引的 pending lookup,以及 startup validation/recovery。

Runtime continuation 与状态边界

  • 增加 Run-scoped Runtime authority binding,承载 hosted Permission 与 Question continuation。Producer settlement 不携带 Host-owned identity;Runtime 根据已绑定 Run 与 request 派生身份。
  • Answer 只以 Interaction Store 为权威事实。Run ledger 仅记录包含 request identity 的有界 answer-accepted audit fact,不复制第二份答案。
  • 每个 hosted answer acknowledgement 都必须匹配已跟踪 request 的 kind、Turn 与 tool call。只有该 request 的精确 settlement 已开始,且其他所有 continuation 均已 settled,Run 才能恢复。
  • 在 AgentRun、Turn、child result、Task Ledger、CLI 与 UI projection 中统一使用 generic waiting_for_user。更窄的 waiting_permission 只保留在确实表示工具权限等待的 tool activity 边界。
  • 恢复时先持久化 canonical Run header,再将 Session 发布为 running。Best-effort trace/audit 写入失败不能抑制该必要状态写入,也不能让系统发布虚假的 ready 状态。

Host-owned arbitration 与 continuity

  • 增加封闭的 interaction.queryinteraction.answer operation,具有精确 input/output 解码与 typed failure。
  • 由一个 HostInteractionCoordinator 统一持有 durable admission、并发 Client answer arbitration、remembered-permission sibling、Run terminal fence、recovery、continuity refresh 与 Interaction residency。
  • 等价 retry 返回同一个 canonical first winner;冲突 retry 失败。Remembered permission sibling 会先全部 durable commit,再执行一次有序的 Runtime apply。
  • Pending Interaction 进入 canonical Session snapshot。Queue 与 Interaction candidate 共用同一个经过 preflight 的 snapshot 字节上限,避免 admission 发布 continuity 无法编码的状态。
  • Permission 与 Question answer 被接纳后同步刷新 canonical continuity,使 subscriber 一并观察到已 settled 的 Interaction 与恢复后的 Run/Session 状态。

设计决策

  • Durable fact 是权威事实。 Client 断开或 Host 重启后,内存 callback 不会成为事实源。
  • Host 与 Runtime 持有不同职责。 Host 持有 Interaction lifecycle 与 durable winner;Runtime 只持有 live execution continuation。
  • Review safety 不修改执行内容。 脱敏与转义只作用于持久化和展示的 review text,不作用于实际 tool argument。
  • Resume 是精确的 durability boundary。 未知或不匹配的 acknowledgement 会 fail closed;canonical Run state 先推进,随后才发布 Session readiness。
  • Tool boundary 之上的 user wait 是通用状态。 Question 不能仅因为同样需要人类输入,就被错误标记为 permission wait。
  • Wire 保持领域专用且有界。 本 PR 不增加 generic Store RPC、callback serialization、durable event replay 或无界 Interaction stream。

验证证据

覆盖范围包括真实文件 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

@M4n5ter
M4n5terforce-pushed the feat/runtime-host-interaction-authority branch 2 times, most recently from c26f508 to 8dad88cCompareJuly 23, 2026 03:24

@Astro-HanAstro-Han 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.

No P0 or P1 found in the current Interaction slice. Two P2 issues remain:

  • [P2] projectToolReview() in packages/core/src/interaction-permission-review.ts accepts only a fixed set of tool names. Runtime already has valid promptable tools outside that set, including agent_spawn, agent_swarm, and MCP proxy tools. When hosted execution routes one of those permission requests, projection throws and HostInteractionCoordinator returns invalid_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. CanonicalSessionProjection calls 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.

@M4n5ter
M4n5terforce-pushed the feat/runtime-host-interaction-authority branch 4 times, most recently from 7f3946a to cc3f943CompareJuly 26, 2026 06:17
@M4n5ter
M4n5ter marked this pull request as ready for review July 26, 2026 06:18

@Astro-HanAstro-Han 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.

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_permission value on reads.AGENT_RUN_STATUSES replaces the persisted waiting_permission value with waiting_for_user, while decodeAgentRunHeader() accepts only the new set. Existing runs written by released versions are therefore dropped by listSessionRuns() 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 key suffix logic checks only the immediately preceding segment, so awsSecretAccessKey, secretAccessKey, and AWS_SECRET_ACCESS_KEY retain their values. Generic tool review then persists and exposes those values. Handle the standard secret_access_key shape 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 calling backend.stop(), and the abandoned-consumer cleanup awaits it before abortController.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_CHARACTER omits 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 creates interactions/<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 redirect request.json or outcome.json outside 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_flight representation. 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 and published flag, 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.

@M4n5ter

M4n5ter commented Jul 26, 2026

Copy link
Copy Markdown
MemberAuthor
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.

  1. Legacy waiting_permission reads

    • Trigger: a workspace contains an AgentRun header written by a released version with status: "waiting_permission".
    • Handling: reads now accept and normalize that value to waiting_for_user; new writes emit only the canonical value. Recovery coverage uses an old on-disk header and verifies that reading does not rewrite its bytes.
  2. AWS Secret Access Key redaction

    • Trigger: review input contains awsSecretAccessKey, secretAccessKey, AWS_SECRET_ACCESS_KEY, aws configure set aws_secret_access_key VALUE, or --secret-access-key VALUE.
    • Handling: the central secret-key classifier and bounded command-token projection redact those standard forms across JSON, assignment, Bash, generic tool, and MCP reviews. The original tool arguments remain unchanged for execution.
    • Boundary: review redaction remains a bounded best-effort safety layer, not a general shell parser or a claim that every possible secret syntax is recognized.
  3. Backend cleanup after Interaction closure failure

    • Trigger: durable Interaction close, continuity refresh, or settlement fails while the backend is still running; the same backend may also be stopped concurrently by explicit stop and iterator cleanup.
    • Handling: durable close and backend cleanup are now independent cleanup attempts. ActiveSession owns one single-flight stop operation shared by explicit stop and AiSdkFlow; concurrent callers observe the same first mode/result, failures are aggregated, and a later retry is allowed after the attempt settles. Abandoned consumers also abort without waiting for close to succeed.
    • Boundary: if the backend's own stop promise never settles, process-level bounded drain and fail-stop containment still belongs to the Host shutdown foundation rather than this Interaction slice.
  4. Missing bidi controls

    • Trigger: review text or Question labels contain U+061C, U+200E, or U+200F, including labels that become visually identical after safe projection.
    • Handling: the review projector now visibly escapes the complete bidi-control set used by this boundary, and post-projection option-label collisions fail closed.
    • Boundary: this is bidi/control-character safety, not a general Unicode-confusable defense.
  5. Canonical Interaction locator symlinks and replacement races

    • Trigger: a locator directory or canonical document is a pre-existing symlink, or its identity changes during establish, commit, recovery, or read.
    • Handling: locator creation is non-recursive, canonical opens use no-follow flags, directory and file identities are checked by device/inode, and one bound epoch is retained through stabilization and read. Commit uses separate bound epochs before and after publication. An ENOENT after binding now validates the original bindings and fails closed instead of being reported as ordinary absence. Tests verify that valid external JSON targets remain untouched.
    • Boundary: Node path APIs cannot provide the same mathematical guarantee as a complete dirfd/openat/linkat design against a continuously malicious same-UID renamer. Within the single-writer owner-lease model, replacement is detected and rejected; POSIX is the primary guarantee and Windows remains best effort.
  6. Queue worst-case snapshot preflight

    • Trigger: queued steering fits the current snapshot, but its normal transition to the larger in_flight representation would exceed the canonical snapshot byte limit after an Interaction admission.
    • Handling: queue admission and Interaction admission now use one shared worst-case queue projection, including revision growth and queued-to-in-flight expansion. Exact boundary tests cover the transition.
  7. Publication after close or settlement begins

    • Trigger: stop/terminal closure races with Permission or Question request publication.
    • Handling: close and settlement synchronously linearize the continuation state before asynchronous work; publication rechecks Run closure, settlement, prior publication, and exact pending request identity. Once closure or settlement has begun, late publication is rejected.
  8. One durable authority for permission answers

    • Trigger: a hosted permission answer is durably committed to Interaction Store, but a second full answer in the Runtime ledger is missing or diverges; this also occurs for auto/remembered decisions with no ledger permission request.
    • Handling: the hosted Runtime ledger now records only an identity acknowledgement. Public projection joins that identity with the canonical Interaction outcome and fails closed on missing, failed, or mismatched reads. Active-run projection overlays the same canonical outcome. Branch/revision copies materialize a bounded child-owned permission decision, including toolName, so the copied conversation remains self-contained after the source Interaction reader is unavailable; coverage includes no ledger request, no function call, public reads, and the next backend turn.
    • Boundary: the embedded/legacy path retains its existing decision event. A conversation copy intentionally owns derived history in its new Session; it does not create a competing authority for the original Session's Interaction outcome.

Verification remains covered by the affected suites and independent review.

简体中文

@Astro-Han 感谢细致审查。八项问题都已复现,并在当前 head 中完成处理。下面分别说明触发场景、处理方式和仍然保留的边界。

  1. 兼容读取旧 waiting_permission

    • 触发场景: workspace 中存在已发布版本写入的 AgentRun header,其状态仍为 waiting_permission
    • 处理: 读取时接纳并规范化为 waiting_for_user;新写入只产生 canonical 状态。Recovery 测试直接使用旧格式磁盘 header,并确认读取不会改写原始字节。
  2. AWS Secret Access Key 脱敏

    • 触发场景: review input 含有 awsSecretAccessKeysecretAccessKeyAWS_SECRET_ACCESS_KEYaws configure set aws_secret_access_key VALUE--secret-access-key VALUE
    • 处理: 由中心 secret-key classifier 与有界 command-token projection 统一脱敏这些标准形式,覆盖 JSON、assignment、Bash、generic tool 与 MCP review。实际执行仍使用未经修改的原始 tool argument。
    • 边界: review 脱敏仍是有界的 best-effort 安全层,不是通用 shell parser,也不承诺识别所有可能的 secret 写法。
  3. Interaction close 失败后的 Backend 清理

    • 触发场景: durable Interaction close、continuity refresh 或 settlement 失败时 Backend 仍在运行;explicit stop 与 iterator cleanup 也可能并发停止同一个 Backend。
    • 处理: durable close 与 Backend cleanup 现在作为彼此独立的清理尝试执行。ActiveSession 持有唯一的 single-flight stop,供 explicit stop 与 AiSdkFlow 共用;并发调用者观察同一个首次 mode/result,失败会被聚合,attempt settled 后仍允许后续 retry。Consumer abandon 也不会再等待 close 成功才 abort。
    • 边界: 如果 Backend 自身的 stop promise 永不结束,进程级 bounded drain 与 fail-stop containment 仍属于 Host shutdown foundation,不在本 Interaction slice 内重复实现。
  4. 缺失的 bidi control

    • 触发场景: review text 或 Question label 包含 U+061C、U+200E、U+200F,包括 safe projection 后产生视觉碰撞的 option label。
    • 处理: review projector 会显式转义该边界采用的完整 bidi-control 集合;投影后发生 option label 碰撞时 fail closed。
    • 边界: 这里解决的是 bidi/control character 安全,不是通用 Unicode confusable 防御。
  5. Canonical Interaction locator 的 symlink 与替换竞态

    • 触发场景: locator directory 或 canonical document 是预先存在的 symlink,或其身份在 establish、commit、recovery、read 期间发生变化。
    • 处理: locator 使用非递归创建;canonical open 使用 no-follow flag;目录与文件通过 device/inode 复核身份;stabilize 与 read 在同一个 bound epoch 内完成。Commit 在 publication 前后分别建立独立 bound epoch。Binding 后遇到 ENOENT 也会先验证原 bindings,按不稳定 locator fail closed,而不再误报成普通不存在。测试确认即使外部目标包含合法 JSON,也不会被读取或修改。
    • 边界: 面对持续恶意的同 UID rename,Node path API 无法提供完整 dirfd/openat/linkat 设计的数学保证。在 single-writer owner lease 的威胁模型内,替换会被检测并拒绝;POSIX 是主要保证,Windows 为 best effort。
  6. Queue 最坏状态的 snapshot preflight

    • 触发场景: queued steering 在当前 snapshot 中可以编码,但正常转为更大的 in_flight 形态后,与新 Interaction 合并会超过 canonical snapshot 字节上限。
    • 处理: Queue admission 与 Interaction admission 现在共用同一个 worst-case queue projection,包含 revision 增长以及 queued 到 in-flight 的展开;精确边界测试覆盖该转换。
  7. Close 或 settlement 开始后的迟到 publication

    • 触发场景: stop/terminal closure 与 Permission 或 Question request publication 竞态。
    • 处理: close 与 settlement 会在任何异步工作前同步线性化 continuation 状态;publication 会重新检查 Run closure、settlement、既有 publication 和精确 pending request identity。一旦 close 或 settlement 已开始,迟到 publication 会被拒绝。
  8. Permission answer 只有一个 durable authority

    • 触发场景: hosted permission answer 已提交到 Interaction Store,但 Runtime ledger 中第二份完整答案缺失或发生分歧;auto/remembered decision 还可能根本没有 ledger permission request。
    • 处理: Hosted Runtime ledger 现在只记录 identity acknowledgement。公开投影将该 identity 与 canonical Interaction outcome join;读取缺失、失败或身份不匹配时 fail closed。Active Run projection 同样叠加 canonical outcome。Branch/revision copy 会物化一份有界、由新 Session 自己持有的 permission decision,并包含 toolName;因此 source Interaction reader 不可用后,复制出的 conversation 仍然自包含。测试覆盖了无 ledger request、无 function call、公开读取和下一轮 Backend context。
    • 边界: Embedded/legacy 路径保留其既有 decision event。Conversation copy 持有的是新 Session 的派生历史,不会成为原 Session Interaction outcome 的竞争权威。

最终验证: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、git diff --check,以及独立正确性与可维护性复审均通过。

@M4n5ter
M4n5terforce-pushed the feat/runtime-host-interaction-authority branch from e052386 to f2ce184CompareJuly 26, 2026 13:11
@Astro-Han

Copy link
Copy Markdown
Contributor

Reviewing current head f2ce1841 after rechecking both reachability and severity. The branch is clean and CI is green. I found one P1. The P2 and P3 items below are non-blocking under the current merge policy.

P1

  • Migrate legacy persisted subagent tool results, not only AgentRun headers. Released versions could persist a subagent tool result with status: "waiting_permission". ToolResultContentSchema now accepts only waiting_for_user, while normalizeToolResultContentForRead() only migrates shell results. An ordinary Session read therefore replaces the old record with a corruption note, and strict recovery throws. Since Host recovery reads every recoverable Session, one old record can also abort recovery preparation for the rest. The earlier AgentRun-header migration does not cover this separate persisted shape. Normalize the exact legacy subagent result in the shared read-compatibility layer used by both normal reads and strict recovery, keep new writes canonical, and add a fixture using the old JSONL bytes that verifies reads do not rewrite the file.

P2

  • Delete Session-owned Interaction records with the Session.SessionManager.deleteSession() removes the Runtime and Session Store directory, but InteractionStore has no Session-scoped removal path. Resolved Interaction records remain under the root-level interactions/ directory and can still be returned by a query using the deleted sessionId. This also conflicts with the desktop confirmation that the Session and all messages are permanently removed. Coordinate Interaction cleanup with Session deletion, make post-delete queries return not_found, and cover both the same process and a restart. I am rating this P2 because the production Host composition still registers only FakeBackend; if real hosted permission interactions are enabled in this PR, it becomes P1.

  • Make stop and answer share one fence.session.stop commits its fence and releases the Session lane before Runtime stop closes the active Interaction. An already queued interaction.answer can then commit and resume the tool while the stop is in flight. ToolRuntime proceeds from the answer acknowledgement to the durable attempt and tool implementation without a fresh abort check. When stop wins, close the exact Interaction Run in the same admission lane, or make the queued answer observe the stopping fence. Add a deterministic blocked-gate test for stop followed by answer.

  • Do not send a legacy permission acknowledgement through hosted authority on a deny rule. The hosted auto-deny path appends permission_decision and pushes permission_decision_ack, but interactionResumeAllowed() treats every such acknowledgement as an invariant violation for hosted bindings. A real hosted ToolRuntime can therefore fail instead of returning the synthetic denied tool result. Either skip the legacy acknowledgement for hosted decisions or accept it only when no hosted continuation owns the request. Cover a hosted deny that returns the denied result and lets the Run continue.

  • Unify terminal and stop cleanup when backend stop fails. There are three related failure paths: cleanup can wait forever for runnerResult when backend.stop() rejects without ending send(); terminal cleanup and concurrent stop can close the same Interaction with conflicting reasons; and a failed stop operation remains cached and can be reused by a later Run generation. Use one terminal/stop claim, scope stop operations to the Run generation, and force disposal or invalidation after a failed stop. Tests should include a backend whose stop rejects while send remains blocked.

  • Redact AWS secret assignments across POSIX line continuations. A valid shell form such as aws configure set aws_secret_access_key \ followed by a newline and the value is parsed by the shell as one command, but the current redactor masks only the first line and leaves the value visible in the durable review projection. The assignment form has the same gap. Make the command-token grammar consume backslash-newline continuations and cover both direct redaction and Interaction projection.

  • Bound Interaction history work after restart and during Session projection. The pending index fixes the earlier cross-Session listPending() scan, but two linear paths remain. InteractionStore recovery opens and stabilizes every retained locator, including resolved records, and RuntimeReadModel collects all historical permission request IDs and issues an unbounded Promise.all of point reads for each view. This makes startup and repeated Session reads grow with retained history. Add a checkpoint or resolved-record collection strategy for recovery, and use batched or cached outcome reads for projection. A large resolved-history test should enforce the bound.

P3

  • The locator hardening still uses path-based open and link operations after binding. A same-UID process that swaps the parent directory during the operation may still win a narrow race. This is defense in depth rather than a privilege boundary, so P3 is appropriate.
  • Hosted auto-review outcomes drop the rule rationale before the user-visible decision text is built. Preserve a bounded rationale if that explanation is intended to be part of the product contract; otherwise document the privacy choice.
  • toolName is length-bounded but does not receive the bidi-control projection applied to review text, and the desktop dialog renders it directly. External MCP names are already sanitized, so this is limited to trusted custom tools, but the display boundary should still escape controls.
  • ARCHITECTURE.md still describes the Runtime event log as canonical for permission decisions. The implementation now makes InteractionStore canonical and keeps only an identity acknowledgement in the ledger. Update the ownership statement so future changes do not restore two authorities.

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.

@M4n5ter
M4n5ter marked this pull request as draft July 26, 2026 14:46
@M4n5ter
M4n5terforce-pushed the feat/runtime-host-interaction-authority branch from f2ce184 to 91d9085CompareJuly 26, 2026 18:34
@M4n5ter

M4n5ter commented Jul 26, 2026

Copy link
Copy Markdown
MemberAuthor
English

@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

  • Persisted compatibility: released waiting_permission AgentRun headers and subagent tool results are normalized to waiting_for_user in the shared read-compatibility layer used by ordinary reads and strict recovery. New writes remain canonical, and compatibility reads do not rewrite existing files.
  • Stop, answer, and terminal arbitration: the Session admission lane now owns the queue fence, exact Interaction closure, and stop decision. Runtime execution claims attach to the exact Run before activation, so queued answers and late startup cannot cross a winning stop fence. During terminal handoff, only an exact, unbound, durably settled closure-only claim may be reclaimed; bound or unsettled Runs, live continuations, and durable pending Interactions still fail closed.
  • Generation-scoped cleanup: backend build, stop, and disposal are single-flight per generation. A failed stop quarantines that generation instead of leaking it into a later Run, and stop observes the canonical Run finalization result rather than reporting premature success.
  • One hosted answer authority:InteractionStore owns hosted Permission and Question outcomes. The Runtime ledger keeps bounded identity/audit facts, and public projection joins them with the canonical outcome. Hosted auto-deny no longer emits the legacy acknowledgement that conflicts with this authority.
  • Review safety: standard AWS secret assignment and CLI forms, including POSIX backslash-newline continuations, are redacted only at the durable review/display boundary. Bidi controls are made visible and projected Question-label collisions fail closed. Original tool input remains unchanged for execution.
  • Ownership documentation: the architecture text now reflects the Interaction Store authority and the Runtime ledger's audit role.

Kept in follow-up slices

  • Session-owned Interaction deletion belongs with the Session lifecycle fence.
  • Resolved-history collection/checkpointing and bounded historical projection reads belong with retention and read-model work.
  • A backend whose own stop or disposal promise never settles remains the responsibility of the process-level bounded-drain and fail-stop shutdown foundation.
  • Production surface activation and the remaining same-UID path-substitution defense stay outside this non-serving slice.

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 已处理

  • 持久化兼容: 已发布版本写入的 waiting_permission AgentRun header 与 subagent tool result,会在普通读取和严格恢复共用的兼容层规范化为 waiting_for_user。新写入仍保持 canonical,兼容读取也不会改写既有文件。
  • Stop、answer 与 terminal 仲裁: Session admission lane 统一持有 queue fence、精确 Interaction closure 与 stop 决策。Runtime execution claim 会在 activation 前绑定精确 Run,因此排队中的 answer 与迟到的 startup 都不能越过已经获胜的 stop fence。Terminal handoff 只允许回收精确、未绑定且 durable closure 已完成的 closure-only claim;已绑定或尚未 settled 的 Run、live continuation 与 durable pending Interaction 仍然 fail closed。
  • 按 generation 隔离清理: Backend build、stop 与 disposal 在每个 generation 内 single-flight。Stop 失败会 quarantine 当前 generation,不会泄漏到后续 Run;stop 也会观察 canonical Run finalization 结果,而不是过早报告成功。
  • Hosted answer 只有一个权威: Hosted Permission 与 Question outcome 以 InteractionStore 为权威。Runtime ledger 只保留有界 identity/audit fact,公开投影再与 canonical outcome join。Hosted auto-deny 不再产生会与该权威冲突的 legacy acknowledgement。
  • Review safety: 标准 AWS secret assignment 与 CLI 形式,包括 POSIX 反斜杠换行,只在 durable review/display 边界脱敏;bidi control 会被显式展示,Question label 投影后发生冲突时 fail closed。实际执行始终使用未经修改的原始 tool input。
  • 权威说明: 架构文档现已明确 Interaction Store 的权威地位,以及 Runtime ledger 的 audit 职责。

保留在后续 slice

  • Session 拥有的 Interaction 删除应与 Session lifecycle fence 一起处理。
  • Resolved history collection/checkpoint 与有界历史 projection read 属于 retention/read-model 工作。
  • Backend 自身的 stop 或 disposal promise 永不结束时,仍由进程级 bounded drain 与 fail-stop shutdown foundation 处理。
  • Production surface activation 与剩余的同 UID path substitution 防御不属于这个 non-serving slice。

最终实现已通过受影响的完整测试、GitHub checks、Biome 与独立正确性复审。PR 现已重新进入 Ready for review。

@M4n5ter
M4n5ter marked this pull request as ready for review July 27, 2026 05:14

@Astro-HanAstro-Han 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.

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 production

    redactSecrets() 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 returns shell_unsafe before 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:

    Inputmain29b17c9
    12KB0.5ms39ms
    24KB0.1ms161ms
    48KB0.2ms648ms
    96KB0.4ms2.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 remains running.

  • Hosted questions still have a legacy settlement path below SessionManager.RuntimeKernel, AiSdkBackend, FakeBackend, and ToolRuntime can 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 AskUserQuestion calls in one Turn, answering the first leaves one question pending but projects the Run as running. 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 production

    redactSecrets() 返回:

    # " 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 重新扫描剩余输入。本机结果:

    输入main29b17c9
    12KB0.5ms39ms
    24KB0.1ms161ms
    48KB0.2ms648ms
    96KB0.4ms2.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 路径。RuntimeKernelAiSdkBackendFakeBackendToolRuntime 可以绕过捕获的 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

Copy link
Copy Markdown
MemberAuthor
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 __proto__ property.

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 __proto__ 属性也不会丢失。

本 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 均已通过。

@M4n5ter
M4n5ter requested a review from Astro-HanJuly 27, 2026 08:12

@Astro-HanAstro-Han 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.

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.

@Astro-Han
Astro-Han merged commit 0e80fe1 into apache:mainJul 27, 2026
3 checks passed
@M4n5ter
M4n5ter deleted the feat/runtime-host-interaction-authority branch July 27, 2026 08:16
jackwener added a commit that referenced this pull request Jul 27, 2026
`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.
jackwener added a commit that referenced this pull request Jul 27, 2026
…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.
Astro-Han added a commit that referenced this pull request Jul 30, 2026
… 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).
Astro-Han added a commit that referenced this pull request Jul 30, 2026
… 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).
Astro-Han added a commit that referenced this pull request Aug 4, 2026
… 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
Sign up for freeto 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.

2 participants

@M4n5ter@Astro-Han