fix(managed-agent): child-wait wire + contract — Java predicate pin, design-doc truth pass

Review round-1 Suggestions (Java/docs close-out):

- java (R1-15): agentWaitWithUnknownOutcomeIsNotReady — an await_agent
  recovery carrying any unknown outcome is neither continuation- nor
  cancellation-ready; witness W6 drops the all-known conjunct and the
  pin REDs (restored byte-identical, suite 63/63).
- docs (R1-16): the broken nested list item in the EN design doc's
  rationale bullet re-joined into one paragraph.
- docs (R1-31 ×5, both languages, synchronized): §1 names the one
  spreading constructor and why it is safe plus the absent-when-null
  encode; §2 records turn-required and the boundary validation; §4
  records runtimeSessionId=hostedRuntimeSessionId(promptId) and its
  inertness; §5 defines the gap fill (inputs, three answer shapes,
  exactly-once discipline, per-family wording); risks now point the
  wire-drift mitigation at the predicate-level tests; the validation
  plan attributes coverage to the layers that actually hold it
  (classification-only recovery, turn arm, routes, dedicated suite)
  and marks close/delete rig-facing with acceptance criterion 3 to
  match.
- tests (R1-58): the carried-group case documents it pins the
  constructor's contract rather than a production transition.
This commit is contained in:
Shaojin Wen 2026-10-10 11:19:45 +08:00
parent 41660056c2
commit 91747afb1f
4 changed files with 129 additions and 41 deletions

View file

@ -109,10 +109,17 @@ results_ready | turn_settled`. Durable waits exist for approvals
`HarnessCheckpointV1.agentWait: HarnessAgentWaitGroup | null`, added to
`ROOT_KEYS` and the parser. Every existing constructor sets `agentWait`
explicitly (carried over only where a wait may persist across an adjacent
checkpoint; otherwise `null`), so the group is never silently propagated.
(Earlier drafts also carried `partIndex`/`ordinal`/`inputDigest`; the round
a resume re-derives from the journal itself, so the record keeps only
fields with a live consumer — the schema was trimmed before merge, and
checkpoint; otherwise `null`) **except**
`createHookStoppedRuntimeHarnessCheckpoint`, which spreads `previous` —
sound because its `results_ready` precondition cannot carry a group
(`assertPhaseShape` fails any `results_ready` that does), so the spread can
only ever see `null`. The encode omits the key whenever no group rides the
checkpoint: an absent key parses as `null` on both reader generations, so a
pre-`agentWait` daemon can still open every non-agent checkpoint, and only
checkpoints that actually carry the group become unreadable to an older
reader. (Earlier drafts also carried `partIndex`/`ordinal`/`inputDigest`;
the round a resume re-derives from the journal itself, so the record keeps
only fields with a live consumer — the schema was trimmed before merge, and
since the group is new in this PR no stored bytes can carry them.)
- New constructor `createAwaitAgentHarnessCheckpoint`: phase `await_agent`,
@ -128,9 +135,13 @@ results_ready | turn_settled`. Durable waits exist for approvals
- `commitAwaitAgent(runs, { turnId, promptId })` commits the wait with the
same transaction discipline as `commitAwaitRuntimeBatch` (one
`commitHarnessCheckpoint` under `HARNESS_DURABLE_WAIT_BOUNDARY`; turn-binding
rules identical). Replay-safe: restating the identical run set against an
existing `await_agent` wait answers the same boundary; a conflicting set
conflicts, never rewrites.
rules identical). `turn` is required — the absent branch would skip both
the binding guard and the activation adoption. Boundary-side validation
mirrors the parser: duplicate `childRunId`/`functionCallId` and
already-`consumed` runs are refused at the call instead of becoming a
durable-blocked checkpoint at the next read. Replay-safe: restating the
identical run set against an existing `await_agent` wait answers the same
boundary; a conflicting set conflicts, never rewrites.
- `resolveAwaitAgent(childRunId)` marks one run `consumed`. It does **not**
remove the run; while runs remain the phase stays `await_agent`, and with
every run consumed the continuation advances to `model_output_committed`
@ -183,7 +194,11 @@ status: { state: 'executing' } }` when waiting and
`{ ..., status: { state: 'settled' } }` when folded. Outcomes are always
`known`: the relay ledger makes a waiting child an observable fact, never an
unknown outcome, so the coordinator's `managed_runtime_recovery_blocked`
gate cannot fire on this phase.
gate cannot fire on this phase. Each entry also carries
`runtimeSessionId` — required on the wire for every execution entry — filled
with `hostedRuntimeSessionId(promptId)`: this phase holds no Runtime
session, the hosted runtime-session identity of the parked Turn rides the
field, and no consumer reads it today.
- **The carried group closes the second-death window:** the branch also
classifies `model_output_committed` checkpoints that still carry an
`agentWait` group — by the phase-shape invariant that group is fully
@ -212,6 +227,26 @@ issues for `results_ready`) gains an `await_agent` arm:
both are replay-safe. After folding, the route re-projects the journal so
the resume request (`resumeFromToolResults`) carries the fold's own
tool result into the next model round.
- **The gap fill (`fillParkedRoundAgentGaps`):** a foreground batch parks
with some later calls of its last assistant round never reached by the
dead loop — the durable wait names only the admitted ones. The fill takes
the checkpoint's `agentWait.runs` (fresh from the run authorization: the
round is the one the wait's `modelMessageId`s name, never whatever
assistant record came later), the Turn's assistant record, the journaled
`tool_result` id set, and the session's child ledger; it answers each
remaining function call so the resume request is well-formed. Three
answer shapes, exactly once each behind the journaled set (which gates
the fold only — the replay-safe marks still run): a call with no ledger
record fills the never-admitted answer (per-family wording: the continue
route and the interrupted-turn funnel name the interruption, the cancel
route names the cancellation, so the durable journal never asserts a
cause that never happened); a call whose child was admitted in the
admit→commit gap drives to its own terminal through the same poll the
live wait ran and folds what really happened (a background-completion
record folds the live arm's started receipt instead of being polled);
and a leftover from the wait's own carried group is not a gap at all —
its marks belong to the wait arm. The same fill runs on the cancel
route and from the interrupted-turn funnel's settlement.
- Cancellation (`CANCELLING` takeover + `cancelManagedRuntime`) settles an
`await_agent` wait deterministically: each unconsumed run gets a cancelled
`tool_result` fold ("The turn was cancelled before the child agent
@ -268,9 +303,9 @@ DELETED` end-to-end after a wedge at interruption point 1.
group is always fully consumed), so the worst case of a second crash is
an idempotent no-op re-entry and a re-driven model round.
- **Waiting runs report `executing`, never `unknown`:** the coordinator fails
a Turn on any `unknown` outcome. A waiting child is fully observed (ledger
- relay), so `known`/`executing` is the honest encoding and needs no new
wire vocabulary.
a Turn on any `unknown` outcome. A waiting child is fully observed through
the ledger-relay pair, so `known`/`executing` is the honest encoding and
needs no new wire vocabulary.
- **Crashes between `admit` and `commitAwaitAgent` stay bounded and honest:**
pre-wait crashes keep the `model_start` verdict outright; crashes past a
consumed wait leave the carried group, and the resume's ledger-honest fill
@ -303,7 +338,11 @@ DELETED` end-to-end after a wedge at interruption point 1.
branch. Mitigation: `grep` audit of the phase strings across
`packages/sdk-java` (done: `HostedHarnessClient`,
`HarnessRuntimeRecovery`, `HarnessCoordinator`, `ToolPublicationStore` —
dispositions above) plus coordinator tests pinning the new predicate.
dispositions above) plus the predicate-level tests pinning the new
behavior (`HostedHarnessClientTest`'s `await_agent` wire round-trip and
its unknown-outcome negative) — `HarnessCoordinatorTest` stubs the
recovery report at every site, so the guarantee lives with the
predicates, not the coordinator suite.
- **Duplicate fold:** the re-fold must survive a crash after committing
`tool_result` but before `resolveAwaitAgent`. Mitigation: the journaled-set
dedupe is the same mechanism `settleParkedTurnCancelled` already relies on,
@ -317,15 +356,26 @@ DELETED` end-to-end after a wedge at interruption point 1.
- **Core (unit):** checkpoint parser round-trip for `await_agent` +
`agentWait`, unknown-phase and unknown-field refusals; constructor guard
(durable-wait exclusivity); `commitAwaitAgent` replay-safety and
turn-binding rules; `resolveAwaitAgent` marking.
- **Recovery (unit, real local authority):** the `await_agent` branch —
point-1 classification (waiting runs reported `executing`, report phase
`await_agent`), point-2 fold exactly-once (pre-journaled `tool_result`
never rewritten), failed/cancelled folds, mixed batches, passive
classification.
(durable-wait exclusivity); `commitAwaitAgent` replay-safety, turn-binding
rules and boundary validation (duplicate ids, already-consumed runs, empty
batch); `resolveAwaitAgent` marking plus takeover-activation adoption on
the advancing fold. The encode's mixed-version shape is pinned too:
`agentWait` is absent whenever no group rides the checkpoint and the parse
still reads `null`.
- **Recovery (unit, real local authority):** classification only, matching
§4's own charter — the `await_agent` branch reports point-1 runs
`executing`, the carried all-consumed group reports `settled`, both load
shapes classify identically, and each execution entry's
`runtimeSessionId` is `hostedRuntimeSessionId(promptId)`. No fold coverage
belongs here: §4 bars this layer from folding.
- **Turn arm (unit):** admission commits the wait (ordering: admit then
checkpoint), every terminal fold resolves.
checkpoint), every terminal fold resolves, and the resume/consumption
marks run outside the journaled gate.
- **Routes (integration, real daemon):** the wedge is minted through the
production admission (execute → admit → `commitAwaitAgent`); the continue
route admits the parked wait (the gate's reversion answers 409), and the
cancel route settles the takeover with the abandoned fold journaled and
the checkpoint advanced past the wait exactly once.
- **Dedicated end-to-end suite** (new, `hosted-child-wait-recovery`-style,
real local authority + restarted authority over the same store):
1. Wedge at interruption point 1 → restart → takeover load attaches →
@ -334,16 +384,26 @@ DELETED` end-to-end after a wedge at interruption point 1.
→ restart → takeover folds the committed answer into the original tool
call exactly once (journal counted) → parent Turn completes.
3. Wedge at point 1 → close parent → (previously stuck `CLOSING`) reaches
`CLOSED`, then `DELETE` succeeds.
`CLOSED`, then `DELETE` succeeds. _Rig-facing:_ no suite drives a real
close/delete today; this and acceptance criterion 3 are the post-merge
real-stack rig's probes, not shipped evidence.
4. Cancellation takeover of the wedged Turn settles it cancelled and the
child keeps its ledger line.
child keeps its ledger line (covered at the route level above and at
the library level here).
5. Second-death and orphan shapes: a crash past the last fold classifies
the carried group, the gap fill answers never-reached sibling calls
(never-admitted wording by family), an admitted orphan folds its own
outcome (completed, failed/cancelled, background receipt, oversized
with the truncation marker), and the interrupted-turn funnel settles
the same way the takeover cancel does.
- **Java:** `HostedHarnessClientTest` accepts the new phase — the wire
round-trip plus the predicate-level negative (`await_agent` with empty
executions is neither continuation- nor cancellation-ready; the wire
parser independently floors executions at 1–1024). The coordinator's
recovery-admission mechanism is phase-generic by its existing mock-driven
suites (`HarnessCoordinatorTest` mocks the predicates); the phase-specific
behavior lives in the predicates, which the wire tests pin.
round-trip plus the predicate-level negatives (`await_agent` with empty
executions or any `unknown` outcome is neither continuation- nor
cancellation-ready; the wire parser independently floors executions at
1–1024). The coordinator's recovery-admission mechanism is phase-generic
by its existing mock-driven suites (`HarnessCoordinatorTest` mocks the
predicates); the phase-specific behavior lives in the predicates, which
the wire tests pin.
- **Mutation witnesses:** each new mechanism gets a witness proven RED
against the unmutated expectation (e.g. drop the `commitAwaitAgent` call →
point-1 probe stays wedged; drop the journaled dedupe → point-2 probe
@ -358,8 +418,10 @@ DELETED` end-to-end after a wedge at interruption point 1.
the wait and its later Turns complete.
2. Probe 2 passes: parent recovered after interruption point 2 folds the
committed child answer into the original tool call exactly once.
3. Probe 3 passes: close/delete of a parent wedged at point 1 reaches
`CLOSED` / `DELETED`.
3. Probe 3 (rig-facing): close/delete of a parent wedged at point 1 reaches
`CLOSED` / `DELETED` — a post-merge real-stack probe; this PR's shipped
evidence is the companion section above, and the criterion is met when
the rig reports green.
4. The rig's two TypeScript foreground-recovery expectations turn green.
5. No regression in the existing recovery, coordinator, publication, and
wire-validation suites.

View file

@ -59,7 +59,7 @@ Hosted Workspace Turn 里的**前台** child-agent 调用会跳过 Runtime reser
}
```
`HarnessCheckpointV1.agentWait: HarnessAgentWaitGroup | null`,加入 `ROOT_KEYS` 与解析器。所有既有构造器显式设置 `agentWait`(仅在相邻 checkpoint 间允许等待存续时结转,否则为 `null`)——该组绝不被静默传播。(早期草案还携带 `partIndex`/`ordinal`/`inputDigest`;resume 的完整回合是从 journal 自身重推的,因此记录只保留有活消费者的字段——schema 在合并前已收紧;该组由本 PR 新建,任何已存字节都不可能携带它们。)
`HarnessCheckpointV1.agentWait: HarnessAgentWaitGroup | null`,加入 `ROOT_KEYS` 与解析器。所有既有构造器显式设置 `agentWait`(仅在相邻 checkpoint 间允许等待存续时结转,否则为 `null`)——**唯一例外**是 `createHookStoppedRuntimeHarnessCheckpoint`:它通过展开 `previous` 结转——这是安全的,因为其 `results_ready` 前置相位不可能携带该组(`assertPhaseShape` 对携带组的 `results_ready` 判失败),展开只会看到 `null`——该组绝不被静默传播。编码时凡不携带组的 checkpoint 一律省略该键:两台解析器都把「缺键」读作 `null`,因此旧版 daemon 仍能打开全部不含组的 checkpoint,只有真正携带组的 checkpoint 才对旧读者不可读。(早期草案还携带 `partIndex`/`ordinal`/`inputDigest`;resume 的完整回合是从 journal 自身重推的,因此记录只保留有活消费者的字段——schema 在合并前已收紧;该组由本 PR 新建,任何已存字节都不可能携带它们。)
- 新构造器 `createAwaitAgentHarnessCheckpoint`:相位 `await_agent`,`approval: null`,`tools`/`runtime` 从上一 checkpoint 结转,`agentWait` 设置。requested 审批存活或 `await_runtime` 批次存活时拒绝提交(同一时刻只有一个持久等待域——与 `commitDurableWait` vs `commitAwaitRuntimeBatch` 的既有纪律相同)。
@ -67,7 +67,7 @@ Hosted Workspace Turn 里的**前台** child-agent 调用会跳过 Runtime reser
`packages/core/src/managed-runtime/managed-harness-factory.ts`:
- `commitAwaitAgent(runs, { turnId, promptId })` 以与 `commitAwaitRuntimeBatch` 相同的事务纪律提交等待(`HARNESS_DURABLE_WAIT_BOUNDARY` 下单次 `commitHarnessCheckpoint`;turn-binding 规则相同)。重放安全:对已存在的 `await_agent` 等待重述相同 run 集合回答同一边界;冲突集合判冲突,绝不改写。
- `commitAwaitAgent(runs, { turnId, promptId })` 以与 `commitAwaitRuntimeBatch` 相同的事务纪律提交等待(`HARNESS_DURABLE_WAIT_BOUNDARY` 下单次 `commitHarnessCheckpoint`;turn-binding 规则相同)。`turn` 为必传——缺省分支会同时跳过绑定守卫与激活收养。边界侧校验与解析器同规:重复的 `childRunId`/`functionCallId` 与已 `consumed` 的 run 在调用处直接拒绝,而不是在下一次读取时落成 durable-blocked checkpoint。重放安全:对已存在的 `await_agent` 等待重述相同 run 集合回答同一边界;冲突集合判冲突,绝不改写。
- `resolveAwaitAgent(childRunId)` 把一个 run 标记为 `consumed`。它**不**移除 run:仍有 run 时相位保持 `await_agent`,全部 run consumed 后相位推进到 `model_output_committed` 并**携带**全部 consumed 的组——已折叠的结果欠下一轮模型,而携带的组让等待对二次崩溃仍可重入:§4 的分支正好分类这个形状,绝不把已结算的工作读成 `model_start`。
### 3. Turn 臂:admission 时提交,折叠后 resolve(cli)
@ -83,7 +83,7 @@ Hosted Workspace Turn 里的**前台** child-agent 调用会跳过 Runtime reser
- 从 `checkpoint.agentWait` 读出等待的 runs;分类仅由 checkpoint 供证——`consumed` 读作 `settled`,未结算读作 `executing`。relay 台账只被 §5 的 resume 臂轮询,分类器绝不查询。
- **只分类——折叠归 §5**。recovery 同时服务普通 attach 与 takeover 两条路径,但只有路由的 resume 臂持有 Turn 的提交通道、inline 上限断言与消费集合。recovery 在该分支决不提交任何记录。
- **报告仍然挂起的部分(中断点 1)**:未 settled 也未折叠的 run 保持挂起。报告用 `phase: 'await_agent'`;每个 run 映射为一条 execution:等待中 `{ executionCallId: childRunId, functionCallId, toolName, outcome: 'known', status: { state: 'executing' } }`,已折叠 `{ ..., status: { state: 'settled' } }`。outcome 永远 `known`:relay 台账让等待中的 child 是可观察事实,绝不是未知结局——coordinator 的 `managed_runtime_recovery_blocked` 闸在该相位上不可能触发。
- **报告仍然挂起的部分(中断点 1)**:未 settled 也未折叠的 run 保持挂起。报告用 `phase: 'await_agent'`;每个 run 映射为一条 execution:等待中 `{ executionCallId: childRunId, functionCallId, toolName, outcome: 'known', status: { state: 'executing' } }`,已折叠 `{ ..., status: { state: 'settled' } }`。outcome 永远 `known`:relay 台账让等待中的 child 是可观察事实,绝不是未知结局——coordinator 的 `managed_runtime_recovery_blocked` 闸在该相位上不可能触发。每条 execution 还带 `runtimeSessionId`——线路上它是必填——填 `hostedRuntimeSessionId(promptId)`:本相位没有 Runtime session,用停驻 Turn 的 hosted runtime-session 身份填充,目前没有任何消费者读取它。
- **结转组关闭二次死亡窗口**:该分支同样分类仍携带 `agentWait` 组的 `model_output_committed` checkpoint——按相位形状不变量,该组必然全 consumed(欠一轮模型),因此用同一 `await_agent` 报告形状(全部 run `settled`)把 takeover 引入 continue 臂,绝不让已结算的工作落回 `model_start`。
- 被动 load 回答同样的分类但不折叠(无提交权),使 coordinator 在只读 attach 上也能得知真相。
@ -93,6 +93,7 @@ daemon 的 `continueManagedRuntime` 路由(coordinator 现在已对 `results_rea
- 路由承认前的相位闸放宽为三种可续形状:`results_ready`、`await_agent`、以及携带 `agentWait` 组的 `model_output_committed`(即上次折叠刚完成的同一个等待)。
- 该臂以 resume 模式实例化新的 `HostedWorkspaceToolTurn`,执行其 agent-wait 重建(`resumeAgentWaitRuns`):对每个未消费的 run,以新 authority 视图重建的 `children` 跑与存活臂相同的 `awaitChildToolResult` 轮询,把每个终局折叠回原 tool call。**恰好一次靠 journal 而不是进程**:臂从本 Turn 已提交的 `tool_result` id 构建 journaled 集合;重放的 resume 只跳过 commit,`markAccepted` 与 `resolveAwaitAgent` 仍照常(两者本就重放安全)。折叠后路由重新投影 journal,使恢复请求(`resumeFromToolResults`)带上这次折叠产生的 tool_result,进入下一轮模型。
- **缺口填充(`fillParkedRoundAgentGaps`)**:前台批次停车时,其最后一个 assistant 回合里部分靠后的调用是已死循环从未到达的——持久等待只记录已 admitted 的那些。填充的输入是 checkpoint 的 `agentWait.runs`(取自实时 run 授权:回合由等待的 `modelMessageId` 集合指认,绝不取后来才出现的 assistant 记录)、本 Turn 的 assistant 记录、已 journal 的 `tool_result` id 集合、以及会话的 child 台账;它把余下的每个 function call 配上回答,使 resume 请求合法。三种回答形态,各由 journaled 集守护恰好一次(它只闸折叠——重放安全的标记照跑):没有台账记录的调用填「未被 admitted」答案(按族区分措辞:continue 路由与被中断 Turn funnel 措辞为「被中断」,cancel 路由措辞为「被取消」,持久 journal 绝不记录从未发生的原因);在 admit→commit 间隙内已 admitted 的孤儿按与存活等待相同的轮询驱动到自身终局,按真相折叠(后台 delegation 记录则折叠存活臂的 started 回执,而不是被轮询);等待自身结转组里的调用不算缺口——它的标记归等待臂。同一填充也跑在 cancel 路由与被中断 Turn funnel 的结算里。
- 取消(`CANCELLING` takeover + `cancelManagedRuntime`)把 `await_agent` 等待确定性地结算:每个未消费 run 折叠一条 cancelled `tool_result`(「The turn was cancelled before the child agent finished; the child keeps running and its committed result is retained.」——存活臂自己的措辞)并 `resolveAwaitAgent`,由同一 journaled 去重守护;Turn 按 cancelled 结算。被遗弃的 child 不撤销;它的台账行归 relay 所有。
### 6. 线路协议(Java,qwencode + managed-agent-server)
@ -123,21 +124,23 @@ daemon 的 `continueManagedRuntime` 路由(coordinator 现在已对 `results_rea
## 风险
- **线路校验漂移**:若还有其他 Java 消费者按恢复相位集合做模式匹配,`await_agent` 不得静默落入失败分支。缓解:对 `packages/sdk-java` 全量 grep 相位字符串(已做:`HostedHarnessClient`、`HarnessRuntimeRecovery`、`HarnessCoordinator`、`ToolPublicationStore`——处置如上),并以 coordinator 测试钉死新谓词。
- **线路校验漂移**:若还有其他 Java 消费者按恢复相位集合做模式匹配,`await_agent` 不得静默落入失败分支。缓解:对 `packages/sdk-java` 全量 grep 相位字符串(已做:`HostedHarnessClient`、`HarnessRuntimeRecovery`、`HarnessCoordinator`、`ToolPublicationStore`——处置如上),并以**谓词级**测试钉死新行为(`HostedHarnessClientTest` 的 `await_agent` 线路往返与 unknown-outcome 负向)——`HarnessCoordinatorTest` 在每个恢复点都桩掉报告对象,保证在谓词层而不在 coordinator 套件。
- **重复折叠**:重折叠必须挺过「`tool_result` 已提交而 `resolveAwaitAgent` 未及」的崩溃。缓解:journaled 集合去重与 `settleParkedTurnCancelled` 依赖的机制相同,且 acceptance/consumption 记录本身幂等。
- **resume 模式 ToolTurn 漂移**:重建不得重跑 agent 等待以外的任何工具。缓解:resume 入口只从 checkpoint 取 run 清单,其余一概不碰;套件断言恰好只发生折叠提交。
## 验证计划
- **core(单元)**:checkpoint 解析器对 `await_agent` + `agentWait` 的往返、未知相位与未知字段拒绝;构造器守卫(持久等待互斥);`commitAwaitAgent` 重放安全与 turn-binding 规则;`resolveAwaitAgent` 标记。
- **recovery(单元,真 local authority)**:`await_agent` 分支——点 1 分类(等待 run 报 `executing`、报告相位 `await_agent`)、点 2 恰好一次折叠(预置 journal 的 `tool_result` 绝不重写)、failed/cancelled 折叠、混合批次、被动分类。
- **Turn 臂(单元)**:admission 提交等待(顺序:先 admit 后 checkpoint),每个终局折叠都 resolve。
- **core(单元)**:checkpoint 解析器对 `await_agent` + `agentWait` 的往返、未知相位与未知字段拒绝;构造器守卫(持久等待互斥);`commitAwaitAgent` 重放安全、turn-binding 规则与边界校验(重复 id、已 consumed 运行、空批次);`resolveAwaitAgent` 标记外加推进折叠时对 takeover 激活的收养。混合版本编码形状一并钉住:不携带组时 `agentWait` 键不出现,解析仍读作 `null`。
- **recovery(单元,真 local authority)**:只覆盖分类,与 §4 自身宪章一致——`await_agent` 分支把点 1 的 run 报 `executing`,结转的全 consumed 组报 `settled`,两种 load 同形分类,且每条 execution 的 `runtimeSessionId` 为 `hostedRuntimeSessionId(promptId)`。折叠覆盖不属于这一层:§4 禁止本层折叠。
- **Turn 臂(单元)**:admission 提交等待(顺序:先 admit 后 checkpoint),每个终局折叠都 resolve,resume/consumption 标记在 journaled 闸外照常。
- **路由(集成,真 daemon)**:楔死由生产 admission 本身铸造(execute → admit → `commitAwaitAgent`);continue 路由承认该停车(闸回退即回答 409),cancel 路由完成 takeover 结算:abandoned 折叠落账、checkpoint 前进越过等待,恰好一次。
- **专项端到端套件**(新建,`hosted-child-wait-recovery` 风格,真 local authority + 同一 store 上的重启 authority):
1. 点 1 楔死 → 重启 → takeover load 可 attach → continue 重进等待 → child 结算 → 父 Turn 完成。
2. 点 2 楔死(acceptance 已提交、tool result 未提交)→ 重启 → takeover 把已提交答案恰好一次折回原 tool call(journal 计数)→ 父 Turn 完成。
3. 点 1 楔死 → close 父会话 →(此前卡死 `CLOSING`)到达 `CLOSED` → `DELETE` 成功。
4. 对楔死 Turn 的取消 takeover 将其按 cancelled 结算,child 台账行保留。
- **Java**:`HostedHarnessClientTest` 接受新相位——线路往返外加一条谓词级负向(空 executions 的 `await_agent` 既非 continuation-ready 也非 cancellation-ready;线路解析器独立地把 executions 下限钉在 1–1024)。coordinator 的 recovery 准入机制本就相位无关,由其既有的 mock 驱动套件覆盖(`HarnessCoordinatorTest` mock 谓词);相位相关行为在谓词内,由线路测试钉死。
3. 点 1 楔死 → close 父会话 →(此前卡死 `CLOSING`)到达 `CLOSED` → `DELETE` 成功。_面向 rig_:目前没有任何套件驱动真实 close/delete;本条与验收标准 3 是合并后真栈 rig 的探针,不是已交付证据。
4. 对楔死 Turn 的取消 takeover 将其按 cancelled 结算,child 台账行保留(路由级与库级均已覆盖)。
5. 二次死亡与孤儿形态:最后折叠之后崩溃分类到结转组;缺口填充回答从未到达的兄弟调用(按族措辞「未被 admitted」);已 admitted 的孤儿折叠其自身结局(完成、失败/取消、后台回执、超限带截断标记);被中断 Turn funnel 按与 takeover cancel 相同的方式结算。
- **Java**:`HostedHarnessClientTest` 接受新相位——线路往返外加两条谓词级负向(空 executions 或任一 outcome 为 `unknown` 的 `await_agent` 均既非 continuation-ready 也非 cancellation-ready;线路解析器独立地把 executions 下限钉在 1–1024)。coordinator 的 recovery 准入机制本就相位无关,由其既有的 mock 驱动套件覆盖(`HarnessCoordinatorTest` mock 谓词);相位相关行为在谓词内,由线路测试钉死。
- **变异见证**:每个新机制配一个见证,先对未变异代码证 RED(如去掉 `commitAwaitAgent` → 点 1 探针保持楔死;去掉 journaled 去重 → 点 2 探针双重折叠),再字节一致地还原后才提交。
- **复现记录回填 `.qwen/issues/issue-13708.md`**:新套件对修复前代码的红色状态作为可执行的复现报告,绿色状态作为验证报告。
@ -145,7 +148,7 @@ daemon 的 `continueManagedRuntime` 路由(coordinator 现在已对 `results_rea
1. #13708 探针 1 通过:点 1 楔死的父会话重进等待,其后的 Turn 完成。
2. 探针 2 通过:点 2 后恢复的父会话把已提交 child 答案恰好一次折回原 tool call。
3. 探针 3 通过:点 1 楔死的父会话 close/delete 到达 `CLOSED` / `DELETED`。
3. 探针 3(面向 rig):点 1 楔死的父会话 close/delete 到达 `CLOSED` / `DELETED`——合并后真栈探针;本 PR 已交付证据见上文各层配套,该条在 rig 报告转绿时成立。
4. rig 的两个 TypeScript 前台恢复期望转绿。
5. 既有 recovery、coordinator、publication、线路校验套件无回归。

View file

@ -875,6 +875,11 @@ describe('harness checkpoint v1', () => {
]);
// The carried advance is the exact shape §4's branch classifies: a
// crash here classifies the wait instead of declining model_start.
// (R1-58: this case pins the constructor's contract — production
// delivers the carry through createConsumedAgentWaitHarnessCheckpoint
// above; createModelOutputCommitted's sole production caller,
// resolveDurableWait, can never see a non-null group, and the guard's
// legal-carry side keeps exactly this case alive.)
expect(parseHarnessCheckpointV1(bytesOf(round))).toEqual(round);
});

View file

@ -724,6 +724,24 @@ class HostedHarnessClientTest {
assertFalse(recovery.isCancellationReady());
}
@Test
void agentWaitWithUnknownOutcomeIsNotReady() {
// A wait run the relay can no longer observe reports unknown: the
// all-known conjunct must keep the gates shut, or the coordinator
// would re-enter a wait it cannot observe (R1-15).
HarnessRuntimeRecovery recovery = new HarnessRuntimeRecovery(
"await_agent", "checkpoint-4", "activation-4", List.of(
new HarnessRuntimeExecutionRecovery("call-1", "agent",
"execution-1", "runtime-session", null,
"known", Map.of("state", "executing")),
new HarnessRuntimeExecutionRecovery("call-2", "agent",
"execution-2", "runtime-session", null,
"unknown", Map.of("state", "executing"))));
assertTrue(recovery.hasUnknownOutcome());
assertFalse(recovery.isContinuationReady());
assertFalse(recovery.isCancellationReady());
}
@Test
void parsesAResultsReadyRuntimeRecovery() {
AtomicReference<String> continuationBody = new AtomicReference<>();