diff --git a/apps/web/tests/live-interactions.e2e.ts b/apps/web/tests/live-interactions.e2e.ts
index bde84fe2d1..394da150c3 100644
--- a/apps/web/tests/live-interactions.e2e.ts
+++ b/apps/web/tests/live-interactions.e2e.ts
@@ -478,9 +478,9 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
const userMessages = sessionEvents.flatMap(event => event.type === 'user/message' && event.data.source.kind === 'user'
? [event.data] : [])
expect(userMessages.map(message => message.content)).toEqual([[{ type: 'text', text: PROMPT }]])
- expect(sessionEvents.filter(event => event.type === 'agent/retry')).toEqual([
- expect.objectContaining({ data: expect.objectContaining({ turn: 2, step: 1, source: expect.objectContaining({ kind: 'retry' }) }) }),
- ])
+ const retries = sessionEvents.filter(event => event.type === 'agent/retry')
+ expect(retries).toHaveLength(1)
+ expect(retries[0]?.data).toMatchObject({ turn: 2, step: 1, source: { kind: 'retry' } })
expect(sessionEvents.filter(event => event.type === 'turn/start')).toHaveLength(2)
expect(sessionEvents.filter(event => event.type === 'turn/end')).toHaveLength(2)
expect(await resume.count()).toBe(0)
diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml
index 5dfa2c2995..ca357a4a9c 100644
--- a/docs/architecture.i18n.yaml
+++ b/docs/architecture.i18n.yaml
@@ -24,8 +24,8 @@
en: 693407efb233f961
zh: 37b9dd0677d4405c
/deepseek-harness-architecture/turn-flow:
- en: 3770eacd9c3bae28
- zh: bd7b8a0374ba0ff8
+ en: d67fe476bec779ea
+ zh: 774a0913a8fb03d0
/deepseek-harness-architecture/session-log:
en: 8a7f19b95d426c99
zh: d5bb0538404e20ea
diff --git a/docs/architecture.md b/docs/architecture.md
index 064026d4d2..1ffee7bf2f 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -107,9 +107,9 @@ turn/end
Factory setup follows SessionStore entry and `session/created`, preceding `agent/created`; closing events commit before removal.
-One inbox feeds the driver; injected context waits for a waking message. AgentLoop’s durable `inbox` projection exposes pending input without live Agents.
+`inbox` persists pending input; injected context waits for waking input. Retry controls record `agent/retry` without changing turn settlement.
-Before assembly, the loop snapshots Session model selection in `AssembleContext.modelSelection` and `agent/pre-step.modelSelection`; retries retain it; omitted reasoning effort uses adapter defaults. `agent/pre-step` listeners accept, rewrite, or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, the deprecated `agent/request` hook adjusts controls without changing the provider, model, or effort selection, and `prepareCall()` binds the adapter before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Empty retry-source controls record `agent/retry` instead of entering model history; they retain ordinary inbox and turn lifecycle. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The system prompt travels as `system/message` history: empty text clears it, capable routes append updates, and other routes consolidate it at the first system node. [Agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) owns prompt admission and replacement details.
+Before assembly, the loop snapshots Session model selection in `AssembleContext.modelSelection` and `agent/pre-step.modelSelection`; retries retain it; omitted reasoning effort uses adapter defaults. `agent/pre-step` listeners accept, rewrite, or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, the deprecated `agent/request` hook adjusts controls without changing the provider, model, or effort selection, and `prepareCall()` binds the adapter before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The system prompt travels as `system/message` history: empty text clears it, capable routes append updates, and other routes consolidate it at the first system node. [Agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) owns prompt admission and replacement details.
The loop sends immutable requests with live cancellation and reuses freeze evidence for frozen identities; [agent-loop](../packages/core/agent-loop/README.md) owns construction and cancellation causes.
diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md
index e836ab7452..42297aca67 100644
--- a/docs/architecture.zh.md
+++ b/docs/architecture.zh.md
@@ -111,9 +111,9 @@ turn/end
工厂 setup 在 Session 进入 SessionStore 并完成 `session/created` 之后、`agent/created` 之前运行;收尾事件在移除之前提交。
-输入通过同一个 inbox 到达驱动器;注入的上下文等待一条唤醒消息。AgentLoop 的持久 `inbox` 投影使待处理输入在没有活跃 Agent 时仍可读取。
+`inbox` 持久化待消费输入;注入上下文等待唤醒输入。重试控制输入记录 `agent/retry`,不改变轮次结算。
-循环在组装前将 Session 模型选择快照到 `AssembleContext.modelSelection` 与 `agent/pre-step.modelSelection`;重试保留该选择,省略推理强度时使用适配器默认值。`agent/pre-step` 监听器接纳、改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变提供方、模型或推理强度选择的前提下调整控制参数,`prepareCall()` 绑定适配器,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。系统提示词通过 `system/message` 历史传递:空文本清除提示词,具备能力的路由追加更新,其他路由将其归并到首个系统节点。[Agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation) 说明提示词准入与替换的细节。 空的 retry 来源控制输入记录 `agent/retry`,不进入模型历史;inbox 与轮次生命周期仍按普通流程处理。
+循环在组装前将 Session 模型选择快照到 `AssembleContext.modelSelection` 与 `agent/pre-step.modelSelection`;重试保留该选择,省略推理强度时使用适配器默认值。`agent/pre-step` 监听器接纳、改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变提供方、模型或推理强度选择的前提下调整控制参数,`prepareCall()` 绑定适配器,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。系统提示词通过 `system/message` 历史传递:空文本清除提示词,具备能力的路由追加更新,其他路由将其归并到首个系统节点。[Agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation) 说明提示词准入与替换的细节。
循环发送不可变请求,同时保持取消有效,仅对完全冻结的对象复用冻结证据;[agent-loop](../packages/core/agent-loop/README.zh.md) 负责请求构建与取消原因。
diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml
index 3214b08f10..546008ca82 100644
--- a/docs/persistence-catalog.i18n.yaml
+++ b/docs/persistence-catalog.i18n.yaml
@@ -21,8 +21,8 @@
en: 9188862058c6986b
zh: 9188862058c6986b
/session-persistence-event-catalog/events/agent/agent-inbox-spliced-log-only:
- en: 9ae614c041a27169
- zh: 8d669dd582b88f79
+ en: b647f3f80ec8b407
+ zh: d5e43856cfd85ddb
/session-persistence-event-catalog/events/agent/agent-retry-log-only:
en: 559e0484e8ae8904
zh: 8967a336e5aef50c
@@ -144,8 +144,8 @@
en: 9edf4ef31e60e880
zh: 9edf4ef31e60e880
/session-persistence-event-catalog/events/model/model-selection-log-only:
- en: 404c54b08c088438
- zh: eb86ed4e7d0e9228
+ en: 608081c311c1bf13
+ zh: bcd14effec5f6c78
/session-persistence-event-catalog/events/permission:
en: 19917bc5438e60af
zh: 19917bc5438e60af
@@ -1545,8 +1545,8 @@
en: 930d9c64fe39857d
zh: 65cd07dc3c7789db
/session-persistence-event-catalog/resolved-persistence-types/inserted-outcome-removedcount-start:
- en: 4edbaf2329a66619
- zh: 7d29428252d9d84f
+ en: 473f9e6ab2b776dc
+ zh: ce701e3ff6ab63ab
/session-persistence-event-catalog/resolved-persistence-types/interrupted-message-step-stream:
en: 8e86cd838e3e4a49
zh: f5fec2a8adf1f3cc
diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md
index 70e16e7ce7..f3cfff9e7f 100644
--- a/docs/persistence-catalog.md
+++ b/docs/persistence-catalog.md
@@ -186,7 +186,7 @@ Sources: [`packages/core/session/src/types.ts:439`](../packages/core/session/src
}
```
-Source: [`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:124`](../packages/core/agent/src/types.ts)
@@ -194,7 +194,12 @@ Source: [`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/type
```ts persistence-catalog
/** An admitted retry control, correlated with its durable inbox input; never enters model history. */
-'agent/retry': { turn: number; step: number; inputId: MessageId; source: Extract }
+'agent/retry': {
+ turn: number
+ step: number
+ inputId: MessageId
+ source: Extract
+}
```
Types: [MessageSource](subsystems/core.md)
@@ -714,7 +719,7 @@ Source: [`packages/llm/llm-retry/src/types.ts:11`](../packages/llm/llm-retry/src
'model/selection': ModelSelection
```
-Source: [`packages/core/agent/src/types.ts:130`](../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:135`](../packages/core/agent/src/types.ts)
### `permission/*`
@@ -6468,7 +6473,7 @@ Sources: [`packages/core/agent/src/types.ts:113`](../packages/core/agent/src/typ
SHA-256: `c66c6c3b411abf46c1f9b99e0019c10c1b6d13ee716a4b5e88f7e1b65feb3039`
-Sources: [`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/types.ts)
+Sources: [`packages/core/agent/src/types.ts:124`](../packages/core/agent/src/types.ts)
| Property | Presence | Type |
|---|---|---|
diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md
index 47f1c30ed9..05543778cb 100644
--- a/docs/persistence-catalog.zh.md
+++ b/docs/persistence-catalog.zh.md
@@ -188,7 +188,7 @@ export type SessionEvent = {
}
```
-来源:[`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/types.ts)
+来源:[`packages/core/agent/src/types.ts:124`](../packages/core/agent/src/types.ts)
@@ -196,7 +196,12 @@ export type SessionEvent = {
```ts persistence-catalog
/** An admitted retry control, correlated with its durable inbox input; never enters model history. */
-'agent/retry': { turn: number; step: number; inputId: MessageId; source: Extract }
+'agent/retry': {
+ turn: number
+ step: number
+ inputId: MessageId
+ source: Extract
+}
```
类型:[MessageSource](subsystems/core.zh.md)
@@ -716,7 +721,7 @@ export type SessionEvent = {
'model/selection': ModelSelection
```
-来源:[`packages/core/agent/src/types.ts:130`](../packages/core/agent/src/types.ts)
+来源:[`packages/core/agent/src/types.ts:135`](../packages/core/agent/src/types.ts)
### `permission/*`
@@ -6470,7 +6475,7 @@ SHA-256: `2a668a48dec1739752123d67c3bc254412d6743dbe3347365e0ed83a542f2bc3`
SHA-256: `c66c6c3b411abf46c1f9b99e0019c10c1b6d13ee716a4b5e88f7e1b65feb3039`
-来源:[`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/types.ts)
+来源:[`packages/core/agent/src/types.ts:124`](../packages/core/agent/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
diff --git a/docs/persistence-changes/historical-formats/README.i18n.yaml b/docs/persistence-changes/historical-formats/README.i18n.yaml
index 40d842e063..cae6902733 100644
--- a/docs/persistence-changes/historical-formats/README.i18n.yaml
+++ b/docs/persistence-changes/historical-formats/README.i18n.yaml
@@ -15,8 +15,8 @@
en: 14c57f30e676f6e3
zh: b1a0ccf26fab3727
/session-persistence-formats/format-references:
- en: e3511a4ac3bc4457
- zh: 828f3c39f80d5370
+ en: 83fd4c4276a2600d
+ zh: 4f3431c960a7f8da
/session-persistence-formats/scope-and-evidence:
en: b220062139ddac1e
zh: 7c4c0efe05dafa9c
diff --git a/docs/persistence-changes/historical-formats/README.md b/docs/persistence-changes/historical-formats/README.md
index 74c575b300..da1a2d629e 100644
--- a/docs/persistence-changes/historical-formats/README.md
+++ b/docs/persistence-changes/historical-formats/README.md
@@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant.
| 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 |
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 |
| 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 |
-| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 65 / 622 |
+| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 66 / 629 |
diff --git a/docs/persistence-changes/historical-formats/README.zh.md b/docs/persistence-changes/historical-formats/README.zh.md
index 12eb600771..8a2460c46c 100644
--- a/docs/persistence-changes/historical-formats/README.zh.md
+++ b/docs/persistence-changes/historical-formats/README.zh.md
@@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式
| 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 |
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 |
| 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 |
-| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 65 / 622 |
+| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 66 / 629 |
diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json
index 07e2ce9415..f7630fdb61 100644
--- a/docs/persistence-schema.json
+++ b/docs/persistence-schema.json
@@ -568,7 +568,7 @@
{"digest":"c589e938cc0e4989df8b82abef6339de621f0dc4fe62c230d145b94996af7f92","names":[],"sources":[]},
{"digest":"c592ce75aab73fcab19c1d7845684c72cf402b78d2e1f2833a58ecf9f3598ed6","names":["packages/util/values/src/index.ts#JsonValue"],"sources":["packages/util/values/src/index.ts:4"]},
{"digest":"c5bbcb3435908af2036df7105883e0a49c30ecc1fae4420aa5c4cf944066eff5","names":[],"sources":[]},
- {"digest":"c66c6c3b411abf46c1f9b99e0019c10c1b6d13ee716a4b5e88f7e1b65feb3039","names":[],"sources":["packages/core/agent/src/types.ts:119"]},
+ {"digest":"c66c6c3b411abf46c1f9b99e0019c10c1b6d13ee716a4b5e88f7e1b65feb3039","names":[],"sources":["packages/core/agent/src/types.ts:124"]},
{"digest":"c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262","names":[],"sources":[]},
{"digest":"c8e6f48878cc2a2f0dc59682280d81f407943fb503f32c64bf9ff3d9393c3eff","names":[],"sources":[]},
{"digest":"c99e20441bf15341e9db54385e558e5c03d21dff2eb1ac08ccbf47c2d8747954","names":[],"sources":[]},
diff --git a/packages/api/session-controller/tests/empty-prompt.host.spec.ts b/packages/api/session-controller/tests/empty-prompt.host.spec.ts
index 835d7ae442..50d4871ee8 100644
--- a/packages/api/session-controller/tests/empty-prompt.host.spec.ts
+++ b/packages/api/session-controller/tests/empty-prompt.host.spec.ts
@@ -43,9 +43,10 @@ it('records retry controls without user messages or duplicate requests', async (
const messages = events.flatMap(event => event.type === 'user/message' && event.data.source.kind === 'user' ? [event.data] : [])
expect(messages).toHaveLength(1)
const retries = events.flatMap(event => event.type === 'agent/retry' ? [event.data] : [])
- expect(retries).toEqual([
- { turn: 2, step: 1, inputId: expect.any(String), source: { kind: 'retry', rpcId: 'continue-1' } },
- { turn: 3, step: 1, inputId: expect.any(String), source: { kind: 'retry', rpcId: 'continue-2' } },
+ expect(retries).toHaveLength(2)
+ expect(retries).toMatchObject([
+ { turn: 2, step: 1, source: { kind: 'retry', rpcId: 'continue-1' } },
+ { turn: 3, step: 1, source: { kind: 'retry', rpcId: 'continue-2' } },
])
const controls = events.flatMap(event => event.type === 'agent/inbox/spliced' ? event.data.inserted : [])
.filter(message => message.source.kind === 'retry')
diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py
index 0ea6f3831c..24bc9f15f1 100644
--- a/scripts/smoke-python-runtime.py
+++ b/scripts/smoke-python-runtime.py
@@ -1930,6 +1930,13 @@ def smoke_sdk_scheduler_recovery(base_url: str, executable: Path, update_snapsho
retries = [record for record in records if record.get("type") == "agent/retry"]
if len(prompts) != 1 or len(retries) != 1:
raise AssertionError("scheduler recovery must record one original prompt and one retry control")
+ retry_id = retries[0]["data"]["inputId"]
+ queued_retry_ids = [message.get("id") for record in records
+ if record.get("type") == "agent/inbox/spliced"
+ for message in record["data"]["inserted"]
+ if message.get("source", {}).get("kind") == "retry"]
+ if not isinstance(retry_id, str) or queued_retry_ids != [retry_id]:
+ raise AssertionError("scheduler recovery lost the retry control's durable inbox identity")
calls = [record["data"]["callId"] for record in records if record.get("type") == "tool/call"]
if calls != list(RECOVERY_CALL_IDS[:2]):
raise AssertionError(f"scheduler recovery dispatched the unstarted call: {calls}")
@@ -1946,6 +1953,7 @@ def smoke_sdk_scheduler_recovery(base_url: str, executable: Path, update_snapsho
replacements = [
*snapshot_directory_replacements(root, "{{cwd}}"),
(RECOVERY_SESSION_ID, "{{parent}}"),
+ (retry_id, "{{messageId}}"),
]
result_value = [{
"session_id": result.session_id,