test(session): align turn-end contract artifacts
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md
|
||||
2026-07-31-claimed-pre-step-inbox-lifecycle.md: 06a806375b5f4fe67271ebab842e9e27d5e4824d
|
||||
2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md: ad6da9285a5a9f6882b887f2c4e62903ae2b0f09
|
||||
2026-07-31-claimed-pre-step-inbox-lifecycle.md: ed56723ab8dcaaba9fbcd8e4c52ecc264141df2e
|
||||
2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md: aecfece99c49a31d429fc3229432911dc4b83f82
|
||||
|
||||
@@ -14,7 +14,7 @@ Occurrence-local inbox wrappers also duplicated the identity already carried by
|
||||
|
||||
Before every proposed step, `Inbox.claim(target)` atomically removes the complete batch: all `next-step` messages and, at a turn boundary, one `next-turn` message. At the initial boundary the loop first commits `turn/start`, so the claim and its single `agent/pre-step` decision have durable turn ownership. Claiming records normalized `agent/inbox/spliced` pure deletions with no outcome. The loop then emits `agent/inbox/claimed { message, turn }` once per claimed message and awaits the waterfall with that exclusive batch and `{ turn, step, signal }`.
|
||||
|
||||
`PreStepDecision` is `{ kind: 'reject' } | { kind: 'enter'; messages: UserMessage[] }`. Reject opens no step, leaves the claimed batch removed, and closes the turn as blocked with `step: 0`. Empty entry, cancellation, and failure before `step/start` likewise close a balanced no-step turn. Enter supplies the complete batch appended as `user/message` events after `step/start`. A listener wrapping `next()` preserves downstream changes unless it intentionally replaces them, so all message rewrites settle once in the final return value. There is no `agent/prompt-prepare`, `agent/prompt-submit`, or `agent/step` seam.
|
||||
`PreStepDecision` is `{ kind: 'reject' } | { kind: 'enter'; messages: UserMessage[] }`. Reject opens no step, leaves the claimed batch removed, and closes the turn as blocked without any step events. Empty entry, cancellation, and failure before `step/start` likewise close a balanced no-step turn. Enter supplies the complete batch appended as `user/message` events after `step/start`. A listener wrapping `next()` preserves downstream changes unless it intentionally replaces them, so all message rewrites settle once in the final return value. There is no `agent/prompt-prepare`, `agent/prompt-submit`, or `agent/step` seam.
|
||||
|
||||
The durable inbox remains two `UserMessage[]` lists addressed by `MessageId`. `append`, `prepend`, and `splice` take a target, while `replace(messageId, newMessage)` and `remove(messageId)` locate the pending message across both lists before committing a normalized splice. Replacement may change identity and emits the old message as discarded followed by the new message as inserted. Every insertion emits `agent/inbox/inserted { message }`; an ordinary removal records `outcome: 'canceled'` and emits `agent/inbox/discarded { message }`. These live events add no placement, outcome, or batch fields.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
每个拟议步骤之前,`Inbox.claim(target)` 会原子移除完整批次:全部 `next-step` 消息,以及轮次边界上的一条 `next-turn` 消息。在首次边界,循环会先提交 `turn/start`,使领取及其唯一一次 `agent/pre-step` 决策拥有持久轮次归属。领取会记录规范化、不带 outcome 的纯删除 `agent/inbox/spliced`。随后,循环针对每条已领取消息发出一次 `agent/inbox/claimed { message, turn }`,并用该独占批次与 `{ turn, step, signal }` 等待 waterfall(瀑布式事件)。
|
||||
|
||||
`PreStepDecision` 为 `{ kind: 'reject' } | { kind: 'enter'; messages: UserMessage[] }`。reject 不会打开步骤,会让已领取批次保持已删除,并以 `step: 0` 将轮次关闭为 blocked。空的 enter、取消以及 `step/start` 前的失败同样会关闭一个边界平衡的无步骤轮次。enter 提供在 `step/start` 后以 `user/message` 追加的完整批次。包装 `next()` 的监听器会保留下游变更,除非有意替换,因此全部消息改写只在最终返回值中一次性结算。系统不再存在 `agent/prompt-prepare`、`agent/prompt-submit` 或 `agent/step` seam。
|
||||
`PreStepDecision` 为 `{ kind: 'reject' } | { kind: 'enter'; messages: UserMessage[] }`。reject 不会打开步骤,会让已领取批次保持已删除,并将轮次关闭为 blocked,且不产生任何步骤事件。空的 enter、取消以及 `step/start` 前的失败同样会关闭一个边界平衡的无步骤轮次。enter 提供在 `step/start` 后以 `user/message` 追加的完整批次。包装 `next()` 的监听器会保留下游变更,除非有意替换,因此全部消息改写只在最终返回值中一次性结算。系统不再存在 `agent/prompt-prepare`、`agent/prompt-submit` 或 `agent/step` seam。
|
||||
|
||||
持久 inbox 仍是两份通过 `MessageId` 寻址的 `UserMessage[]` 列表。`append`、`prepend` 与 `splice` 接受 target;`replace(messageId, newMessage)` 与 `remove(messageId)` 则在提交规范化 splice 前,通过 `MessageId` 跨两份列表定位待处理消息。替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。每次插入发出 `agent/inbox/inserted { message }`;普通删除记录 `outcome: 'canceled'` 并发出 `agent/inbox/discarded { message }`。这些实时事件不增加 placement、outcome 或批次字段。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-04-load-pre-react-loop-sessions.md
|
||||
2026-08-04-load-pre-react-loop-sessions.md: da204822251ef646b982683e74413a001a19873a
|
||||
2026-08-04-load-pre-react-loop-sessions.zh.md: 1c2818fd3afbefb3af91f53514b9cfc2baa01739
|
||||
2026-08-04-load-pre-react-loop-sessions.md: b7dcad1ff7fe0aa8239ac03f52b50dfa55417515
|
||||
2026-08-04-load-pre-react-loop-sessions.zh.md: 14c6dcc7b4da016ebef424a66fae0b96fb469cd4
|
||||
|
||||
@@ -6,21 +6,21 @@ English | [中文](2026-08-04-load-pre-react-loop-sessions.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The react-loop simplification changed durable events while retaining `SESSION_FORMAT_VERSION` 0. Stored sessions from the change's base contain `steering/message`, a `turn/start.trigger`, and `turn/end` records without the current top-level `step`; their terminal reasons also use coarse `aborted`, separate `disposed`, and two older error payloads. Current surface and turn invariants cannot replay those records directly.
|
||||
The react-loop simplification changed durable events while retaining `SESSION_FORMAT_VERSION` 0. Stored sessions from the change's base contain `steering/message` and `turn/start.trigger`; their terminal reasons also use coarse `aborted`, separate `disposed`, and two older error payloads. Current surface and turn invariants cannot replay those records directly.
|
||||
|
||||
The new durable inbox is not part of this compatibility problem. The base emitted process-local inbox notifications but no `agent/inbox/*` session events, so replaying old history as pending work would resurrect already claimed or discarded prompts.
|
||||
|
||||
## Decision
|
||||
|
||||
`PersistenceCoordinator` recognizes the exact pre-react-loop shapes after backend decoding and projects them into the current read view. It removes the obsolete `turn/start.trigger`, converts `steering/message` to the same identified `user/message`, adds the last entered step to `turn/end`, maps old failure facts into the current structured error, folds `disposed` into an aborted turn with the `disposed` cause, and represents coarse aborted records with the persistence-only `{ kind: 'legacy' }` cause because their caller is unavailable.
|
||||
`PersistenceCoordinator` recognizes the exact pre-react-loop shapes after backend decoding and projects them into the current read view. It removes the obsolete `turn/start.trigger`, converts `steering/message` to the same identified `user/message`, maps old failure facts into the current structured error, folds `disposed` into an aborted turn with the `disposed` cause, and represents coarse aborted records with the persistence-only `{ kind: 'legacy' }` cause because their caller is unavailable.
|
||||
|
||||
The coordinator applies the projection to `load`, `inspect`, adoption, HMR prefix comparison, and `readFrom`. A seek-capable `readFrom` normally reads only its suffix; when that suffix contains a legacy event needing an earlier step or replacement identity, the coordinator loads and normalizes the complete prefix before returning the requested seq range.
|
||||
The coordinator applies the projection to `load`, `inspect`, adoption, HMR prefix comparison, and `readFrom`. A seek-capable `readFrom` normally reads only its suffix; when that suffix contains a legacy event needing an earlier replacement identity, the coordinator loads and normalizes the complete prefix before returning the requested seq range.
|
||||
|
||||
The importer does not synthesize inbox splices. A resumed pre-react-loop agent begins with empty pending lists, matching the base runtime's inability to persist pending inbox work. The stored artifact remains append-only and later events use the current format.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Treat the same-version records as unsupported.** This follows the pre-release default but strands sessions produced by the PR base even though the removed steering content, turn step, and terminal facts have complete mappings.
|
||||
**Treat the same-version records as unsupported.** This follows the pre-release default but strands sessions produced by the PR base even though the removed steering content and terminal facts have complete mappings.
|
||||
|
||||
**Replay old inbox notifications into durable splices.** Those notifications were not session events and do not provide a trustworthy pending-state snapshot. Inferring insertions without every claim and discard would re-run consumed work.
|
||||
|
||||
|
||||
@@ -6,21 +6,21 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
react-loop 简化在保持 `SESSION_FORMAT_VERSION` 为 0 的同时更改了持久事件。该变更基线所存储的会话包含 steering(中途引导)事件 `steering/message`、`turn/start.trigger`,以及缺少当前顶层 `step` 的 `turn/end` 记录;其终止原因还使用粗粒度 `aborted`、独立的 `disposed` 和两种旧版错误载荷。当前 surface 和轮次不变量无法直接回放这些记录。
|
||||
react-loop 简化在保持 `SESSION_FORMAT_VERSION` 为 0 的同时更改了持久事件。该变更基线所存储的会话包含 steering(中途引导)事件 `steering/message` 和 `turn/start.trigger`;其终止原因还使用粗粒度 `aborted`、独立的 `disposed` 和两种旧版错误载荷。当前 surface 和轮次不变量无法直接回放这些记录。
|
||||
|
||||
新的持久 inbox 不属于此兼容性问题。该基线会发出进程本地 inbox 通知,但不会产生 `agent/inbox/*` 会话事件,因此将旧历史回放为待处理工作会让已经领取或丢弃的提示词再次执行。
|
||||
|
||||
## 决策
|
||||
|
||||
`PersistenceCoordinator` 会在后端解码后识别 react-loop 重构前的确切形状,并将其投影为当前读取视图。它移除已废弃的 `turn/start.trigger`,把 `steering/message` 转换为同一条带标识的 `user/message`,为 `turn/end` 补上最后进入的步骤,将旧版失败事实映射为当前结构化错误,把 `disposed` 折叠为带 `disposed` 原因的已中止轮次,并用仅供持久化导入使用的 `{ kind: 'legacy' }` 原因表示粗粒度中止记录,因为无法获得其调用方。
|
||||
`PersistenceCoordinator` 会在后端解码后识别 react-loop 重构前的确切形状,并将其投影为当前读取视图。它移除已废弃的 `turn/start.trigger`,把 `steering/message` 转换为同一条带标识的 `user/message`,将旧版失败事实映射为当前结构化错误,把 `disposed` 折叠为带 `disposed` 原因的已中止轮次,并用仅供持久化导入使用的 `{ kind: 'legacy' }` 原因表示粗粒度中止记录,因为无法获得其调用方。
|
||||
|
||||
协调器会把该投影应用于 `load`、`inspect`、接管、HMR 前缀比较和 `readFrom`。可寻址的 `readFrom` 通常只读取后缀;如果后缀包含需要更早步骤或替换标识的旧版事件,协调器会先加载并规范化完整前缀,再返回所请求的 seq 范围。
|
||||
协调器会把该投影应用于 `load`、`inspect`、接管、HMR 前缀比较和 `readFrom`。可寻址的 `readFrom` 通常只读取后缀;如果后缀包含需要更早替换标识的旧版事件,协调器会先加载并规范化完整前缀,再返回所请求的 seq 范围。
|
||||
|
||||
导入器不会合成 inbox splice。恢复后的 react-loop 重构前 agent 从空的待处理列表开始,这与基线运行时无法持久化待处理 inbox 工作的行为一致。已存储产物仍然仅追加,后续事件使用当前格式。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**将同版本记录视为不受支持。** 这符合预发布阶段的默认立场,但会使 PR 基线产生的会话无法恢复,尽管已移除的 steering 内容、轮次步骤和终止事实都有完整映射。
|
||||
**将同版本记录视为不受支持。** 这符合预发布阶段的默认立场,但会使 PR 基线产生的会话无法恢复,尽管已移除的 steering 内容和终止事实都有完整映射。
|
||||
|
||||
**将旧 inbox 通知回放为持久 splice。** 这些通知不是会话事件,也无法提供可信的待处理状态快照。如果无法获知每一次领取和丢弃,就推断插入操作,会让已消费的工作再次执行。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.md
|
||||
2026-07-19-same-session-goal-round-driver.md: 29f804ff9de5e69eb8af3063af1bed023fef1146
|
||||
2026-07-19-same-session-goal-round-driver.zh.md: bcee71e28c61dfd43b1b2b797503554b3397bd73
|
||||
2026-07-19-same-session-goal-round-driver.md: a989b9d3487ebbf8ff11a4e34cb08029f1e4ace4
|
||||
2026-07-19-same-session-goal-round-driver.zh.md: 6fb97a8beb477479adb37596708ff5cc7fa05dc6
|
||||
|
||||
@@ -53,9 +53,9 @@ No abnormal outcome requests an automatic retry. A later human prompt can ask to
|
||||
|
||||
Every `goal/changed` notification creates a checkpoint obligation. The driver awaits `ctx.sessions.flush(session)` before reserving work, then checks for a newer mutation, agent lifecycle change, or competing prompt. Turn-end flush failure is reported by the existing `agent/error` notification after `turn/end`; the driver finds that exact closed turn even when a concurrent one-shot injection appended a later turn, associates the failure with the exact attempt, and disarms before the next idle decision.
|
||||
|
||||
Broad cancellation previously exposed only its effects after queues were cleared or the request aborted. The public agent vocabulary now includes observe-only `agent/cancel-requested(agent, reason)`. The concrete loop emits it for effective cancellation before either action; fused notification containment means a broken listener cannot veto cancellation. The goal driver uses this edge to clear its reservation before the loop destroys the queued-work evidence. When that reservation is a queued or admitted goal attempt, cancellation durably pauses the goal; when cancellation belongs to unrelated human work with no goal attempt, it only removes process-local activation. If the pause mutation throws, the driver falls back to disarming rather than allowing cancelled automatic work to restart.
|
||||
Broad cancellation clears pending inbox work and aborts the active loop phase. The goal driver follows the reserved message through inbox claim/discard events and the durable aborted turn ending. Because a turn now opens before its initial claim, cancellation can close a claimed no-step attempt; the driver marks that attempt cancelled and lets the following idle edge pause the goal, just as it does for an admitted attempt. Cancellation with no matching goal attempt only removes process-local activation. If the pause mutation throws, the driver falls back to disarming rather than allowing cancelled automatic work to restart.
|
||||
|
||||
This is a coordination notification, not a second stop API. `Agent.cancel()` remains the only public broad cancellation verb, idle calls remain no-ops, and custom `Agent` implementations that claim the interface must honor the event ordering if consumers depend on it.
|
||||
`Agent.cancel()` remains the only public broad cancellation verb. Custom `Agent` implementations that claim the interface must honor the inbox, turn-ending, status, and quiescence ordering if consumers depend on it.
|
||||
|
||||
### Process lifecycle
|
||||
|
||||
|
||||
@@ -53,9 +53,9 @@ Status: implemented
|
||||
|
||||
每次 `goal/changed` 通知都会产生一个检查点义务。驱动器在预留工作前等待 `ctx.sessions.flush(session)`,随后检查是否出现了更新的变更、agent 生命周期变化或竞争提示词。轮次结束时的 flush 失败会在 `turn/end` 之后通过现有 `agent/error` 通知报告;即使并发的一次性注入已追加后续轮次,驱动器仍会找到该精确的已关闭轮次,把失败关联到精确尝试,并在下一次空闲决策前解除激活。
|
||||
|
||||
广义取消此前只在队列已清除或请求已中止后暴露结果。公共 agent 词汇现在新增只观察的 `agent/cancel-requested(agent, reason)`。具体循环仅在取消有效时发出该事件,并且发生在清除队列和中止步骤之前;融合通知会隔离失败,因此损坏的监听器不能否决取消。目标驱动器利用该边沿在循环销毁排队工作证据前清除预留。若该预留是排队中或已接纳的目标尝试,取消会持久暂停目标;若取消属于没有目标尝试的无关人类工作,则只移除进程内激活态。若暂停变更抛错,驱动器会回退到解除激活,避免已取消的自动工作重新启动。
|
||||
广义取消会清除待处理 inbox 工作,并中止活跃 loop 阶段。目标驱动器通过 inbox 的领取/丢弃事件和持久的 aborted 轮次结束来跟踪预留消息。由于轮次现在会在首次领取前打开,取消可以关闭已领取的无步骤尝试;驱动器会把该尝试标记为已取消,并让随后的 idle 边沿暂停目标,与已准入尝试的处理方式相同。没有匹配目标尝试的取消只会移除进程内激活态。若暂停变更抛错,驱动器会回退到解除激活,避免已取消的自动工作重新启动。
|
||||
|
||||
该通知是协调事件,不是第二个停止 API。`Agent.cancel()` 仍是唯一的公共广义取消动词,空闲调用仍是无操作;若消费者依赖此接缝,自定义 `Agent` 实现就必须满足该事件顺序。
|
||||
`Agent.cancel()` 仍是唯一的公共广义取消动词。若消费方依赖其顺序,实现该接口的自定义 `Agent` 必须遵守 inbox、轮次结束、status 与完全停稳的顺序。
|
||||
|
||||
### 进程生命周期
|
||||
|
||||
|
||||
Reference in New Issue
Block a user