Browse Source

Merge commit 'be5db297c7' into worktree/session-format-05-v1-v2-chunk-migration

# Conflicts:
#	.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.md
#	.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.zh.md
#	docs/event-producer-consumer.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/event-producer-consumer.zh.md
#	packages/api/session-controller/README.i18n.yaml
#	packages/api/session-controller/README.md
#	packages/api/session-controller/README.zh.md
#	packages/api/session-controller/src/assistant-stream.ts
#	packages/api/session-controller/src/client/sessions/assistant-stream.ts
#	packages/api/session-controller/src/types.ts
#	packages/api/session-controller/tests/assistant-stream.client.spec.ts
#	packages/api/session-controller/tests/session-history-journal.host.spec.ts
#	packages/api/session-controller/tests/sessions-service.client.spec.ts
#	packages/api/session-controller/tests/transport.client.spec.ts
#	packages/core/agent/README.i18n.yaml
#	packages/core/agent/README.md
#	packages/core/agent/README.zh.md
#	packages/extensions/tool-cordis/src/api-catalog.ts
#	scripts/package-dependency-policy.ts
Tianyi Cui 1 month ago
parent
commit
d58964a010
58 changed files with 224 additions and 238 deletions
  1. 2 2
      .agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.i18n.yaml
  2. 1 1
      .agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.md
  3. 1 1
      .agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md
  7. 2 2
      docs/event-producer-consumer.i18n.yaml
  8. 18 18
      docs/event-producer-consumer.md
  9. 18 18
      docs/event-producer-consumer.zh.md
  10. 2 2
      docs/subsystems/core.i18n.yaml
  11. 0 2
      docs/subsystems/core.md
  12. 0 2
      docs/subsystems/core.zh.md
  13. 2 2
      packages/api/session-controller/README.i18n.yaml
  14. 0 0
      packages/api/session-controller/README.md
  15. 0 0
      packages/api/session-controller/README.zh.md
  16. 0 3
      packages/api/session-controller/src/assistant-stream.ts
  17. 0 3
      packages/api/session-controller/src/types.ts
  18. 1 2
      packages/api/session-controller/tests/assistant-stream.client.spec.ts
  19. 3 6
      packages/api/session-controller/tests/assistant-stream.host.spec.ts
  20. 14 17
      packages/api/session-controller/tests/session-history-journal.host.spec.ts
  21. 3 5
      packages/api/session-controller/tests/sessions-service.client.spec.ts
  22. 3 7
      packages/api/session-controller/tests/transport.client.spec.ts
  23. 1 2
      packages/bundle/headless/tests/headless.spec.ts
  24. 2 7
      packages/client/connection/src/client/fixture.ts
  25. 0 1
      packages/client/connection/tests/fixture.client.spec.ts
  26. 0 1
      packages/core/agent-loop/src/assistant-stream.ts
  27. 0 2
      packages/core/agent-loop/tests/loop.spec.ts
  28. 2 2
      packages/core/agent/README.i18n.yaml
  29. 1 1
      packages/core/agent/README.md
  30. 1 1
      packages/core/agent/README.zh.md
  31. 0 2
      packages/core/agent/src/runtime-types.ts
  32. 1 2
      packages/core/scope/tests/invariant.spec.ts
  33. 3 3
      packages/extensions/tool-cordis/src/api-catalog.ts
  34. 1 0
      packages/session-query/session-log-export/package.json
  35. 2 1
      packages/session-query/session-log-export/src/archive.ts
  36. 1 0
      packages/session-query/session-log-export/tsconfig.host.json
  37. 2 1
      packages/session/session-format-v0-to-v1/package.json
  38. 2 18
      packages/session/session-format-v0-to-v1/src/relationships.ts
  39. 3 0
      packages/session/session-format-v0-to-v1/tsconfig.json
  40. 2 2
      packages/session/session-format/README.i18n.yaml
  41. 1 0
      packages/session/session-format/README.md
  42. 1 0
      packages/session/session-format/README.zh.md
  43. 3 0
      packages/session/session-format/package.json
  44. 32 0
      packages/session/session-format/src/filename.ts
  45. 1 0
      packages/session/session-format/src/index.ts
  46. 5 56
      packages/session/session-format/src/json.ts
  47. 31 0
      packages/session/session-format/tests/filename.spec.ts
  48. 4 6
      packages/session/session-format/tests/json.spec.ts
  49. 3 0
      packages/session/session-format/tsconfig.json
  50. 1 0
      packages/session/session-persistence-jsonl/package.json
  51. 10 12
      packages/session/session-persistence-jsonl/src/format.ts
  52. 4 9
      packages/session/session-persistence-jsonl/src/generation.ts
  53. 3 0
      packages/session/session-persistence-jsonl/tsconfig.json
  54. 1 0
      packages/test-support/session-snapshot/package.json
  55. 6 12
      packages/test-support/session-snapshot/src/session-files.ts
  56. 3 0
      packages/test-support/session-snapshot/tsconfig.json
  57. 16 0
      pnpm-lock.yaml
  58. 1 0
      scripts/package-dependency-policy.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.i18n.yaml

@@ -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-08-31-live-assistant-stream-frames.md
-2026-08-31-live-assistant-stream-frames.md: 7bf62b84c2adc0bf98e8f6ab00fed917a2064422
-2026-08-31-live-assistant-stream-frames.zh.md: 01e537b4676abb9844f9f668bdb98c12ba48bce7
+2026-08-31-live-assistant-stream-frames.md: 25d86d152714baeca1df18c353f40bc0052bdeee
+2026-08-31-live-assistant-stream-frames.zh.md: 924649932b29520ad3effa88f18267a3ae20ac02

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.md

@@ -10,7 +10,7 @@ The v2 session log keeps one `assistant/message` or `assistant/attempt` settleme
 
 ## Decision
 
-`dsh-agent-loop` emits scoped `agent/assistant-stream` frames for each model attempt. `start`, `chunk`, and `end` carry a branded `LlmAttemptId` unique within one Agent lifecycle; every frame advances one revision local to that lifecycle. The start frame captures a safe-integer wall-clock `startedTime`, chunk indexes are dense from zero, chunk timestamps are reused by the compact stream, and `end.index` equals the next chunk position. Stream acquisition and its final cancellation check occur before `start`; a failure there emits no frame. Every started attempt emits one terminal end: the loop appends the final `assistant/message` or `assistant/attempt` before a committed end names that event and seq, while assembly or settlement failure emits an abandoned end without a durable target. Authenticated Session-follow accepts an explicit Web opt-in, opens with a cached active-attempt compact baseline, and carries durable events and cursorless frames in one FIFO. Each follower captures a local arrival ordinal with the opening baseline and drops buffered frames at or before that cut; frame revisions can restart at one with a replacement Agent, so they do not define the opening cut. A settlement arriving after an active opening is owned by that attempt only when its seq follows `startedAfterSeq` and its Turn and Step match; it remains staged until the matching end index, type, and seq arrive, while an earlier retry at the same Turn and Step remains visible. Revision, dense-index, or settlement gaps for a known attempt reopen follow and replace the baseline; unknown-attempt frames fall back to the durable settlement. The TypeScript and Python SDK protocols do not expose these frames. Durable settlements remain the source of replay and model history; the [v2 stream decision](2026-09-01-v2-embedded-assistant-streams.md) owns their representation.
+`dsh-agent-loop` emits scoped `agent/assistant-stream` frames for each model attempt. `start`, `chunk`, and `end` carry a branded `LlmAttemptId` unique within one Agent lifecycle; every frame advances one revision local to that lifecycle. The start frame names the attempt's turn and step, chunk indexes are dense from zero, chunk timestamps are reused by the compact stream, and `end.index` equals the next chunk position. Stream acquisition and its final cancellation check occur before `start`; a failure there emits no frame. Every started attempt emits one terminal end: the loop appends the final `assistant/message` or `assistant/attempt` before a committed end names that event and seq, while assembly or settlement failure emits an abandoned end without a durable target. Authenticated Session-follow accepts an explicit Web opt-in, opens with a cached active-attempt compact baseline, and carries durable events and cursorless frames in one FIFO. Each follower captures a local arrival ordinal with the opening baseline and drops buffered frames at or before that cut; frame revisions can restart at one with a replacement Agent, so they do not define the opening cut. A settlement arriving after an active opening is owned by that attempt only when its seq follows `startedAfterSeq` and its Turn and Step match; it remains staged until the matching end index, type, and seq arrive, while an earlier retry at the same Turn and Step remains visible. Revision, dense-index, or settlement gaps for a known attempt reopen follow and replace the baseline; unknown-attempt frames fall back to the durable settlement. The TypeScript and Python SDK protocols do not expose these frames. Durable settlements remain the source of replay and model history; the [v2 stream decision](2026-09-01-v2-embedded-assistant-streams.md) owns their representation.
 
 ## Alternatives considered
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.zh.md

@@ -10,7 +10,7 @@ v2 Session log 通过一个 `assistant/message` 或 `assistant/attempt` settleme
 
 ## 决定
 
-`dsh-agent-loop` 为每次模型 attempt 发出作用域内的 `agent/assistant-stream` frame。`start`、`chunk` 和 `end` 带有在单个 Agent lifecycle 内唯一的 branded `LlmAttemptId`;每个 frame 都会推进一次该 lifecycle 本地 revision。start frame 会把壁钟时间捕获为安全整数 `startedTime`,chunk index 从零开始密集递增,chunk 时间戳会被紧凑 stream 复用,`end.index` 等于下一个 chunk 位置。loop 会先取得 stream 并执行最终取消检查,再发出 `start`;这些步骤失败时不发出任何 frame。每个已开始 attempt 都会发出一个终态 end:loop 会在 committed end 命名事件与 seq 前追加最终 `assistant/message` 或 `assistant/attempt`,而 assembly 或 settlement failure 会发出不命名持久目标的 abandoned end。已认证 Session-follow 接受显式 Web opt-in,以缓存的活跃 attempt 紧凑 baseline 打开,并在一个 FIFO 中携带持久事件和无 cursor frame。每个 follower 会随 opening baseline 捕获本地到达序号,并丢弃该 cut 及之前的 buffered frame;replacement Agent 的 frame revision 可以从一重新开始,因此 revision 不定义 opening cut。活跃 opening 之后到达的 settlement 只有在其 seq 晚于 `startedAfterSeq` 且 Turn 与 Step 匹配时才属于该 attempt;它会保持暂存,直到匹配的 end index、type 与 seq 到达,而同一 Turn 和 Step 中更早的 retry 仍保持可见。已知 attempt 的 revision、密集 index 或 settlement 缺口会重新打开 follow 并替换 baseline;unknown-attempt frame 回退到持久 settlement。TypeScript 和 Python SDK 协议不公开这些 frame。持久 settlement 仍是 replay 与模型历史的真源;其表示由 [v2 stream 决策](2026-09-01-v2-embedded-assistant-streams.zh.md)负责。
+`dsh-agent-loop` 为每次模型 attempt 发出作用域内的 `agent/assistant-stream` frame。`start`、`chunk` 和 `end` 带有在单个 Agent lifecycle 内唯一的 branded `LlmAttemptId`;每个 frame 都会推进一次该 lifecycle 本地 revision。start frame 给出该 attempt 的 turn 与 step,chunk index 从零开始密集递增,chunk 时间戳会被紧凑 stream 复用,`end.index` 等于下一个 chunk 位置。loop 会先取得 stream 并执行最终取消检查,再发出 `start`;这些步骤失败时不发出任何 frame。每个已开始 attempt 都会发出一个终态 end:loop 会在 committed end 命名事件与 seq 前追加最终 `assistant/message` 或 `assistant/attempt`,而 assembly 或 settlement failure 会发出不命名持久目标的 abandoned end。已认证 Session-follow 接受显式 Web opt-in,以缓存的活跃 attempt 紧凑 baseline 打开,并在一个 FIFO 中携带持久事件和无 cursor frame。每个 follower 会随 opening baseline 捕获本地到达序号,并丢弃该 cut 及之前的 buffered frame;replacement Agent 的 frame revision 可以从一重新开始,因此 revision 不定义 opening cut。活跃 opening 之后到达的 settlement 只有在其 seq 晚于 `startedAfterSeq` 且 Turn 与 Step 匹配时才属于该 attempt;它会保持暂存,直到匹配的 end index、type 与 seq 到达,而同一 Turn 和 Step 中更早的 retry 仍保持可见。已知 attempt 的 revision、密集 index 或 settlement 缺口会重新打开 follow 并替换 baseline;unknown-attempt frame 回退到持久 settlement。TypeScript 和 Python SDK 协议不公开这些 frame。持久 settlement 仍是 replay 与模型历史的真源;其表示由 [v2 stream 决策](2026-09-01-v2-embedded-assistant-streams.zh.md)负责。
 
 ## 曾考虑的替代方案
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.i18n.yaml

@@ -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-08-31-released-session-format-migrations.md
-2026-08-31-released-session-format-migrations.md: d044350b9c09ec67f7e5ac502e28b21418ae1993
-2026-08-31-released-session-format-migrations.zh.md: 4ac6ddd97e6d6154853cd6cf514844f0f6180252
+2026-08-31-released-session-format-migrations.md: 2c75d0b57a0b513c218b6a67b8c2b31c7cae4d0f
+2026-08-31-released-session-format-migrations.zh.md: d88c643cbfaf7f3d4f52ca6e5fa244a26917eb99

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md

@@ -22,7 +22,7 @@ Cancellation belongs to the `open`, `stat`, or `list` call that supplied it. Dis
 
 The configured JSONL encoding owns one full suffix, `.jsonl` or `.jsonl.zstd`. Migration reads a stable exact source, decodes the recoverable logical prefix, composes every required edge in memory, validates and syncs a same-directory temporary stage for only the final target, rechecks the source fingerprint, publishes that previously absent target without overwrite, syncs the namespace, and reopens it through current validation before returning a handle. The source never moves or changes; only disposable temporary stages may be moved, linked, or removed. Migration does not synthesize interrupted-turn events: agent-loop appends those repairs through the write handle, while read-only query paths balance them in memory.
 
-Canonical filenames encode the physical format generation: v0 is `session.jsonl` or `session.jsonl.zstd`; every positive generation is lowercase `session.vN.jsonl` or `session.vN.jsonl.zstd`. Publication never renames, replaces, or deletes a committed generation path. If the target already exists, it is accepted only as a regular current-format file with exactly the expected bytes; any other target refuses. Lower generations remain for operator inspection or explicit copying, but normal runtime operations select the numerically highest canonical name and never use retained predecessors as automatic fallback, restore, or downgrade support.
+Canonical filenames encode the physical format generation: v0 is `session.jsonl` or `session.jsonl.zstd`; every positive generation is lowercase `session.vN.jsonl` or `session.vN.jsonl.zstd`. `dsh-session-format` owns the raw basename rule (`sessionFormatLogFilename`, `parseSessionFormatLogFilename`); the JSONL provider, the session-log export archive, and recorded-session fixtures append only the compression suffix. Publication never renames, replaces, or deletes a committed generation path. If the target already exists, it is accepted only as a regular current-format file with exactly the expected bytes; any other target refuses. Lower generations remain for operator inspection or explicit copying, but normal runtime operations select the numerically highest canonical name and never use retained predecessors as automatic fallback, restore, or downgrade support.
 
 The current-format fast path classifies the header from one stable source snapshot, invokes no historical converter or generation write, and passes that snapshot to current decoding without another file read. The decoded log enters the existing bounded revision-keyed memo for an immediate observe-to-resume handoff, while `stat` and `list` deliberately rescan. Multiple edges leave the original generation unchanged and publish only the final target; intermediate versions exist only in memory. A source fingerprint recheck restarts migration when content changes, and exclusive target publication accepts a racing winner only when its bytes match exactly. Cross-process append fencing remains outside this guarantee.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md

@@ -22,7 +22,7 @@ JSONL provider 在 `open` 为已存储 Session 返回句柄前完成 ensure-curr
 
 配置的 JSONL 编码拥有一个完整后缀:`.jsonl` 或 `.jsonl.zstd`。迁移读取稳定的精确源,解码可恢复逻辑前缀,在内存中组合全部必需迁移边,只为最终目标校验并同步同目录临时 stage,重新检查源 fingerprint,以不覆盖方式发布此前不存在的目标,同步 namespace,并在返回句柄前通过当前格式校验重新打开。源永不移动或改变;只有可丢弃临时 stage 可以被移动、链接或移除。迁移不会合成中断轮次事件:agent-loop 通过写句柄追加这些修复,而只读查询路径在内存中补齐它们。
 
-规范文件名编码物理格式 generation:v0 是 `session.jsonl` 或 `session.jsonl.zstd`;每个正 generation 都是小写 `session.vN.jsonl` 或 `session.vN.jsonl.zstd`。发布绝不重命名、替换或删除已提交 generation 路径。目标已经存在时,只有它是普通当前格式文件且字节与预期完全相同时才接受;其他目标都会拒绝。低 generation 为 operator 检查或显式复制而保留,但普通 runtime 操作选择数值最高的规范名称,绝不把保留的前任当作自动 fallback、restore 或 downgrade 支持。
+规范文件名编码物理格式 generation:v0 是 `session.jsonl` 或 `session.jsonl.zstd`;每个正 generation 都是小写 `session.vN.jsonl` 或 `session.vN.jsonl.zstd`。`dsh-session-format` 拥有原始 basename 规则(`sessionFormatLogFilename`、`parseSessionFormatLogFilename`);JSONL provider、session-log 导出归档与 recorded-session fixture 只追加压缩后缀。发布绝不重命名、替换或删除已提交 generation 路径。目标已经存在时,只有它是普通当前格式文件且字节与预期完全相同时才接受;其他目标都会拒绝。低 generation 为 operator 检查或显式复制而保留,但普通 runtime 操作选择数值最高的规范名称,绝不把保留的前任当作自动 fallback、restore 或 downgrade 支持。
 
 当前格式快速路径从一个稳定源快照分类 header,不调用历史 converter,不写 generation,并把该快照交给当前格式解码,而不再次读取文件。解码日志进入现有按 revision 为键的有界 memo,供紧接的观察到恢复交接复用,而 `stat` 与 `list` 会有意重新扫描。多条迁移边保持原 generation 不变,并只发布最终目标;中间版本只存在于内存。源 fingerprint 重新检查会在内容变化时重启迁移,排他目标发布只在竞争胜者字节完全相同时接受它。跨进程 append 隔离不在此保证内。
 

+ 2 - 2
docs/event-producer-consumer.i18n.yaml

@@ -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 docs/event-producer-consumer.md
-event-producer-consumer.md: 5759a76782c874053834caee2a037fb1c5614f3c
-event-producer-consumer.zh.md: debfdc74cf0f86576b17269ede48dda8df014eb3
+event-producer-consumer.md: 62eba828604ba1e90d9d7ef22743cf10daf84f70
+event-producer-consumer.zh.md: f82c379516d9aec00dade2acdf3f8291a340da44

+ 18 - 18
docs/event-producer-consumer.md

@@ -9,24 +9,24 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | --- | --- | --- | --- | --- |
 | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:246`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
 | `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:80`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` |
-| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:206`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:215`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:347`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
-| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
-| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:252`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
-| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:233`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
-| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:278`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:291`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
-| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
-| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:264`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:225`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
-| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:335`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:584`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:564`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:591`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:570`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:577`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:315`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
+| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:204`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:213`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:345`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
+| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:242`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
+| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:250`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
+| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
+| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:276`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:289`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
+| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:305`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
+| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:262`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
+| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:223`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
+| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:333`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
+| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:581`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:561`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:588`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:567`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:574`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
 | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
 | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
 | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:81`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |

+ 18 - 18
docs/event-producer-consumer.zh.md

@@ -11,24 +11,24 @@
 | --- | --- | --- | --- | --- |
 | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:246`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
 | `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:80`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` |
-| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:206`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:215`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:347`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
-| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
-| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:252`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
-| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:233`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
-| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:278`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:291`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
-| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
-| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:264`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:225`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
-| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:335`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:584`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:564`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:591`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:570`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:577`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:315`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
+| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:204`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:213`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:345`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
+| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:242`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
+| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:250`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
+| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
+| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:276`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:289`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
+| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:305`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
+| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:262`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
+| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:223`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
+| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:333`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
+| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:581`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:561`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:588`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:567`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:574`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
 | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
 | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
 | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:81`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |

+ 2 - 2
docs/subsystems/core.i18n.yaml

@@ -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 docs/subsystems/core.md
-core.md: 8050447620bf882da898dc6d2cec9559716a3d5e
-core.zh.md: 236a17e31c4f7921126b5cf222f63de14f3bd725
+core.md: 2f907062e89b9af12cd1b47d52d195866df62254
+core.zh.md: a5649032b81cf4adf49253f74c46c46beb800969

+ 0 - 2
docs/subsystems/core.md

@@ -160,8 +160,6 @@ type AssistantStreamFrame =
     readonly attemptId: LlmAttemptId
     /** Monotone within one attached Agent lifecycle; replacement restarts at 1. */
     readonly revision: number
-    /** Safe-integer wall-clock time captured when this attempt started. */
-    readonly startedTime: number
     readonly turn: number
     readonly step: number
   }

+ 0 - 2
docs/subsystems/core.zh.md

@@ -164,8 +164,6 @@ type AssistantStreamFrame =
     readonly attemptId: LlmAttemptId
     /** Monotone within one attached Agent lifecycle; replacement restarts at 1. */
     readonly revision: number
-    /** Safe-integer wall-clock time captured when this attempt started. */
-    readonly startedTime: number
     readonly turn: number
     readonly step: number
   }

+ 2 - 2
packages/api/session-controller/README.i18n.yaml

@@ -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 packages/api/session-controller/README.md
-README.md: ae900465f46186e5fb9fcdd5a0dceb5a419b3cfe
-README.zh.md: 6bb3ad7804a43ac2869f684e8d120d3ce8de2303
+README.md: 1dbbeb34b845273d34b3323b6a4134da277126a0
+README.zh.md: 99c29a9043623b3d84fd12aeff42ec1781db699c

File diff suppressed because it is too large
+ 0 - 0
packages/api/session-controller/README.md


File diff suppressed because it is too large
+ 0 - 0
packages/api/session-controller/README.zh.md


+ 0 - 3
packages/api/session-controller/src/assistant-stream.ts

@@ -11,7 +11,6 @@ import type {
 
 interface MutableAttempt {
   readonly attemptId: SessionAssistantStreamAttempt['attemptId']
-  readonly startedTime: number
   readonly startedAfterSeq: SessionSeqCursor
   readonly turn: number
   readonly step: number
@@ -52,7 +51,6 @@ export class SessionAssistantStreamAccumulator {
       case 'start':
         this.activeAttempt = {
           attemptId: frame.attemptId,
-          startedTime: frame.startedTime,
           startedAfterSeq: durableCursor,
           turn: frame.turn,
           step: frame.step,
@@ -90,7 +88,6 @@ export class SessionAssistantStreamAccumulator {
       ...this.activeAttempt === undefined ? {} : {
         activeAttempt: {
           attemptId: this.activeAttempt.attemptId,
-          startedTime: this.activeAttempt.startedTime,
           startedAfterSeq: this.activeAttempt.startedAfterSeq,
           turn: this.activeAttempt.turn,
           step: this.activeAttempt.step,

+ 0 - 3
packages/api/session-controller/src/types.ts

@@ -436,8 +436,6 @@ export interface SessionFollowRequest {
 /** One active assistant attempt in a reconnect opening snapshot. */
 export interface SessionAssistantStreamAttempt {
   readonly attemptId: LlmAttemptId
-  /** Safe-integer wall-clock time copied from the attempt's start frame. */
-  readonly startedTime: number
   /** Last durable Session seq observed when this attempt started. */
   readonly startedAfterSeq: SessionSeqCursor
   readonly turn: number
@@ -460,7 +458,6 @@ export type SessionAssistantStreamFrame =
     readonly type: 'start'
     readonly attemptId: LlmAttemptId
     readonly revision: number
-    readonly startedTime: number
     readonly startedAfterSeq: SessionSeqCursor
     readonly turn: number
     readonly step: number

+ 1 - 2
packages/api/session-controller/tests/assistant-stream.client.spec.ts

@@ -57,7 +57,7 @@ function start(
   startedAfterSeq = -1,
 ): SessionAssistantStreamFrame {
   return {
-    type: 'start', attemptId, revision: 1, startedTime: 10,
+    type: 'start', attemptId, revision: 1,
     startedAfterSeq: startedAfterSeq === -1 ? -1 : SessionSeq(startedAfterSeq),
     turn: 1, step: 1,
   }
@@ -86,7 +86,6 @@ function baseline(nextIndex = 1): SessionAssistantStreamBaseline {
     revision: nextIndex + 1,
     activeAttempt: {
       attemptId: ATTEMPT,
-      startedTime: 10,
       startedAfterSeq: -1,
       turn: 1,
       step: 1,

+ 3 - 6
packages/api/session-controller/tests/assistant-stream.host.spec.ts

@@ -10,14 +10,12 @@ describe('SessionAssistantStreamAccumulator', () => {
     expect(accumulator.snapshot()).toBe(empty)
 
     accumulator.accept({
-      type: 'start', attemptId: LlmAttemptId('stale'), revision: 2,
-      startedTime: 1, turn: 1, step: 1,
+      type: 'start', attemptId: LlmAttemptId('stale'), revision: 2, turn: 1, step: 1,
     }, -1)
     expect(accumulator.snapshot()).toEqual({ revision: 2 })
 
     accumulator.accept({
-      type: 'start', attemptId: LlmAttemptId('current'), revision: 1,
-      startedTime: 2, turn: 2, step: 3,
+      type: 'start', attemptId: LlmAttemptId('current'), revision: 1, turn: 2, step: 3,
     }, SessionSeq(5))
     expect(accumulator.snapshot()).toMatchObject({
       revision: 1,
@@ -33,8 +31,7 @@ describe('SessionAssistantStreamAccumulator', () => {
     expect(accumulator.snapshot()).toEqual({ revision: 2 })
 
     accumulator.accept({
-      type: 'start', attemptId: LlmAttemptId('settled'), revision: 3,
-      startedTime: 3, turn: 2, step: 4,
+      type: 'start', attemptId: LlmAttemptId('settled'), revision: 3, turn: 2, step: 4,
     }, SessionSeq(8))
     accumulator.accept({
       type: 'chunk', attemptId: LlmAttemptId('settled'), revision: 4, index: 0,

+ 14 - 17
packages/api/session-controller/tests/session-history-journal.host.spec.ts

@@ -138,7 +138,7 @@ describe('Session history raw journal', () => {
     const attemptId = LlmAttemptId('buffered-opening-attempt')
     ctx.emit('agent/assistant-stream', {
       agent,
-      frame: { type: 'start', attemptId, revision: 1, startedTime: 1, turn: 1, step: 1 },
+      frame: { type: 'start', attemptId, revision: 1, turn: 1, step: 1 },
     })
     ctx.emit('agent/assistant-stream', {
       agent,
@@ -151,8 +151,7 @@ describe('Session history raw journal', () => {
     ctx.emit('agent/assistant-stream', {
       agent: { id: foreign.id, session: foreign, status: 'running', ctx } as Agent,
       frame: {
-        type: 'start', attemptId: LlmAttemptId('foreign-attempt'), revision: 1,
-        startedTime: 1, turn: 1, step: 1,
+        type: 'start', attemptId: LlmAttemptId('foreign-attempt'), revision: 1, turn: 1, step: 1,
       },
     })
     release.resolve(undefined)
@@ -180,7 +179,7 @@ describe('Session history raw journal', () => {
       ctx.emit('agent/assistant-stream', { agent, frame })
     }
     emit({
-      type: 'start', attemptId, revision: 1, startedTime: 100,
+      type: 'start', attemptId, revision: 1,
       turn: 1, step: 1,
     })
     const firstChunk = { type: 'text-delta', index: 0, text: 'a' } as const
@@ -202,7 +201,6 @@ describe('Session history raw journal', () => {
           revision: 2,
           activeAttempt: {
             attemptId,
-            startedTime: 100,
             startedAfterSeq: -1,
             turn: 1,
             step: 1,
@@ -247,7 +245,7 @@ describe('Session history raw journal', () => {
     ctx.emit('agent/assistant-stream', {
       agent,
       frame: {
-        type: 'start', attemptId, revision: 1, startedTime: 100,
+        type: 'start', attemptId, revision: 1,
         turn: 1, step: 1,
       },
     })
@@ -274,7 +272,6 @@ describe('Session history raw journal', () => {
             revision: 2,
             activeAttempt: {
               attemptId,
-              startedTime: 100,
               startedAfterSeq: -1,
               turn: 1,
               step: 1,
@@ -288,7 +285,7 @@ describe('Session history raw journal', () => {
       ctx.emit('agent/disposed', { agent })
       const replacementAgent = { id: session.id, session, status: 'running', ctx } as Agent
       const replacement: AssistantStreamFrame = {
-        type: 'start', attemptId, revision: 1, startedTime: 200,
+        type: 'start', attemptId, revision: 1,
         turn: 2, step: 1,
       }
       ctx.emit('agent/assistant-stream', { agent: replacementAgent, frame: replacement })
@@ -310,7 +307,7 @@ describe('Session history raw journal', () => {
     ctx.emit('agent/assistant-stream', {
       agent,
       frame: {
-        type: 'start', attemptId, revision: 1, startedTime: 100,
+        type: 'start', attemptId, revision: 1,
         turn: 1, step: 1,
       },
     })
@@ -350,7 +347,7 @@ describe('Session history raw journal', () => {
     ctx.emit('agent/assistant-stream', {
       agent,
       frame: {
-        type: 'start', attemptId, revision: 1, startedTime: 100,
+        type: 'start', attemptId, revision: 1,
         turn: 1, step: 1,
       },
     })
@@ -390,7 +387,7 @@ describe('Session history raw journal', () => {
     ctx.emit('agent/assistant-stream', {
       agent,
       frame: {
-        type: 'start', attemptId, revision: 1, startedTime: 100,
+        type: 'start', attemptId, revision: 1,
         turn: 1, step: 1,
       },
     })
@@ -466,7 +463,7 @@ describe('Session history raw journal', () => {
         agent: otherAgent,
         frame: {
           type: 'start', attemptId: LlmAttemptId('other-session-attempt'),
-          revision: 1, startedTime: 100, turn: 1, step: 1,
+          revision: 1, turn: 1, step: 1,
         },
       })
       const targetEvent = target.append('turn/start', { turn: 1 })
@@ -503,7 +500,7 @@ describe('Session history raw journal', () => {
       await observationStarted.promise
       const frame: AssistantStreamFrame = {
         type: 'start', attemptId: LlmAttemptId('opening-cut-attempt'),
-        revision: 1, startedTime: 100, turn: 1, step: 1,
+        revision: 1, turn: 1, step: 1,
       }
       ctx.emit('agent/assistant-stream', { agent, frame })
       releaseObservation.resolve(undefined)
@@ -538,7 +535,7 @@ describe('Session history raw journal', () => {
     ctx.emit('agent/assistant-stream', {
       agent,
       frame: {
-        type: 'start', attemptId, revision: 1, startedTime: 100,
+        type: 'start', attemptId, revision: 1,
         turn: 1, step: 1,
       },
     })
@@ -570,7 +567,7 @@ describe('Session history raw journal', () => {
       ctx.emit('agent/assistant-stream', {
         agent,
         frame: {
-          type: 'start', attemptId, revision: 1, startedTime: 200,
+          type: 'start', attemptId, revision: 1,
           turn: 2, step: 1,
         },
       })
@@ -582,7 +579,7 @@ describe('Session history raw journal', () => {
           assistantStream: {
             revision: 1,
             activeAttempt: {
-              attemptId, startedTime: 200, startedAfterSeq: -1,
+              attemptId, startedAfterSeq: -1,
               turn: 2, step: 1, nextIndex: 0, stream: [],
             },
           },
@@ -619,7 +616,7 @@ describe('Session history raw journal', () => {
     ctx.emit('agent/assistant-stream', {
       agent,
       frame: {
-        type: 'start', attemptId, revision: 1, startedTime: 200,
+        type: 'start', attemptId, revision: 1,
         turn: 1, step: 1,
       },
     })

+ 3 - 5
packages/api/session-controller/tests/sessions-service.client.spec.ts

@@ -171,7 +171,7 @@ describe('scope tree', () => {
     await b.api.pushFollow(sid('s1'), {
       type: 'assistant-stream',
       frame: {
-        type: 'start', attemptId, revision: 1, startedTime: 1, startedAfterSeq: -1,
+        type: 'start', attemptId, revision: 1, startedAfterSeq: -1,
         turn: 1, step: 1,
       },
     })
@@ -216,7 +216,7 @@ describe('scope tree', () => {
     b.api.assistantStreamBaseline = {
       revision: 2,
       activeAttempt: {
-        attemptId, startedTime: 1, startedAfterSeq: -1, turn: 1, step: 1,
+        attemptId, startedAfterSeq: -1, turn: 1, step: 1,
         nextIndex: 1,
         stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [], texts: ['a'] }],
       },
@@ -233,7 +233,7 @@ describe('scope tree', () => {
     b.api.assistantStreamBaseline = {
       revision: 3,
       activeAttempt: {
-        attemptId, startedTime: 1, startedAfterSeq: -1, turn: 1, step: 1,
+        attemptId, startedAfterSeq: -1, turn: 1, step: 1,
         nextIndex: 2,
         stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [1], texts: ['a', 'b'] }],
       },
@@ -298,7 +298,6 @@ describe('scope tree', () => {
       revision: 2,
       activeAttempt: {
         attemptId,
-        startedTime: 20,
         startedAfterSeq: SessionSeq(0),
         turn: 1,
         step: 1,
@@ -373,7 +372,6 @@ describe('scope tree', () => {
       revision: 2,
       activeAttempt: {
         attemptId,
-        startedTime: 20,
         startedAfterSeq: SessionSeq(0),
         turn: 1,
         step: 1,

+ 3 - 7
packages/api/session-controller/tests/transport.client.spec.ts

@@ -144,7 +144,6 @@ describe('Session Client stream adapters', () => {
       revision: 2,
       activeAttempt: {
         attemptId,
-        startedTime: 1,
         startedAfterSeq: -1,
         turn: 1,
         step: 1,
@@ -212,7 +211,7 @@ describe('Session Client stream adapters', () => {
     const remote = new ScriptedSessionRemote([{
       frames: [assistantFrame({
         type: 'start', attemptId: LlmAttemptId('pre-opening-attempt'),
-        revision: 1, startedTime: 1, startedAfterSeq: -1, turn: 1, step: 1,
+        revision: 1, startedAfterSeq: -1, turn: 1, step: 1,
       })],
     }], [])
     const stream = new SessionEventStream(sessionClient(remote), ADDRESS, {
@@ -234,7 +233,7 @@ describe('Session Client stream adapters', () => {
   it('rebaselines after a transient assistant revision gap without advancing the durable cursor', async () => {
     const attemptId = LlmAttemptId('gapped-attempt')
     const start: SessionAssistantStreamFrame = {
-      type: 'start', attemptId, revision: 1, startedTime: 1, startedAfterSeq: -1,
+      type: 'start', attemptId, revision: 1, startedAfterSeq: -1,
       turn: 1, step: 1,
     }
     const gap: SessionAssistantStreamFrame = {
@@ -245,7 +244,6 @@ describe('Session Client stream adapters', () => {
       revision: 3,
       activeAttempt: {
         attemptId,
-        startedTime: 1,
         startedAfterSeq: -1,
         turn: 1,
         step: 1,
@@ -289,7 +287,6 @@ describe('Session Client stream adapters', () => {
       revision: 2,
       activeAttempt: {
         attemptId,
-        startedTime: 1,
         startedAfterSeq: -1,
         turn: 1,
         step: 1,
@@ -298,14 +295,13 @@ describe('Session Client stream adapters', () => {
       },
     }
     const replacementStart: SessionAssistantStreamFrame = {
-      type: 'start', attemptId, revision: 1, startedTime: 2, startedAfterSeq: -1,
+      type: 'start', attemptId, revision: 1, startedAfterSeq: -1,
       turn: 2, step: 1,
     }
     const replacement: SessionAssistantStreamBaseline = {
       revision: 1,
       activeAttempt: {
         attemptId,
-        startedTime: 2,
         startedAfterSeq: -1,
         turn: 2,
         step: 1,

+ 1 - 2
packages/bundle/headless/tests/headless.spec.ts

@@ -26,8 +26,7 @@ function startFrames(agent: Agent, turn = 1, step = 1): void {
   agent.ctx.emit('agent/assistant-stream', {
     agent,
     frame: {
-      type: 'start', attemptId: state.attemptId, revision: state.revision,
-      startedTime: Date.now(), turn, step,
+      type: 'start', attemptId: state.attemptId, revision: state.revision, turn, step,
     },
   })
 }

+ 2 - 7
packages/client/connection/src/client/fixture.ts

@@ -158,7 +158,6 @@ interface FixtureAssistantStreamBaseline {
   readonly revision: number
   readonly activeAttempt?: {
     readonly attemptId: ReturnType<typeof LlmAttemptId>
-    readonly startedTime: number
     readonly startedAfterSeq: number
     readonly turn: number
     readonly step: number
@@ -173,7 +172,6 @@ export type FixtureAssistantStreamFrame =
     readonly type: 'start'
     readonly attemptId: ReturnType<typeof LlmAttemptId>
     readonly revision: number
-    readonly startedTime: number
     readonly startedAfterSeq: SessionSeqCursor
     readonly turn: number
     readonly step: number
@@ -2051,7 +2049,6 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
   const followConns = new Map<SessionId, Set<StreamConn<FixtureFollowEventFrame>>>()
   interface FixtureAttemptState {
     readonly attemptId: ReturnType<typeof LlmAttemptId>
-    readonly startedTime: number
     readonly startedAfterSeq: number
     readonly turn: number
     readonly step: number
@@ -2087,17 +2084,16 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
   }
   const beginAssistant = (sessionId: SessionId, turn: number, step: number): FixtureAttemptState => {
     const attemptId = LlmAttemptId(`${sessionId}:fixture:${String(nextAssistantRevision(sessionId))}`)
-    const startedTime = Date.now()
     const lastSeq = logOf(sessionId).length - 1
     const startedAfterSeq = lastSeq < 0 ? -1 : SessionSeq(lastSeq)
     const attempt = {
-      attemptId, startedTime, startedAfterSeq, turn, step,
+      attemptId, startedAfterSeq, turn, step,
       stream: new AssistantStreamAccumulator(), index: 0,
     }
     activeAttempts.set(sessionId, attempt)
     emitAssistant(sessionId, {
       type: 'start', attemptId, revision: assistantRevisions.get(sessionId) as number,
-      startedTime, startedAfterSeq, turn, step,
+      startedAfterSeq, turn, step,
     })
     return attempt
   }
@@ -3352,7 +3348,6 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
               ? {}
               : { activeAttempt: {
                 attemptId: (activeAttempts.get(sessionId) as FixtureAttemptState).attemptId,
-                startedTime: (activeAttempts.get(sessionId) as FixtureAttemptState).startedTime,
                 startedAfterSeq: (activeAttempts.get(sessionId) as FixtureAttemptState).startedAfterSeq,
                 turn: (activeAttempts.get(sessionId) as FixtureAttemptState).turn,
                 step: (activeAttempts.get(sessionId) as FixtureAttemptState).step,

+ 0 - 1
packages/client/connection/tests/fixture.client.spec.ts

@@ -76,7 +76,6 @@ type FixtureFollowFrame =
       readonly revision: number
       readonly activeAttempt?: {
         readonly attemptId: string
-        readonly startedTime: number
         readonly startedAfterSeq: number
         readonly turn: number
         readonly step: number

+ 0 - 1
packages/core/agent-loop/src/assistant-stream.ts

@@ -51,7 +51,6 @@ export class AssistantStreamAttempt {
       type: 'start',
       attemptId: this.attemptId,
       revision: this.nextRevision(),
-      startedTime: Date.now(),
       turn: this.turn,
       step: this.step,
     })

+ 0 - 2
packages/core/agent-loop/tests/loop.spec.ts

@@ -74,8 +74,6 @@ describe('agent loop', () => {
     expect(frames.at(0)?.type).toBe('start')
     const start = frames.at(0)
     if (start?.type === 'start') {
-      expect(typeof start.startedTime).toBe('number')
-      expect(Number.isSafeInteger(start.startedTime)).toBe(true)
     }
     expect(frames.at(-1)?.type).toBe('end')
     expect(frames.map(frame => frame.revision)).toEqual(

+ 2 - 2
packages/core/agent/README.i18n.yaml

@@ -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 packages/core/agent/README.md
-README.md: 1a5392cd3d83c4b56150d053aa37e3e4b49d7360
-README.zh.md: 357ca7210f786a54796c8f24801cc15e47ea90d5
+README.md: 37d5d24192ed09bfdeb5210cdd9fcf2c2bd058e2
+README.zh.md: bb56687edaa9debc937908ff26770bbce92a8a80

+ 1 - 1
packages/core/agent/README.md

@@ -64,7 +64,7 @@ await handle.agent.whenIdle()
 
 ### Intercept or observe work in flight
 
-The `agent/*` events let plugins act on live work without depending on the loop package. `agent/pre-step` can reject a proposed step or replace the messages entering it; `agent/request-error` lets a listener retry a failed model request; `agent/turn-stopping` runs before an otherwise completed turn closes and can steer to keep it open. `agent/assistant-stream` carries one process-local Assistant attempt's ordered start, transient chunk, and end frames. Start records the safe-integer wall-clock `startedTime`, chunk indexes are dense from zero, and `end.index` is the next chunk position. The loop commits the complete compact stream as one `assistant/message` or `assistant/attempt` before a committed end frame, so the live event remains presentation data rather than the replay source. `agent/status`, `agent/created`, and `agent/disposed` drive UI and coordination state, and the per-message `agent/inbox/*` notifications keep inbox projections in sync. Exact signatures, dispatch modes, and payload contracts live in the generated region of the [core subsystem page](../../../docs/subsystems/core.md#cordis-surface).
+The `agent/*` events let plugins act on live work without depending on the loop package. `agent/pre-step` can reject a proposed step or replace the messages entering it; `agent/request-error` lets a listener retry a failed model request; `agent/turn-stopping` runs before an otherwise completed turn closes and can steer to keep it open. `agent/assistant-stream` carries one process-local Assistant attempt's ordered start, transient chunk, and end frames. Start names the attempt's turn and step, chunk indexes are dense from zero, and `end.index` is the next chunk position. The loop commits the complete compact stream as one `assistant/message` or `assistant/attempt` before a committed end frame, so the live event remains presentation data rather than the replay source. `agent/status`, `agent/created`, and `agent/disposed` drive UI and coordination state, and the per-message `agent/inbox/*` notifications keep inbox projections in sync. Exact signatures, dispatch modes, and payload contracts live in the generated region of the [core subsystem page](../../../docs/subsystems/core.md#cordis-surface).
 
 -----
 

+ 1 - 1
packages/core/agent/README.zh.md

@@ -64,7 +64,7 @@ await handle.agent.whenIdle()
 
 ### 拦截或观察进行中的工作
 
-`agent/*` 事件让插件无需依赖循环包即可作用于实时工作。`agent/pre-step` 可以拒绝拟进入的步骤或替换进入它的消息;`agent/request-error` 让监听器重试失败的模型请求;`agent/turn-stopping` 在本可完成的轮次关闭前运行,并可通过 steer 使其保持打开。`agent/assistant-stream` 携带一个进程本地 Assistant attempt 的有序 start、瞬态 chunk 与 end frame。start 会把壁钟时间记录为安全整数 `startedTime`,chunk index 从零开始密集递增,`end.index` 则是下一个 chunk 位置。loop 会在 committed end frame 前把完整紧凑 stream 提交为一个 `assistant/message` 或 `assistant/attempt`,因此 live event 仍是呈现数据而非重放来源。`agent/status`、`agent/created` 与 `agent/disposed` 驱动 UI 与协调状态,逐消息的 `agent/inbox/*` 通知则让收件箱投影保持同步。确切签名、分发 mode 与 payload 约定见 [core 子系统页](../../../docs/subsystems/core.zh.md#cordis-surface) 的生成区块。
+`agent/*` 事件让插件无需依赖循环包即可作用于实时工作。`agent/pre-step` 可以拒绝拟进入的步骤或替换进入它的消息;`agent/request-error` 让监听器重试失败的模型请求;`agent/turn-stopping` 在本可完成的轮次关闭前运行,并可通过 steer 使其保持打开。`agent/assistant-stream` 携带一个进程本地 Assistant attempt 的有序 start、瞬态 chunk 与 end frame。start 给出该 attempt 的 turn 与 step,chunk index 从零开始密集递增,`end.index` 则是下一个 chunk 位置。loop 会在 committed end frame 前把完整紧凑 stream 提交为一个 `assistant/message` 或 `assistant/attempt`,因此 live event 仍是呈现数据而非重放来源。`agent/status`、`agent/created` 与 `agent/disposed` 驱动 UI 与协调状态,逐消息的 `agent/inbox/*` 通知则让收件箱投影保持同步。确切签名、分发 mode 与 payload 约定见 [core 子系统页](../../../docs/subsystems/core.zh.md#cordis-surface) 的生成区块。
 
 -----
 

+ 0 - 2
packages/core/agent/src/runtime-types.ts

@@ -77,8 +77,6 @@ export type AssistantStreamFrame =
     readonly attemptId: LlmAttemptId
     /** Monotone within one attached Agent lifecycle; replacement restarts at 1. */
     readonly revision: number
-    /** Safe-integer wall-clock time captured when this attempt started. */
-    readonly startedTime: number
     readonly turn: number
     readonly step: number
   }

+ 1 - 2
packages/core/scope/tests/invariant.spec.ts

@@ -57,8 +57,7 @@ describe('scoped-dispatch invariants', () => {
       'agent/assistant-stream': [{
         agent,
         frame: {
-          type: 'start', attemptId: 'attempt-1' as never, revision: 1,
-          startedTime: 1, turn: 1, step: 1,
+          type: 'start', attemptId: 'attempt-1' as never, revision: 1, turn: 1, step: 1,
         },
       }],
       'agent/request-error': [

+ 3 - 3
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -3512,7 +3512,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'AssistantStreamFrame',
-    declaration: 'export type AssistantStreamFrame = {\n    readonly type: \'start\';\n    readonly attemptId: LlmAttemptId;\n    readonly revision: number;\n    readonly startedTime: number;\n    readonly turn: number;\n    readonly step: number;\n} | {\n    readonly type: \'chunk\';\n    readonly attemptId: LlmAttemptId;\n    readonly revision: number;\n    readonly index: number;\n    readonly time: number;\n    readonly chunk: StreamChunk;\n} | {\n    readonly type: \'end\';\n    readonly attemptId: LlmAttemptId;\n    readonly revision: number;\n    readonly index: number;\n    readonly outcome: {\n        readonly kind: \'committed\';\n        readonly eventType: \'assistant/message\' | \'assistant/attempt\';\n        readonly seq: SessionSeq;\n    } | {\n        readonly kind: \'abandoned\';\n    };\n};',
+    declaration: 'export type AssistantStreamFrame = {\n    readonly type: \'start\';\n    readonly attemptId: LlmAttemptId;\n    readonly revision: number;\n    readonly turn: number;\n    readonly step: number;\n} | {\n    readonly type: \'chunk\';\n    readonly attemptId: LlmAttemptId;\n    readonly revision: number;\n    readonly index: number;\n    readonly time: number;\n    readonly chunk: StreamChunk;\n} | {\n    readonly type: \'end\';\n    readonly attemptId: LlmAttemptId;\n    readonly revision: number;\n    readonly index: number;\n    readonly outcome: {\n        readonly kind: \'committed\';\n        readonly eventType: \'assistant/message\' | \'assistant/attempt\';\n        readonly seq: SessionSeq;\n    } | {\n        readonly kind: \'abandoned\';\n    };\n};',
   },
   {
     name: 'AssistantStreamRecord',
@@ -4780,7 +4780,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'SessionAssistantStreamAttempt',
-    declaration: 'export interface SessionAssistantStreamAttempt {\n    readonly attemptId: LlmAttemptId;\n    readonly startedTime: number;\n    readonly startedAfterSeq: SessionSeqCursor;\n    readonly turn: number;\n    readonly step: number;\n    readonly nextIndex: number;\n    readonly stream: readonly JsonValue[];\n}',
+    declaration: 'export interface SessionAssistantStreamAttempt {\n    readonly attemptId: LlmAttemptId;\n    readonly startedAfterSeq: SessionSeqCursor;\n    readonly turn: number;\n    readonly step: number;\n    readonly nextIndex: number;\n    readonly stream: readonly JsonValue[];\n}',
   },
   {
     name: 'SessionAssistantStreamBaseline',
@@ -4788,7 +4788,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'SessionAssistantStreamFrame',
-    declaration: 'export type SessionAssistantStreamFrame = {\n    readonly type: \'start\';\n    readonly attemptId: LlmAttemptId;\n    readonly revision: number;\n    readonly startedTime: number;\n    readonly startedAfterSeq: SessionSeqCursor;\n    readonly turn: number;\n    readonly step: number;\n} | {\n    readonly type: \'chunk\';\n    readonly attemptId: LlmAttemptId;\n    readonly revision: number;\n    readonly index: number;\n    readonly time: number;\n    readonly chunk: JsonValue;\n} | {\n    readonly type: \'end\';\n    readonly attemptId: LlmAttemptId;\n    readonly revision: number;\n    readonly index: number;\n    readonly outcome: {\n        readonly kind: \'committed\';\n        readonly eventType: \'assistant/message\' | \'assistant/attempt\';\n        readonly seq: number;\n    } | {\n        readonly kind: \'abandoned\';\n    };\n};',
+    declaration: 'export type SessionAssistantStreamFrame = {\n    readonly type: \'start\';\n    readonly attemptId: LlmAttemptId;\n    readonly revision: number;\n    readonly startedAfterSeq: SessionSeqCursor;\n    readonly turn: number;\n    readonly step: number;\n} | {\n    readonly type: \'chunk\';\n    readonly attemptId: LlmAttemptId;\n    readonly revision: number;\n    readonly index: number;\n    readonly time: number;\n    readonly chunk: JsonValue;\n} | {\n    readonly type: \'end\';\n    readonly attemptId: LlmAttemptId;\n    readonly revision: number;\n    readonly index: number;\n    readonly outcome: {\n        readonly kind: \'committed\';\n        readonly eventType: \'assistant/message\' | \'assistant/attempt\';\n        readonly seq: number;\n    } | {\n        readonly kind: \'abandoned\';\n    };\n};',
   },
   {
     name: 'SessionAttachmentRequest',

+ 1 - 0
packages/session-query/session-log-export/package.json

@@ -36,6 +36,7 @@
   },
   "license": "MIT",
   "dependencies": {
+    "@deepseek-ai/dsh-session-format": "workspace:^",
     "@deepseek-ai/schemastery": "workspace:^",
     "fflate": "^0.8.2",
     "@deepseek-ai/dsh-brand": "workspace:^"

+ 2 - 1
packages/session-query/session-log-export/src/archive.ts

@@ -26,6 +26,7 @@ import type { Context } from '@deepseek-ai/cordis'
 import type { AttachmentStore, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
 import type { SessionLineageNode, SessionQueryEngine } from '@deepseek-ai/dsh-session-query'
 import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
+import { sessionFormatLogFilename } from '@deepseek-ai/dsh-session-format'
 import type { SessionEvent, SessionHeader, SessionId, SessionStore } from '@deepseek-ai/dsh-session'
 import type { SessionHandle, SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
 import { SessionPersistenceNotFoundError } from '@deepseek-ai/dsh-session-persistence'
@@ -94,7 +95,7 @@ export type SessionLogZipEntry =
   | { readonly path: string; readonly data: Uint8Array }
 
 /** The current generation's canonical base filename for every exported session log. */
-export const SESSION_LOG_FILENAME = `session.v${SESSION_FORMAT_VERSION}.jsonl`
+export const SESSION_LOG_FILENAME = sessionFormatLogFilename(SESSION_FORMAT_VERSION)
 
 /**
  * Serialize one session's logical log as canonical JSONL text: the header

+ 1 - 0
packages/session-query/session-log-export/tsconfig.host.json

@@ -16,6 +16,7 @@
     { "path": "../../util/brand" },
     { "path": "../../core/session" },
     { "path": "../../interaction/commands" },
+    { "path": "../../session/session-format" },
     { "path": "../../session/session-persistence" },
     { "path": "../session-query" }
   ]

+ 2 - 1
packages/session/session-format-v0-to-v1/package.json

@@ -39,7 +39,8 @@
     }
   },
   "dependencies": {
-    "@deepseek-ai/dsh-session-format": "workspace:^"
+    "@deepseek-ai/dsh-session-format": "workspace:^",
+    "@deepseek-ai/dsh-util-values": "workspace:^"
   },
   "peerDependencies": {
     "@deepseek-ai/cordis": "workspace:^"

+ 2 - 18
packages/session/session-format-v0-to-v1/src/relationships.ts

@@ -1,3 +1,4 @@
+import { deepEqualJson } from '@deepseek-ai/dsh-util-values'
 import { SessionFormatError } from '@deepseek-ai/dsh-session-format'
 import type { SessionFormatArtifact, SessionFormatEvent, SessionFormatJsonValue } from '@deepseek-ai/dsh-session-format'
 import { releasedV0Record } from './validation-helpers.ts'
@@ -190,7 +191,7 @@ export function assertReleasedArtifactRelationships(
           const start = ptcStarts.get(child)
           if (start === undefined || start.settled) throw new SessionFormatError('tool/code-dispatch has no unique start')
           if (start.root !== root || start.parent !== parent || start.name !== data['name']
-            || !sameJson(start.arguments, data['arguments'] as SessionFormatJsonValue)) {
+            || !deepEqualJson(start.arguments, data['arguments'])) {
             throw new SessionFormatError('tool/code-dispatch does not match its start')
           }
           start.settled = true
@@ -338,23 +339,6 @@ function assertRetryChain(
   }
 }
 
-function sameJson(left: SessionFormatJsonValue, right: SessionFormatJsonValue): boolean {
-  if (left === right) return true
-  if (Array.isArray(left) || Array.isArray(right)) {
-    if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false
-    const leftArray = left as readonly SessionFormatJsonValue[]
-    const rightArray = right as readonly SessionFormatJsonValue[]
-    return leftArray.every((value, index) => sameJson(value, rightArray[index] as SessionFormatJsonValue))
-  }
-  if (typeof left !== 'object' || left === null || typeof right !== 'object' || right === null) return false
-  const leftKeys = Object.keys(left)
-  const leftRecord = left as Record<string, SessionFormatJsonValue>
-  const rightRecord = right as Record<string, SessionFormatJsonValue>
-  return leftKeys.length === Object.keys(right).length
-    && leftKeys.every(key => Object.hasOwn(right, key)
-      && sameJson(leftRecord[key] as SessionFormatJsonValue, rightRecord[key] as SessionFormatJsonValue))
-}
-
 function requireOpenStep(
   event: SessionFormatEvent,
   data: Record<string, SessionFormatJsonValue>,

+ 3 - 0
packages/session/session-format-v0-to-v1/tsconfig.json

@@ -19,6 +19,9 @@
     },
     {
       "path": "../session-format"
+    },
+    {
+      "path": "../../util/values"
     }
   ]
 }

+ 2 - 2
packages/session/session-format/README.i18n.yaml

@@ -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 packages/session/session-format/README.md
-README.md: cd95942128531b2a8b16b7ffd29ac42753d9b889
-README.zh.md: 812874b8b8966c6a0f1297b024601c4dce36fa96
+README.md: ffaa1f7a610fb915227c260d4fe2d4d1b60c04e9
+README.zh.md: ab9f0dd8d66d4f92bcdf2a136f76b9b7587037f8

+ 1 - 0
packages/session/session-format/README.md

@@ -55,6 +55,7 @@ The chain validates unique gap-free ordering at construction. A current artifact
 | [`src/chain.ts`](src/chain.ts) | Adjacent plan construction and current bypass |
 | [`src/catalog.ts`](src/catalog.ts) | Physical version dispatch and header classification |
 | [`src/json.ts`](src/json.ts) | Detached lossless JSON snapshots and common coordinate checks |
+| [`src/filename.ts`](src/filename.ts) | Canonical `session[.vN].jsonl` basename shared by persistence, export, and fixtures |
 
 </details>
 

+ 1 - 0
packages/session/session-format/README.zh.md

@@ -55,6 +55,7 @@ const descriptor = catalog.readHeader(physicalHeader)
 | [`src/chain.ts`](src/chain.ts) | 相邻计划构造与当前格式绕过 |
 | [`src/catalog.ts`](src/catalog.ts) | 物理版本分派与标头分类 |
 | [`src/json.ts`](src/json.ts) | 分离的无损 JSON 快照与通用坐标校验 |
+| [`src/filename.ts`](src/filename.ts) | 持久化、导出与 fixture 共用的规范 `session[.vN].jsonl` 文件名 |
 
 </details>
 

+ 3 - 0
packages/session/session-format/package.json

@@ -26,6 +26,9 @@
     "lib/types/**/*.d.ts"
   ],
   "license": "MIT",
+  "dependencies": {
+    "@deepseek-ai/dsh-util-values": "workspace:^"
+  },
   "peerDependencies": {
     "@deepseek-ai/cordis": "workspace:^"
   },

+ 32 - 0
packages/session/session-format/src/filename.ts

@@ -0,0 +1,32 @@
+/** Canonical raw log basename shared by every generation-addressed Session artifact. */
+
+import { sessionFormatVersion } from './json.ts'
+
+const CANONICAL_LOG_FILENAME = /^session(?:\.v([1-9][0-9]*))?\.jsonl$/u
+
+/**
+ * Name the raw JSONL log of one immutable Session format generation. Version
+ * zero keeps the original `session.jsonl`; every later generation carries a
+ * lowercase numeric `.vN` component before the `.jsonl` suffix.
+ * @param version - non-negative safe integer Session format version.
+ * @returns the canonical basename, without any compression suffix.
+ */
+export function sessionFormatLogFilename(version: number): string {
+  const generation = sessionFormatVersion(version, 'Session log generation version')
+  return generation === 0 ? 'session.jsonl' : `session.v${generation}.jsonl`
+}
+
+/**
+ * Read the generation named by one raw JSONL log basename. Temporary,
+ * uppercase, leading-zero, `.v0`, and compression-suffixed names are not
+ * canonical.
+ * @param filename - one basename from a Session directory or archive.
+ * @returns its Session format version, or `undefined` when the name is not canonical.
+ */
+export function parseSessionFormatLogFilename(filename: string): number | undefined {
+  const match = CANONICAL_LOG_FILENAME.exec(filename)
+  if (match === null) return undefined
+  if (match[1] === undefined) return 0
+  const version = Number(match[1])
+  return Number.isSafeInteger(version) ? version : undefined
+}

+ 1 - 0
packages/session/session-format/src/index.ts

@@ -3,5 +3,6 @@
 export * from './chain.ts'
 export * from './catalog.ts'
 export * from './error.ts'
+export * from './filename.ts'
 export * from './json.ts'
 export * from './types.ts'

+ 5 - 56
packages/session/session-format/src/json.ts

@@ -1,3 +1,4 @@
+import { deepFreeze, snapshotJsonValue } from '@deepseek-ai/dsh-util-values'
 import { SessionFormatError } from './error.ts'
 import type {
   SessionFormatArtifact,
@@ -70,63 +71,11 @@ export function inspectSessionFormatVersion(headerValue: unknown): number {
  * @returns an immutable detached JSON snapshot.
  */
 export function snapshotSessionFormatJson(value: unknown, label = 'Session value'): SessionFormatJsonValue {
-  return snapshotValue(value, label, new Set<object>())
-}
-
-function snapshotValue(value: unknown, label: string, ancestors: Set<object>): SessionFormatJsonValue {
-  if (value === null || typeof value === 'string' || typeof value === 'boolean') return value
-  if (typeof value === 'number') {
-    if (!Number.isFinite(value) || Object.is(value, -0)) {
-      throw new SessionFormatError(`${label} contains a number that JSON cannot preserve`)
-    }
-    return value
-  }
-  if (typeof value !== 'object') throw new SessionFormatError(`${label} contains a non-JSON value`)
-  if (ancestors.has(value)) throw new SessionFormatError(`${label} contains a cycle`)
-  ancestors.add(value)
-  try {
-    if (Array.isArray(value)) {
-      if (Reflect.getPrototypeOf(value) !== Array.prototype) {
-        throw new SessionFormatError(`${label} contains a non-intrinsic JSON array`)
-      }
-      const ownKeys = Reflect.ownKeys(value)
-      const expectedKeys = new Set(['length', ...Array.from({ length: value.length }, (_, index) => String(index))])
-      if (ownKeys.some(key => typeof key !== 'string' || !expectedKeys.has(key))) {
-        throw new SessionFormatError(`${label} contains an array property that JSON cannot preserve`)
-      }
-      for (let index = 0; index < value.length; index += 1) {
-        if (!Object.hasOwn(value, index)) throw new SessionFormatError(`${label} contains a sparse array`)
-        const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index)) as PropertyDescriptor
-        if (!descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
-          throw new SessionFormatError(`${label} contains an array member that JSON cannot preserve`)
-        }
-      }
-      return Object.freeze(value.map(member => snapshotValue(member, label, ancestors)))
-    }
-    const prototype = Reflect.getPrototypeOf(value)
-    if (prototype !== Object.prototype && prototype !== null) {
-      throw new SessionFormatError(`${label} contains a non-plain object`)
-    }
-    const output: Record<string, SessionFormatJsonValue> = {}
-    for (const key of Reflect.ownKeys(value)) {
-      if (typeof key !== 'string') {
-        throw new SessionFormatError(`${label} contains a symbol property that JSON cannot preserve`)
-      }
-      const descriptor = Reflect.getOwnPropertyDescriptor(value, key) as PropertyDescriptor
-      if (!descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
-        throw new SessionFormatError(`${label} contains a property that JSON cannot preserve`)
-      }
-      Object.defineProperty(output, key, {
-        configurable: true,
-        enumerable: true,
-        value: snapshotValue(descriptor.value, label, ancestors),
-        writable: true,
-      })
-    }
-    return Object.freeze(output)
-  } finally {
-    ancestors.delete(value)
+  const snapshot = snapshotJsonValue(value)
+  if (snapshot === undefined) {
+    throw new SessionFormatError(`${label} is not lossless JSON`)
   }
+  return deepFreeze(snapshot) as SessionFormatJsonValue
 }
 
 /**

+ 31 - 0
packages/session/session-format/tests/filename.spec.ts

@@ -0,0 +1,31 @@
+import { describe, expect, it } from 'vitest'
+import { parseSessionFormatLogFilename, sessionFormatLogFilename } from '../src/index.ts'
+
+describe('canonical Session log filenames', () => {
+  it('names version zero without a generation component and later generations with .vN', () => {
+    expect(sessionFormatLogFilename(0)).toBe('session.jsonl')
+    expect(sessionFormatLogFilename(1)).toBe('session.v1.jsonl')
+    expect(sessionFormatLogFilename(27)).toBe('session.v27.jsonl')
+    for (const invalid of [-1, -0, 1.5, Number.MAX_SAFE_INTEGER + 1]) {
+      expect(() => sessionFormatLogFilename(invalid)).toThrow(/non-negative safe integer/)
+    }
+  })
+
+  it('parses only canonical raw generation names', () => {
+    expect(parseSessionFormatLogFilename('session.jsonl')).toBe(0)
+    expect(parseSessionFormatLogFilename('session.v1.jsonl')).toBe(1)
+    expect(parseSessionFormatLogFilename('session.v27.jsonl')).toBe(27)
+    expect(parseSessionFormatLogFilename('session.v9007199254740992.jsonl')).toBeUndefined()
+    for (const name of [
+      'session.v0.jsonl',
+      'session.v01.jsonl',
+      'session.V1.jsonl',
+      'session.v1.backup.jsonl',
+      'session.migration.deadbeef.tmp.jsonl',
+      'session.v1.jsonl.zstd',
+      'session.1.jsonl',
+    ]) {
+      expect(parseSessionFormatLogFilename(name)).toBeUndefined()
+    }
+  })
+})

+ 4 - 6
packages/session/session-format/tests/json.spec.ts

@@ -16,11 +16,9 @@ describe('lossless Session format JSON snapshots', () => {
     ['sparse array', Array(1)],
     ['symbol member', { [Symbol('hidden')]: true }],
     ['non-enumerable member', Object.defineProperty({}, 'hidden', { value: true })],
-    ['accessor member', Object.defineProperty({}, 'value', { enumerable: true, get: () => 1 })],
     ['array property', Object.assign([], { extra: true })],
-    ['array accessor', Object.defineProperty([1], '0', { enumerable: true, get: () => 1 })],
   ])('refuses %s that JSON cannot preserve', (_name, value) => {
-    expect(() => snapshotSessionFormatJson(value)).toThrow(/JSON|sparse|member|property|number/)
+    expect(() => snapshotSessionFormatJson(value, 'payload')).toThrow('payload is not lossless JSON')
   })
 
   it('detaches, freezes, and retains repeated non-cyclic values and __proto__ keys', () => {
@@ -46,9 +44,9 @@ describe('lossless Session format JSON snapshots', () => {
     expect(() => sessionFormatCount(-1, 'count')).toThrow(/non-negative/)
     expect(() => sessionFormatSafeInteger(1.5, 'integer')).toThrow(/safe integer/)
     expect(() => inspectSessionFormatVersion([])).toThrow(/header/)
-    expect(() => snapshotSessionFormatJson(cyclic)).toThrow(/cycle/)
-    expect(() => snapshotSessionFormatJson(new RecordValue())).toThrow(/non-plain/)
-    expect(() => snapshotSessionFormatJson(new ArrayValue(1))).toThrow(/non-intrinsic/)
+    expect(() => snapshotSessionFormatJson(cyclic)).toThrow(/not lossless JSON/)
+    expect(() => snapshotSessionFormatJson(new RecordValue())).toThrow(/not lossless JSON/)
+    expect(() => snapshotSessionFormatJson(new ArrayValue(1))).toThrow(/not lossless JSON/)
   })
 
   it.each([

+ 3 - 0
packages/session/session-format/tsconfig.json

@@ -13,6 +13,9 @@
     },
     {
       "path": "../../../vendor/cordis"
+    },
+    {
+      "path": "../../util/values"
     }
   ]
 }

+ 1 - 0
packages/session/session-persistence-jsonl/package.json

@@ -32,6 +32,7 @@
     "@deepseek-ai/cordis": "workspace:^"
   },
   "dependencies": {
+    "@deepseek-ai/dsh-session-format": "workspace:^",
     "@deepseek-ai/dsh-session-format-catalog": "workspace:^",
     "koffi": "^3.1.0",
     "@deepseek-ai/schemastery": "workspace:^"

+ 10 - 12
packages/session/session-persistence-jsonl/src/format.ts

@@ -19,6 +19,7 @@ import type {
   SessionId,
   SessionLogOffset as SessionLogOffsetType,
 } from '@deepseek-ai/dsh-session'
+import { parseSessionFormatLogFilename, sessionFormatLogFilename } from '@deepseek-ai/dsh-session-format'
 import {
   SessionFormatUnsupportedError,
   sessionFormatVersionRefusal,
@@ -34,7 +35,11 @@ export type JsonlCompression = 'zstd' | 'none'
  * @returns `.jsonl.zstd` for Zstandard or `.jsonl` for plaintext.
  */
 export function logSuffix(compression: JsonlCompression): '.jsonl.zstd' | '.jsonl' {
-  return compression === 'zstd' ? '.jsonl.zstd' : '.jsonl'
+  return `.jsonl${compressionSuffix(compression)}`
+}
+
+function compressionSuffix(compression: JsonlCompression): '.zstd' | '' {
+  return compression === 'zstd' ? '.zstd' : ''
 }
 
 /**
@@ -46,11 +51,7 @@ export function logSuffix(compression: JsonlCompression): '.jsonl.zstd' | '.json
  * @returns the generation filename inside one Session directory.
  */
 export function generationLogFilename(version: number, compression: JsonlCompression): string {
-  if (!Number.isSafeInteger(version) || version < 0 || Object.is(version, -0)) {
-    throw new TypeError('session log generation version must be a non-negative safe integer')
-  }
-  const generation = version === 0 ? '' : `.v${version}`
-  return `session${generation}${logSuffix(compression)}`
+  return `${sessionFormatLogFilename(version)}${compressionSuffix(compression)}`
 }
 
 /**
@@ -65,12 +66,9 @@ export function parseGenerationLogFilename(
   filename: string,
   compression: JsonlCompression,
 ): number | undefined {
-  const suffix = logSuffix(compression)
-  if (filename === `session${suffix}`) return 0
-  const match = new RegExp(`^session\\.v([1-9][0-9]*)${suffix.replaceAll('.', '\\.')}$`).exec(filename)
-  if (match === null) return undefined
-  const version = Number(match[1])
-  return Number.isSafeInteger(version) ? version : undefined
+  const suffix = compressionSuffix(compression)
+  if (!filename.endsWith(suffix)) return undefined
+  return parseSessionFormatLogFilename(filename.slice(0, filename.length - suffix.length))
 }
 
 /**

+ 4 - 9
packages/session/session-persistence-jsonl/src/generation.ts

@@ -20,7 +20,7 @@ import {
 } from 'node:fs/promises'
 import { basename, dirname, join } from 'node:path'
 import type { JsonlCompression } from './format.ts'
-import { logSuffix } from './format.ts'
+import { generationLogFilename, logSuffix } from './format.ts'
 import { publishNewFileWin32 } from './win32.ts'
 import {
   compressZstdFrame,
@@ -445,10 +445,6 @@ function readPhysicalHeader(
   return readRawHeader(bytes)
 }
 
-function generationFilename(version: number, suffix: string): string {
-  return version === 0 ? `session${suffix}` : `session.v${version}${suffix}`
-}
-
 function assertGenerationPaths(
   sourcePath: string,
   sourceVersion: number,
@@ -456,9 +452,8 @@ function assertGenerationPaths(
   currentVersion: number,
   compression: JsonlCompression,
 ): string {
-  const suffix = logSuffix(compression)
-  const expectedSource = generationFilename(sourceVersion, suffix)
-  const expectedCurrent = generationFilename(currentVersion, suffix)
+  const expectedSource = generationLogFilename(sourceVersion, compression)
+  const expectedCurrent = generationLogFilename(currentVersion, compression)
   if (basename(sourcePath) !== expectedSource) {
     throw new Error(`resolved JSONL source path must end with "${expectedSource}": ${sourcePath}`)
   }
@@ -468,7 +463,7 @@ function assertGenerationPaths(
   if (dirname(sourcePath) !== dirname(currentPath)) {
     throw new Error('source and current JSONL generations must share one Session directory')
   }
-  return suffix
+  return logSuffix(compression)
 }
 
 async function syncDirectory(path: string, internals: JsonlGenerationInternals): Promise<void> {

+ 3 - 0
packages/session/session-persistence-jsonl/tsconfig.json

@@ -23,6 +23,9 @@
     {
       "path": "../session-persistence"
     },
+    {
+      "path": "../session-format"
+    },
     {
       "path": "../session-format-catalog"
     }

+ 1 - 0
packages/test-support/session-snapshot/package.json

@@ -31,6 +31,7 @@
     "@deepseek-ai/cordis-plugin-include": "workspace:*",
     "@deepseek-ai/dsh-llm-replay": "workspace:^",
     "@deepseek-ai/dsh-loader-smoke": "workspace:*",
+    "@deepseek-ai/dsh-session-format": "workspace:^",
     "js-yaml": "^4.2.0",
     "vitest": "^4.1.8"
   },

+ 6 - 12
packages/test-support/session-snapshot/src/session-files.ts

@@ -1,6 +1,7 @@
 /** Immutable Session-generation filenames used by recorded-session fixtures. */
 
 import { basename, dirname } from 'node:path'
+import { parseSessionFormatLogFilename, sessionFormatLogFilename } from '@deepseek-ai/dsh-session-format'
 
 /** One canonical recorded-session fixture filename. */
 export interface SessionFixtureFile {
@@ -23,7 +24,6 @@ export interface PersistedSessionFile {
 }
 
 const FIXTURE_FILE = /^session(?:\.([1-9]\d*))?(?:\.v([1-9]\d*))?\.jsonl$/u
-const PERSISTED_FILE = /^session(?:\.v([1-9]\d*))?\.jsonl(\.zstd)?$/u
 
 function nonNegativeSafeInteger(value: number, label: string): void {
   if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) {
@@ -182,8 +182,7 @@ export function persistedSessionFilename(
   version: number,
   compression: 'raw' | 'zstd' = 'raw',
 ): string {
-  nonNegativeSafeInteger(version, 'Session format version')
-  return `session${version === 0 ? '' : `.v${version}`}.jsonl${compression === 'zstd' ? '.zstd' : ''}`
+  return `${sessionFormatLogFilename(version)}${compression === 'zstd' ? '.zstd' : ''}`
 }
 
 /**
@@ -193,15 +192,10 @@ export function persistedSessionFilename(
  * @returns Its generation and compression, or `undefined` for noise and noncanonical names.
  */
 export function parsePersistedSessionFilename(name: string): PersistedSessionFile | undefined {
-  const match = PERSISTED_FILE.exec(name)
-  if (match === null) return undefined
-  const version = match[1] === undefined ? 0 : Number(match[1])
-  if (!Number.isSafeInteger(version)) return undefined
-  return {
-    version,
-    compression: match[2] === undefined ? 'raw' : 'zstd',
-    name,
-  }
+  const compression = name.endsWith('.zstd') ? 'zstd' : 'raw'
+  const version = parseSessionFormatLogFilename(compression === 'zstd' ? name.slice(0, -'.zstd'.length) : name)
+  if (version === undefined) return undefined
+  return { version, compression, name }
 }
 
 /**

+ 3 - 0
packages/test-support/session-snapshot/tsconfig.json

@@ -20,6 +20,9 @@
     {
       "path": "../../core/session"
     },
+    {
+      "path": "../../session/session-format"
+    },
     {
       "path": "../../util/http-proxy"
     }

+ 16 - 0
pnpm-lock.yaml

@@ -6865,6 +6865,9 @@ importers:
       '@deepseek-ai/dsh-brand':
         specifier: workspace:^
         version: link:../../util/brand
+      '@deepseek-ai/dsh-session-format':
+        specifier: workspace:^
+        version: link:../../session/session-format
       '@deepseek-ai/schemastery':
         specifier: link:../../../vendor/schemastery
         version: link:../../../vendor/schemastery
@@ -7083,6 +7086,10 @@ importers:
         version: link:../../core/tools
 
   packages/session/session-format:
+    dependencies:
+      '@deepseek-ai/dsh-util-values':
+        specifier: workspace:^
+        version: link:../../util/values
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -7112,6 +7119,9 @@ importers:
       '@deepseek-ai/dsh-session-format':
         specifier: workspace:^
         version: link:../session-format
+      '@deepseek-ai/dsh-util-values':
+        specifier: workspace:^
+        version: link:../../util/values
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -7191,6 +7201,9 @@ importers:
 
   packages/session/session-persistence-jsonl:
     dependencies:
+      '@deepseek-ai/dsh-session-format':
+        specifier: workspace:^
+        version: link:../session-format
       '@deepseek-ai/dsh-session-format-catalog':
         specifier: workspace:^
         version: link:../session-format-catalog
@@ -9113,6 +9126,9 @@ importers:
       '@deepseek-ai/dsh-loader-smoke':
         specifier: workspace:*
         version: link:../loader-smoke
+      '@deepseek-ai/dsh-session-format':
+        specifier: workspace:^
+        version: link:../../session/session-format
       js-yaml:
         specifier: ^4.2.0
         version: 4.2.0

+ 1 - 0
scripts/package-dependency-policy.ts

@@ -44,6 +44,7 @@ const SAFE_HOST_DEPENDENCY_EXPORTS = {
   '@deepseek-ai/dsh-credentials': ['credentialKey'],
   '@deepseek-ai/dsh-deque': ['Deque'],
   '@deepseek-ai/dsh-llm': ['BlockAssembler', 'callConfigEquals', 'expandAssistantStream'],
+  '@deepseek-ai/dsh-session-format': ['sessionFormatLogFilename'],
   '@deepseek-ai/dsh-timeout': ['MAX_TIMER_DELAY_MS'],
   '@deepseek-ai/schemastery': ['default'],
 } as const satisfies HostDependencyExports

Some files were not shown because too many files changed in this diff