Selaa lähdekoodia

refactor(session)!: distinguish event seqs from log offsets

Tianyi Cui 3 viikkoa sitten
vanhempi
sitoutus
27bf1039db
100 muutettua tiedostoa jossa 1050 lisäystä ja 589 poistoa
  1. 2 2
      .agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml
  2. 1 1
      .agents/notes/implemented/architecture/2026-06-20-branded-ids.md
  3. 1 1
      .agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.zh.md
  7. 2 2
      .agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.i18n.yaml
  8. 24 25
      .agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md
  9. 28 29
      .agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.zh.md
  10. 6 0
      .agents/notes/implemented/architecture/2026-08-31-session-sequence-and-log-offset-brands.i18n.yaml
  11. 45 0
      .agents/notes/implemented/architecture/2026-08-31-session-sequence-and-log-offset-brands.md
  12. 45 0
      .agents/notes/implemented/architecture/2026-08-31-session-sequence-and-log-offset-brands.zh.md
  13. 2 2
      .agents/notes/implemented/feature/2026-06-30-session-store-fork-api.i18n.yaml
  14. 3 3
      .agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md
  15. 3 3
      .agents/notes/implemented/feature/2026-06-30-session-store-fork-api.zh.md
  16. 2 2
      .agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml
  17. 2 2
      .agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md
  18. 2 2
      .agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md
  19. 2 2
      .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml
  20. 0 0
      .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md
  21. 0 0
      .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md
  22. 2 2
      .agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.i18n.yaml
  23. 1 1
      .agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md
  24. 1 1
      .agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md
  25. 2 2
      .agents/notes/implemented/feature/2026-08-05-durable-web-schedule.i18n.yaml
  26. 2 2
      .agents/notes/implemented/feature/2026-08-05-durable-web-schedule.md
  27. 2 2
      .agents/notes/implemented/feature/2026-08-05-durable-web-schedule.zh.md
  28. 2 2
      .agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.i18n.yaml
  29. 7 7
      .agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md
  30. 7 7
      .agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.zh.md
  31. 7 6
      apps/cli/tests/profiles/headless/tests/semantic-checkpoint.expected.e2e.ts
  32. 5 4
      apps/cli/tests/profiles/headless/tests/session-format-guard.expected.e2e.ts
  33. 8 6
      apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts
  34. 7 6
      apps/cli/tests/profiles/headless/tests/subagent-inheritance.expected.e2e.ts
  35. 6 4
      apps/cli/tests/profiles/headless/tests/workspace-context-resume.expected.e2e.ts
  36. 6 4
      apps/cli/tests/web-agent-presets.e2e.ts
  37. 11 7
      apps/web/tests/agent-preset-selection.e2e.ts
  38. 1 1
      apps/web/tests/chat-long-interactions.e2e.ts
  39. 2 2
      apps/web/tests/complex-history.perf.ts
  40. 2 1
      apps/web/tests/reference-composer.e2e.ts
  41. 17 2
      apps/web/tests/scaffold.ts
  42. 7 3
      apps/web/tests/schedule-after.e2e.ts
  43. 26 18
      apps/web/tests/subagent-conversation.e2e.ts
  44. 2 2
      docs/config-catalog.i18n.yaml
  45. 15 15
      docs/config-catalog.md
  46. 11 11
      docs/config-catalog.zh.md
  47. 2 2
      docs/event-producer-consumer.i18n.yaml
  48. 7 7
      docs/event-producer-consumer.md
  49. 7 7
      docs/event-producer-consumer.zh.md
  50. 2 2
      docs/persistence-catalog.i18n.yaml
  51. 32 32
      docs/persistence-catalog.md
  52. 31 31
      docs/persistence-catalog.zh.md
  53. 2 2
      docs/subsystems/commands.i18n.yaml
  54. 1 1
      docs/subsystems/commands.md
  55. 1 1
      docs/subsystems/commands.zh.md
  56. 2 2
      docs/subsystems/compaction.i18n.yaml
  57. 9 9
      docs/subsystems/compaction.md
  58. 9 9
      docs/subsystems/compaction.zh.md
  59. 2 2
      docs/subsystems/core.i18n.yaml
  60. 1 1
      docs/subsystems/core.md
  61. 1 1
      docs/subsystems/core.zh.md
  62. 2 2
      docs/subsystems/persistence.i18n.yaml
  63. 55 20
      docs/subsystems/persistence.md
  64. 55 20
      docs/subsystems/persistence.zh.md
  65. 2 2
      docs/subsystems/schedule.i18n.yaml
  66. 1 1
      docs/subsystems/schedule.md
  67. 1 1
      docs/subsystems/schedule.zh.md
  68. 2 2
      docs/subsystems/session-projection.i18n.yaml
  69. 16 12
      docs/subsystems/session-projection.md
  70. 16 12
      docs/subsystems/session-projection.zh.md
  71. 2 2
      docs/subsystems/session-query.i18n.yaml
  72. 17 11
      docs/subsystems/session-query.md
  73. 17 11
      docs/subsystems/session-query.zh.md
  74. 2 2
      docs/subsystems/session-title.i18n.yaml
  75. 5 5
      docs/subsystems/session-title.md
  76. 5 5
      docs/subsystems/session-title.zh.md
  77. 2 2
      docs/subsystems/session.i18n.yaml
  78. 75 25
      docs/subsystems/session.md
  79. 75 25
      docs/subsystems/session.zh.md
  80. 2 2
      docs/subsystems/subagent.i18n.yaml
  81. 1 1
      docs/subsystems/subagent.md
  82. 1 1
      docs/subsystems/subagent.zh.md
  83. 2 2
      docs/subsystems/token-meter.i18n.yaml
  84. 2 2
      docs/subsystems/token-meter.md
  85. 2 2
      docs/subsystems/token-meter.zh.md
  86. 9 8
      packages/acp/acp/tests/bridge.spec.ts
  87. 4 4
      packages/acp/acp/tests/updates.spec.ts
  88. 8 3
      packages/api/session-controller/src/agent.ts
  89. 3 3
      packages/api/session-controller/src/client/contract/session.ts
  90. 12 7
      packages/api/session-controller/src/client/sessions/manager.ts
  91. 5 4
      packages/api/session-controller/src/client/sessions/projection-store.ts
  92. 2 2
      packages/api/session-controller/src/client/sessions/service.ts
  93. 26 12
      packages/api/session-controller/src/client/sessions/session.ts
  94. 12 7
      packages/api/session-controller/src/commands.ts
  95. 70 28
      packages/api/session-controller/src/history.ts
  96. 8 3
      packages/api/session-controller/src/index.ts
  97. 4 1
      packages/api/session-controller/src/list.ts
  98. 22 3
      packages/api/session-controller/src/types.ts
  99. 31 19
      packages/api/session-controller/tests/agent.host.spec.ts
  100. 32 8
      packages/api/session-controller/tests/client-contract.client.spec.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-06-20-branded-ids.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-06-20-branded-ids.md
-2026-06-20-branded-ids.md: 954fd89aa229ba587cd1293973b4038cfeb20473
-2026-06-20-branded-ids.zh.md: 0dd761da2e5b5fc3e864fe03c250b9781be9ee59
+2026-06-20-branded-ids.md: 1f579a7afb7ac5f7facd6c5e8040d5df719c4bed
+2026-06-20-branded-ids.zh.md: eaf032027f2f61bec9e4ab624622a5012c97e9b9

+ 1 - 1
.agents/notes/implemented/architecture/2026-06-20-branded-ids.md

@@ -51,7 +51,7 @@ Kept deliberately narrow per the "not every string needs a brand" policy. Each o
 - **`ModelId`** (`GenerateOptions.model`, the `LlmRuntime` adapter-registry key) — a real cross-package lookup key (config → agent → llm → adapter); a reasonable next brand, left out only to keep this decision's blast radius focused.
 - **`ToolName`** (the `ToolRuntime` key) — author-defined, human-readable, and rarely confused with another id; the weakest candidate, likely not worth a brand.
 - **`ErrorCode`** (`HarnessError.code`) — a closed vocabulary (`ABORTED`, `NO_ADAPTER`, …), not a per-instance id; better served by a string-literal union than a brand, if anything.
-- **Numeric ordinals** — turn number, step number, and the event `seq` are `number`, not `string`, so `Branded<string>` does not apply; a parallel `number & { readonly [BRAND]: B }` variant could brand them, but they are positional ordinals rarely passed across boundaries, so the payoff is low.
+- **Other numeric ordinals** — the [Session sequence and log-offset decision](2026-08-31-session-sequence-and-log-offset-brands.md) brands event identities and log gaps because they cross persistence and reference seams. Turn and step numbers remain plain numbers: they are payload-local ordinals and are not interchangeable with Session event positions.
 - **Validated construction** — `brandString<T>()` performs no runtime check, and every boundary (ACP `sessionId`, provider-issued `call.id`, the empty-string fallback in `dsh-llm-deepseek`) trusts the raw string. A `SessionId.parse()` / `isValid()` companion that throws on malformed input at boundaries is a genuine gap, but it is a runtime-behavior change with its own design (what is "malformed"? what happens on failure?) and belongs in its own decision.
 
 ## Verification

+ 1 - 1
.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md

@@ -51,7 +51,7 @@ const owner = brandString<OwnerToken>('session-1')
 - **`ModelId`**(`GenerateOptions.model`,`LlmRuntime` 适配器注册表的键):一个真正的跨包查找键(config → agent → llm → 适配器);合理的下一个 brand,仅为控制本决策的影响范围而暂不纳入。
 - **`ToolName`**(`ToolRuntime` 的键):由作者定义、人类可读,且很少与其他 id 混淆;最弱的候选,可能不值得加 brand。
 - **`ErrorCode`**(`HarnessError.code`):一个封闭词汇(`ABORTED`、`NO_ADAPTER`……),不是逐实例的 id;如果要做,string 字面量联合类型比 brand 更合适。
-- **数值序号**:轮次号、步骤号和事件 `seq` 是 `number` 而非 `string`,`Branded<string>` 不适用;可以用并行的 `number & { readonly [BRAND]: B }` 变体来 brand 它们,但它们是位置序号、很少跨边界传递,收益较低。
+- **其他数值序号**:[Session 序列号与日志偏移决策](2026-08-31-session-sequence-and-log-offset-brands.zh.md)会为事件身份与日志间隙加 brand,因为它们跨越 persistence 与引用 seam。turn 与 step number 保持普通 number:它们是 payload-local ordinal,不会与 Session 事件位置互换。
 - **带校验的构造**:`brandString<T>()` 不执行运行时检查,且每个边界(ACP `sessionId`、提供方签发的 `call.id`、`dsh-llm-deepseek` 中的空字符串回退)都信任裸 string。一个在边界处对格式错误的输入抛异常的 `SessionId.parse()` / `isValid()` 配套工具确实是缺口,但它属于运行时行为变更,有自己的设计问题(什么算「格式错误」?失败时会怎样?),应在独立决策中处理。
 
 ## 验证

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.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-07-30-session-end-seed-log-boundary.md
-2026-07-30-session-end-seed-log-boundary.md: 1c5a8097a6b901f133205dcd52d674a8e3594b28
-2026-07-30-session-end-seed-log-boundary.zh.md: ea3549543229a15d0fba7ad0316a8683c674a558
+2026-07-30-session-end-seed-log-boundary.md: c6ed3911a797480804d064273922d85412664c79
+2026-07-30-session-end-seed-log-boundary.zh.md: 1e9517f9a5aed819fdaff6194ab952c322c85b82

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md

@@ -40,7 +40,7 @@ The predicate holds for a bracket *this* session inherited, not as a liveness si
 
 **A boundary appended at loop start.** The loop calls `resumeWith`, so it covers the resume paths, but it misses `fork()` and `adopt()` entirely, and the event would have to fire on `'startup'` — the source a fork child publishes — so `SessionStartSource` would stop discriminating. It also publishes the session before the marker is appended, so a `session/created` listener could observe a seeded log with no boundary.
 
-**Reusing `header.seedLength`.** It is the durable *fork-lineage* boundary and deliberately keeps the original fork value across a resume, where the constructor seed is the whole stored log. The two facts differ and conflating them would lose both.
+**Reusing `Session.inheritedEventCount`.** It is the durable *fork-lineage* cut and deliberately keeps the original fork value across a resume, where the constructor seed is the whole stored log. The two facts differ and conflating them would lose both.
 
 **Crash repair closing `compaction/*` alongside turn boundaries.** Rejected: it moves every plugin's bracket semantics into core's repair pass, and core cannot know what closing another package's bracket should record.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.zh.md

@@ -40,7 +40,7 @@ Status: implemented
 
 **在 loop 启动时追加边界。** loop 调用 `resumeWith`,因此覆盖恢复路径,但完全漏掉 `fork()` 与 `adopt()`,而且事件不得不在 `'startup'` 上触发——那是 fork 子会话发布的来源——于是 `SessionStartSource` 将不再具有区分力。它还会在追加标记之前就发布会话,因此 `session/created` 监听方可能观察到一份没有边界的带种子日志。
 
-**复用 `header.seedLength`。** 它是持久的 *fork 血缘*边界,并且刻意在恢复时保留原始 fork 取值——而恢复时构造种子是整份存储日志。这两个事实并不相同,混同会同时失去两者。
+**复用 `Session.inheritedEventCount`。** 它是持久的 *fork 血缘* cut,并且刻意在恢复时保留原始 fork 取值——而恢复时构造种子是整份存储日志。这两个事实并不相同,混同会同时失去两者。
 
 **让崩溃修复连同轮次边界一起关闭 `compaction/*`。** 否决:这会把每个插件的括号语义搬进核心的修复流程,而核心无法知道关闭另一个包的括号应该记录什么。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.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-06-subagent-list-identity-projection.md
-2026-08-06-subagent-list-identity-projection.md: aeed828530f615b1bb4958a360b5ba4db543f714
-2026-08-06-subagent-list-identity-projection.zh.md: b2b64eaa7c06b738734a7b975adb5948704465bd
+2026-08-06-subagent-list-identity-projection.md: cbb15696314930acfaf20ba8651699c53c5dbde2
+2026-08-06-subagent-list-identity-projection.zh.md: dbb62dbfc6bb6ca3ab63504de1ba8dd35327bbbf

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 24 - 25
.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md


+ 28 - 29
.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.zh.md

@@ -14,16 +14,16 @@ Status: implemented
 
 ## 决策
 
-mode 与 label 由新的 `subagent` projection unit(纯身份两臂)折叠,unit 是折叠规则的唯一权威;`listChildren` 不再依赖 session-query——枚举是 subagent 自管的 live-preferred 合并,取值走三级「算完即止」阶梯:live child 同步读注册表的既有水位缓存(零日志读);cold child 先问可选的 `sessionProjectionCache` checkpoint,取到过 seq 门的身份即定值;否则一次 `persistence.inspect` 整读加经注册的 `subagent` unit 折叠。无索引、不自建缓存、无回写。
+mode 与 label 由 `subagent` projection unit(纯身份两臂)折叠,unit 是折叠规则的唯一权威。枚举使用共享 Session query corpus,取值则走三级「算完即止」阶梯:live child 同步读注册表的既有水位缓存(零日志读);unseeded cold child 可以使用可选 `sessionProjectionCache` checkpoint,因为其精确 inherited cut 已知为零;每个 seeded child 与每次 cache miss 都执行一次含正文的 Session observation,再经注册的 `subagent` unit 折叠。无索引、不自建缓存、列表侧无回写。
 
 消除逐 child 扫描的出路有三类:把 mode/label 提升进 header(写路承担);为投影建持久派生(checkpoint 阶梯,或随查询索引重建落值、读端对账);读时现算(live 走水位缓存,cold 一次整读)。本记录取第三条。「值随查询索引落库」已整体退役:查询基础设施被迫认识领域词汇,而唯一消费方读时现算即可满足——live child 的零读由 session-projection 既有水位缓存白拿,cold child 的一次整读被「算完即止」显式接受。前两条与退役理由详见考虑过的替代方案一节。
 
 要点:
 
-- **subagent 列表不依赖 session-query**:枚举由 subagent 自管的 live-preferred 合并完成,mode/label 经 `ctx.sessionProjections` 取值;没有 query backend 的部署照常列表。
-- **取值三级「算完即止」阶梯**:live child 读 `sessionProjections.snapshot(session, ['subagent'])`(注册表既有水位缓存,零日志读);cold child 先读可选 `sessionProjectionCache.cachedSnapshot(header, ['subagent'])`,非 null 身份通过 seq 门(`seq >= seedLength ?? 0`)即直接使用;否则执行一次完整 Session 观察,再经注册的 `subagent` unit 折叠;再没有就没有——不自建缓存、无回写、无索引。
+- **subagent 列表使用 Session query corpus 完成枚举与含正文 observation**:mode/label 仍经 `ctx.sessionProjections` 获取,列表不拥有 descriptor parser 或领域索引。
+- **取值三级「算完即止」阶梯**:live child 读 `sessionProjections.snapshot(session, ['subagent'])`(注册表既有水位缓存,零日志读);unseeded cold child 可读 `sessionProjectionCache.cachedSnapshot(header, SessionLogOffset(0), ['subagent'])`;seeded child 或 cache miss 执行一次携带 `inheritedEventCount` 的 Session observation,再经注册的 `subagent` unit 折叠。再没有就没有——不自建缓存、列表侧无回写、无索引。
 - **`subagent` projection unit 是折叠规则唯一权威**:live 与 cold 快照都运行同一份已注册 unit,不存在第二份描述符解释逻辑。
-- **header、描述符(v2)、session-persistence、session-projection(-cache)、session-query(-sqlite) 全部零改动**;存量数据第一次被列表时一次 `inspect` 现算获得精确值,无 unknown 降级态、无迁移。
+- **描述符(v2)保持不变**。Session、persistence、projection cache 与 query 在 logical header 之外单独携带精确 inherited cut;listing 无法证明 cut 为零时,存量数据经一次含正文 observation 获得精确值——无 unknown 降级态,也无持久格式迁移。
 
 与既有记录的关系:
 
@@ -36,8 +36,8 @@ mode 与 label 由新的 `subagent` projection unit(纯身份两臂)折叠
 
 ```ts ignore-check
 export type SubagentIdentityProjection =
-  | { mode: 'one-shot'; label?: string; seq: number }
-  | { mode: 'continuable'; label: string; seq: number }
+  | { mode: 'one-shot'; label?: string; seq: SessionSeq }
+  | { mode: 'continuable'; label: string; seq: SessionSeq }
 
 declare module '@deepseek-ai/dsh-session-projection/types' {
   interface SessionProjectionStateMap {
@@ -52,19 +52,19 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
 
 - 投影是纯身份,**projection 体系不做失败通道**:unit 永不抛错;载荷损坏、版本不认识与整日志没有描述符一样。host checkpoint 状态使用可序列化的包装 `{ identity?: SubagentIdentityProjection }`,缺席为 `{}`;客户端 view 则是非可选的 `SubagentIdentityProjection | null` 条目。`null` 完好通过 JSON,因此推送 reset 会替换旧身份,而不会被 stringify 丢掉。判定纪律:消费面把 null 与客户端 key 缺席一律视为无值。「算出来没有」如何呈现是消费方自己的事(见下文 `listChildren` 四态映射)。
 - label 强度由描述符 schema 决定:continuable 的 label 解析强制必有,one-shot 的本就可选;mode/label 判别与下文 child 行的强约定完全一致(行不携带 `seq`——它是投影内部的 own-suffix 证明)。
-- 身份携带 `seq`:折出该身份的 `subagent/descriptor` 事件 seq,两臂必有、null 哨兵无——`seq >= header.seedLength ?? 0` 证明身份折叠自 child 自身后缀,而非 fork 种子回放的祖先描述符。unit 把包装状态中校验后的身份映射为客户端 wire view,并与其他 unit 一律检查点化(`persist` 选项已删除);`stateVersion` 为 2,在增加 `seq` 时升版。更早的 checkpoint 行按 registry 约定版本失配失效、落权威重折。
+- 身份携带品牌化 `seq`:折出该身份的 `subagent/descriptor` 事件 seq,两臂必有、null 哨兵无。live Session 通过 `isOwnSeq()` 检查它;cold 含正文 observation 则与 `inheritedEventCount` 比较。仅 header 的 seeded candidate 会跳过 cache,因为 header 有意不暴露整数 cut;unseeded candidate 知道 cut 为零。unit 把包装状态中校验后的身份映射为客户端 wire view,并与其他 unit 一律检查点化(`persist` 选项已删除);`stateVersion` 为 2,在增加 `seq` 时升版。更早的 checkpoint 行按 registry 约定版本失配失效、落权威重折。
 - 折叠规则:`subagent/descriptor` last-wins,与 `subagentTiming` 同一条 descriptor-reset 纪律——fork 前缀里的祖先描述符被自身描述符覆盖。损坏或版本不认识的载荷同样 last-wins:重置为 null 哨兵而非保留先前身份,健康祖先的 fork 不会继承自身描述符立不住的身份。
 
-### 枚举:subagent 自管 live-preferred 合并
+### 枚举:query corpus 与 live preference
 
-`listChildren`([list-children.ts](../../../../packages/subagent/subagent/src/list-children.ts))的枚举不经任何查询服务:`ctx.sessions.list()` 与 `ctx.get('sessionPersistence')?.list()` 两个来源按 id 合并,live 记录整条覆盖同 id 持久化记录、不做 header 一致性校验。枚举所需全部是 header 事实:
+`listChildren`([list-children.ts](../../../../packages/subagent/subagent/src/list-children.ts))通过 `sessionQuery.listSessions()` 取得 canonical live-preferred corpus,再把每个 listed id 与可能存在的 `ctx.sessions.get(id)` 配对;同 id 存在 live Session 时使用 live header。枚举所需全部是 header 事实:
 
 - 过滤:`header.origin === 'subagent' && header.parentSession === parentSessionId`。
 - `hasChildren`:同一份合并材料向下看一层——存在 `origin === 'subagent'` 且 `parentSession` 为该 child 的直接后代。
 - `activity`:live 记录为 `running`,仅存在于持久化的为 `inactive`。
 - 排序:`createdAt` 升序、再按 child id 升序(与旧约定一致)。
-- **persistence 缺席退为 live-only 枚举,不报错**:没有 persistence 的部署,cold child 本就无法 resume,列出 live child 仍然有意义。(对照:旧实现在 sessionQuery 缺失时整体拒绝。)
-- persistence 列表失败使整次枚举失败;per-child 隔离只作用于逐 child 的冷读。
+- `sessionQuery` 服务缺席时以 `SUBAGENT_CONTROL_QUERY_UNAVAILABLE` 失败;共享 query corpus 负责决定部署能枚举 live-only 还是持久化 Session。
+- query corpus 失败使整次枚举失败;per-child 隔离只适用于逐 child cold observation。
 
 ### 取值:三级「算完即止」阶梯
 
@@ -73,20 +73,20 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
 | 级 | 读法 | 成本 |
 | --- | --- | --- |
 | 1:live child | `ctx.sessionProjections.snapshot(session, ['subagent'])` | 零日志读——注册表既有水位缓存,同步取值 |
-| 2:cold child,cache 命中 | 可选 `sessionProjectionCache.cachedSnapshot(header, ['subagent'])`,非 null 身份满足 `identity.seq >= header.seedLength ?? 0` 才直接使用——own descriptor 一经追加不可变,seq 门证明该值折叠自 child 自身后缀,无视行水位 | 零日志读 |
-| 3:cold child,兜底 | `persistence.inspect(id)` 整读 + 经注册的 `subagent` unit 折叠 | 每次列表一次整读现算 |
+| 2:unseeded cold child,cache 命中 | 可选 `sessionProjectionCache.cachedSnapshot(header, SessionLogOffset(0), ['subagent'])`;精确 cut 为零时,每个合法 seq 都归 child 自有 | 零日志读 |
+| 3:seeded child 或 cold 兜底 | 一次含正文 `sessionQuery.observeSession(id)` 加已注册的 `subagent` projection,使用 `inheritedEventCount` 做 own-suffix 检查 | 每次列表一次整读现算 |
 
-- 错误约定:`sessionProjections` 是必需注入——`SubagentRuntime` 在 inject 集里声明它,没有 registry 的部署根本无法激活服务(与 loop),`listChildren` 不可达,而不是供出降级行([mandatory-seam 记录](2026-08-19-session-projection-mandatory-seam.zh.md));响亮运行时检查与 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 随之删除。会话存储保留显式姿态:`ctx.get('sessions')`(严格全局读取,不走调用方作用域的属性代理)缺席以 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` 失败。apiproxy 为 `PROJECTIONS_UNAVAILABLE` 设的专门 wire 脸随码删除;`SESSION_STORE_UNAVAILABLE` 走通用 internal 兜底——apiproxy 组合自身就 inject `sessions`,该错误在其部署不可达,专门映射违反 need 原则。`SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` 已随 session-query 依赖删除。
-- cache 是纯可选加速层:服务缺席判空跳过——无错误码、不进配置校验(与 `sessionProjections` 的必需注入相对)。第二级任何抛错(包括缓存内任一 unit 行中毒使 `viewCheckpoint` 引爆)静默落第三级——缓存是派生数据,其故障不产生 `corrupt` 判决,终审归权威重折;checkpoint 切面早于描述符的行,`subagent` key 天然缺席,自动落底,无特判;行里的 null 哨兵同样不作数——一律落第三级,由权威重折裁决。创建窗口内的 count/interval checkpoint 可能把 fork 种子回放的祖先身份落进行——祖先 seq 落在 seed 区间,被 seq 门拒绝,同样落第三级裁决。
+- 错误约定:`sessionProjections`、Session store 与 `sessionQuery` 都是 listing 所需的 runtime service。三者分别以 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE`、`SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` 与 `SUBAGENT_CONTROL_QUERY_UNAVAILABLE` 显式失败;缺失分类或 corpus 能力不会伪装成空结果。
+- cache 是纯可选加速层:服务缺席判空跳过——无错误码、不进配置校验(与 `sessionProjections` 的必需注入相对)。seeded header 会跳过该级,因为不读取正文就无法提供 cache identity 所需的精确 cut。对 unseeded child,第二级任何抛错(包括中毒 unit 行引爆 `viewCheckpoint`)都会静默落第三级——缓存是派生数据,其故障不产生 `corrupt` 判决,终审归权威重折;checkpoint 早于 descriptor、key 缺席或 null 哨兵也都会落底。
 - per-child 隔离:单 child 的 cold 整读失败只使该行成为 `unavailable` diagnostic,下次列表自然重试,不影响 sibling(见四态映射)。
-- 冷路径的生命周期见证:preparation 的结果必须仍指向枚举时的那个生命周期——见证字段集与旧 SOURCE_CONFLICT 检查同款七字段(version、id、createdAt、cwd、parentSession、seedLength、delegationDepth);同 id 删除后重新发布的会话对旧 parent 的目录降级为 `corrupt` 行,不外漏新 owner 的 child。
+- 冷路径的生命周期见证:observation 必须仍指向枚举时的那个生命周期。见证字段为 version、id、createdAt、cwd、parentSession、isSeeded、delegationDepth、origin 与 agentPreset;同 id 删除后重新发布的 Session 对旧 parent 的目录降级为 `corrupt` 行,不外漏新 owner 的 child。
 - 冷读并发以常数 4 有界——它约束的是本地介质的一次只读扫描而非部署行为;出现联网 persistence backend 时提升为验证过的 `Config` 字段。
-- 冷读成本如实记录:cache 未挂载或未命中时,cold child 每次列表才付一次整读,成本与其 transcript 大小成正比;定案「算完即止」,不自建缓存。整读经 `inspect()` 走 [Session 准备阶段](2026-08-05-session-preparation.zh.md)的冷读,同 id 短期重复读取可命中其 LRU 复用,但列表不依赖此。live child 全程零日志读。
+- 冷读成本如实记录:每个 seeded child 与每次 unseeded cache miss 都会在每次列表时支付一次完整 query observation,成本与其 transcript 大小成正比;定案「算完即止」,不自建缓存。observation 可以复用 query/persistence preparation 层,但列表不依赖该优化。live child 全程零日志读。
 - 取消:每次 persistence 读前后检查调用方 signal,abort 之后才结算的读拒绝归一化为稳定错误码 `CANCELLED`。
 
 ### 权威模型
 
-- session log 是唯一权威;本方案不新增任何派生持久化——没有索引值、没有自己的 checkpoint、没有进程 memo;第二级读取的 `sessionProjectionCache` checkpoint 是既有组合项的派生数据,本方案只读不写。取值现算现弃,值的新鲜度就是读取时点的 live 状态或持久化 revision(own descriptor 一经追加不可变——缓存身份过 seq 门后无陈旧性问题,门防的是种子回放的祖先身份)。
+- session log 是唯一权威;本方案不新增领域索引、自有 checkpoint 或进程 memo。第二级读取的 `sessionProjectionCache` checkpoint 是既有组合项的派生数据,列表只读。取值现算现弃;seeded candidate 用含正文 observation 按精确 cut 分类,unseeded cached identity 无需 seq 门,因为每个合法 seq 都归自身所有。
 - Session 与 persistence 写路完全不感知列表与投影消费:没有事件监听回写,没有写时折叠。
 - 枚举与取值不构成第二个鉴权来源,也不让尚未发布的 child 可见——两个来源只见已发布的 live 记录与已落盘的持久化记录,与 durable-subagent-catalog 记录对派生读面立下的规则一致。
 
@@ -127,25 +127,24 @@ export type SubagentListEntry =
 
 已知边界偏差(有意接受,随本记录留档):
 
-- 死于发布窗口的 fork child,seed 里若有祖先描述符,last-wins 会给出祖先身份,误现为 child 行;恢复仍按 own-suffix 折叠权威失败(`NOT_RESUMABLE`)。旧实现靠 `seedLength` 过滤将其 omit;projection unit 看不到 header,接受此残骸级偏差(`subagentTiming` 有同类既有暴露)。
 - own suffix 出现多个描述符,旧实现判 corrupt,现 last-wins 取末者(提供方约定本就保证恰一)。
 - live/persisted header 冲突,旧实现是 per-child corrupt;现枚举 live 优先、不做一致性校验,冲突不再被察觉,以 live 记录成行。
 - 损坏存储的源读失败(如坏 surface 被冷读整读拒收),旧实现映射 per-child `corrupt`,现统一成 `unavailable` 行(读侧无从区分成因)。
 - 未知 parent,旧实现经 session-query 抛 not-found(「parent session … was not found」);现自管合并对不存在的 parent 得到空子集,枚举返回空列表,wire 上后续操作落到 child 级 subagent-not-found——语义与文案的静默变化,显式接受。
-- rung 2 的更晚事件窗口:cache 行恰在首个自有描述符之后落盘,日志随后追加第二个自有描述符(或 malformed 载荷置 null 哨兵),且进程在下一次 checkpoint 前崩溃——此后冷列表的 rung 2 凭 seq≥seedLength 门持续供出行内旧身份(第一个自有描述符的值),与权威重折(last-wins 第二个)分歧,且 rung 2 命中期间不触发重折、无从察觉。边界三条:①前提是同一 child 出现第二个自有描述符,违反建档提供方「恰追加一次」约定,属损坏类数据,与多描述符偏差同族同源;②需「损坏 + 崩溃错过 checkpoint(turn/end 与 disposal 两个 mandatory 点及 count/interval 节流点全部未及)」双条件同时成立;③健康 child(恰一自有描述符)不受影响——seq 门放行的正是唯一真身份。自愈条件:该 child 任一次 live 运行(turn/end mandatory checkpoint)或任何触发 cache.write 的时点,都会以新 fold 整行覆写(whole-record replace),rung 2 随即供正;权威路径(rung 3 重折、live snapshot、resume 折叠)自始正确,分歧只存在于持续冷、行未再更新期间的列表读。机制修法不采:gate 对账需知日志末端 seq,冷路径零读不可得;cache 行携 revision 是 opaque token,无法比较且跨域改 schema——按「cache 永不为权威」总纲归档为接受项。
+- rung 2 的更晚事件窗口只适用于 unseeded child:cache 行恰在首个 descriptor 后落盘,日志随后追加第二个 descriptor(或 malformed 载荷置 null 哨兵),且进程在下一次 checkpoint 前崩溃。cold listing 可能持续供出旧身份,直到一次 live 运行或 cache write 替换该行。其前提违反 provider 的「恰追加一次」约定,并且还需错过所有 mandatory checkpoint;健康 child 不受影响。seeded child 没有 body-owned cut 时绝不进入 rung 2。
 
-消费面:wire、tool、GUI 的 diagnostic 处理**全部保持原状零改动**(`list_agents` 的 description 与 output schema 未动;该插件的加载要求变化——inject 去掉 `sessionQuery`、新增必需注入 `sessionProjections`)。行为上动的只有 apiproxy:路由段的 `hasSubagentDescriptor()` 扫描已删除,`hasSubagentOwner` 只看 `header.origin`——pre-#1569 的无 `origin` 存量不再被认作 subagent 属主,其本就不进目录,pre-release 立场接受;`subagents.history` 与 `session.history` 同源对齐——live child 用内存事件与注册表水位快照,cold child 用 `inspectServable` 直读持久化并 detached 折叠,不经查询服务,SESSION_QUERY_* 错误臂随之退役,wire 形状不变(`history` 的 JSDoc 措辞改为 live 内存快照/cold 持久日志双臂)。
+消费面保持相同的 row 与 diagnostic wire 形状。`list_agents` 使用必需的 query corpus 与 projection registry;live identity 来自 registry snapshot,cold identity 来自 cache 或 query observation。Host ownership 仍使用 `header.origin`,history 使用共享的 live/cold Session query source;没有消费方独立解析 descriptor event。
 
 ### 改动落点
 
 | 区域 | 文件 | 改动 |
 | --- | --- | --- |
 | subagent | projection.ts、projection-types.ts、index.ts | 新客户端可见 `subagent` unit 与注册 |
-| subagent | list-children.ts 及类型 | 重写为自管枚举 + 投影阶梯四态映射;删 session-query 依赖、逐 child 事件读取与就地分类机器;错误码 `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` 删除,`sessionProjections` 转为必需注入(不再存在投影错误码);新增可选依赖 dsh-session-projection-cache(纯加速读取,缺席跳过) |
-| host/apiproxy | api-proxy.ts | 删 `hasSubagentDescriptor`,属主判定只看 `header.origin`;`subagents.history` 与 `session.history` 同源——live 用内存事件与注册表水位快照,cold 用 `inspectServable` 直读持久化并 detached 折叠,不经查询服务,SESSION_QUERY_* 错误臂与 `PROJECTIONS_UNAVAILABLE` 专门 wire 脸随之退役 |
-| tool | tool-subagent-control/list-agents.ts | 加载要求收窄(inject 去 `sessionQuery`);model-visible schema、描述与渲染零改动 |
+| subagent | list-children.ts 及类型 | query-corpus 枚举加 projection 阶梯四态映射;必需 projections/query service 与可选 projection-cache 加速 |
+| host/apiproxy | Session controller/query integration | owner 检查使用 `header.origin`;live/cold history 与 listing 消费共享 query 和 projection source |
+| tool | tool-subagent-control/list-agents.ts | model-visible schema、描述与渲染保持不变 |
 | wire/client | api/subagents.ts、runtime sessions/service.ts、GUI | 类型、行形状与 diagnostic 处理**零改动**;api/subagents.ts 仅 `history` 的 JSDoc 措辞改为双臂 |
-| core/session、session-persistence、session-projection(-cache)、session-query(-sqlite) | — | **零改动** |
+| core/session、session-persistence、session-projection(-cache)、session-query(-sqlite) | 含正文 cut 与品牌化 seq 传递 | Logical header 暴露 `isSeeded`;Session、persistence observation、cache identity 与 query record 单独携带精确 `inheritedEventCount` |
 
 ## 考虑过的替代方案
 
@@ -169,20 +168,20 @@ export type SubagentListEntry =
 
 ## 验证
 
-`packages/subagent/subagent/tests/list-children.spec.ts` 重写为本约定:无 persistence、query 服务与继续运行时的 live-only 列表;registry 缺席时服务根本不激活(mandatory seam——`setup` 变体断言 `ctx.get('subagents')` 保持 undefined);live child 全程零 `inspect`、cold child 每次列表恰一次;多描述符 last-wins 取末者;损坏载荷与未知版本折为 `corrupt`;冷读失败映射 `unavailable` 且下次列表重试;fork seed 里的祖先描述符按该身份成行(偏差一钉住);普通 fork 与无 subagent origin 的后代不入列也不计入 `hasChildren`;`createdAt`→id 排序;提供方未挂载不影响列表;压缩与未压缩孪生一致;预中止、持久化列表与冷读取消三例归一 `CANCELLED`;空列表与稳定错误码(存储缺席时 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE`)。第二级例:own-seq 身份直用零 `inspect`、fork 种子祖先身份(seq 落在 seed 区间)被门拒绝落底、行内无身份(null 哨兵或 key 缺席)落底、cache 服务缺席落底、缓存行中毒静默落底重折;冷路径 lifecycle 篡改按见证七字段逐一(`it.each`)降级为 `corrupt`。`tool-subagent-control` 的 list-agents 测试随加载要求收窄更新;`optional-session-query.spec.ts` 随依赖消失删除;既有无密钥快照(`subagent-list-agents` 等)零变化,钉住健康路径的 wire 与 model-visible 面不变;新增无密钥快照 `subagent-diagnostic`(examples/headless-agent)钉住四态映射的诊断分类——descriptor-less 定局残骸成 `corrupt` 行等模型可见变化。
+`packages/subagent/subagent/tests/list-children.spec.ts` 固定本约定:live identity 通过 `Session.isOwnSeq()` 检查;unseeded cold identity 可在 cut 零时使用 cache;seeded candidate 跳过该 cache rung,转而使用携带 `inheritedEventCount` 的 observation;祖先 identity 无法通过 own-suffix 检查;缺席、null、中毒与不可用的 cache/observation 会按约定落底或产生 diagnostic;lifecycle 篡改按完整见证字段集降级为 `corrupt`。既有无密钥快照保持健康 wire 与 model-visible 面不变,`subagent-diagnostic` 则固定诊断分类。
 
 ## 后果
 
 - live child 的列表全程零日志读;cold child 在 cache 未挂载或未命中时每次列表一次 `inspect` 整读,成本与其 transcript 大小成正比、随列表频率重复——定案「算完即止」,不自建缓存、不回写,同 id 短期重复整读可命中准备阶段 LRU 但列表不依赖它。
-- subagent 列表不再要求 query backend:纯 live 与无 persistence 的部署都能列表;`SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` 消失,`list_agents` 插件加载不再要求 `sessionQuery`,而 `sessionProjections` 转为 `SubagentRuntime` 的必需注入——没有投影 registry 的部署根本不会激活服务(mandatory seam)。
+- subagent 列表要求 Session query corpus 与 projection registry;服务缺失会显式失败,而不是供出不完整 row。可选 projection cache 只改变正文读取次数。
 - 身份解释只存在于 registry 注册的一份 unit:列表三级阶梯与 GUI history 冷读使用其 live、cached 或 observed wire 快照,不存在手写旁路折叠;若未来某消费面绕开该 unit 手写折叠,各读面的值将漂移——这是本设计要求维持的纪律,不是机制保证。
 - per-child 隔离回归:单 child 冷读失败只损失该行,healthy sibling 不受影响;persistence 列表失败仍使整次枚举失败。
-- 诊断与枚举语义留下六处边界偏差(stillborn fork 祖先身份误现、多描述符取末者、header 冲突不再被察觉、损坏源读失败由 `corrupt` 转 `unavailable`、未知 parent 由 not-found 改为空列表、rung 2 更晚事件窗口),完整语义见已知边界偏差清单;前四处为残骸级数据的展示或分类偏差,未知 parent 一处是查询语义的静默变化,rung 2 窗口一处是损坏加崩溃双条件下可自愈的缓存供值分歧;恢复鉴权均不受影响,显式接受。
+- 诊断与枚举语义留下五处边界偏差(多描述符取末者、header 冲突不再被察觉、损坏源读失败改变分类、未知 parent 由 not-found 改为空列表、unseeded rung 2 更晚事件窗口)。seeded 祖先 identity 已不再构成偏差,因为含正文读取会把它与 `inheritedEventCount` 比较;恢复鉴权始终不受影响。
 - pre-#1569 的无 `origin` 存量不再被认作 subagent 属主;其本就不进目录,pre-release 无兼容承诺。
 
 ## 相关
 
-- [durable-subagent-catalog 与 list_agents](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md)——被本记录部分取代:描述符仍是 mode/label 的持久权威与折叠输入,列表的枚举与取值改为自管合并加投影阶梯。
+- [durable-subagent-catalog 与 list_agents](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md)——被本记录部分取代:描述符仍是 mode/label 的持久权威与折叠输入,取值改为共享 query corpus 上的 projection 阶梯。
 - [session projections 与命令生命周期日志](../../proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md)——registry 约定的权威;本记录为其新增 `subagent` 身份 unit,并消费其 live 与 cold wire 快照。
 - [session projection 状态与客户端视图](2026-08-19-session-projection-state-and-client-views.zh.md)——state/client 拆分;`subagent` 与 `subagentTiming` 都提供客户端 wire view。
 - [session projections 作为必需接缝](2026-08-19-session-projection-mandatory-seam.zh.md)——`sessionProjections` 转为必需注入;列表的错误约定随其变化(registry 缺席是激活期失败,投影错误码删除)。

+ 6 - 0
.agents/notes/implemented/architecture/2026-08-31-session-sequence-and-log-offset-brands.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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-session-sequence-and-log-offset-brands.md
+2026-08-31-session-sequence-and-log-offset-brands.md: 0057b2b6c95391abbb4ec2464ee58b54e2fe24f2
+2026-08-31-session-sequence-and-log-offset-brands.zh.md: f51ad5953cab34294a53db6e3c91884292497faf

+ 45 - 0
.agents/notes/implemented/architecture/2026-08-31-session-sequence-and-log-offset-brands.md

@@ -0,0 +1,45 @@
+# Agent Note: Distinguish Session event identities from log offsets
+
+Status: implemented
+
+English | [中文](2026-08-31-session-sequence-and-log-offset-brands.zh.md)
+
+## Problem
+
+Session positions used one structural `number` type for two incompatible meanings. An event reference names an existing row, while a prefix length, next append position, or read cut names a gap and may equal the event count. The compiler therefore accepted an offset where an event identity was required and could not expose a missed sequence-field migration.
+
+`SessionHeader.seedLength` also mixed a v0 storage coordinate into metadata used by body-free readers. Listing needs to know whether a Session has fork lineage, but only a reader that holds the event body can interpret the exact inherited prefix length.
+
+## Decision
+
+`@deepseek-ai/dsh-brand` exports the erased numeric primitive `BrandedNumber<B>` and the runtime-identity helper `brandNumber()`. `@deepseek-ai/dsh-session` owns two validated brands: `SessionSeq` names one existing event and `SessionLogOffset` names a log gap, prefix length, or read offset. `SessionSeqCursor = SessionSeq | -1` represents an inclusive watermark before or after the first event, and `OptionalSessionSeq = SessionSeq | null` represents an event identity whose absence is data.
+
+`SessionEvent.seq`, surface replacement endpoints, provenance, and owner payload fields that identify Session events use `SessionSeq`. `Session.seq`, `Session.firstLiveSeq`, `Session.inheritedEventCount`, body-read offsets, and inherited prefix cuts use `SessionLogOffset`. Arithmetic returns an ordinary number and re-enters either domain through its validating constructor.
+
+The logical `SessionHeader` carries `isSeeded: boolean` and no numeric seed cut. Body-bearing storage values and observations carry `inheritedEventCount` beside the header; `Session.ownEvents()` and `Session.isOwnSeq()` hide the comparison from ordinary consumers. A seeded constructor requires an explicit seed and exact cut, including an empty seed with cut zero, because constructor input may contain child-owned setup events after the inherited prefix.
+
+The v0 JSONL header remains byte-compatible: absent `seedLength` decodes to `isSeeded: false` with cut zero, while present zero or nonzero values decode to `isSeeded: true` with the exact cut. Header-only listing translates only the presence bit. API, SDK, DeepSeek, telemetry, query-row, and JSON representations continue to carry ordinary numbers; their owning adapters validate and brand values when they enter same-process domain code.
+
+## Admission and ownership
+
+Domain constructors reject negative, fractional, non-finite, and unsafe integer values. Parsers validate a raw number once and retain the parsed object where the brand does not require a runtime wrapper. A compile-time brand does not discover unknown numeric fields in an external event; a format migration still needs an exhaustive owner disposition and must refuse schemas it cannot safely rewrite.
+
+`session/end-seed` remains a lifecycle marker, not the source of the inherited cut. Every constructor restore appends or retains that marker, including unseeded replay, so projections and cold readers receive `inheritedEventCount` explicitly instead of scanning the log.
+
+## Alternatives considered
+
+**Keep every position as `number`.** Rejected because event identities, counts, and cursors cross package and persistence seams frequently enough that accidental interchange is a migration risk, not a local arithmetic convenience.
+
+**Use one branded Session position for identities and offsets.** Rejected because it would again permit `eventCount` or `fromSeq` where an existing event is required and would force the `-1` and `null` sentinels into unrelated operations.
+
+**Derive the inherited cut from `session/end-seed`.** Rejected because the marker records constructor lifecycle, not only fork lineage, and a constructor seed may contain child-owned events after the inherited prefix.
+
+## Consequences
+
+Sequence-bearing code now states whether a number identifies an event or a gap. Header-only readers receive stable lineage metadata without opening the body, while persistence, projection, query, and authorization paths retain the exact cut they need. The on-disk v0 format and public numeric wires do not change.
+
+The cost is explicit conversion at durable and wire parsers and a separate exact-cut field on body-bearing observations. Projection-cache identity includes the lineage bit and exact cut, so its disposable storage domain advances and older rows rebuild on demand; body-free readers skip seeded cache hints when they do not hold the cut. Turn numbers, step numbers, message-list indexes, workflow member ordinals, token counts, and unrelated numeric domains remain plain numbers because they do not identify Session events.
+
+## Testing
+
+Type assertions pin that `SessionSeq` and `SessionLogOffset` are not interchangeable. Runtime suites cover constructor validation, mixed inherited and child-owned seeds, empty seeds, `ownEvents()` and `isOwnSeq()`, v0 JSONL absent/zero/nonzero headers in plain and Zstandard encodings, header-only listing, cold prepare and reopen, query and projection cuts, and unchanged numeric wire values.

+ 45 - 0
.agents/notes/implemented/architecture/2026-08-31-session-sequence-and-log-offset-brands.zh.md

@@ -0,0 +1,45 @@
+# Agent Note: 区分 Session 事件身份与日志偏移
+
+Status: implemented
+
+[English](2026-08-31-session-sequence-and-log-offset-brands.md) | 中文
+
+## Problem
+
+Session 位置曾用同一个结构化 `number` 类型表达两种不兼容的含义。事件引用指向一条已存在的记录,而前缀长度、下一追加位置或读取切点指向记录间隙,并且可以等于事件总数。因此,编译器会在需要事件身份的位置接受偏移,也无法暴露迁移时漏改的序号字段。
+
+`SessionHeader.seedLength` 还把 v0 存储坐标混入了无须读取正文的 metadata consumer。列表只需要知道 Session 是否有 fork lineage,只有同时持有事件正文的读取方才能解释精确的继承前缀长度。
+
+## Decision
+
+`@deepseek-ai/dsh-brand` 导出编译后消失的数值原语 `BrandedNumber<B>` 与运行时保持原值的 helper `brandNumber()`。`@deepseek-ai/dsh-session` 拥有两个经验证的 brand:`SessionSeq` 指明一条已存在事件,`SessionLogOffset` 指明日志间隙、前缀长度或读取偏移。`SessionSeqCursor = SessionSeq | -1` 表达首条事件之前或之后的闭区间 watermark,`OptionalSessionSeq = SessionSeq | null` 表达允许以缺失为数据的事件身份。
+
+`SessionEvent.seq`、surface 替换端点、provenance 以及 owner payload 中指向 Session 事件的字段使用 `SessionSeq`。`Session.seq`、`Session.firstLiveSeq`、`Session.inheritedEventCount`、带正文读取的偏移与继承前缀切点使用 `SessionLogOffset`。算术结果恢复为普通 number,并通过对应的验证构造函数重新进入任一领域。
+
+逻辑 `SessionHeader` 携带 `isSeeded: boolean`,不携带数值 seed cut。包含正文的存储值和 observation 在 header 旁携带 `inheritedEventCount`;`Session.ownEvents()` 与 `Session.isOwnSeq()` 向普通 consumer 隐藏比较。seeded constructor 必须显式提供 seed 与精确 cut,包括 cut 为零的空 seed,因为 constructor 输入可能在继承前缀之后还包含 child-owned setup event。
+
+v0 JSONL header 保持字节兼容:缺少 `seedLength` 时解码为 `isSeeded: false` 和零 cut,存在零或非零值时解码为 `isSeeded: true` 和对应精确 cut。仅 header 的 listing 只转换字段是否存在。API、SDK、DeepSeek、telemetry、query row 与 JSON 表示继续携带普通 number;由它们各自的 adapter 在值进入同进程 domain code 时完成验证与 brand。
+
+## Admission and ownership
+
+Domain constructor 拒绝负数、小数、非有限值与非安全整数。parser 验证原始 number 一次;brand 不需要运行时 wrapper 时,保留原解析对象。编译期 brand 无法发现外部事件里的未知数值字段;格式迁移仍须获得穷尽的 owner disposition,并拒绝无法安全改写的 schema。
+
+`session/end-seed` 仍是 lifecycle marker,不是继承 cut 的来源。每次 constructor restore 都会追加或保留该 marker,unseeded replay 也一样,因此 projection 与 cold reader 会显式接收 `inheritedEventCount`,而不是扫描日志。
+
+## Alternatives considered
+
+**继续让所有位置都使用 `number`。** 拒绝,因为事件身份、计数与 cursor 已频繁跨越 package 与 persistence seam,意外混用是迁移风险,而不是局部算术便利。
+
+**用同一个 branded Session position 表达身份和偏移。** 拒绝,因为这样仍会在需要已存在事件的位置接受 `eventCount` 或 `fromSeq`,还会迫使 `-1` 与 `null` sentinel 进入互不相关的操作。
+
+**从 `session/end-seed` 推导继承 cut。** 拒绝,因为该 marker 记录 constructor lifecycle,并不只记录 fork lineage,而且 constructor seed 可以在继承前缀之后包含 child-owned event。
+
+## Consequences
+
+携带序号的代码会明确说明一个 number 指向事件还是间隙。仅 header 的 reader 无须打开正文即可取得稳定 lineage metadata,persistence、projection、query 与 authorization path 则保留所需的精确 cut。磁盘 v0 格式与公共数值 wire 不变。
+
+代价是在 durable 与 wire parser 处显式转换,并让含正文 observation 携带独立的精确 cut 字段。Projection cache identity 包含 lineage bit 与精确 cut,因此其可丢弃的 storage domain 会推进,旧 row 按需重建;不持有 cut 的仅 header reader 会跳过 seeded cache hint。turn number、step number、message-list index、workflow member ordinal、token count 与无关数值领域保持普通 number,因为它们不指向 Session 事件。
+
+## Testing
+
+类型断言钉住 `SessionSeq` 与 `SessionLogOffset` 不可互换。运行时 suite 覆盖 constructor 验证、混合继承与 child-owned seed、空 seed、`ownEvents()` 与 `isOwnSeq()`、plain 与 Zstandard 编码中的 v0 JSONL 缺失/零/非零 header、仅 header 的 listing、cold prepare 与 reopen、query 和 projection cut,以及不变的数值 wire 值。

+ 2 - 2
.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.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/feature/2026-06-30-session-store-fork-api.md
-2026-06-30-session-store-fork-api.md: ff7617f4bb306926a7b0782751ae82f6ef0c6371
-2026-06-30-session-store-fork-api.zh.md: ecef7ba2985321677b29d3fc7692a8dcb7afb2b6
+2026-06-30-session-store-fork-api.md: a2d169a36a8a0b624abef98377d644d713e5d64c
+2026-06-30-session-store-fork-api.zh.md: 5ebf05e677f2d3676f860220b1cf9ff960565402

+ 3 - 3
.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md

@@ -20,11 +20,11 @@ The store exposes one operation:
 type SessionForkSource = Session | SessionId
 
 class SessionStore extends Service {
-  fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
+  fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
 }
 ```
 
-`boundary` is the inclusive source event `seq` to copy through. When omitted, it defaults to the source session's current last event; on an empty source, omitted `boundary` creates an empty child. Fork-specific validation checks that the requested boundary exists and that the selected prefix's latest turn boundary is not an unmatched `turn/start`. The selected prefix may therefore end at `turn/end` or at a later standalone event, then is deep-cloned into the child seed. The child inherits the source session's `cwd`, stamps `parentSession` to the source id, and sets `seedLength` to the copied prefix length. When `childSessionId` is omitted, `SessionStore` generates one using its existing id policy.
+`boundary` is the branded inclusive source event `seq` to copy through. When omitted, it defaults to the source session's current last event; on an empty source, omitted `boundary` creates an empty child. Fork-specific validation checks that the requested boundary exists and that the selected prefix's latest turn boundary is not an unmatched `turn/start`. The selected prefix may therefore end at `turn/end` or at a later standalone event, then is deep-cloned into the child seed. The child inherits the source session's `cwd`, stamps `parentSession` to the source id, sets logical `isSeeded: true`, and supplies the copied prefix length separately as `inheritedEventCount`. When `childSessionId` is omitted, `SessionStore` generates one using its existing id policy.
 
 An empty prefix is forkable; any non-empty boundary must be a safe existing sequence outside an open turn. Typed errors distinguish missing sources, stale objects, duplicate child ids, invalid boundaries, and prefixes ending during execution. Broader log validation and crash repair remain with their existing owners.
 
@@ -44,6 +44,6 @@ The Host creates the child through the agent registry with the selected seed and
 
 ## Consequences
 
-The public API stays small and discoverable: live session branching is part of `ctx.sessions`, next to `create({ seed })`, rather than a standalone service or a two-step helper pair. Persistence continues to work through existing `session/created` and `session/flush` behavior: a forked child starts life with seeded events, so existing backends persist that seed once and preserve `parentSession` / `seedLength` in the header.
+The public API stays small and discoverable: live session branching is part of `ctx.sessions`, next to `create({ seed })`, rather than a standalone service or a two-step helper pair. Persistence continues to work through existing `session/created` and `session/flush` behavior: a forked child starts life with seeded events, so the JSONL backend persists that seed once and preserves logical `parentSession` / `isSeeded` plus the separate exact cut (encoded as v0 physical `seedLength`).
 
 This decision excludes ACP `session/fork`, unloaded persisted-session forking, model-facing tools, and subagent refactors. If a future ACP method is added, it should advertise the capability only after it has protocol and snapshot coverage; this Agent Note adds no ACP wire behavior, so no ACP snapshot is required. Fork-child replay remains covered by the existing [seed-boundary testing Agent Note](../testing/2026-06-22-fork-child-replay-seed-boundary.md); focused store, Host, carrier, and client tests pin the boundary and reconciliation contracts, while the real Chromium scenario pins the assembled message action and lineage tree.

+ 3 - 3
.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.zh.md

@@ -20,11 +20,11 @@ store 暴露一个操作:
 type SessionForkSource = Session | SessionId
 
 class SessionStore extends Service {
-  fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
+  fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
 }
 ```
 
-`boundary` 是要复制到的源事件 `seq`(含该序号)。省略时默认为源会话当前的最后一个事件;对空源会话省略 `boundary` 则创建一个空的子会话。fork 特有的校验会检查请求的边界存在,并确认所选前缀最近的轮次边界不是未匹配的 `turn/start`。因此,所选前缀可以结束于 `turn/end` 或更晚的独立事件,随后被深拷贝到子会话的种子中。子会话继承源会话的 `cwd`,将 `parentSession` 设为源会话 id,并将 `seedLength` 设为已复制前缀的长度。省略 `childSessionId` 时,`SessionStore` 使用其现有的 id 策略生成一个。
+`boundary` 是要复制到的品牌化源事件 `seq`(含该序号)。省略时默认为源会话当前的最后一个事件;对空源会话省略 `boundary` 则创建一个空的子会话。fork 特有的校验会检查请求的边界存在,并确认所选前缀最近的轮次边界不是未匹配的 `turn/start`。因此,所选前缀可以结束于 `turn/end` 或更晚的独立事件,随后被深拷贝到子会话的种子中。子会话继承源会话的 `cwd`,将 `parentSession` 设为源会话 id,设置 logical `isSeeded: true`,并把复制前缀的长度单独作为 `inheritedEventCount` 传入。省略 `childSessionId` 时,`SessionStore` 使用其现有的 id 策略生成一个。
 
 空前缀可以被 fork;任何非空边界都必须是位于开放轮次之外且安全、已存在的序号。类型化的错误区分源缺失、对象陈旧、子 id 重复、边界无效和前缀结束于执行过程中等情况。更广泛的日志校验与崩溃恢复仍由其现有的负责方处理。
 
@@ -44,6 +44,6 @@ Host 通过 agent(智能体)注册表,以选定的种子和谱系创建子
 
 ## 后果
 
-公开 API 保持精简且易于发现:活跃会话分支是 `ctx.sessions` 的一部分,紧邻 `create({ seed })`,而非一个独立服务或一对两步辅助函数。持久化继续通过现有的 `session/created` 和 `session/flush` 行为运作:fork 出的子会话创建时便带有种子事件,因此现有后端只需持久化该种子一次,并在 header 中保存 `parentSession`/`seedLength`。
+公开 API 保持精简且易于发现:活跃会话分支是 `ctx.sessions` 的一部分,紧邻 `create({ seed })`,而非一个独立服务或一对两步辅助函数。持久化继续通过现有的 `session/created` 和 `session/flush` 行为运作:fork 出的子会话创建时便带有种子事件,因此 JSONL 后端只需持久化该种子一次,并保留 logical `parentSession`/`isSeeded` 与单独的精确 cut(编码为 v0 物理 `seedLength`)。
 
 本决策排除 ACP(Agent Client Protocol)`session/fork`、对未加载的已持久化会话执行 fork、面向模型的工具,以及 subagent 重构。如果未来添加 ACP 方法,应在具备协议与快照覆盖后才声明支持该能力;本 Agent Note 不添加任何 ACP 协议行为,因此不需要 ACP 快照。fork 子会话的回放仍由现有的[种子边界测试 Agent Note](../testing/2026-06-22-fork-child-replay-seed-boundary.zh.md)覆盖;store、Host、载体与客户端的专项测试固定边界和对账约定,真实 Chromium 场景则固定组装后的消息操作与谱系树。

+ 2 - 2
.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.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/feature/2026-07-21-continuable-background-subagents.md
-2026-07-21-continuable-background-subagents.md: e1adf8efa19cce8e0632170886770b8d12bc9e3d
-2026-07-21-continuable-background-subagents.zh.md: f62c314320a1dace43f3028b68a1bb7d65203e84
+2026-07-21-continuable-background-subagents.md: 24ab64879fa18f5de9952af2dba321e2cae17477
+2026-07-21-continuable-background-subagents.zh.md: 0cf06f7fe30d0953bb6e7f1a4c96527fa181805a

+ 2 - 2
.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md

@@ -71,9 +71,9 @@ Human input uses the same `followup` operation. The UI may display the child tra
 
 ### Durable child handle and cold resume
 
-The continuation manager snapshots every descriptor input with the seam's `snapshotSubagentDescriptor()` (built on [`snapshotJsonValue`](../../../../packages/util/values/src/index.ts)) before Task creation, matching the detached lossless-JSON boundary already used by Agent messages. A child-scoped setup contribution — a prepended one-shot `agent/prompt-submit` listener installed by the in-process driver — appends one model-hidden `subagent/descriptor` event before downstream prompt admission can block or throw. Allowed admission opens the initial child turn afterward; rejected admission leaves the descriptor as a pre-turn log-only fact, and the activation's final required checkpoint persists it. The event carries no `surfaceOp`, remains outside model history, and survives when compaction replaces surface history. A known child id is resumable only when loading that child session yields a supported descriptor in the child's own suffix (after `seedLength`, so a fork seed cannot leak an ancestor's descriptor) and its header identifies the caller as the direct parent.
+The continuation manager snapshots every descriptor input with the seam's `snapshotSubagentDescriptor()` (built on [`snapshotJsonValue`](../../../../packages/util/values/src/index.ts)) before Task creation, matching the detached lossless-JSON boundary already used by Agent messages. A child-scoped setup contribution — a prepended one-shot `agent/prompt-submit` listener installed by the in-process driver — appends one model-hidden `subagent/descriptor` event before downstream prompt admission can block or throw. Allowed admission opens the initial child turn afterward; rejected admission leaves the descriptor as a pre-turn log-only fact, and the activation's final required checkpoint persists it. The event carries no `surfaceOp`, remains outside model history, and survives when compaction replaces surface history. A known child id is resumable only when loading that child session yields a supported descriptor at or after its exact `inheritedEventCount`, so a fork seed cannot leak an ancestor's descriptor, and its header identifies the caller as the direct parent.
 
-The continuable arm of the versioned descriptor (`SUBAGENT_DESCRIPTOR_VERSION` in [descriptor.ts](../../../../packages/subagent/subagent/src/descriptor.ts)) carries `mode: 'continuable'`, the subagent provider name, resolved child `agentOptions.provider` and `agentOptions.model`, and optional `persona` and `toolFilter`. It does not snapshot the merge-extensible `AgentOptions` object: unrelated extension values cannot make continuation fail merely because they are not JSON. It deliberately omits `subagentDepth`; cold resume relies on the persisted header's `delegationDepth` rather than reconstructing depth from the descriptor. `outputSchema` belongs to one activation's result contract rather than durable child composition. The child header remains authoritative for the child id, `cwd`, `parentSession`, `seedLength`, and `delegationDepth`, while the persisted child transcript owns the fork seed and subsequent history. [`delegationDepthOf()`](../../../../packages/subagent/subagent/src/index.ts) takes the maximum of header and runtime values, so reconstructed runtime options may deepen the persisted value but never lower it and a resumed child cannot regain a top-level delegation budget.
+The continuable arm of the versioned descriptor (`SUBAGENT_DESCRIPTOR_VERSION` in [descriptor.ts](../../../../packages/subagent/subagent/src/descriptor.ts)) carries `mode: 'continuable'`, the subagent provider name, resolved child `agentOptions.provider` and `agentOptions.model`, and optional `persona` and `toolFilter`. It does not snapshot the merge-extensible `AgentOptions` object: unrelated extension values cannot make continuation fail merely because they are not JSON. It deliberately omits `subagentDepth`; cold resume relies on the persisted header's `delegationDepth` rather than reconstructing depth from the descriptor. `outputSchema` belongs to one activation's result contract rather than durable child composition. The child header remains authoritative for the child id, `cwd`, `parentSession`, `isSeeded`, and `delegationDepth`; body-bearing persistence metadata owns the exact `inheritedEventCount`, while the child transcript owns the fork seed and subsequent history. [`delegationDepthOf()`](../../../../packages/subagent/subagent/src/index.ts) takes the maximum of header and runtime values, so reconstructed runtime options may deepen the persisted value but never lower it and a resumed child cannot regain a top-level delegation budget.
 
 Cold resume cannot depend on an optional method of `SubagentRun`, because that run has been disposed and is not retained across process restart. A run represents one disposable activation and exposes only activation-scoped operations. `SubagentRun.steer?()` names the confirmed live-only capability so it cannot be confused with service orchestration or the model-facing tool.
 

+ 2 - 2
.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md

@@ -71,9 +71,9 @@ durable child Session
 
 ### 持久化 child handle 与从持久化存储恢复
 
-继续执行管理器在创建 Task 前,通过 seam 的 `snapshotSubagentDescriptor()`(基于 [`snapshotJsonValue`](../../../../packages/util/values/src/index.ts) 构建)对每项描述符输入建立快照;这一边界与 Agent 消息现有的分离式无损 JSON 边界一致。作用于 child 作用域的 setup contribution——由进程内驱动前置安装的一次性 `agent/prompt-submit` 监听器——会在下游 prompt admission 能够阻止请求或抛出异常之前追加一个对模型隐藏的 `subagent/descriptor` 事件。admission 获准后才会开启 child 的初始轮次;admission 被拒绝时,描述符会作为轮次前的仅日志事实保留,并由该 activation 最终的必需检查点持久化。该事件不携带 `surfaceOp`,不进入模型历史,并在压缩替换 surface 历史时继续保留。只有在加载已知 child id 对应的 child 会话后,能在该 child 自身的后缀中(`seedLength` 之后,因此 fork seed 不会泄露祖先的描述符)得到受支持的描述符,且会话 header 将调用方标识为直接 parent 时,该 id 才可恢复。
+继续执行管理器在创建 Task 前,通过 seam 的 `snapshotSubagentDescriptor()`(基于 [`snapshotJsonValue`](../../../../packages/util/values/src/index.ts) 构建)对每项描述符输入建立快照;这一边界与 Agent 消息现有的分离式无损 JSON 边界一致。作用于 child 作用域的 setup contribution——由进程内驱动前置安装的一次性 `agent/prompt-submit` 监听器——会在下游 prompt admission 能够阻止请求或抛出异常之前追加一个对模型隐藏的 `subagent/descriptor` 事件。admission 获准后才会开启 child 的初始轮次;admission 被拒绝时,描述符会作为轮次前的仅日志事实保留,并由该 activation 最终的必需检查点持久化。该事件不携带 `surfaceOp`,不进入模型历史,并在压缩替换 surface 历史时继续保留。只有在加载已知 child id 对应的 child 会话后,能在其精确 `inheritedEventCount` 位置或之后得到受支持的描述符,从而阻止 fork seed 泄露祖先描述符,且会话 header 将调用方标识为直接 parent 时,该 id 才可恢复。
 
-版本化描述符的可继续分支([descriptor.ts](../../../../packages/subagent/subagent/src/descriptor.ts) 中的 `SUBAGENT_DESCRIPTOR_VERSION`)携带 `mode: 'continuable'`、subagent 提供方名称、已解析的 child `agentOptions.provider` 和 `agentOptions.model`,以及可选的 `persona` 与 `toolFilter`。它不会对可通过声明合并扩展的 `AgentOptions` 对象建立快照:与此无关的扩展值不会仅因无法表示为 JSON 而导致继续执行失败。描述符会特意省略 `subagentDepth`;从持久化存储恢复时,系统依赖持久化 header 中的 `delegationDepth`,而不根据描述符重建深度。`outputSchema` 属于单次激活的结果约定,不属于持久化 child 组合配置。child header 仍是 child id、`cwd`、`parentSession`、`seedLength` 和 `delegationDepth` 的权威信息,持久化 child transcript 则负责保存 fork seed 和后续历史。[`delegationDepthOf()`](../../../../packages/subagent/subagent/src/index.ts) 会在 header 值和运行时值中取最大值,因此重建后的运行时选项可以加深持久化值,但绝不能降低它,恢复后的 child 无法重新获得顶层委派预算。
+版本化描述符的可继续分支([descriptor.ts](../../../../packages/subagent/subagent/src/descriptor.ts) 中的 `SUBAGENT_DESCRIPTOR_VERSION`)携带 `mode: 'continuable'`、subagent 提供方名称、已解析的 child `agentOptions.provider` 和 `agentOptions.model`,以及可选的 `persona` 与 `toolFilter`。它不会对可通过声明合并扩展的 `AgentOptions` 对象建立快照:与此无关的扩展值不会仅因无法表示为 JSON 而导致继续执行失败。描述符会特意省略 `subagentDepth`;从持久化存储恢复时,系统依赖持久化 header 中的 `delegationDepth`,而不根据描述符重建深度。`outputSchema` 属于单次激活的结果约定,不属于持久化 child 组合配置。child header 仍是 child id、`cwd`、`parentSession`、`isSeeded` 和 `delegationDepth` 的权威信息;含正文的持久化 metadata 拥有精确 `inheritedEventCount`,child transcript 则负责保存 fork seed 和后续历史。[`delegationDepthOf()`](../../../../packages/subagent/subagent/src/index.ts) 会在 header 值和运行时值中取最大值,因此重建后的运行时选项可以加深持久化值,但绝不能降低它,恢复后的 child 无法重新获得顶层委派预算。
 
 从持久化存储恢复不能依赖 `SubagentRun` 的可选方法,因为该 run 已被 dispose,并且进程重启后不会保留。run 表示一次可 dispose 的激活,只暴露作用于当前激活的操作。`SubagentRun.steer?()` 这一名称明确指代提供确认语义且仅适用于在线消息的功能,以免该功能与服务编排或面向模型的工具混淆。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.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/feature/2026-07-23-session-telemetry-otel-revival.md
-2026-07-23-session-telemetry-otel-revival.md: 836531f605eee5c0dcdf108e9ee2b2744d6f72aa
-2026-07-23-session-telemetry-otel-revival.zh.md: 9874598a58ff91bd2cc5ef58c771744d3f3d9610
+2026-07-23-session-telemetry-otel-revival.md: 1110db4b0dfcfd68bde97f9964383ddd041f328b
+2026-07-23-session-telemetry-otel-revival.zh.md: 6c046c5f963760638deacacda51f8d94a13e0efd

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md


+ 2 - 2
.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.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/feature/2026-07-25-subagent-policy-inheritance.md
-2026-07-25-subagent-policy-inheritance.md: 34751a4e29e48c84d37425857b8b1b56c8d866eb
-2026-07-25-subagent-policy-inheritance.zh.md: 3d5a6c7a88579583a0ab027c52dd3063eed0a8b2
+2026-07-25-subagent-policy-inheritance.md: 6df9ad53f8588018fca53ebae3a9dfba0d4382de
+2026-07-25-subagent-policy-inheritance.zh.md: f3f7328d558ea55379cb95e53bb010f9f2d2c792

+ 1 - 1
.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md

@@ -12,7 +12,7 @@ Sandbox and approval overrides are per-session log folds. An in-process subagent
 
 The delegation boundary snapshots `sandboxPolicy.overrideOf(parent.session)` before its first await, through the shared child-agent helpers (`captureDelegatedPolicyOverrides`/`appendDelegatedPolicyOverrides` in `dsh-subagent`), which the one-shot driver and the [continuable start](2026-08-10-continuable-subagent-policy-inheritance.md) both call. A later parent switch belongs to the parent's future; cancel-and-redelegate takes a new snapshot. The sandbox-policy service is optional, and only the explicit session override is copied, never deployment defaults or one-shot grants. The approval policy is not inherited: the same capture pins every child to `'never'` — the [approvals-pinned decision](2026-08-10-subagent-approval-pinned-never.md) supersedes this note's original approval-override inheritance.
 
-Each captured value becomes a source-tagged `sandbox/mode` or `approval/policy` event appended during the child factory's unpublished setup. The session constructor has already fixed `Session.firstLiveSeq` at the fork-prefix length, so the inherited facts follow fork history, reach telemetry when the child is announced, and leave `SessionHeader.seedLength` at the prefix length. Existing last-event-wins folds therefore make the delegation snapshot beat stale fork history and let a later child switch beat the snapshot. A grandchild folds its parent's logged state, so the rule composes without another inheritance mechanism.
+Each captured value becomes a source-tagged `sandbox/mode` or `approval/policy` event appended during the child factory's unpublished setup. The session constructor has already fixed `Session.firstLiveSeq` after the constructor seed, while `Session.inheritedEventCount` keeps the exact fork-prefix length, so the inherited facts follow fork history and reach telemetry when the child is announced without changing its lineage cut. Existing last-event-wins folds therefore make the delegation snapshot beat stale fork history and let a later child switch beat the snapshot. A grandchild folds its parent's logged state, so the rule composes without another inheritance mechanism.
 
 Ordinary session appends validate the inherited events before publication, and persistence captures the complete unpublished log when the session is announced. Any materialized child log therefore stores the inherited events with its first batch; there is no second policy store, schema field, or query index. The `source: 'delegation'` marker lets approval narration distinguish inheritance from a child-side user switch.
 

+ 1 - 1
.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 委派边界在第一次 await 之前,经由共享的子 agent 辅助函数(`dsh-subagent` 中的 `captureDelegatedPolicyOverrides`/`appendDelegatedPolicyOverrides`)对 `sandboxPolicy.overrideOf(parent.session)` 获取快照;一次性驱动器与[可继续启动](2026-08-10-continuable-subagent-policy-inheritance.zh.md)都会调用这些辅助函数。父级后续的切换属于父级的未来;取消后重新委派会取得新快照。沙箱策略服务为可选,仅复制显式会话覆盖项,绝不复制部署默认值或一次性授权。审批策略不继承:同一次捕获会把每个子 agent 钉定为 `'never'`——[审批钉定决策](2026-08-10-subagent-approval-pinned-never.zh.md)取代了本 note 原先的审批覆盖项继承。
 
-每个捕获值都会成为子 agent 工厂在未发布设置阶段追加的一条带来源标记的 `sandbox/mode` 或 `approval/policy` 事件。会话构造函数已将 `Session.firstLiveSeq` 固定为 fork 前缀的长度,因此继承事实会排在 fork 历史之后,在子 agent 公布时进入遥测,同时让 `SessionHeader.seedLength` 保持为此前缀的长度。因此,既有的末事件胜出折叠会让委派快照压过陈旧的 fork 历史,并让子 agent 后续的切换压过该快照。孙代 agent 会折叠其父级已记录的状态,因此无需另一套继承机制即可组合此规则。
+每个捕获值都会成为子 agent 工厂在未发布设置阶段追加的一条带来源标记的 `sandbox/mode` 或 `approval/policy` 事件。会话构造函数已把 `Session.firstLiveSeq` 固定在 constructor seed 之后,而 `Session.inheritedEventCount` 保留精确的 fork 前缀长度,因此继承事实会排在 fork 历史之后,并在子 agent 公布时进入遥测,却不改变其谱系 cut。因此,既有的末事件胜出折叠会让委派快照压过陈旧的 fork 历史,并让子 agent 后续的切换压过该快照。孙代 agent 会折叠其父级已记录的状态,因此无需另一套继承机制即可组合此规则。
 
 普通的会话追加会在发布前校验继承事件,持久化层则在会话公布时捕获完整的未发布日志。因此,任何已物化的子 agent 日志都会在首批数据中存下继承事件;不存在第二套策略存储、schema 字段或查询索引。`source: 'delegation'` 标记让审批叙述能够区分继承与子 agent 侧的用户切换。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-08-05-durable-web-schedule.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/feature/2026-08-05-durable-web-schedule.md
-2026-08-05-durable-web-schedule.md: bac4a5cfd8965032dad2cf689ca42b1f8e5da1e3
-2026-08-05-durable-web-schedule.zh.md: 266a90d3a56caccda56be9fe648c059939b8155f
+2026-08-05-durable-web-schedule.md: 1e7a343472cce638a7d1476871cec221fa9fd889
+2026-08-05-durable-web-schedule.zh.md: 50f3347a2c312e3c1a2ea28c582a277cc93a614e

+ 2 - 2
.agents/notes/implemented/feature/2026-08-05-durable-web-schedule.md

@@ -22,11 +22,11 @@ The user-visible boundary is `session-local`: the original Session runs an on-ti
 | Due while busy | Active create remains in the fold | Owner waits for idle maintenance, queues one follow-up, then appends dispatch | A later ordinary conversation turn |
 | Several Every records are overdue | Each active record retains its earliest unaccepted anchor-aligned target | One decision selects each record's latest occurrence and advances it past now | One ordinary follow-up containing one occurrence per record |
 | Process stopped or Session cold | Active create remains persisted | No timer or background scan; resume rebuilds the owner | Future target waits; overdue target is attempted |
-| Fork | Parent events remain in the inherited prefix | Child fold starts at `seedLength` | Parent work does not become active in the child |
+| Fork | Parent events remain in the inherited prefix | Child fold starts at the exact `inheritedEventCount` | Parent work does not become active in the child |
 
 ### Session-log authority and tools
 
-The version-1 `schedule/change` stream is the only durable Schedule authority. A create record owns a Session-local, non-reused branded id, the trimmed prompt, its rule discriminator, and UTC target. Delete and one-shot dispatch are terminal transitions. Every dispatch stores its id and decision time so the fold advances that record directly past missed occurrences. The strict decoder and pure fold reject unknown versions, extra fields, reused ids, mismatched dispatch shapes, and transitions against inactive records. A normal Session folds its complete stream; a fork folds only events at or after `SessionHeader.seedLength`.
+The version-1 `schedule/change` stream is the only durable Schedule authority. A create record owns a Session-local, non-reused branded id, the trimmed prompt, its rule discriminator, and UTC target. Delete and one-shot dispatch are terminal transitions. Every dispatch stores its id and decision time so the fold advances that record directly past missed occurrences. The strict decoder and pure fold reject unknown versions, extra fields, reused ids, mismatched dispatch shapes, and transitions against inactive records. A normal Session folds its complete stream; a fork folds only events at or after the `inheritedEventCount` passed into projection initialization.
 
 When `ctx.sessionProjections` exists, Schedule registers a strict unit that uses the same transition and publishes the complete active `ScheduleRecord[]`; the shared [projection state decision](../architecture/2026-08-19-session-projection-state-and-client-views.md) owns its initialization and restore contract. Corrupt durable input fails the existing read path rather than yielding a partial array. The browser-safe record vocabulary is exposed through the type-only `@deepseek-ai/dsh-schedule/client` subpath.
 

+ 2 - 2
.agents/notes/implemented/feature/2026-08-05-durable-web-schedule.zh.md

@@ -22,11 +22,11 @@ Status: implemented
 | 到期时繁忙 | 活动 create 仍在 fold 中 | owner 等待 idle maintenance,排入一个 follow-up,再追加 dispatch | 后续一个普通对话轮次 |
 | 多条 Every 记录逾期 | 每条活动记录都保留最早一个尚未接受且与锚点对齐的目标 | 一次决策选择每条记录的最新发生时点,并将其推进到当前时刻之后 | 一个普通 follow-up,其中每条记录各有一个发生时点 |
 | 进程停止或 Session cold | 活动 create 仍在 persistence 中 | 不存在 timer 或后台扫描;resume 重建 owner | 未来目标继续等待;overdue 目标会被尝试 |
-| fork | 父 event 留在继承前缀 | child fold 从 `seedLength` 开始 | 父工作不会在 child 中变为活动状态 |
+| fork | 父 event 留在继承前缀 | child fold 从精确 `inheritedEventCount` 开始 | 父工作不会在 child 中变为活动状态 |
 
 ### Session 日志权威与工具
 
-版本 1 `schedule/change` stream 是唯一持久的 Schedule 权威。create 记录拥有一个 Session 内不复用的品牌 id、trim 后的提示词、规则判别字段和 UTC 目标。delete 与一次性 dispatch 是终结转换。Every dispatch 会存储 id 与决策时点,使 fold 将该记录直接推进到错过的发生时点之后。严格 decoder 与纯 fold 会拒绝未知版本、额外字段、重复使用的 id、形状不匹配的 dispatch,以及针对非活动记录的转换。普通 Session 折叠完整 stream;fork 只折叠 `SessionHeader.seedLength` 位置及其后的 event。
+版本 1 `schedule/change` stream 是唯一持久的 Schedule 权威。create 记录拥有一个 Session 内不复用的品牌 id、trim 后的提示词、规则判别字段和 UTC 目标。delete 与一次性 dispatch 是终结转换。Every dispatch 会存储 id 与决策时点,使 fold 将该记录直接推进到错过的发生时点之后。严格 decoder 与纯 fold 会拒绝未知版本、额外字段、重复使用的 id、形状不匹配的 dispatch,以及针对非活动记录的转换。普通 Session 折叠完整 stream;fork 只折叠传入 projection 初始化的 `inheritedEventCount` 位置及其后的 event。
 
 `ctx.sessionProjections` 存在时,Schedule 会注册一个复用同一 transition 的严格单元,并发布完整的活动 `ScheduleRecord[]`;共享的 [projection state 决策](../architecture/2026-08-19-session-projection-state-and-client-views.zh.md)拥有其初始化与 restore 约定。损坏的持久输入会使既有读取路径失败,而不会产生部分数组。浏览器安全的记录词汇通过纯类型子路径 `@deepseek-ai/dsh-schedule/client` 暴露。
 

+ 2 - 2
.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.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/testing/2026-06-22-fork-child-replay-seed-boundary.md
-2026-06-22-fork-child-replay-seed-boundary.md: cf4a974035b38ee61e4c2d1cab34776b2ad186c8
-2026-06-22-fork-child-replay-seed-boundary.zh.md: bcc60a583a5f9a4d1fd10ec16e90c49c879d295c
+2026-06-22-fork-child-replay-seed-boundary.md: 1fefc3546ebe029ba95318dad3ac2d463a3fe497
+2026-06-22-fork-child-replay-seed-boundary.zh.md: bb43eacf075318dc0101fca2cacb45a03d847764

+ 7 - 7
.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md

@@ -18,19 +18,19 @@ Deriving the child script from the whole fork-child log therefore replays the **
 
 Record where a session's **inherited** prefix ends, persist it, and have the replay harness derive a child's script from its **own** events only.
 
-### 1. `seedLength` on the session header
+### 1. Lineage metadata and an exact body-owned cut
 
-`SessionHeader` gains an optional `seedLength: number` — how many leading events were inherited via a seed rather than produced by this session. The fork backend stamps it (= the seeded-prefix length) when it creates the child; a fresh spawn leaves it absent (≡ 0). It is threaded through `CreateSessionOptions.meta` (and `CreateAgentOptions.meta`), set in `SessionStore.prepare`.
+`SessionHeader.isSeeded` records whether a Session has inherited lineage without exposing a body coordinate to header-only readers. The exact leading-event count is the separately branded `SessionLogOffset` `inheritedEventCount`; a fork supplies both `isSeeded: true` and the copied-prefix length, while a fresh spawn supplies an unseeded header and cut zero. The cut travels through `CreateSessionOptions`, `CreateAgentOptions`, persistence inspection, and restored Session state.
 
-`seedLength` is **explicit**, never inferred from `seed.length`. A reconstruction (resume/load) seeds the session with its WHOLE stored log, so `seed.length` there is the full length, not the original boundary — the resume path passes the persisted `seedLength` back from the loaded header instead. (Same shape as `createdAt`, which is also explicitly preserved on reconstruction rather than re-defaulted to now.)
+`inheritedEventCount` is **explicit**, never inferred from `seed.length`. A reconstruction (resume/load) seeds the session with its WHOLE stored log, so `seed.length` there is the full length, not the original boundary — the resume path passes the decoded cut beside the logical header instead.
 
 ### 2. JSONL round-trips it
 
-JSONL stores `seedLength` on the header line (`toHeaderLine`/`fromHeaderLine`) and returns it through the shared persistence contract.
+The v0 JSONL header keeps its optional numeric `seedLength` for byte compatibility. `toHeaderLine` / `fromHeaderLine` translate it to and from logical `isSeeded` plus the exact `inheritedEventCount`, which the shared body-bearing persistence values return separately.
 
 ### 3. Replay derives a child script after the boundary
 
-`dsh-llm-replay`'s `parseSessionHeader` now also reads `seedLength` (absent ⇒ 0), and `loadSessionScripts` derives a child's entries from `parseSessionLog(text).slice(seedLength)` — the events at or after the boundary, i.e. the child's own model calls. For a spawn child `seedLength` is 0 and this is a no-op, so spawn scenarios are byte-for-byte unchanged.
+`dsh-llm-replay`'s private v0 parser reads physical `seedLength` into `inheritedEventCount` (absent ⇒ 0), and `loadSessionScripts` derives a child's entries from `parseSessionLog(text).slice(inheritedEventCount)` — the events at or after the boundary, i.e. the child's own model calls. For a spawn child the cut is 0 and this is a no-op, so spawn scenarios are byte-for-byte unchanged.
 
 This closes the routing correctness gap, and two recorded fork scenarios exercise it end to end — see [Record fork and mixed spawn+fork snapshot scenarios](../../archived/testing/2026-06-22-fork-snapshot-scenarios.md).
 
@@ -40,5 +40,5 @@ This closes the routing correctness gap, and two recorded fork scenarios exercis
 
 ## Consequences
 
-- A new persisted header field spans core and the JSONL provider; the subsystems catalog (`persistence.md`) is updated in the same change (its `SessionHeader` / `CreateSessionOptions` `type-equiv` blocks).
-- Spawn replay is unchanged (`seedLength` 0). Fork replay now routes a child to its own script; covered by a regression in `llm-replay`'s tests (a child fixture whose seeded prefix carries a parent chunk — the derived child script must exclude it, proven red without the slice) and a JSONL persistence round trip through the shared coordinator contract.
+- The lineage bit spans logical Session metadata while the exact cut spans only body-bearing core, persistence, query, and replay values; the v0 physical header remains unchanged.
+- Spawn replay is unchanged (cut 0). Fork replay routes a child to its own script; covered by a regression in `llm-replay`'s tests (a child fixture whose seeded prefix carries a parent chunk — the derived child script must exclude it, proven red without the slice) and a JSONL persistence round trip through the shared coordinator contract.

+ 7 - 7
.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.zh.md

@@ -18,19 +18,19 @@ subagent 脚本由 [`deriveReplayScript`](../../../../packages/test-support/llm-
 
 记录会话**继承**前缀的结束位置,将其持久化,并让回放 harness 仅从子会话**自身**的事件推导脚本。
 
-### 1. 会话头部的 `seedLength`
+### 1. 谱系 metadata 与正文拥有的精确 cut
 
-`SessionHeader` 新增可选字段 `seedLength: number`——表示有多少前导事件是通过 seed 继承而来、而非本会话产生的。fork 后端在创建子会话时设置它(= 播种前缀的长度);全新的 spawn 子会话不设置(等同于 0)。它通过 `CreateSessionOptions.meta`(及 `CreateAgentOptions.meta`)传递,在 `SessionStore.prepare` 中设置。
+`SessionHeader.isSeeded` 记录 Session 是否具有继承谱系,而不向仅 header 的 reader 暴露正文坐标。精确的前导事件数量是单独品牌化为 `SessionLogOffset` 的 `inheritedEventCount`;fork 同时提供 `isSeeded: true` 与复制前缀的长度,全新的 spawn 则提供 unseeded header 与零 cut。该 cut 经 `CreateSessionOptions`、`CreateAgentOptions`、持久化 inspection 与恢复后的 Session 状态传递。
 
-`seedLength` 是**显式**的,绝不从 `seed.length` 推断。恢复/加载时用会话的完整已存储日志作为 seed,此时 `seed.length` 是全长而非原始边界——恢复路径改为从加载的 header 中取回持久化的 `seedLength`。(做法与 `createdAt` 相同:恢复时显式保留,而非重新默认为当前时间。)
+`inheritedEventCount` 是**显式**的,绝不从 `seed.length` 推断。恢复/加载时用会话的完整已存储日志作为 seed,此时 `seed.length` 是全长而非原始边界——恢复路径改为在 logical header 之外传递解码后的 cut。
 
 ### 2. JSONL 完整往返
 
-JSONL 把 `seedLength` 存在 header 行(`toHeaderLine`/`fromHeaderLine`),并通过共享持久化约定返回它。
+v0 JSONL header 为保持字节兼容而继续携带可选数值 `seedLength`。`toHeaderLine`/`fromHeaderLine` 在它与 logical `isSeeded` 加精确 `inheritedEventCount` 之间转换,共享的含正文持久化值再单独返回该 cut。
 
 ### 3. 回放从边界之后推导子会话脚本
 
-`dsh-llm-replay` 的 `parseSessionHeader` 现在也读取 `seedLength`(缺失则为 0),`loadSessionScripts` 从 `parseSessionLog(text).slice(seedLength)` 推导子会话条目——即边界及之后的事件,也就是子会话自身的模型调用。对 spawn 子会话而言 `seedLength` 为 0,此操作是空操作,spawn 场景逐字节不变。
+`dsh-llm-replay` 的私有 v0 parser 把物理 `seedLength` 读入 `inheritedEventCount`(缺失则为 0),`loadSessionScripts` 从 `parseSessionLog(text).slice(inheritedEventCount)` 推导子会话条目——即边界及之后的事件,也就是子会话自身的模型调用。对 spawn 子会话而言 cut 为 0,此操作是空操作,spawn 场景逐字节不变。
 
 这弥补了路由正确性的缺口,两个已录制的 fork 场景对其进行端到端验证——见[记录 fork 与混合 spawn+fork 快照场景](../../archived/testing/2026-06-22-fork-snapshot-scenarios.md)。
 
@@ -40,5 +40,5 @@ JSONL 把 `seedLength` 存在 header 行(`toHeaderLine`/`fromHeaderLine`),
 
 ## 后果
 
-- core 与 JSONL provider 新增一个持久化 header 字段;子系统目录(`persistence.md`)在同一变更中更新(其 `SessionHeader` / `CreateSessionOptions` 的 `type-equiv` 块)。
-- spawn 回放不变(`seedLength` 为 0)。fork 回放现在将子会话路由到自身的脚本;由 `llm-replay` 测试中的一个回归用例覆盖(一个子会话 fixture,其播种前缀包含父会话的分片——推导出的子会话脚本必须排除它,不做 slice 时该用例会失败),以及通过共享 coordinator 约定执行的 JSONL 持久化往返测试。
+- 谱系 bit 横跨 logical Session metadata,精确 cut 则只横跨含正文的 core、持久化、query 与 replay 值;v0 物理 header 保持不变。
+- spawn 回放不变(cut 为 0)。fork 回放将子会话路由到自身的脚本;由 `llm-replay` 测试中的一个回归用例覆盖(一个子会话 fixture,其播种前缀包含父会话的分片——推导出的子会话脚本必须排除它,不做 slice 时该用例会失败),以及通过共享 coordinator 约定执行的 JSONL 持久化往返测试。

+ 7 - 6
apps/cli/tests/profiles/headless/tests/semantic-checkpoint.expected.e2e.ts

@@ -5,7 +5,7 @@ import { Context } from '@deepseek-ai/cordis'
 import { normalizeSessionSnapshot, type NormalizeContext } from '@deepseek-ai/dsh-session-snapshot'
 import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
 import { createUserMessage, ToolCallId , createMessage } from '@deepseek-ai/dsh-llm'
-import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
+import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionSeq, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
 import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
 import { describe, expect, it } from 'vitest'
 
@@ -29,17 +29,18 @@ async function seedInterruptedSession(root: string, cwd: string): Promise<string
     id: sessionId,
     createdAt: 1,
     cwd,
+    isSeeded: false,
     delegationDepth: 0,
   }
   const events: SessionEvent[] = [
-    { type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
-    { type: 'user/message', seq: 1, time: 11, data: createUserMessage({
+    { type: 'turn/start', seq: SessionSeq(0), time: 10, data: { turn: 1 } },
+    { type: 'user/message', seq: SessionSeq(1), time: 11, data: createUserMessage({
       content: [{ type: 'text', text: 'Perform one side-effecting remote mutation.' }], source: { kind: 'user' },
     }), surfaceOp: 'append' },
-    { type: 'step/start', seq: 2, time: 12, data: { turn: 1, step: 1 } },
+    { type: 'step/start', seq: SessionSeq(2), time: 12, data: { turn: 1, step: 1 } },
     {
       type: 'assistant/message',
-      seq: 3,
+      seq: SessionSeq(3),
       time: 13,
       data: {
         turn: 1,
@@ -57,7 +58,7 @@ async function seedInterruptedSession(root: string, cwd: string): Promise<string
     },
     {
       type: 'tool/call',
-      seq: 4,
+      seq: SessionSeq(4),
       time: 14,
       data: {
         turn: 1,

+ 5 - 4
apps/cli/tests/profiles/headless/tests/session-format-guard.expected.e2e.ts

@@ -13,6 +13,7 @@ import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-l
 import SessionStore, {
   SESSION_FORMAT_VERSION,
   SessionId,
+  SessionSeq,
   type SessionEvent,
   type SessionHeader,
 } from '@deepseek-ai/dsh-session'
@@ -32,7 +33,7 @@ async function seedSession(root: string, cwd: string, version: number, events: S
   const ctx = new Context()
   await ctx.plugin(SessionStore)
   await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' })
-  const meta: SessionHeader = { version, id: sessionId, createdAt: 1, cwd }
+  const meta: SessionHeader = { version, id: sessionId, createdAt: 1, cwd, isSeeded: false }
   try {
     await ctx.sessionPersistence.create(meta)
     await ctx.sessionPersistence.append(sessionId, events)
@@ -46,8 +47,8 @@ async function seedSession(root: string, cwd: string, version: number, events: S
 
 function closedTurn(): SessionEvent[] {
   return [
-    { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
-    { type: 'turn/end', seq: 1, time: 2, data: { turn: 1, reason: { kind: 'completed' } } },
+    { type: 'turn/start', seq: SessionSeq(0), time: 1, data: { turn: 1 } },
+    { type: 'turn/end', seq: SessionSeq(1), time: 2, data: { turn: 1, reason: { kind: 'completed' } } },
   ]
 }
 
@@ -92,7 +93,7 @@ describe('session format guard through the assembled app', () => {
       prepare: async (runCwd) => {
         sessionPath = await seedSession(join(runCwd, '.sessions'), runCwd, SESSION_FORMAT_VERSION, [
           ...closedTurn(),
-          { type: 'future/event', seq: 2, time: 3, data: { payload: 1 } } as unknown as SessionEvent,
+          { type: 'future/event', seq: SessionSeq(2), time: 3, data: { payload: 1 } } as unknown as SessionEvent,
         ])
       },
     })

+ 8 - 6
apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts

@@ -11,7 +11,7 @@ import { Context } from '@deepseek-ai/cordis'
 import { normalizeSessionSnapshot, type NormalizeContext } from '@deepseek-ai/dsh-session-snapshot'
 import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
-import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
+import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionSeq, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
 import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
 import { describe, expect, it } from 'vitest'
 
@@ -40,12 +40,13 @@ async function seedDescriptorlessChild(root: string, cwd: string): Promise<void>
     id: parentId,
     createdAt: 1,
     cwd,
+    isSeeded: false,
     delegationDepth: 0,
   }
   const parentEvents: SessionEvent[] = [
-    { type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
-    { type: 'user/message', seq: 1, time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Start a background job.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
-    { type: 'turn/end', seq: 2, time: 12, data: { turn: 1, reason: { kind: 'completed' } } },
+    { type: 'turn/start', seq: SessionSeq(0), time: 10, data: { turn: 1 } },
+    { type: 'user/message', seq: SessionSeq(1), time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Start a background job.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
+    { type: 'turn/end', seq: SessionSeq(2), time: 12, data: { turn: 1, reason: { kind: 'completed' } } },
   ]
   const childMeta: SessionHeader = {
     version: SESSION_FORMAT_VERSION,
@@ -53,12 +54,13 @@ async function seedDescriptorlessChild(root: string, cwd: string): Promise<void>
     createdAt: 2,
     cwd,
     parentSession: parentId,
+    isSeeded: false,
     origin: 'subagent',
     delegationDepth: 1,
   }
   const childEvents: SessionEvent[] = [
-    { type: 'turn/start', seq: 0, time: 20, data: { turn: 1 } },
-    { type: 'turn/end', seq: 1, time: 21, data: { turn: 1, reason: { kind: 'interrupted' } } },
+    { type: 'turn/start', seq: SessionSeq(0), time: 20, data: { turn: 1 } },
+    { type: 'turn/end', seq: SessionSeq(1), time: 21, data: { turn: 1, reason: { kind: 'interrupted' } } },
   ]
   try {
     await ctx.sessionPersistence.create(parentMeta)

+ 7 - 6
apps/cli/tests/profiles/headless/tests/subagent-inheritance.expected.e2e.ts

@@ -10,7 +10,7 @@ import { Context } from '@deepseek-ai/cordis'
 import { normalizeSessionSnapshot, type NormalizeContext } from '@deepseek-ai/dsh-session-snapshot'
 import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
 import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
-import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
+import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionSeq, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
 import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
 import { describe, expect, it } from 'vitest'
 
@@ -36,15 +36,16 @@ async function seedReadOnlyParent(root: string, cwd: string): Promise<void> {
     id: sessionId,
     createdAt: 1,
     cwd,
+    isSeeded: false,
     delegationDepth: 0,
   }
   const events: SessionEvent[] = [
-    { type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
-    { type: 'user/message', seq: 1, time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Tighten this session to read-only.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
-    { type: 'sandbox/mode', seq: 2, time: 12, data: { mode: 'read-only' } },
+    { type: 'turn/start', seq: SessionSeq(0), time: 10, data: { turn: 1 } },
+    { type: 'user/message', seq: SessionSeq(1), time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Tighten this session to read-only.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
+    { type: 'sandbox/mode', seq: SessionSeq(2), time: 12, data: { mode: 'read-only' } },
     {
       type: 'request/header',
-      seq: 3,
+      seq: SessionSeq(3),
       time: 13,
       data: {
         header: {
@@ -57,7 +58,7 @@ async function seedReadOnlyParent(root: string, cwd: string): Promise<void> {
         reason: 'initial',
       },
     },
-    { type: 'turn/end', seq: 4, time: 14, data: { turn: 1, reason: { kind: 'completed' } } },
+    { type: 'turn/end', seq: SessionSeq(4), time: 14, data: { turn: 1, reason: { kind: 'completed' } } },
   ]
   try {
     await ctx.sessionPersistence.create(meta)

+ 6 - 4
apps/cli/tests/profiles/headless/tests/workspace-context-resume.expected.e2e.ts

@@ -14,6 +14,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
 import SessionStore, {
   SESSION_FORMAT_VERSION,
   SessionId,
+  SessionSeq,
   type SessionEvent,
   type SessionHeader,
 } from '@deepseek-ai/dsh-session'
@@ -53,6 +54,7 @@ async function seedVisibleBaseline(
     id: sessionId,
     createdAt: 1,
     cwd,
+    isSeeded: false,
     delegationDepth: 0,
   }
   const files = options.files ?? [{ name: 'AGENTS.md', content: oldInstruction }]
@@ -69,17 +71,17 @@ async function seedVisibleBaseline(
       : { instructionFileCandidates: options.instructionFileCandidates },
   })
   const events: SessionEvent[] = [
-    { type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
+    { type: 'turn/start', seq: SessionSeq(0), time: 10, data: { turn: 1 } },
     {
       type: 'user/message',
-      seq: 1,
+      seq: SessionSeq(1),
       time: 11,
       data: createUserMessage({ content: [{ type: 'text', text: 'Remember the workspace instruction.' }], source: { kind: 'user' } }),
       surfaceOp: 'append',
     },
     {
       type: 'user/message',
-      seq: 2,
+      seq: SessionSeq(2),
       time: 12,
       data: createUserMessage({
         content: [{ type: 'text', text: baseline.text }],
@@ -98,7 +100,7 @@ async function seedVisibleBaseline(
       }),
       surfaceOp: 'append',
     },
-    { type: 'turn/end', seq: 3, time: 13, data: { turn: 1, reason: { kind: 'completed' } } },
+    { type: 'turn/end', seq: SessionSeq(3), time: 13, data: { turn: 1, reason: { kind: 'completed' } } },
   ]
   try {
     await ctx.sessionPersistence.create(meta)

+ 6 - 4
apps/cli/tests/web-agent-presets.e2e.ts

@@ -6,7 +6,7 @@ import { dirname, join } from 'node:path'
 import { Context } from '@deepseek-ai/cordis'
 import { boot, healProfilesModuleFallback, loadOverlayPatches, loadProfile } from '@deepseek-ai/dsh-app-boot'
 import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
-import { SessionId } from '@deepseek-ai/dsh-session'
+import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
 import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
@@ -655,9 +655,11 @@ describe('a forked session', () => {
     const inherited = ctx.sessionProjections.stateOf(parent.agent.session, 'agentPreset') ?? undefined
     const child = await ctx.agents.create({
       sessionId: SessionId('preset-fork-child'),
+      seed: [],
+      inheritedEventCount: SessionLogOffset(0),
       meta: {
         parentSession: SessionId('preset-fork-parent'),
-        seedLength: 0,
+        isSeeded: true,
         ...inherited === undefined ? {} : { agentPreset: inherited },
       },
       setup: agentCtx => ctx.agentPresets.mount(agentCtx, inherited).then(() => undefined),
@@ -685,7 +687,7 @@ describe('a delegated child', () => {
     // Exactly what an in-process subagent driver's creation window does.
     const child = await parent.agent.ctx.agents.create({
       sessionId: SessionId('preset-child'),
-      meta: childSessionMeta(parent.agent, 1, 0),
+      meta: childSessionMeta(parent.agent, 1, false),
       setup: (agentCtx) => {
         applyChildComposition(agentCtx, parent.agent, {})
       },
@@ -711,7 +713,7 @@ describe('a delegated child', () => {
     await ctx.agentPresets.recompose(parent.agent.ctx, 'minimal')
     const child = await parent.agent.ctx.agents.create({
       sessionId: SessionId('preset-child-switch'),
-      meta: childSessionMeta(parent.agent, 1, 0),
+      meta: childSessionMeta(parent.agent, 1, false),
       setup: (agentCtx) => {
         applyChildComposition(agentCtx, parent.agent, {})
       },

+ 11 - 7
apps/web/tests/agent-preset-selection.e2e.ts

@@ -15,8 +15,9 @@ import { join } from 'node:path'
 import type { Browser, Page } from 'playwright'
 import { chromium } from 'playwright'
 import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import { MessageId } from '@deepseek-ai/dsh-llm'
 import {
-  SESSION_FORMAT_VERSION, SessionId as sessionId, type SessionEvent, type SessionHeader, type SessionId,
+  SESSION_FORMAT_VERSION, SessionId as sessionId, SessionSeq, type SessionHeader, type SessionId,
 } from '@deepseek-ai/dsh-session'
 import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
 import {
@@ -116,6 +117,7 @@ async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise
     createdAt,
     cwd: scaffold.workspaceCwd,
     parentSession: parentId,
+    isSeeded: false,
     origin: 'subagent',
     delegationDepth: 1,
     agentPreset: 'minimal',
@@ -124,15 +126,17 @@ async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise
   await scaffold.ctx.sessionPersistence.append(childId, [
     {
       type: 'turn/start',
-      seq: 0,
+      seq: SessionSeq(0),
       time: createdAt,
-      data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
+      data: { turn: 1 },
     },
     {
       type: 'user/message',
-      seq: 1,
+      seq: SessionSeq(1),
       time: createdAt + 1,
       data: {
+        id: MessageId(`legacy-message:${childId}:1`),
+        role: 'user',
         content: [{ type: 'text', text: 'Check the session-header action order.' }],
         source: { kind: 'user' },
       },
@@ -140,7 +144,7 @@ async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise
     },
     {
       type: 'subagent/descriptor',
-      seq: 2,
+      seq: SessionSeq(2),
       time: createdAt + 2,
       data: snapshotSubagentDescriptor({
         mode: 'one-shot', provider: 'spawn', label: 'header order probe',
@@ -148,11 +152,11 @@ async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise
     },
     {
       type: 'turn/end',
-      seq: 3,
+      seq: SessionSeq(3),
       time: createdAt + 3,
       data: { turn: 1, reason: { kind: 'completed' } },
     },
-  ] as SessionEvent[])
+  ])
 }
 
 /**

+ 1 - 1
apps/web/tests/chat-long-interactions.e2e.ts

@@ -311,7 +311,7 @@ describe('web e2e: long Chat interaction contract', () => {
     const child = scaffold.ctx.agents.list()
       .find(agent => agent.session.header.parentSession === SessionId(SESSION_ID))
     if (child === undefined) throw new Error('message branch did not create a child session')
-    expect(child.session.header.seedLength).toBe(boundary.seq + 1)
+    expect(child.session.inheritedEventCount).toBe(boundary.seq + 1)
     expect(child.session.snapshotEvents().some(event => carries(event, branchAssistantMarker))).toBe(true)
     expect(child.session.snapshotEvents().some(event => carries(event, FIXTURE.markers.user(BRANCH_TURN + 1)))).toBe(false)
     expect(child.session.snapshotEvents().some(event => carries(event, FIXTURE.markers.user(FIXTURE.turns)))).toBe(false)

+ 2 - 2
apps/web/tests/complex-history.perf.ts

@@ -17,7 +17,7 @@ import {
   createUserMessage,
 } from '@deepseek-ai/dsh-llm'
 import type { ReplayEntry, ReplayOverrideDoc } from '@deepseek-ai/dsh-llm-replay'
-import type { SessionEvent } from '@deepseek-ai/dsh-session'
+import type { SessionEvent, SessionSeq } from '@deepseek-ai/dsh-session'
 import {
   SESSION_FORMAT_VERSION,
   Session,
@@ -187,7 +187,7 @@ function text(value: string): { type: 'text'; text: string }[] {
   return [{ type: 'text', text: value }]
 }
 
-function appendTitle(session: Session, title: string, messageSeq: number): void {
+function appendTitle(session: Session, title: string, messageSeq: SessionSeq): void {
   session.append('session/title', {
     title,
     messageSeqs: [messageSeq],

+ 2 - 1
apps/web/tests/reference-composer.e2e.ts

@@ -12,6 +12,7 @@ import {
   SESSION_FORMAT_VERSION,
   Session,
   SessionId,
+  SessionSeq,
 } from '@deepseek-ai/dsh-session'
 import type {} from '@deepseek-ai/dsh-session-reference/types'
 import type {} from '@deepseek-ai/dsh-session-title'
@@ -80,7 +81,7 @@ function targetSessionFixture(): string {
       references: [{
         sessionId: SOURCE_SESSION_ID,
         label: 'Research notes',
-        capturedThroughSeq: 4,
+        capturedThroughSeq: SessionSeq(4),
         compacted: false,
         originalMessages: 2,
         retainedMessages: 2,

+ 17 - 2
apps/web/tests/scaffold.ts

@@ -67,6 +67,7 @@ import SessionStore, {
   packChunkRuns,
   SESSION_FORMAT_VERSION,
   SessionId,
+  SessionSeq,
   type Session,
   type SessionEvent,
   type SessionHeader,
@@ -801,8 +802,20 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
  * in-memory record-mode harvest, so the on-disk zstd default never matters.
  */
 function rawSessionLog(session: Session): string {
+  const header = session.header
   return [
-    JSON.stringify({ type: 'session', ...session.header }),
+    JSON.stringify({
+      type: 'session',
+      version: header.version,
+      id: header.id,
+      createdAt: header.createdAt,
+      ...header.cwd === undefined ? {} : { cwd: header.cwd },
+      ...header.parentSession === undefined ? {} : { parentSession: header.parentSession },
+      ...header.isSeeded ? { seedLength: Number(session.inheritedEventCount) } : {},
+      ...header.origin === undefined ? {} : { origin: header.origin },
+      ...header.delegationDepth === undefined ? {} : { delegationDepth: header.delegationDepth },
+      ...header.agentPreset === undefined ? {} : { agentPreset: header.agentPreset },
+    }),
     ...packChunkRuns(session.snapshotEvents()).map(record => JSON.stringify(record)),
     '',
   ].join('\n')
@@ -1030,6 +1043,7 @@ export async function seedSession(
     id: SessionId(id),
     createdAt: Date.now() - 60_000,
     cwd: scaffold.workspaceCwd,
+    isSeeded: false,
     delegationDepth: 0,
     ...agentPreset === undefined ? {} : { agentPreset },
   }
@@ -1054,11 +1068,12 @@ export async function seedBlankSession(
     id: SessionId(id),
     createdAt: Date.now() - 60_000,
     cwd,
+    isSeeded: false,
     delegationDepth: 0,
   }
   await persistSeedSession(scaffold, meta, [{
     type: 'session/end-seed',
-    seq: 0,
+    seq: SessionSeq(0),
     time: meta.createdAt,
     data: {},
   }])

+ 7 - 3
apps/web/tests/schedule-after.e2e.ts

@@ -10,7 +10,7 @@ import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent'
 import { composeEntries, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
 import { ToolCallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
 import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
-import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
+import { SessionId, SessionLogOffset, type SessionEvent } from '@deepseek-ai/dsh-session'
 import {
   ScheduleId,
   createEveryScheduleRecord,
@@ -615,8 +615,12 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
     await workspace.attachSession(CATALOG_SESSION_ID)
 
     // Seed the zero-I/O list view before the Session is opened.
-    const catalog = await scaffold.ctx.sessionPersistence.readFrom(CATALOG_SESSION_ID, 0)
-    scaffold.ctx.sessionProjectionCache.coldSnapshot(catalog.meta, catalog.events)
+    const catalog = await scaffold.ctx.sessionPersistence.readFrom(CATALOG_SESSION_ID, SessionLogOffset(0))
+    scaffold.ctx.sessionProjectionCache.coldSnapshot(
+      catalog.meta,
+      catalog.inheritedEventCount,
+      catalog.events,
+    )
 
     browser = await chromium.launch()
     page = await browser.newPage({

+ 26 - 18
apps/web/tests/subagent-conversation.e2e.ts

@@ -5,8 +5,9 @@ import { join } from 'node:path'
 import type { Browser, Page } from 'playwright'
 import { chromium } from 'playwright'
 import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import { MessageId } from '@deepseek-ai/dsh-llm'
 import {
-  SESSION_FORMAT_VERSION, SessionId as sessionId, type SessionEvent, type SessionHeader, type SessionId,
+  SESSION_FORMAT_VERSION, SessionId as sessionId, SessionLogOffset, SessionSeq, type SessionEvent, type SessionHeader, type SessionId,
 } from '@deepseek-ai/dsh-session'
 import type {} from '@deepseek-ai/dsh-agent'
 import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
@@ -66,9 +67,10 @@ async function waitForAgentToSettle(scaffold: WebScaffold, id: SessionId): Promi
 async function waitForCacheRow(
   scaffold: WebScaffold,
   header: SessionHeader,
+  inheritedEventCount: SessionLogOffset,
 ): Promise<void> {
   const deadline = Date.now() + 10_000
-  while (scaffold.ctx.sessionProjectionCache.cachedSnapshot(header) === undefined) {
+  while (scaffold.ctx.sessionProjectionCache.cachedSnapshot(header, inheritedEventCount) === undefined) {
     if (Date.now() >= deadline) throw new Error(`cache row for "${header.id}" did not land`)
     await new Promise<void>(resolve => setTimeout(resolve, 10))
   }
@@ -136,6 +138,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
       createdAt: oneShotAt,
       cwd: scaffold.workspaceCwd,
       parentSession: parent.id,
+      isSeeded: false,
       origin: 'subagent',
       delegationDepth: 1,
     }
@@ -143,15 +146,17 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
     const oneShotEvents = [
       {
         type: 'turn/start',
-        seq: 0,
+        seq: SessionSeq(0),
         time: oneShotAt,
-        data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
+        data: { turn: 1 },
       },
       {
         type: 'user/message',
-        seq: 1,
+        seq: SessionSeq(1),
         time: oneShotAt + 1,
         data: {
+          id: MessageId(`legacy-message:${oneShotId}:1`),
+          role: 'user',
           content: [{ type: 'text', text: 'Review the event sourcing explanation.' }],
           source: { kind: 'user' },
         },
@@ -159,7 +164,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
       },
       {
         type: 'subagent/descriptor',
-        seq: 2,
+        seq: SessionSeq(2),
         time: oneShotAt + 2,
         data: snapshotSubagentDescriptor({
           mode: 'one-shot', provider: 'spawn', label: ONE_SHOT_LABEL,
@@ -167,14 +172,14 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
       },
       {
         type: 'turn/end',
-        seq: 3,
+        seq: SessionSeq(3),
         time: oneShotAt + oneShotDurationMs,
         data: { turn: 1, reason: { kind: 'completed' } },
       },
-    ] as SessionEvent[]
+    ] satisfies SessionEvent[]
     await scaffold.ctx.sessionPersistence.append(oneShotId, oneShotEvents)
-    scaffold.ctx.sessionProjectionCache.coldSnapshot(oneShotHeader, oneShotEvents)
-    await waitForCacheRow(scaffold, oneShotHeader)
+    scaffold.ctx.sessionProjectionCache.coldSnapshot(oneShotHeader, SessionLogOffset(0), oneShotEvents)
+    await waitForCacheRow(scaffold, oneShotHeader, SessionLogOffset(0))
     grandchildId = sessionId('recorded-grandchild')
     const authoredAt = Date.now()
     const grandchildHeader: SessionHeader = {
@@ -183,6 +188,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
       createdAt: authoredAt,
       cwd: scaffold.workspaceCwd,
       parentSession: childId,
+      isSeeded: false,
       origin: 'subagent',
       delegationDepth: 2,
     }
@@ -190,15 +196,17 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
     const grandchildEvents = [
       {
         type: 'turn/start',
-        seq: 0,
+        seq: SessionSeq(0),
         time: authoredAt,
-        data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
+        data: { turn: 1 },
       },
       {
         type: 'user/message',
-        seq: 1,
+        seq: SessionSeq(1),
         time: authoredAt + 1,
         data: {
+          id: MessageId(`legacy-message:${grandchildId}:1`),
+          role: 'user',
           content: [{ type: 'text', text: NESTED_PROMPT }],
           source: { kind: 'user' },
         },
@@ -206,7 +214,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
       },
       {
         type: 'subagent/descriptor',
-        seq: 2,
+        seq: SessionSeq(2),
         time: authoredAt + 2,
         data: snapshotSubagentDescriptor({
           mode: 'continuable', provider: 'spawn', label: NESTED_LABEL,
@@ -214,14 +222,14 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
       },
       {
         type: 'turn/end',
-        seq: 3,
+        seq: SessionSeq(3),
         time: authoredAt + 3,
         data: { turn: 1, reason: { kind: 'completed' } },
       },
-    ] as SessionEvent[]
+    ] satisfies SessionEvent[]
     await scaffold.ctx.sessionPersistence.append(grandchildId, grandchildEvents)
-    scaffold.ctx.sessionProjectionCache.coldSnapshot(grandchildHeader, grandchildEvents)
-    await waitForCacheRow(scaffold, grandchildHeader)
+    scaffold.ctx.sessionProjectionCache.coldSnapshot(grandchildHeader, SessionLogOffset(0), grandchildEvents)
+    await waitForCacheRow(scaffold, grandchildHeader, SessionLogOffset(0))
     expect(scaffold.ctx.agents.get(childId)).toBeUndefined()
     expect(scaffold.ctx.agents.get(oneShotId)).toBeUndefined()
     expect(scaffold.ctx.agents.get(grandchildId)).toBeUndefined()

+ 2 - 2
docs/config-catalog.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/config-catalog.md
-config-catalog.md: f9eb7ae0bc634e655462412e7dd5d341b7a449a2
-config-catalog.zh.md: 827f39d068de067aefa4dfc77fc53c3db74b5336
+config-catalog.md: ec62912c76ad336a7bd7e966be7921c9938a211c
+config-catalog.zh.md: e785cacb15652e4162066cbdae478870cf61bc4e

+ 15 - 15
docs/config-catalog.md

@@ -215,7 +215,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/api/session-controller/src/index.ts:67`](../packages/api/session-controller/src/index.ts)
+Source: [`packages/api/session-controller/src/index.ts:68`](../packages/api/session-controller/src/index.ts)
 
 <a id="deepseek-aidsh-api-settings-controller"></a>
 
@@ -457,7 +457,7 @@ export interface ToolResultPruneConfig {
 }
 ```
 
-Source: [`packages/compaction/compaction-tool-result-pruner/src/types.ts:4`](../packages/compaction/compaction-tool-result-pruner/src/types.ts)
+Source: [`packages/compaction/compaction-tool-result-pruner/src/types.ts:5`](../packages/compaction/compaction-tool-result-pruner/src/types.ts)
 
 <a id="deepseek-aidsh-cordis-host-runner"></a>
 
@@ -763,7 +763,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/goal/goal/src/index.ts:171`](../packages/goal/goal/src/index.ts)
+Source: [`packages/goal/goal/src/index.ts:172`](../packages/goal/goal/src/index.ts)
 
 <a id="deepseek-aidsh-headless"></a>
 
@@ -779,7 +779,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/bundle/headless/src/index.ts:33`](../packages/bundle/headless/src/index.ts)
+Source: [`packages/bundle/headless/src/index.ts:34`](../packages/bundle/headless/src/index.ts)
 
 <a id="deepseek-aidsh-hooks-claude-code"></a>
 
@@ -1355,7 +1355,7 @@ export interface ReplayModelConfig {
 
 Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
 
-Source: [`packages/test-support/llm-replay/src/index.ts:919`](../packages/test-support/llm-replay/src/index.ts)
+Source: [`packages/test-support/llm-replay/src/index.ts:924`](../packages/test-support/llm-replay/src/index.ts)
 
 <a id="deepseek-aidsh-llm-retry"></a>
 
@@ -1499,7 +1499,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts)
+Source: [`packages/feedback/message-feedback/src/index.ts:50`](../packages/feedback/message-feedback/src/index.ts)
 
 <a id="deepseek-aidsh-permission-presets"></a>
 
@@ -1800,7 +1800,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/session/session-log-deepseek/src/index.ts:23`](../packages/session/session-log-deepseek/src/index.ts)
+Source: [`packages/session/session-log-deepseek/src/index.ts:36`](../packages/session/session-log-deepseek/src/index.ts)
 
 <a id="deepseek-aidsh-session-log-export"></a>
 
@@ -1858,7 +1858,7 @@ export interface Config {
 export type JsonlCompression = 'zstd' | 'none'
 ```
 
-Source: [`packages/session/session-persistence-jsonl/src/index.ts:62`](../packages/session/session-persistence-jsonl/src/index.ts)
+Source: [`packages/session/session-persistence-jsonl/src/index.ts:70`](../packages/session/session-persistence-jsonl/src/index.ts)
 
 <a id="deepseek-aidsh-session-projection-cache"></a>
 
@@ -1882,7 +1882,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/session/session-projection-cache/src/index.ts:48`](../packages/session/session-projection-cache/src/index.ts)
+Source: [`packages/session/session-projection-cache/src/index.ts:55`](../packages/session/session-projection-cache/src/index.ts)
 
 <a id="deepseek-aidsh-session-query-sqlite"></a>
 
@@ -1928,7 +1928,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
 
 Depends on: [`SessionQueryConfig`](../packages/session-query/session-query/src/index.ts)
 
-Source: [`packages/session-query/session-query-sqlite/src/index.ts:89`](../packages/session-query/session-query-sqlite/src/index.ts)
+Source: [`packages/session-query/session-query-sqlite/src/index.ts:96`](../packages/session-query/session-query-sqlite/src/index.ts)
 
 <a id="deepseek-aidsh-session-reference"></a>
 
@@ -2014,7 +2014,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/session/session-title/src/index.ts:55`](../packages/session/session-title/src/index.ts)
+Source: [`packages/session/session-title/src/index.ts:56`](../packages/session/session-title/src/index.ts)
 
 <a id="deepseek-aidsh-session-title-all-prompts-llm"></a>
 
@@ -2583,7 +2583,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/context/time-context/src/index.ts:48`](../packages/context/time-context/src/index.ts)
+Source: [`packages/context/time-context/src/index.ts:49`](../packages/context/time-context/src/index.ts)
 
 <a id="deepseek-aidsh-tmux-context"></a>
 
@@ -2612,7 +2612,7 @@ Requires: `sessionProjections`
 export type TokenMeterConfig = Record<string, never>
 ```
 
-Source: [`packages/llm/token-meter/src/types.ts:12`](../packages/llm/token-meter/src/types.ts)
+Source: [`packages/llm/token-meter/src/types.ts:13`](../packages/llm/token-meter/src/types.ts)
 
 <a id="deepseek-aidsh-tool-bash"></a>
 
@@ -2959,7 +2959,7 @@ export interface Config {
 
 Depends on: [`AgentOptions`](subsystems/core.md)
 
-Source: [`packages/subagent/tool-subagent/src/index.ts:46`](../packages/subagent/tool-subagent/src/index.ts)
+Source: [`packages/subagent/tool-subagent/src/index.ts:47`](../packages/subagent/tool-subagent/src/index.ts)
 
 <a id="deepseek-aidsh-tool-terminal"></a>
 
@@ -3128,7 +3128,7 @@ export interface Config {
 export type ApprovalPolicy = 'ask' | 'never'
 ```
 
-Source: [`packages/interaction/user-approval/src/index.ts:126`](../packages/interaction/user-approval/src/index.ts)
+Source: [`packages/interaction/user-approval/src/index.ts:127`](../packages/interaction/user-approval/src/index.ts)
 
 <a id="deepseek-aidsh-web"></a>
 

+ 11 - 11
docs/config-catalog.zh.md

@@ -217,7 +217,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/api/session-controller/src/index.ts:67`](../packages/api/session-controller/src/index.ts)
+来源:[`packages/api/session-controller/src/index.ts:68`](../packages/api/session-controller/src/index.ts)
 
 <a id="deepseek-aidsh-api-settings-controller"></a>
 
@@ -459,7 +459,7 @@ export interface ToolResultPruneConfig {
 }
 ```
 
-来源:[`packages/compaction/compaction-tool-result-pruner/src/types.ts:4`](../packages/compaction/compaction-tool-result-pruner/src/types.ts)
+来源:[`packages/compaction/compaction-tool-result-pruner/src/types.ts:5`](../packages/compaction/compaction-tool-result-pruner/src/types.ts)
 
 <a id="deepseek-aidsh-cordis-host-runner"></a>
 
@@ -765,7 +765,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/goal/goal/src/index.ts:171`](../packages/goal/goal/src/index.ts)
+来源:[`packages/goal/goal/src/index.ts:172`](../packages/goal/goal/src/index.ts)
 
 <a id="deepseek-aidsh-headless"></a>
 
@@ -1357,7 +1357,7 @@ export interface ReplayModelConfig {
 
 依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
 
-来源:[`packages/test-support/llm-replay/src/index.ts:919`](../packages/test-support/llm-replay/src/index.ts)
+来源:[`packages/test-support/llm-replay/src/index.ts:924`](../packages/test-support/llm-replay/src/index.ts)
 
 <a id="deepseek-aidsh-llm-retry"></a>
 
@@ -1501,7 +1501,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts)
+来源:[`packages/feedback/message-feedback/src/index.ts:50`](../packages/feedback/message-feedback/src/index.ts)
 
 <a id="deepseek-aidsh-permission-presets"></a>
 
@@ -1802,7 +1802,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/session/session-log-deepseek/src/index.ts:23`](../packages/session/session-log-deepseek/src/index.ts)
+来源:[`packages/session/session-log-deepseek/src/index.ts:36`](../packages/session/session-log-deepseek/src/index.ts)
 
 <a id="deepseek-aidsh-session-log-export"></a>
 
@@ -1860,7 +1860,7 @@ export interface Config {
 export type JsonlCompression = 'zstd' | 'none'
 ```
 
-来源:[`packages/session/session-persistence-jsonl/src/index.ts:62`](../packages/session/session-persistence-jsonl/src/index.ts)
+来源:[`packages/session/session-persistence-jsonl/src/index.ts:70`](../packages/session/session-persistence-jsonl/src/index.ts)
 
 <a id="deepseek-aidsh-session-projection-cache"></a>
 
@@ -1884,7 +1884,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/session/session-projection-cache/src/index.ts:48`](../packages/session/session-projection-cache/src/index.ts)
+来源:[`packages/session/session-projection-cache/src/index.ts:55`](../packages/session/session-projection-cache/src/index.ts)
 
 <a id="deepseek-aidsh-session-query-sqlite"></a>
 
@@ -1930,7 +1930,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
 
 依赖:[`SessionQueryConfig`](../packages/session-query/session-query/src/index.ts)
 
-来源:[`packages/session-query/session-query-sqlite/src/index.ts:89`](../packages/session-query/session-query-sqlite/src/index.ts)
+来源:[`packages/session-query/session-query-sqlite/src/index.ts:96`](../packages/session-query/session-query-sqlite/src/index.ts)
 
 <a id="deepseek-aidsh-session-reference"></a>
 
@@ -2016,7 +2016,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/session/session-title/src/index.ts:55`](../packages/session/session-title/src/index.ts)
+来源:[`packages/session/session-title/src/index.ts:56`](../packages/session/session-title/src/index.ts)
 
 <a id="deepseek-aidsh-session-title-all-prompts-llm"></a>
 
@@ -2614,7 +2614,7 @@ export interface Config {
 export type TokenMeterConfig = Record<string, never>
 ```
 
-来源:[`packages/llm/token-meter/src/types.ts:12`](../packages/llm/token-meter/src/types.ts)
+来源:[`packages/llm/token-meter/src/types.ts:13`](../packages/llm/token-meter/src/types.ts)
 
 <a id="deepseek-aidsh-tool-bash"></a>
 

+ 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: f9606dcf399519f0c62b6403f300b66e80e47d36
-event-producer-consumer.zh.md: f9e4d35e6ed213046957b5e7fc1e03a334de9992
+event-producer-consumer.md: bdfb3f23e817dadebb090df154af8e1ebf018ee8
+event-producer-consumer.zh.md: 3fe1fe737cb7e99bb8e3a8641dc7bbd7b11b74a4

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

@@ -21,14 +21,14 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:224`](../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:185`](../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:285`](../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:523`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:503`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:530`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:509`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:516`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:542`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:522`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:549`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:528`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:535`](../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:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
+| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:81`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
 | `cordis/dynamic-package` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:380`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
 | `cordis/dynamic-retract` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:386`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
 | `cordis/inspect-query` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:392`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
@@ -45,7 +45,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:67`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
-| `session/created` | `emit` | [`packages/core/session/src/index.ts:52`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| `session/created` | `emit` | [`packages/core/session/src/index.ts:52`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:62`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
 | `session/event` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:83`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |

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

@@ -23,14 +23,14 @@
 | `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:224`](../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:185`](../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:285`](../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:523`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:503`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:530`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:509`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
-| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:516`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:542`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:522`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:549`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:528`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:535`](../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:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
+| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:81`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
 | `cordis/dynamic-package` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:380`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
 | `cordis/dynamic-retract` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:386`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
 | `cordis/inspect-query` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:392`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
@@ -47,7 +47,7 @@
 | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:67`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
-| `session/created` | `emit` | [`packages/core/session/src/index.ts:52`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| `session/created` | `emit` | [`packages/core/session/src/index.ts:52`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:62`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
 | `session/event` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:83`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |

+ 2 - 2
docs/persistence-catalog.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/persistence-catalog.md
-persistence-catalog.md: fd5b3a4a377069fb7b1fbc37b78d11ec037073a4
-persistence-catalog.zh.md: 9d8fc88a44da1620e2de9aa066b9d6c6204b29b1
+persistence-catalog.md: 2fb2b020841a42da6f7505928a87868fc43bc559
+persistence-catalog.zh.md: 4014fd2c10e20193ea68a5f6c761c99b4569b032

+ 32 - 32
docs/persistence-catalog.md

@@ -40,7 +40,7 @@ export type SurfaceEventType =
  */
 export type SurfaceOp =
   | 'append'
-  | { op: 'replace'; start: number; end: number }
+  | { op: 'replace'; start: SessionSeq; end: SessionSeq }
 
 /**
  * One immutable entry in the session log.
@@ -59,7 +59,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
   [K in SessionEventType]: {
     type: K
     /** Monotonic sequence number within the session. */
-    seq: number
+    seq: SessionSeq
     /** Unix epoch milliseconds. */
     time: number
     data: SessionEventMap[K]
@@ -83,14 +83,14 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
      * provider stream; when the field is absent, the event does not record which
      * earlier events produced the message.
      */
-    sourceEventSeqs?: number[]
+    sourceEventSeqs?: SessionSeq[]
     /** How this event entered the surface; absent for non-surface events. */
     surfaceOp?: SurfaceOp
   } : object)
 }[T]
 ```
 
-Sources: [`packages/core/session/src/types.ts:323`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:330`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:359`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:391`](../packages/core/session/src/types.ts)
+Sources: [`packages/core/session/src/types.ts:366`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:373`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:402`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:434`](../packages/core/session/src/types.ts)
 
 ## Events
 
@@ -200,7 +200,7 @@ Source: [`packages/interaction/user-approval/src/types.ts:55`](../packages/inter
 }
 ```
 
-Source: [`packages/interaction/user-approval/src/index.ts:32`](../packages/interaction/user-approval/src/index.ts)
+Source: [`packages/interaction/user-approval/src/index.ts:33`](../packages/interaction/user-approval/src/index.ts)
 
 ### `assistant/*`
 
@@ -215,7 +215,7 @@ Source: [`packages/interaction/user-approval/src/index.ts:32`](../packages/inter
 
 Types: [StreamChunk](subsystems/llm-streaming.md)
 
-Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:289`](../packages/core/session/src/types.ts)
 
 <a id="assistantmessage--surface"></a>
 
@@ -237,7 +237,7 @@ Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/
 
 Types: [TokenUsage](subsystems/llm-streaming.md)
 
-Source: [`packages/core/session/src/types.ts:257`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:300`](../packages/core/session/src/types.ts)
 
 ### `command/*`
 
@@ -256,11 +256,11 @@ Source: [`packages/core/session/src/types.ts:257`](../packages/core/session/src/
   commandId: CommandId
   kind: 'success' | 'error'
   text?: string
-  sourceEventSeq?: number
+  sourceEventSeq?: import('@deepseek-ai/dsh-session/types').SessionSeq
 }
 ```
 
-Source: [`packages/interaction/commands/src/types.ts:103`](../packages/interaction/commands/src/types.ts)
+Source: [`packages/interaction/commands/src/types.ts:104`](../packages/interaction/commands/src/types.ts)
 
 <a id="commandrun--log-only"></a>
 
@@ -280,7 +280,7 @@ Source: [`packages/interaction/commands/src/types.ts:103`](../packages/interacti
 'command/run': { commandId: CommandId; name: string; args?: string; source: CommandSource }
 ```
 
-Source: [`packages/interaction/commands/src/types.ts:96`](../packages/interaction/commands/src/types.ts)
+Source: [`packages/interaction/commands/src/types.ts:97`](../packages/interaction/commands/src/types.ts)
 
 ### `compaction/*`
 
@@ -296,7 +296,7 @@ Source: [`packages/interaction/commands/src/types.ts:96`](../packages/interactio
 'compaction/end': { compactionId: CompactionId; sourceCommandId?: CommandId; turn: number | null; error?: string }
 ```
 
-Source: [`packages/compaction/compaction/src/types.ts:71`](../packages/compaction/compaction/src/types.ts)
+Source: [`packages/compaction/compaction/src/types.ts:72`](../packages/compaction/compaction/src/types.ts)
 
 <a id="compactionprune--log-only"></a>
 
@@ -314,15 +314,15 @@ Source: [`packages/compaction/compaction/src/types.ts:71`](../packages/compactio
  */
 'compaction/prune': {
   /** The replaced range's first and last surface-node seqs (a surface-position span, like {@link CompactionResult.shadowedRange}). */
-  shadowedRange: { start: number; end: number }
+  shadowedRange: { start: SessionSeq; end: SessionSeq }
   /** The seqs of all shadowed surface nodes, in surface order. */
-  shadowedSeqs: number[]
+  shadowedSeqs: SessionSeq[]
   /** Heuristic price of the shadowed content under the token-meter's fixed estimator. */
   shadowedTokenCount: number
 }
 ```
 
-Source: [`packages/compaction/compaction/src/types.ts:81`](../packages/compaction/compaction/src/types.ts)
+Source: [`packages/compaction/compaction/src/types.ts:82`](../packages/compaction/compaction/src/types.ts)
 
 <a id="compactionstart--log-only"></a>
 
@@ -337,7 +337,7 @@ Source: [`packages/compaction/compaction/src/types.ts:81`](../packages/compactio
 'compaction/start': { compactionId: CompactionId; sourceCommandId?: CommandId; turn: number | null }
 ```
 
-Source: [`packages/compaction/compaction/src/types.ts:23`](../packages/compaction/compaction/src/types.ts)
+Source: [`packages/compaction/compaction/src/types.ts:24`](../packages/compaction/compaction/src/types.ts)
 
 <a id="compactionsummary--log-only"></a>
 
@@ -357,8 +357,8 @@ Source: [`packages/compaction/compaction/src/types.ts:23`](../packages/compactio
   compactionId: CompactionId
   sourceCommandId?: CommandId
   summary: ContentBlock[]
-  shadowedRange: { start: number; end: number }
-  shadowedSeqs: number[]
+  shadowedRange: { start: SessionSeq; end: SessionSeq }
+  shadowedSeqs: SessionSeq[]
   shadowedTokenCount: number
   /** The provider route that wrote the summary. */
   provider: string
@@ -391,7 +391,7 @@ Source: [`packages/compaction/compaction/src/types.ts:23`](../packages/compactio
 
 Types: [ContentBlock](subsystems/core.md) · [TokenUsage](subsystems/llm-streaming.md)
 
-Source: [`packages/compaction/compaction/src/types.ts:33`](../packages/compaction/compaction/src/types.ts)
+Source: [`packages/compaction/compaction/src/types.ts:34`](../packages/compaction/compaction/src/types.ts)
 
 ### `feedback/*`
 
@@ -563,7 +563,7 @@ Source: [`packages/plan/plan-mode/src/index.ts:46`](../packages/plan/plan-mode/s
 'request/context': RequestContext
 ```
 
-Source: [`packages/core/session/src/types.ts:296`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:339`](../packages/core/session/src/types.ts)
 
 <a id="requestheader--log-only"></a>
 
@@ -582,7 +582,7 @@ Source: [`packages/core/session/src/types.ts:296`](../packages/core/session/src/
 }
 ```
 
-Source: [`packages/core/session/src/types.ts:286`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:329`](../packages/core/session/src/types.ts)
 
 ### `sandbox/*`
 
@@ -657,7 +657,7 @@ Source: [`packages/schedule/schedule/src/types.ts:219`](../packages/schedule/sch
 'session/end-seed': Record<string, never>
 ```
 
-Source: [`packages/core/session/src/types.ts:319`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:362`](../packages/core/session/src/types.ts)
 
 <a id="sessiontitle--log-only"></a>
 
@@ -673,7 +673,7 @@ Source: [`packages/core/session/src/types.ts:319`](../packages/core/session/src/
 
 Types: [SessionTitleEventData](subsystems/session-title.md)
 
-Source: [`packages/session/session-title/src/index.ts:76`](../packages/session/session-title/src/index.ts)
+Source: [`packages/session/session-title/src/index.ts:77`](../packages/session/session-title/src/index.ts)
 
 <a id="sessiontitle-llm-request--log-only"></a>
 
@@ -686,7 +686,7 @@ Source: [`packages/session/session-title/src/index.ts:76`](../packages/session/s
 
 Types: [SessionTitleLlmRequestEventData](subsystems/session-title.md)
 
-Source: [`packages/session/session-title-llm/src/index.ts:44`](../packages/session/session-title-llm/src/index.ts)
+Source: [`packages/session/session-title-llm/src/index.ts:45`](../packages/session/session-title-llm/src/index.ts)
 
 ### `session-log-deepseek/*`
 
@@ -700,11 +700,11 @@ Source: [`packages/session/session-title-llm/src/index.ts:44`](../packages/sessi
   /** Session identity the accepted delivery carried; inherited fork markers retain the parent's id. */
   sessionId: import('@deepseek-ai/dsh-session/types').SessionId
   /** Last canonical event included in the accepted request. */
-  throughSeq: number
+  throughSeq: import('@deepseek-ai/dsh-session/types').SessionSeq
 }
 ```
 
-Source: [`packages/session/session-log-deepseek/src/types.ts:26`](../packages/session/session-log-deepseek/src/types.ts)
+Source: [`packages/session/session-log-deepseek/src/types.ts:57`](../packages/session/session-log-deepseek/src/types.ts)
 
 ### `step/*`
 
@@ -717,7 +717,7 @@ Source: [`packages/session/session-log-deepseek/src/types.ts:26`](../packages/se
 'step/end': { turn: number; step: number }
 ```
 
-Source: [`packages/core/session/src/types.ts:236`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)
 
 <a id="stepstart--log-only"></a>
 
@@ -728,7 +728,7 @@ Source: [`packages/core/session/src/types.ts:236`](../packages/core/session/src/
 'step/start': { turn: number; step: number }
 ```
 
-Source: [`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:277`](../packages/core/session/src/types.ts)
 
 ### `subagent/*`
 
@@ -859,7 +859,7 @@ Source: [`packages/todo/tool-todo/src/types.ts:31`](../packages/todo/tool-todo/s
 
 Types: [ToolCallId](subsystems/core.md)
 
-Source: [`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:306`](../packages/core/session/src/types.ts)
 
 <a id="toolcode-dispatch--log-only"></a>
 
@@ -934,7 +934,7 @@ Source: [`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types
 }
 ```
 
-Source: [`packages/core/session/src/types.ts:275`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:318`](../packages/core/session/src/types.ts)
 
 ### `tool-workflow/*`
 
@@ -1014,7 +1014,7 @@ Source: [`packages/workflow/tool-workflow/src/types.ts:47`](../packages/workflow
 
 Types: [TurnEndReason](subsystems/session.md)
 
-Source: [`packages/core/session/src/types.ts:232`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:275`](../packages/core/session/src/types.ts)
 
 <a id="turnstart--log-only"></a>
 
@@ -1030,7 +1030,7 @@ Source: [`packages/core/session/src/types.ts:232`](../packages/core/session/src/
 'turn/start': { turn: number }
 ```
 
-Source: [`packages/core/session/src/types.ts:223`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:266`](../packages/core/session/src/types.ts)
 
 ### `user/*`
 
@@ -1049,7 +1049,7 @@ Source: [`packages/core/session/src/types.ts:223`](../packages/core/session/src/
 'user/message': UserMessage
 ```
 
-Source: [`packages/core/session/src/types.ts:244`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:287`](../packages/core/session/src/types.ts)
 
 ### `web/*`
 

+ 31 - 31
docs/persistence-catalog.zh.md

@@ -42,7 +42,7 @@ export type SurfaceEventType =
  */
 export type SurfaceOp =
   | 'append'
-  | { op: 'replace'; start: number; end: number }
+  | { op: 'replace'; start: SessionSeq; end: SessionSeq }
 
 /**
  * One immutable entry in the session log.
@@ -61,7 +61,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
   [K in SessionEventType]: {
     type: K
     /** Monotonic sequence number within the session. */
-    seq: number
+    seq: SessionSeq
     /** Unix epoch milliseconds. */
     time: number
     data: SessionEventMap[K]
@@ -85,14 +85,14 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
      * provider stream; when the field is absent, the event does not record which
      * earlier events produced the message.
      */
-    sourceEventSeqs?: number[]
+    sourceEventSeqs?: SessionSeq[]
     /** How this event entered the surface; absent for non-surface events. */
     surfaceOp?: SurfaceOp
   } : object)
 }[T]
 ```
 
-来源:[`packages/core/session/src/types.ts:323`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:330`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:359`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:391`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:366`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:373`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:402`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:434`](../packages/core/session/src/types.ts)
 
 ## 事件
 
@@ -217,7 +217,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 
 类型:[StreamChunk](subsystems/llm-streaming.zh.md)
 
-来源:[`packages/core/session/src/types.ts:246`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:289`](../packages/core/session/src/types.ts)
 
 <a id="assistantmessage--surface"></a>
 
@@ -239,7 +239,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 
 类型:[TokenUsage](subsystems/llm-streaming.zh.md)
 
-来源:[`packages/core/session/src/types.ts:257`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:300`](../packages/core/session/src/types.ts)
 
 ### `command/*`
 
@@ -258,11 +258,11 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
   commandId: CommandId
   kind: 'success' | 'error'
   text?: string
-  sourceEventSeq?: number
+  sourceEventSeq?: import('@deepseek-ai/dsh-session/types').SessionSeq
 }
 ```
 
-来源:[`packages/interaction/commands/src/types.ts:103`](../packages/interaction/commands/src/types.ts)
+来源:[`packages/interaction/commands/src/types.ts:104`](../packages/interaction/commands/src/types.ts)
 
 <a id="commandrun--log-only"></a>
 
@@ -282,7 +282,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 'command/run': { commandId: CommandId; name: string; args?: string; source: CommandSource }
 ```
 
-来源:[`packages/interaction/commands/src/types.ts:96`](../packages/interaction/commands/src/types.ts)
+来源:[`packages/interaction/commands/src/types.ts:97`](../packages/interaction/commands/src/types.ts)
 
 ### `compaction/*`
 
@@ -298,7 +298,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 'compaction/end': { compactionId: CompactionId; sourceCommandId?: CommandId; turn: number | null; error?: string }
 ```
 
-来源:[`packages/compaction/compaction/src/types.ts:71`](../packages/compaction/compaction/src/types.ts)
+来源:[`packages/compaction/compaction/src/types.ts:72`](../packages/compaction/compaction/src/types.ts)
 
 <a id="compactionprune--log-only"></a>
 
@@ -316,15 +316,15 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
  */
 'compaction/prune': {
   /** The replaced range's first and last surface-node seqs (a surface-position span, like {@link CompactionResult.shadowedRange}). */
-  shadowedRange: { start: number; end: number }
+  shadowedRange: { start: SessionSeq; end: SessionSeq }
   /** The seqs of all shadowed surface nodes, in surface order. */
-  shadowedSeqs: number[]
+  shadowedSeqs: SessionSeq[]
   /** Heuristic price of the shadowed content under the token-meter's fixed estimator. */
   shadowedTokenCount: number
 }
 ```
 
-来源:[`packages/compaction/compaction/src/types.ts:81`](../packages/compaction/compaction/src/types.ts)
+来源:[`packages/compaction/compaction/src/types.ts:82`](../packages/compaction/compaction/src/types.ts)
 
 <a id="compactionstart--log-only"></a>
 
@@ -339,7 +339,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 'compaction/start': { compactionId: CompactionId; sourceCommandId?: CommandId; turn: number | null }
 ```
 
-来源:[`packages/compaction/compaction/src/types.ts:23`](../packages/compaction/compaction/src/types.ts)
+来源:[`packages/compaction/compaction/src/types.ts:24`](../packages/compaction/compaction/src/types.ts)
 
 <a id="compactionsummary--log-only"></a>
 
@@ -359,8 +359,8 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
   compactionId: CompactionId
   sourceCommandId?: CommandId
   summary: ContentBlock[]
-  shadowedRange: { start: number; end: number }
-  shadowedSeqs: number[]
+  shadowedRange: { start: SessionSeq; end: SessionSeq }
+  shadowedSeqs: SessionSeq[]
   shadowedTokenCount: number
   /** The provider route that wrote the summary. */
   provider: string
@@ -393,7 +393,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 
 类型:[ContentBlock](subsystems/core.zh.md) · [TokenUsage](subsystems/llm-streaming.zh.md)
 
-来源:[`packages/compaction/compaction/src/types.ts:33`](../packages/compaction/compaction/src/types.ts)
+来源:[`packages/compaction/compaction/src/types.ts:34`](../packages/compaction/compaction/src/types.ts)
 
 ### `feedback/*`
 
@@ -565,7 +565,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 'request/context': RequestContext
 ```
 
-来源:[`packages/core/session/src/types.ts:296`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:339`](../packages/core/session/src/types.ts)
 
 <a id="requestheader--log-only"></a>
 
@@ -584,7 +584,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 }
 ```
 
-来源:[`packages/core/session/src/types.ts:286`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:329`](../packages/core/session/src/types.ts)
 
 ### `sandbox/*`
 
@@ -659,7 +659,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 'session/end-seed': Record<string, never>
 ```
 
-来源:[`packages/core/session/src/types.ts:319`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:362`](../packages/core/session/src/types.ts)
 
 <a id="sessiontitle--log-only"></a>
 
@@ -675,7 +675,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 
 类型:[SessionTitleEventData](subsystems/session-title.zh.md)
 
-来源:[`packages/session/session-title/src/index.ts:76`](../packages/session/session-title/src/index.ts)
+来源:[`packages/session/session-title/src/index.ts:77`](../packages/session/session-title/src/index.ts)
 
 <a id="sessiontitle-llm-request--log-only"></a>
 
@@ -688,7 +688,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 
 类型:[SessionTitleLlmRequestEventData](subsystems/session-title.zh.md)
 
-来源:[`packages/session/session-title-llm/src/index.ts:44`](../packages/session/session-title-llm/src/index.ts)
+来源:[`packages/session/session-title-llm/src/index.ts:45`](../packages/session/session-title-llm/src/index.ts)
 
 ### `session-log-deepseek/*`
 
@@ -702,11 +702,11 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
   /** Session identity the accepted delivery carried; inherited fork markers retain the parent's id. */
   sessionId: import('@deepseek-ai/dsh-session/types').SessionId
   /** Last canonical event included in the accepted request. */
-  throughSeq: number
+  throughSeq: import('@deepseek-ai/dsh-session/types').SessionSeq
 }
 ```
 
-来源:[`packages/session/session-log-deepseek/src/types.ts:26`](../packages/session/session-log-deepseek/src/types.ts)
+来源:[`packages/session/session-log-deepseek/src/types.ts:57`](../packages/session/session-log-deepseek/src/types.ts)
 
 ### `step/*`
 
@@ -719,7 +719,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 'step/end': { turn: number; step: number }
 ```
 
-来源:[`packages/core/session/src/types.ts:236`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)
 
 <a id="stepstart--log-only"></a>
 
@@ -730,7 +730,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 'step/start': { turn: number; step: number }
 ```
 
-来源:[`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:277`](../packages/core/session/src/types.ts)
 
 ### `subagent/*`
 
@@ -861,7 +861,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 
 类型:[ToolCallId](subsystems/core.zh.md)
 
-来源:[`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:306`](../packages/core/session/src/types.ts)
 
 <a id="toolcode-dispatch--log-only"></a>
 
@@ -936,7 +936,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 }
 ```
 
-来源:[`packages/core/session/src/types.ts:275`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:318`](../packages/core/session/src/types.ts)
 
 ### `tool-workflow/*`
 
@@ -1016,7 +1016,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 
 类型:[TurnEndReason](subsystems/session.zh.md)
 
-来源:[`packages/core/session/src/types.ts:232`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:275`](../packages/core/session/src/types.ts)
 
 <a id="turnstart--log-only"></a>
 
@@ -1032,7 +1032,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 'turn/start': { turn: number }
 ```
 
-来源:[`packages/core/session/src/types.ts:223`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:266`](../packages/core/session/src/types.ts)
 
 ### `user/*`
 
@@ -1051,7 +1051,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 'user/message': UserMessage
 ```
 
-来源:[`packages/core/session/src/types.ts:244`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:287`](../packages/core/session/src/types.ts)
 
 ### `web/*`
 

+ 2 - 2
docs/subsystems/commands.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/commands.md
-commands.md: f3760558b04253701915a240375f13e096bc0c51
-commands.zh.md: 2b4377b76b55203d84a956f66879ab2c50a9fd23
+commands.md: 46d3b736afd3a72d1119c9744cd592a86b9cbc29
+commands.zh.md: db8d6e722e3a1742887aaa4023ab67b9ed914552

+ 1 - 1
docs/subsystems/commands.md

@@ -83,7 +83,7 @@ type CommandResult =
     readonly kind: 'success'
     readonly text?: string
     /** Earlier authoritative domain event that owns a richer presentation. */
-    readonly sourceEventSeq?: number
+    readonly sourceEventSeq?: SessionSeq
   }
   | { readonly kind: 'error'; readonly text: string }
 ```

+ 1 - 1
docs/subsystems/commands.zh.md

@@ -83,7 +83,7 @@ type CommandResult =
     readonly kind: 'success'
     readonly text?: string
     /** Earlier authoritative domain event that owns a richer presentation. */
-    readonly sourceEventSeq?: number
+    readonly sourceEventSeq?: SessionSeq
   }
   | { readonly kind: 'error'; readonly text: string }
 ```

+ 2 - 2
docs/subsystems/compaction.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/compaction.md
-compaction.md: 03642b32cc3cb3caf743d908e9c988c1a3c8fa1e
-compaction.zh.md: af37c4824cc4b01515400049525fbd7b91d018f3
+compaction.md: b49957a5f476a02ccd12b791f287a9675073c0ae
+compaction.zh.md: 4b4c6845b8ff48bce019b352a811f968628cbdbf

+ 9 - 9
docs/subsystems/compaction.md

@@ -34,11 +34,11 @@ interface CompactionResult {
   /** Human command that initiated this compaction, when it was manual. */
   sourceCommandId?: CommandId
   /** The seq of the appended `compaction/start` event. */
-  startSeq: number
+  startSeq: SessionSeq
   /** The seq of the appended `compaction/summary` event. */
-  summarySeq: number
+  summarySeq: SessionSeq
   /** The seq of the appended `compaction/end` event. */
-  endSeq: number
+  endSeq: SessionSeq
   /** The summary content blocks produced by the backend. */
   summary: ContentBlock[]
   /**
@@ -49,9 +49,9 @@ interface CompactionResult {
    * can be GREATER than `end`. {@link CompactionResult.shadowedSeqs} is the
    * authoritative set of shadowed nodes, in surface order.
    */
-  shadowedRange: { start: number; end: number }
+  shadowedRange: { start: SessionSeq; end: SessionSeq }
   /** The seqs of all shadowed surface nodes, in surface order. */
-  shadowedSeqs: number[]
+  shadowedSeqs: SessionSeq[]
   /** Estimated token count of the shadowed content. */
   shadowedTokenCount: number
 }
@@ -95,9 +95,9 @@ The optional tool-result pruning service reports each durable content replacemen
 /** Cited source event and size accounting for one landed surface replacement. */
 interface PrunedEntry {
   /** Full-fidelity tool-result event shadowed by the replacement. */
-  readonly originalSeq: number
+  readonly originalSeq: SessionSeq
   /** Newly appended pruned tool-result event. */
-  readonly replacementSeq: number
+  readonly replacementSeq: SessionSeq
   /** Tool call shared by the original and replacement. */
   readonly callId: ToolCallId
   /** Original text size in Unicode code points. */
@@ -187,10 +187,10 @@ abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, sour
  * @throws when compaction is active or the range is missing, reversed, or unbalanced.
  * @returns the appended event seqs, summary, replaced range, and token accounting.
  */
-abstract compactRegion( start: number, end: number, agent: CompactionAgentContext, signal?: AbortSignal, ): Promise<CompactionResult>
+abstract compactRegion( start: SessionSeq, end: SessionSeq, agent: CompactionAgentContext, signal?: AbortSignal, ): Promise<CompactionResult>
 ```
 
-Types: [CommandId](commands.md)
+Types: [CommandId](commands.md) · [SessionSeq](session.md)
 
 Source: [`packages/compaction/compaction/src/index.ts`](../../packages/compaction/compaction/src/index.ts)
 

+ 9 - 9
docs/subsystems/compaction.zh.md

@@ -34,11 +34,11 @@ interface CompactionResult {
   /** Human command that initiated this compaction, when it was manual. */
   sourceCommandId?: CommandId
   /** The seq of the appended `compaction/start` event. */
-  startSeq: number
+  startSeq: SessionSeq
   /** The seq of the appended `compaction/summary` event. */
-  summarySeq: number
+  summarySeq: SessionSeq
   /** The seq of the appended `compaction/end` event. */
-  endSeq: number
+  endSeq: SessionSeq
   /** The summary content blocks produced by the backend. */
   summary: ContentBlock[]
   /**
@@ -49,9 +49,9 @@ interface CompactionResult {
    * can be GREATER than `end`. {@link CompactionResult.shadowedSeqs} is the
    * authoritative set of shadowed nodes, in surface order.
    */
-  shadowedRange: { start: number; end: number }
+  shadowedRange: { start: SessionSeq; end: SessionSeq }
   /** The seqs of all shadowed surface nodes, in surface order. */
-  shadowedSeqs: number[]
+  shadowedSeqs: SessionSeq[]
   /** Estimated token count of the shadowed content. */
   shadowedTokenCount: number
 }
@@ -95,9 +95,9 @@ type ManualCompactionErrorCode =
 /** Cited source event and size accounting for one landed surface replacement. */
 interface PrunedEntry {
   /** Full-fidelity tool-result event shadowed by the replacement. */
-  readonly originalSeq: number
+  readonly originalSeq: SessionSeq
   /** Newly appended pruned tool-result event. */
-  readonly replacementSeq: number
+  readonly replacementSeq: SessionSeq
   /** Tool call shared by the original and replacement. */
   readonly callId: ToolCallId
   /** Original text size in Unicode code points. */
@@ -187,10 +187,10 @@ abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, sour
  * @throws when compaction is active or the range is missing, reversed, or unbalanced.
  * @returns the appended event seqs, summary, replaced range, and token accounting.
  */
-abstract compactRegion( start: number, end: number, agent: CompactionAgentContext, signal?: AbortSignal, ): Promise<CompactionResult>
+abstract compactRegion( start: SessionSeq, end: SessionSeq, agent: CompactionAgentContext, signal?: AbortSignal, ): Promise<CompactionResult>
 ```
 
-Types: [CommandId](commands.zh.md)
+Types: [CommandId](commands.zh.md) · [SessionSeq](session.zh.md)
 
 Source: [`packages/compaction/compaction/src/index.ts`](../../packages/compaction/compaction/src/index.ts)
 

+ 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: 124d30a5abeddd81edd2eb03a9bb213487b6a38e
-core.zh.md: c2d67eabb3b223d3f1d20c42f01280f16656ebef
+core.md: a59a692573c2e2756e60626268314a8ade6c546e
+core.zh.md: acea4966a30ab03c24f1aea9bcd2d33a6271a28d

+ 1 - 1
docs/subsystems/core.md

@@ -46,7 +46,7 @@ interface AgentHandle {
 }
 ```
 
-`CreateAgentOptions` carries the shared identity and everything a fresh agent needs before publication: session metadata (`meta` — validated `cwd`, fork lineage, seed boundary, origin classification, delegation depth), an optional `seed` replay prefix for forks, per-agent `AgentOptions`, a creation-only cancellation `signal`, and `setup`. `ResumeAgentOptions` is the persisted-identity counterpart: `resumeSessionId`, `agentOptions`, `signal`, and `setup`. The `setup` callback (`AgentSetup`) composes the agent's scoped world while both ids are still unpublished — everything registered through `agentCtx` exists before `agent/created` and the first prompt assembly — and may return a synchronous commit invoked immediately before publication; a setup rejection, commit throw, or owner disposal rolls the transaction back without publishing either id.
+`CreateAgentOptions` carries the shared identity and everything a fresh agent needs before publication: session metadata (`meta` — validated `cwd`, fork lineage, the `isSeeded` marker, origin classification, delegation depth, and `agentPreset`), the exact fork cut in sibling field `inheritedEventCount`, an optional `seed` replay prefix, per-agent `AgentOptions`, a creation-only cancellation `signal`, and `setup`. `ResumeAgentOptions` is the persisted-identity counterpart: `resumeSessionId`, `agentOptions`, `signal`, and `setup`. The `setup` callback (`AgentSetup`) composes the agent's scoped world while both ids are still unpublished — everything registered through `agentCtx` exists before `agent/created` and the first prompt assembly — and may return a synchronous commit invoked immediately before publication; a setup rejection, commit throw, or owner disposal rolls the transaction back without publishing either id.
 
 `AgentFactory` is the creation interface behind the registry: the loop registers its factory via `ctx.agents.setFactory()`, so consumers use `ctx.agents` without depending on the concrete loop package. The exact `create`/`resume` signatures and rollback contracts are in the [generated section](#ctxagents--agentregistry) below.
 

+ 1 - 1
docs/subsystems/core.zh.md

@@ -48,7 +48,7 @@ interface AgentHandle {
 }
 ```
 
-`CreateAgentOptions` 携带共享标识以及新 agent 发布前所需的一切:会话元数据(`meta`——已校验的 `cwd`、fork 谱系、seed 边界、来源分类、委派深度)、fork 用的可选 `seed` 回放前缀、按 agent 的 `AgentOptions`、仅创建期有效的取消 `signal`,以及 `setup`。`ResumeAgentOptions` 是持久标识的对应项:`resumeSessionId`、`agentOptions`、`signal` 与 `setup`。`setup` 回调(`AgentSetup`)在两个 id 都尚未发布时组装 agent 的作用域世界——凡经 `agentCtx` 注册的内容都先于 `agent/created` 与第一次提示词组装存在——并可返回一个在发布前一刻调用的同步 commit;setup 拒绝、commit 抛出或所有者 dispose(资源释放)都会回滚事务,两个 id 均不发布。
+`CreateAgentOptions` 携带共享标识以及新 agent 发布前所需的一切:会话元数据(`meta`——已校验的 `cwd`、fork 谱系、`isSeeded` 标记、来源分类、委派深度与 `agentPreset`)、同级字段 `inheritedEventCount` 所表示的精确 fork cut、可选的 `seed` 回放前缀、按 agent 的 `AgentOptions`、仅创建期有效的取消 `signal`,以及 `setup`。`ResumeAgentOptions` 是持久标识的对应项:`resumeSessionId`、`agentOptions`、`signal` 与 `setup`。`setup` 回调(`AgentSetup`)在两个 id 都尚未发布时组装 agent 的作用域世界——凡经 `agentCtx` 注册的内容都先于 `agent/created` 与第一次提示词组装存在——并可返回一个在发布前一刻调用的同步 commit;setup 拒绝、commit 抛出或所有者 dispose(资源释放)都会回滚事务,两个 id 均不发布。
 
 `AgentFactory` 是注册表背后的创建接口:循环经 `ctx.agents.setFactory()` 注册其工厂,因此消费方使用 `ctx.agents` 时无需依赖具体循环包。确切的 `create`/`resume` 签名及回滚约定见下方[生成区块](#ctxagents--agentregistry)。
 

+ 2 - 2
docs/subsystems/persistence.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/persistence.md
-persistence.md: cd7eaa126ea16b17526223498d1fa1591792747c
-persistence.zh.md: c96aa87f738e5607c699dfa97e6d2b237767b76d
+persistence.md: b61ddee8d12854b330b967a09a1a685d763f0041
+persistence.zh.md: 08cc35d987360138ec096b70f8c6b11b5cba4c55

+ 55 - 20
docs/subsystems/persistence.md

@@ -40,7 +40,7 @@ interface SessionLocation {
 
 ## `SessionHeader` — metadata beside the log
 
-Per-session metadata travels **separately** from the event log: format version, cwd, lineage, and the seed boundary are storage concerns, not conversation events, so they stay out of `SessionEventMap` and never reach `deriveMessages()`. The header is attached to a `Session` via `session.header`.
+Per-session metadata travels **separately** from the event log: the header carries format version, cwd, and the `isSeeded` lineage bit, while body-bearing storage values carry the exact inherited cut beside it. Neither belongs to `SessionEventMap` or reaches `deriveMessages()`. The logical header is attached through `session.header`; the Session exposes its cut as `inheritedEventCount`.
 
 Source: [`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts)
 
@@ -64,10 +64,10 @@ interface SessionHeader {
   /** The session this one was forked from (seed lineage), if any. */
   readonly parentSession?: SessionId
   /**
-   * How many leading events were inherited through a seed. Persisting this
-   * boundary lets resume and replay distinguish parent history from child work.
+   * Whether this Session contains a fork-inherited event prefix. The exact prefix
+   * length is Session state rather than ordinary header metadata.
    */
-  readonly seedLength?: number
+  readonly isSeeded: boolean
   /**
    * Coarse product classification for a session created as a subagent child.
    * This is presentation metadata, not proof that the child is continuable.
@@ -95,7 +95,7 @@ A backend refuses a log it cannot faithfully interpret with `SessionFormatUnsupp
 
 ## `CreateSessionOptions` — seeding and metadata
 
-Creating a `Session` through the store takes a `seed` (initial replay or fork history) and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller may supply the validated absolute `cwd`, the `parentSession` lineage, the `seedLength` seed boundary, the optional coarse `origin`, the `delegationDepth`, the `agentPreset` the agent was composed from, and an existing `createdAt`. `origin: 'subagent'` lets product navigation hide duplicate child rows; it does not prove that a descriptor is valid or that the child can resume.
+Creating a `Session` through the store takes a `seed` (initial replay or fork history), an optional exact `inheritedEventCount`, and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller may supply the validated absolute `cwd`, `parentSession` lineage, `isSeeded` lineage bit, optional coarse `origin`, `delegationDepth`, `agentPreset`, and an existing `createdAt`. A seeded creation requires both an explicit seed and exact cut because child-owned setup events may follow the inherited prefix. `origin: 'subagent'` lets product navigation hide duplicate child rows; it does not prove that a descriptor is valid or that the child can resume.
 
 ```ts type-equiv
 /**
@@ -107,14 +107,19 @@ interface CreateSessionOptions {
   /** Initial replay or fork history supplied at construction. */
   readonly seed?: readonly SessionEvent[]
   /**
-   * Storage metadata read once before publication. `seedLength` is explicit
-   * because a resumed seed contains the full stored log, not only its inherited prefix.
+   * Exact fork-inherited prefix length when `meta.isSeeded` is true. A
+   * constructor seed may also contain child-owned setup events after this cut.
+   */
+  readonly inheritedEventCount?: SessionLogOffset
+  /**
+   * Storage metadata read once before publication. `isSeeded` marks fork
+   * lineage; supplying replay history alone does not make it inherited.
    */
   readonly meta?: {
     readonly cwd?: string
     readonly parentSession?: SessionId
     readonly createdAt?: number
-    readonly seedLength?: number
+    readonly isSeeded?: boolean
     readonly origin?: 'subagent'
     readonly delegationDepth?: number
     readonly agentPreset?: string
@@ -122,7 +127,21 @@ interface CreateSessionOptions {
 }
 ```
 
-Replay/fork is therefore `ctx.sessions.create(id, { seed: seedEvents })`; resuming a *persisted* session into a live agent is `ctx.agents.resume({ resumeSessionId })`.
+Plain replay is `ctx.sessions.create(id, { seed: seedEvents })`; a fork additionally supplies `inheritedEventCount` and `meta.isSeeded: true`. Resuming a *persisted* session into a live agent is `ctx.agents.resume({ resumeSessionId })`.
+
+## `SessionStorageMetadata` — logical header and inherited cut
+
+Every persistence result that reads a Session body carries `SessionStorageMetadata`: the current logical header plus the separately validated inherited-event cut. Header-only listing intentionally returns only `SessionHeader`.
+
+```ts type-equiv
+/** Logical Session header paired with its exact inherited cut for body-bearing storage operations. */
+interface SessionStorageMetadata {
+  /** Validated immutable Session header. */
+  readonly meta: SessionHeader
+  /** Number of leading events inherited from the Session's fork parent. */
+  readonly inheritedEventCount: SessionLogOffset
+}
+```
 
 ## `SessionRawArtifact` — verbatim stored artifact text
 
@@ -130,9 +149,7 @@ A backend's own artifact text for one session, byte-identical to what it durably
 
 ```ts type-equiv
 /** A backend's own raw artifact text for one session, verbatim. */
-interface SessionRawArtifact {
-  /** The session header parsed from the artifact's own first line. */
-  readonly meta: SessionHeader
+interface SessionRawArtifact extends SessionStorageMetadata {
   /** The artifact's base filename on disk, without any physical encoding suffix. */
   readonly filename: string
   /** The artifact's full text content, decoded from the backend's physical encoding. */
@@ -154,6 +171,8 @@ interface RestoredSessionOptions {
   readonly seed: SessionEvent[]
   /** Fresh detached storage metadata to validate and freeze in place. */
   readonly meta: SessionHeader
+  /** Exact number of fork-inherited leading events decoded from storage. */
+  readonly inheritedEventCount: SessionLogOffset
   /** Select the persistence ownership-transfer path. */
   readonly seedSource: 'persistence'
 }
@@ -198,14 +217,26 @@ declare class SessionPreparation implements Disposable {
 
 ```ts type-equiv
 /** Immutable logical session prepared from persistence or a live owner. */
-interface SessionInspection {
-  /** Validated immutable session metadata. */
-  readonly meta: SessionHeader
+interface SessionInspection extends SessionStorageMetadata {
   /** Validated contiguous logical event log. */
   readonly events: readonly SessionEvent[]
 }
 ```
 
+## Detached stored-log suffixes
+
+`readFrom` returns a detached `SessionEventSuffix` anchored by the requested `fromSeq`. Its event list may start above zero or be empty, so it is not a complete `SessionInspection` and must not be restored as a whole Session.
+
+```ts type-equiv
+/** Detached logical suffix returned by one explicit stored-log offset read. */
+interface SessionEventSuffix extends SessionStorageMetadata {
+  /** First requested log offset; {@link events} contains only seqs at or after it. */
+  readonly fromSeq: SessionLogOffset
+  /** Valid contiguous stored events at or after {@link fromSeq}; not a complete Session log when the offset is nonzero. */
+  readonly events: readonly SessionEvent[]
+}
+```
+
 ## Lightweight source revisions
 
 Consumers of derived state compare a cheap opaque revision before loading a full event log. The persistence backend owns its representation and changes it transactionally with append or mutating load repair; callers compare it only for equality.
@@ -281,8 +312,10 @@ readRaw(_id: SessionId, signal?: AbortSignal): Promise<SessionRawArtifact | unde
  * created-but-never-appended session is absent from {@link list}
  * — abandoned sessions leave nothing behind.
  * @param meta - the immutable header (id, version, cwd, lineage) to record.
+ * @param inheritedEventCount - exact fork-inherited prefix length. Required
+ * for a seeded header and omitted only for an unseeded header.
  */
-abstract create(meta: SessionHeader): Promise<void>
+abstract create(meta: SessionHeader, inheritedEventCount?: SessionLogOffset): Promise<void>
 
 /**
  * Ensure a live session has a durable header even when it has no events.
@@ -297,6 +330,8 @@ ensureMaterialized(_session: Session): Promise<void>
  * seq contracts: the first event's `seq` MUST equal the stored next-seq
  * (after `load` has durably closed any interrupted turn). Rejects non-JSON-
  * serializable `event.data` with an error naming the offending event type.
+ * A seeded session's first materializing batch must reach its complete
+ * inherited prefix.
  * @param id - the session the batch belongs to.
  * @param events - the contiguous batch to persist, in seq order.
  */
@@ -371,11 +406,11 @@ abstract borrowSession(id: SessionId, signal?: AbortSignal): Promise<BorrowedSes
  * forward. The primitive bounds what is returned and refolded, not every
  * backend's physical read.
  * @param id - the persisted session to read.
- * @param fromSeq - first event seq to include; a non-negative safe integer.
+ * @param fromSeq - first event offset to include.
  * @param signal - optional cancellation for queued and backend read work.
- * @returns the header and the stored events with `seq >= fromSeq`.
+ * @returns storage metadata, the requested offset, and stored events with `seq >= fromSeq`.
  */
-abstract readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
+abstract readFrom(id: SessionId, fromSeq: SessionLogOffset, signal?: AbortSignal): Promise<SessionEventSuffix>
 
 /**
  * Lightweight listing from metadata, without a full-log parse.
@@ -397,7 +432,7 @@ abstract list(signal?: AbortSignal): Promise<SessionHeader[]>
 abstract listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>
 ```
 
-Types: [Session](session.md) · [SessionEvent](session.md) · [SessionId](core.md)
+Types: [Session](session.md) · [SessionEvent](session.md) · [SessionId](core.md) · [SessionLogOffset](session.md)
 
 Source: [`packages/session/session-persistence/src/index.ts`](../../packages/session/session-persistence/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 55 - 20
docs/subsystems/persistence.zh.md

@@ -40,7 +40,7 @@ interface SessionLocation {
 
 ## `SessionHeader`:日志旁的元数据
 
-每个会话的元数据与事件日志**分开**存储:格式版本、cwd、血统与 seed 边界是存储层关注点而非对话事件,因此不进入 `SessionEventMap`,也不会到达 `deriveMessages()`。header 通过 `session.header` 附加到 `Session` 上。
+每个会话的元数据与事件日志**分开**存储:header 携带格式版本、cwd 与 `isSeeded` 谱系 bit,含正文的存储值则在其旁边单独携带精确 inherited cut。二者都不进入 `SessionEventMap`,也不会到达 `deriveMessages()`。logical header 通过 `session.header` 附加,Session 则以 `inheritedEventCount` 暴露其 cut。
 
 源码:[`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts)
 
@@ -64,10 +64,10 @@ interface SessionHeader {
   /** The session this one was forked from (seed lineage), if any. */
   readonly parentSession?: SessionId
   /**
-   * How many leading events were inherited through a seed. Persisting this
-   * boundary lets resume and replay distinguish parent history from child work.
+   * Whether this Session contains a fork-inherited event prefix. The exact prefix
+   * length is Session state rather than ordinary header metadata.
    */
-  readonly seedLength?: number
+  readonly isSeeded: boolean
   /**
    * Coarse product classification for a session created as a subagent child.
    * This is presentation metadata, not proof that the child is continuable.
@@ -95,7 +95,7 @@ interface SessionHeader {
 
 ## `CreateSessionOptions`:seed 与元数据
 
-通过 store 创建 `Session` 时会接收 `seed`(初始回放或 fork 历史)与 `meta`(store 整合进 `SessionHeader` 的存储层字段)。store 填充 `version`/`id` 并为 `createdAt` 提供默认值;调用方可以提供已校验的绝对 `cwd`、`parentSession` 谱系、`seedLength` 种子边界、可选的粗粒度 `origin`、`delegationDepth`、用于组装该 agent(智能体)的 `agentPreset` 以及已有的 `createdAt`。`origin: 'subagent'` 让产品导航能够隐藏重复的 child 行;它不证明描述符有效,也不证明 child 可以恢复。
+通过 store 创建 `Session` 时会接收 `seed`(初始回放或 fork 历史)、可选的精确 `inheritedEventCount` 与 `meta`(store 整合进 `SessionHeader` 的存储层字段)。store 填充 `version`/`id` 并为 `createdAt` 提供默认值;调用方可以提供已校验的绝对 `cwd`、`parentSession` 谱系、`isSeeded` 谱系标记、可选的粗粒度 `origin`、`delegationDepth`、用于组装该 agent(智能体)的 `agentPreset` 以及已有的 `createdAt`。seeded 创建必须同时显式提供 seed 与精确 cut,因为继承前缀之后还可能存在 child-owned setup event。`origin: 'subagent'` 让产品导航能够隐藏重复的 child 行;它不证明描述符有效,也不证明 child 可以恢复。
 
 ```ts type-equiv
 /**
@@ -107,14 +107,19 @@ interface CreateSessionOptions {
   /** Initial replay or fork history supplied at construction. */
   readonly seed?: readonly SessionEvent[]
   /**
-   * Storage metadata read once before publication. `seedLength` is explicit
-   * because a resumed seed contains the full stored log, not only its inherited prefix.
+   * Exact fork-inherited prefix length when `meta.isSeeded` is true. A
+   * constructor seed may also contain child-owned setup events after this cut.
+   */
+  readonly inheritedEventCount?: SessionLogOffset
+  /**
+   * Storage metadata read once before publication. `isSeeded` marks fork
+   * lineage; supplying replay history alone does not make it inherited.
    */
   readonly meta?: {
     readonly cwd?: string
     readonly parentSession?: SessionId
     readonly createdAt?: number
-    readonly seedLength?: number
+    readonly isSeeded?: boolean
     readonly origin?: 'subagent'
     readonly delegationDepth?: number
     readonly agentPreset?: string
@@ -122,7 +127,21 @@ interface CreateSessionOptions {
 }
 ```
 
-因此,回放/fork 的调用方式为 `ctx.sessions.create(id, { seed: seedEvents })`;将一个*持久化*会话恢复为活跃 agent 的调用方式为 `ctx.agents.resume({ resumeSessionId })`。
+因此,普通回放的调用方式为 `ctx.sessions.create(id, { seed: seedEvents })`;fork 还会提供 `inheritedEventCount` 与 `meta.isSeeded: true`。将一个*持久化*会话恢复为活跃 agent 的调用方式为 `ctx.agents.resume({ resumeSessionId })`。
+
+## `SessionStorageMetadata`:逻辑 header 与继承 cut
+
+每个读取 Session 正文的持久化结果都携带 `SessionStorageMetadata`:当前逻辑 header,以及单独校验的继承事件 cut。仅 header 的列表操作有意只返回 `SessionHeader`。
+
+```ts type-equiv
+/** Logical Session header paired with its exact inherited cut for body-bearing storage operations. */
+interface SessionStorageMetadata {
+  /** Validated immutable Session header. */
+  readonly meta: SessionHeader
+  /** Number of leading events inherited from the Session's fork parent. */
+  readonly inheritedEventCount: SessionLogOffset
+}
+```
 
 ## `SessionRawArtifact`——逐字存储工件文本
 
@@ -130,9 +149,7 @@ interface CreateSessionOptions {
 
 ```ts type-equiv
 /** A backend's own raw artifact text for one session, verbatim. */
-interface SessionRawArtifact {
-  /** The session header parsed from the artifact's own first line. */
-  readonly meta: SessionHeader
+interface SessionRawArtifact extends SessionStorageMetadata {
   /** The artifact's base filename on disk, without any physical encoding suffix. */
   readonly filename: string
   /** The artifact's full text content, decoded from the backend's physical encoding. */
@@ -154,6 +171,8 @@ interface RestoredSessionOptions {
   readonly seed: SessionEvent[]
   /** Fresh detached storage metadata to validate and freeze in place. */
   readonly meta: SessionHeader
+  /** Exact number of fork-inherited leading events decoded from storage. */
+  readonly inheritedEventCount: SessionLogOffset
   /** Select the persistence ownership-transfer path. */
   readonly seedSource: 'persistence'
 }
@@ -198,14 +217,26 @@ declare class SessionPreparation implements Disposable {
 
 ```ts type-equiv
 /** Immutable logical session prepared from persistence or a live owner. */
-interface SessionInspection {
-  /** Validated immutable session metadata. */
-  readonly meta: SessionHeader
+interface SessionInspection extends SessionStorageMetadata {
   /** Validated contiguous logical event log. */
   readonly events: readonly SessionEvent[]
 }
 ```
 
+## 分离的持久日志后缀
+
+`readFrom` 返回以请求的 `fromSeq` 为锚点、与其他状态分离的 `SessionEventSuffix`。其事件列表可能从非零位置开始,也可能为空,因此它不是完整的 `SessionInspection`,不得作为完整 Session 恢复。
+
+```ts type-equiv
+/** Detached logical suffix returned by one explicit stored-log offset read. */
+interface SessionEventSuffix extends SessionStorageMetadata {
+  /** First requested log offset; {@link events} contains only seqs at or after it. */
+  readonly fromSeq: SessionLogOffset
+  /** Valid contiguous stored events at or after {@link fromSeq}; not a complete Session log when the offset is nonzero. */
+  readonly events: readonly SessionEvent[]
+}
+```
+
 ## 轻量源修订号
 
 派生状态的消费方会在加载完整事件日志之前比较一个低开销的不透明修订号。其表示由持久化后端拥有,并随 append 或会修改数据的 load 修复以事务方式改变;调用方仅比较修订号是否相等。
@@ -281,8 +312,10 @@ readRaw(_id: SessionId, signal?: AbortSignal): Promise<SessionRawArtifact | unde
  * created-but-never-appended session is absent from {@link list}
  * — abandoned sessions leave nothing behind.
  * @param meta - the immutable header (id, version, cwd, lineage) to record.
+ * @param inheritedEventCount - exact fork-inherited prefix length. Required
+ * for a seeded header and omitted only for an unseeded header.
  */
-abstract create(meta: SessionHeader): Promise<void>
+abstract create(meta: SessionHeader, inheritedEventCount?: SessionLogOffset): Promise<void>
 
 /**
  * Ensure a live session has a durable header even when it has no events.
@@ -297,6 +330,8 @@ ensureMaterialized(_session: Session): Promise<void>
  * seq contracts: the first event's `seq` MUST equal the stored next-seq
  * (after `load` has durably closed any interrupted turn). Rejects non-JSON-
  * serializable `event.data` with an error naming the offending event type.
+ * A seeded session's first materializing batch must reach its complete
+ * inherited prefix.
  * @param id - the session the batch belongs to.
  * @param events - the contiguous batch to persist, in seq order.
  */
@@ -371,11 +406,11 @@ abstract borrowSession(id: SessionId, signal?: AbortSignal): Promise<BorrowedSes
  * forward. The primitive bounds what is returned and refolded, not every
  * backend's physical read.
  * @param id - the persisted session to read.
- * @param fromSeq - first event seq to include; a non-negative safe integer.
+ * @param fromSeq - first event offset to include.
  * @param signal - optional cancellation for queued and backend read work.
- * @returns the header and the stored events with `seq >= fromSeq`.
+ * @returns storage metadata, the requested offset, and stored events with `seq >= fromSeq`.
  */
-abstract readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
+abstract readFrom(id: SessionId, fromSeq: SessionLogOffset, signal?: AbortSignal): Promise<SessionEventSuffix>
 
 /**
  * Lightweight listing from metadata, without a full-log parse.
@@ -397,7 +432,7 @@ abstract list(signal?: AbortSignal): Promise<SessionHeader[]>
 abstract listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>
 ```
 
-Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionId](core.zh.md)
+Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionId](core.zh.md) · [SessionLogOffset](session.zh.md)
 
 Source: [`packages/session/session-persistence/src/index.ts`](../../packages/session/session-persistence/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 2 - 2
docs/subsystems/schedule.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/schedule.md
-schedule.md: f98cf319d588f805e4630ca572f10c3c45e94b79
-schedule.zh.md: bee816be9cc49943f5815d4d13869b27e30380d5
+schedule.md: 40aa3592dec89fe8daa45beb630076e0476d2b16
+schedule.zh.md: 53a36ee9b51545ca74ac6166ef31d1e002d1f1f2

+ 1 - 1
docs/subsystems/schedule.md

@@ -149,7 +149,7 @@ type ScheduleDispatchChange = OneShotScheduleDispatchChange | EveryScheduleDispa
 type ScheduleChange = ScheduleCreateChange | ScheduleDeleteChange | ScheduleDispatchChange
 ```
 
-The strict decoder and fold reject unknown versions, extra fields, reused ids, mismatched one-shot or Every dispatch shapes, and delete or dispatch transitions against inactive records. A normal Session folds its complete event stream. A fork folds only events at or after `SessionHeader.seedLength`, so it retains history without adopting the parent Session's active reminders. The Schedule projection derives that boundary from the immutable header passed to `init(header)`, uses the shared transition, and persists both active records and used-id history so cached restore preserves strict replay. The `schedule/change` declaration and source location are also indexed in the [persistence catalog](../persistence-catalog.md#schedulechange--log-only).
+The strict decoder and fold reject unknown versions, extra fields, reused ids, mismatched one-shot or Every dispatch shapes, and delete or dispatch transitions against inactive records. A normal Session folds its complete event stream. A fork folds only events at or after its exact `inheritedEventCount`, so it retains history without adopting the parent Session's active reminders. Projection initialization receives that cut beside the immutable header, uses the shared transition, and persists both the cut, active records, and used-id history so cached restore preserves strict replay. The `schedule/change` declaration and source location are also indexed in the [persistence catalog](../persistence-catalog.md#schedulechange--log-only).
 
 ## Active views and management
 

+ 1 - 1
docs/subsystems/schedule.zh.md

@@ -149,7 +149,7 @@ type ScheduleDispatchChange = OneShotScheduleDispatchChange | EveryScheduleDispa
 type ScheduleChange = ScheduleCreateChange | ScheduleDeleteChange | ScheduleDispatchChange
 ```
 
-严格 decoder 与 fold 会拒绝未知版本、额外字段、复用 id、不匹配的一次性提醒或 Every dispatch 形状,以及针对非活动记录的 delete 或 dispatch 转换。普通 Session 折叠完整事件流。fork 只折叠 `SessionHeader.seedLength` 位置及其后的事件,因此保留历史,但不会接管父 Session 的活动提醒。Schedule projection 从传给 `init(header)` 的不可变 header 派生该边界,复用共享 transition,并持久化活动记录与已使用 id 历史,使缓存恢复继续保持严格回放。`schedule/change` 声明和源码位置也编入[持久化目录](../persistence-catalog.zh.md#schedulechange--log-only)。
+严格 decoder 与 fold 会拒绝未知版本、额外字段、复用 id、不匹配的一次性提醒或 Every dispatch 形状,以及针对非活动记录的 delete 或 dispatch 转换。普通 Session 折叠完整事件流。fork 只折叠精确 `inheritedEventCount` 位置及其后的事件,因此保留历史,但不会接管父 Session 的活动提醒。Projection 初始化会在不可变 header 旁接收该 cut,复用共享 transition,并持久化 cut、活动记录与已使用 id 历史,使缓存恢复继续保持严格回放。`schedule/change` 声明和源码位置也编入[持久化目录](../persistence-catalog.zh.md#schedulechange--log-only)。
 
 ## 活动视图与管理
 

+ 2 - 2
docs/subsystems/session-projection.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/session-projection.md
-session-projection.md: b5bacc4846a9a9709bb8c102aaa78810a2752110
-session-projection.zh.md: 36653721a125ceef13bff8b1a4562e7981a45696
+session-projection.md: c8a5d4c1b05d830be3224e2e04db1762fe4b3c8f
+session-projection.zh.md: d46c4f1589f57e560d70871d011245a3547c7825

+ 16 - 12
docs/subsystems/session-projection.md

@@ -30,9 +30,10 @@ interface ProjectionDefinition<
   /**
    * State for the empty log and its immutable Session metadata.
    * @param header - immutable metadata for the Session being projected.
+   * @param inheritedEventCount - exact fork-inherited prefix length.
    * @returns the initial state.
    */
-  init(header: SessionHeader): NoInfer<S>
+  init(header: SessionHeader, inheritedEventCount: SessionLogOffset): NoInfer<S>
   /**
    * Pure transition: previous state + one committed event → next state. A
    * unit uninterested in an event MUST return the same state reference — an
@@ -78,7 +79,7 @@ The whole-value event rule is load-bearing: a state-carrying log event carries t
  */
 interface ProjectionSnapshot {
   /** Seq of the last event the values reflect; -1 for an empty log. */
-  asOfSeq: number
+  asOfSeq: SessionSeqCursor
   /** Whole current client value per registered key. */
   values: Partial<SessionProjectionMap>
 }
@@ -94,7 +95,7 @@ type ProjectionChangeListener = (
   session: Session,
   key: Extract<keyof SessionProjectionMap, string>,
   value: unknown,
-  seq: number,
+  seq: SessionSeq,
 ) => void
 ```
 
@@ -128,11 +129,13 @@ The persisted projection cache service. Opens the `session_projcache` domain at
  * paths (the history tail baseline) supersede these values whenever a
  * session is actually opened.
  * @param meta - the listed session's header (identity witness; no log read).
+ * @param inheritedEventCount - exact inherited prefix length that completes
+ * the checkpoint identity.
  * @param keys - optional projection keys required by the caller's audience.
  * @returns the cut (`asOfSeq` = lowest served-row watermark), or
  *   `undefined` when no usable row exists for this lifecycle.
  */
-cachedSnapshot( meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot | undefined
+cachedSnapshot( meta: SessionHeader, inheritedEventCount: SessionLogOffset, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot | undefined
 
 /**
  * Hydrate projection cells for an already-prepared Session without another
@@ -140,11 +143,10 @@ cachedSnapshot( meta: SessionHeader, keys?: readonly Extract<keyof SessionProjec
  * advances every unit to the observation cut. No checkpoint is written
  * because the logical observation may contain recovery events not yet durable.
  * @param session - exact unpublished Session retained by persistence.
- * @param meta - observed lifecycle header.
  * @param events - exact logical event prefix represented by the observation.
  * @returns all projection values at the event cut.
  */
-hydratePrepared( session: Session, meta: SessionHeader, events: readonly SessionEvent[], ): ProjectionSnapshot
+hydratePrepared( session: Session, events: readonly SessionEvent[], ): ProjectionSnapshot
 
 /**
  * Durably checkpoint one live session NOW (all mandatory points call
@@ -166,13 +168,14 @@ async write(session: Session): Promise<void>
  * The caller supplies the complete log in seq order: this service never
  * consults the persistence layer.
  * @param meta - the stored session header (identity witness).
+ * @param inheritedEventCount - exact inherited prefix length for projection initialization and identity.
  * @param events - the session's complete log, in seq order.
  * @returns the projection cut at the log end.
  */
-coldSnapshot(meta: SessionHeader, events: readonly SessionEvent[]): ProjectionSnapshot
+coldSnapshot( meta: SessionHeader, inheritedEventCount: SessionLogOffset, events: readonly SessionEvent[], ): ProjectionSnapshot
 ```
 
-Types: [Session](session.md) · [SessionEvent](session.md) · [SessionHeader](persistence.md)
+Types: [Session](session.md) · [SessionEvent](session.md) · [SessionHeader](persistence.md) · [SessionLogOffset](session.md)
 
 Source: [`packages/session/session-projection-cache/src/index.ts`](../../packages/session/session-projection-cache/src/index.ts)
 
@@ -272,7 +275,7 @@ checkpoint(session: Session): ProjectionCheckpoint
  *   when no unit is registered (no read needed — {@link restore} would
  *   serve empty values regardless).
  */
-restoreFloor(checkpoint: ProjectionCheckpoint): number | undefined
+restoreFloor(checkpoint: ProjectionCheckpoint): SessionLogOffset | undefined
 
 /**
  * View a checkpoint's rows without any log read: for every registered
@@ -306,11 +309,12 @@ viewCheckpoint( checkpoint: ProjectionCheckpoint, keys?: readonly Extract<keyof
  * @param events - the stored events with `seq >= baseSeq`, in seq order.
  * @param baseSeq - the seq `events` starts at (its first event's seq when non-empty).
  * @param header - immutable metadata for the Session being restored.
+ * @param inheritedEventCount - exact fork-inherited prefix length supplied to unit initialization.
  * @returns the snapshot cut at the supplied log end (`asOfSeq` is the last
  *   supplied event's seq, `baseSeq - 1` for an empty tail) plus the
  *   refreshed checkpoint rows at that cut, ready for a durable write-back.
  */
-restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number, header: SessionHeader, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
+restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
 
 /**
  * Restore an exact cut and install its states on the supplied prepared Session.
@@ -322,10 +326,10 @@ restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], base
  * @param baseSeq - first supplied event sequence.
  * @returns all projection values at the supplied cut.
  */
-hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number, ): ProjectionSnapshot
+hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, ): ProjectionSnapshot
 ```
 
-Types: [Session](session.md) · [SessionEvent](session.md) · [SessionHeader](persistence.md)
+Types: [Session](session.md) · [SessionEvent](session.md) · [SessionHeader](persistence.md) · [SessionLogOffset](session.md)
 
 Source: [`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 16 - 12
docs/subsystems/session-projection.zh.md

@@ -30,9 +30,10 @@ interface ProjectionDefinition<
   /**
    * State for the empty log and its immutable Session metadata.
    * @param header - immutable metadata for the Session being projected.
+   * @param inheritedEventCount - exact fork-inherited prefix length.
    * @returns the initial state.
    */
-  init(header: SessionHeader): NoInfer<S>
+  init(header: SessionHeader, inheritedEventCount: SessionLogOffset): NoInfer<S>
   /**
    * Pure transition: previous state + one committed event → next state. A
    * unit uninterested in an event MUST return the same state reference — an
@@ -78,7 +79,7 @@ interface ProjectionDefinition<
  */
 interface ProjectionSnapshot {
   /** Seq of the last event the values reflect; -1 for an empty log. */
-  asOfSeq: number
+  asOfSeq: SessionSeqCursor
   /** Whole current client value per registered key. */
   values: Partial<SessionProjectionMap>
 }
@@ -94,7 +95,7 @@ type ProjectionChangeListener = (
   session: Session,
   key: Extract<keyof SessionProjectionMap, string>,
   value: unknown,
-  seq: number,
+  seq: SessionSeq,
 ) => void
 ```
 
@@ -128,11 +129,13 @@ The persisted projection cache service. Opens the `session_projcache` domain at
  * paths (the history tail baseline) supersede these values whenever a
  * session is actually opened.
  * @param meta - the listed session's header (identity witness; no log read).
+ * @param inheritedEventCount - exact inherited prefix length that completes
+ * the checkpoint identity.
  * @param keys - optional projection keys required by the caller's audience.
  * @returns the cut (`asOfSeq` = lowest served-row watermark), or
  *   `undefined` when no usable row exists for this lifecycle.
  */
-cachedSnapshot( meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot | undefined
+cachedSnapshot( meta: SessionHeader, inheritedEventCount: SessionLogOffset, keys?: readonly Extract<keyof SessionProjectionMap, string>[], ): ProjectionSnapshot | undefined
 
 /**
  * Hydrate projection cells for an already-prepared Session without another
@@ -140,11 +143,10 @@ cachedSnapshot( meta: SessionHeader, keys?: readonly Extract<keyof SessionProjec
  * advances every unit to the observation cut. No checkpoint is written
  * because the logical observation may contain recovery events not yet durable.
  * @param session - exact unpublished Session retained by persistence.
- * @param meta - observed lifecycle header.
  * @param events - exact logical event prefix represented by the observation.
  * @returns all projection values at the event cut.
  */
-hydratePrepared( session: Session, meta: SessionHeader, events: readonly SessionEvent[], ): ProjectionSnapshot
+hydratePrepared( session: Session, events: readonly SessionEvent[], ): ProjectionSnapshot
 
 /**
  * Durably checkpoint one live session NOW (all mandatory points call
@@ -166,13 +168,14 @@ async write(session: Session): Promise<void>
  * The caller supplies the complete log in seq order: this service never
  * consults the persistence layer.
  * @param meta - the stored session header (identity witness).
+ * @param inheritedEventCount - exact inherited prefix length for projection initialization and identity.
  * @param events - the session's complete log, in seq order.
  * @returns the projection cut at the log end.
  */
-coldSnapshot(meta: SessionHeader, events: readonly SessionEvent[]): ProjectionSnapshot
+coldSnapshot( meta: SessionHeader, inheritedEventCount: SessionLogOffset, events: readonly SessionEvent[], ): ProjectionSnapshot
 ```
 
-Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionHeader](persistence.zh.md)
+Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionHeader](persistence.zh.md) · [SessionLogOffset](session.zh.md)
 
 Source: [`packages/session/session-projection-cache/src/index.ts`](../../packages/session/session-projection-cache/src/index.ts)
 
@@ -272,7 +275,7 @@ checkpoint(session: Session): ProjectionCheckpoint
  *   when no unit is registered (no read needed — {@link restore} would
  *   serve empty values regardless).
  */
-restoreFloor(checkpoint: ProjectionCheckpoint): number | undefined
+restoreFloor(checkpoint: ProjectionCheckpoint): SessionLogOffset | undefined
 
 /**
  * View a checkpoint's rows without any log read: for every registered
@@ -306,11 +309,12 @@ viewCheckpoint( checkpoint: ProjectionCheckpoint, keys?: readonly Extract<keyof
  * @param events - the stored events with `seq >= baseSeq`, in seq order.
  * @param baseSeq - the seq `events` starts at (its first event's seq when non-empty).
  * @param header - immutable metadata for the Session being restored.
+ * @param inheritedEventCount - exact fork-inherited prefix length supplied to unit initialization.
  * @returns the snapshot cut at the supplied log end (`asOfSeq` is the last
  *   supplied event's seq, `baseSeq - 1` for an empty tail) plus the
  *   refreshed checkpoint rows at that cut, ready for a durable write-back.
  */
-restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number, header: SessionHeader, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
+restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, header: SessionHeader, inheritedEventCount: SessionLogOffset, ): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
 
 /**
  * Restore an exact cut and install its states on the supplied prepared Session.
@@ -322,10 +326,10 @@ restore( checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], base
  * @param baseSeq - first supplied event sequence.
  * @returns all projection values at the supplied cut.
  */
-hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number, ): ProjectionSnapshot
+hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: SessionLogOffset, ): ProjectionSnapshot
 ```
 
-Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionHeader](persistence.zh.md)
+Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionHeader](persistence.zh.md) · [SessionLogOffset](session.zh.md)
 
 Source: [`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 2 - 2
docs/subsystems/session-query.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/session-query.md
-session-query.md: 74fb3e15514afd32c594b083732153a1f03b21d5
-session-query.zh.md: fee4c29f67bf850fa9ed07d869308844e84b2bb4
+session-query.md: 4317fa3d706dd40c596114a396ba746da67a979f
+session-query.zh.md: 4b67706aea3d3f73304e4c8ad0c689a4c77a13a1

+ 17 - 11
docs/subsystems/session-query.md

@@ -34,6 +34,8 @@ interface SessionRecord {
 interface SessionLogSnapshot {
   /** Cloned session header selected from the same observation as `events`. */
   session: SessionHeader
+  /** Exact number of fork-inherited events in the observed log. */
+  inheritedEventCount: SessionLogOffset
   /** Cloned contiguous raw events after persistence repair and replay validation. */
   events: SessionEvent[]
 }
@@ -44,8 +46,10 @@ interface SessionLogSnapshot {
 interface SessionSurfaceSnapshot {
   /** Cloned session header selected from the same corpus observation as `events`. */
   session: SessionHeader
+  /** Exact number of fork-inherited events in the observed log. */
+  inheritedEventCount: SessionLogOffset
   /** Highest raw-log seq included in the observation, or `null` for an empty log. */
-  capturedThroughSeq: number | null
+  capturedThroughSeq: OptionalSessionSeq
   /** Cloned current surface events in model-history order. */
   events: SurfaceEvent[]
 }
@@ -90,7 +94,7 @@ interface SessionEventRecord {
   /** Session that owns the event. */
   sessionId: SessionId
   /** Monotonic event seq within the session. */
-  seq: number
+  seq: SessionSeq
   /** Discriminant of the session event. */
   type: SessionEventType
   /** Event timestamp in Unix epoch milliseconds. */
@@ -266,7 +270,7 @@ interface SessionEventReadRequest {
   /** Session that owns the target event. */
   sessionId: SessionId
   /** Target event seq. */
-  seq: number
+  seq: SessionSeq
   /** Number of preceding raw events to include. */
   before?: number
   /** Number of following raw events to include. */
@@ -279,14 +283,16 @@ interface SessionEventReadRequest {
 interface SessionEventWindow {
   /** Cloned header for the live-preferred source read. */
   session: SessionHeader
+  /** Exact number of fork-inherited events in the observed log. */
+  inheritedEventCount: SessionLogOffset
   /** Full cloned target event. */
   target: SessionEvent
   /** Full cloned events from `startSeq` through `endSeq`. */
   events: SessionEvent[]
   /** First seq included in `events`. */
-  startSeq: number
+  startSeq: SessionSeq
   /** Last seq included in `events`. */
-  endSeq: number
+  endSeq: SessionSeq
 }
 ```
 
@@ -300,7 +306,7 @@ interface SessionEventTraceRequest {
   /** Session that owns the target event. */
   sessionId: SessionId
   /** Target event seq. */
-  seq: number
+  seq: SessionSeq
 }
 ```
 
@@ -310,15 +316,15 @@ interface SessionEventTrace {
   /** Lightweight target record. */
   target: SessionEventRecord
   /** Immediate positional replacement event, when the target was shadowed. */
-  replacedBy?: number
+  replacedBy?: SessionSeq
   /** Positional replacers from the immediate replacement to the final replacement. */
-  replacementChain: number[]
+  replacementChain: SessionSeq[]
   /** Surface nodes directly removed when the target itself performed a replacement. */
-  replacedEventSeqs: number[]
+  replacedEventSeqs: SessionSeq[]
   /** Earlier events cited directly as sources, in their recorded order. */
-  sourceEventSeqs: number[]
+  sourceEventSeqs: SessionSeq[]
   /** Later events that directly cite the target as a source, in log order. */
-  derivedEventSeqs: number[]
+  derivedEventSeqs: SessionSeq[]
 }
 ```
 

+ 17 - 11
docs/subsystems/session-query.zh.md

@@ -34,6 +34,8 @@ interface SessionRecord {
 interface SessionLogSnapshot {
   /** Cloned session header selected from the same observation as `events`. */
   session: SessionHeader
+  /** Exact number of fork-inherited events in the observed log. */
+  inheritedEventCount: SessionLogOffset
   /** Cloned contiguous raw events after persistence repair and replay validation. */
   events: SessionEvent[]
 }
@@ -44,8 +46,10 @@ interface SessionLogSnapshot {
 interface SessionSurfaceSnapshot {
   /** Cloned session header selected from the same corpus observation as `events`. */
   session: SessionHeader
+  /** Exact number of fork-inherited events in the observed log. */
+  inheritedEventCount: SessionLogOffset
   /** Highest raw-log seq included in the observation, or `null` for an empty log. */
-  capturedThroughSeq: number | null
+  capturedThroughSeq: OptionalSessionSeq
   /** Cloned current surface events in model-history order. */
   events: SurfaceEvent[]
 }
@@ -90,7 +94,7 @@ interface SessionEventRecord {
   /** Session that owns the event. */
   sessionId: SessionId
   /** Monotonic event seq within the session. */
-  seq: number
+  seq: SessionSeq
   /** Discriminant of the session event. */
   type: SessionEventType
   /** Event timestamp in Unix epoch milliseconds. */
@@ -266,7 +270,7 @@ interface SessionEventReadRequest {
   /** Session that owns the target event. */
   sessionId: SessionId
   /** Target event seq. */
-  seq: number
+  seq: SessionSeq
   /** Number of preceding raw events to include. */
   before?: number
   /** Number of following raw events to include. */
@@ -279,14 +283,16 @@ interface SessionEventReadRequest {
 interface SessionEventWindow {
   /** Cloned header for the live-preferred source read. */
   session: SessionHeader
+  /** Exact number of fork-inherited events in the observed log. */
+  inheritedEventCount: SessionLogOffset
   /** Full cloned target event. */
   target: SessionEvent
   /** Full cloned events from `startSeq` through `endSeq`. */
   events: SessionEvent[]
   /** First seq included in `events`. */
-  startSeq: number
+  startSeq: SessionSeq
   /** Last seq included in `events`. */
-  endSeq: number
+  endSeq: SessionSeq
 }
 ```
 
@@ -300,7 +306,7 @@ interface SessionEventTraceRequest {
   /** Session that owns the target event. */
   sessionId: SessionId
   /** Target event seq. */
-  seq: number
+  seq: SessionSeq
 }
 ```
 
@@ -310,15 +316,15 @@ interface SessionEventTrace {
   /** Lightweight target record. */
   target: SessionEventRecord
   /** Immediate positional replacement event, when the target was shadowed. */
-  replacedBy?: number
+  replacedBy?: SessionSeq
   /** Positional replacers from the immediate replacement to the final replacement. */
-  replacementChain: number[]
+  replacementChain: SessionSeq[]
   /** Surface nodes directly removed when the target itself performed a replacement. */
-  replacedEventSeqs: number[]
+  replacedEventSeqs: SessionSeq[]
   /** Earlier events cited directly as sources, in their recorded order. */
-  sourceEventSeqs: number[]
+  sourceEventSeqs: SessionSeq[]
   /** Later events that directly cite the target as a source, in log order. */
-  derivedEventSeqs: number[]
+  derivedEventSeqs: SessionSeq[]
 }
 ```
 

+ 2 - 2
docs/subsystems/session-title.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/session-title.md
-session-title.md: 05d20c2d7b278dd0663e50d86f1cd22b64852e81
-session-title.zh.md: 2eaa296871bd90e1708f2cf27298b7bbc1efa2c0
+session-title.md: 20ed32b4a08127e7193433673d22f0dea3a90c46
+session-title.zh.md: c01d5d4bf177169069bbe0a39ef533ada0f7c36a

+ 5 - 5
docs/subsystems/session-title.md

@@ -46,7 +46,7 @@ interface SessionTitleEventData {
   /** Normalized non-empty title text. */
   readonly title: string
   /** Exact human `user/message` seqs used to derive this title; empty for an explicit user rename. */
-  readonly messageSeqs: number[]
+  readonly messageSeqs: SessionSeq[]
   /** Whether the built-in fallback, a registered provider, or the user supplied the title. */
   readonly source: SessionTitleSource
 }
@@ -56,7 +56,7 @@ interface SessionTitleEventData {
 /** Latest folded title plus the title event's durable envelope facts. */
 interface SessionTitleSnapshot extends SessionTitleEventData {
   /** Seq of the latest `session/title` event. */
-  readonly eventSeq: number
+  readonly eventSeq: SessionSeq
   /** Timestamp of the latest `session/title` event. */
   readonly updatedAt: number
 }
@@ -72,7 +72,7 @@ interface SessionTitleLlmRequestEventData {
   /** Registered title-provider identity responsible for the request. */
   readonly titleProvider: SessionTitleProviderId
   /** Exact human `user/message` seqs represented in `messages`. */
-  readonly messageSeqs: number[]
+  readonly messageSeqs: SessionSeq[]
   /** Exact auxiliary LLM route. */
   readonly route: SessionTitleModelProvenance
   /** Exact auxiliary system prompt. */
@@ -92,7 +92,7 @@ The service snapshots eligible messages through one revision. A provider returns
 /** One eligible human text message exposed to title providers. */
 interface SessionTitleUserMessage {
   /** Source `user/message` event seq. */
-  readonly seq: number
+  readonly seq: SessionSeq
   /** Exact concatenated text-block content. */
   readonly text: string
 }
@@ -123,7 +123,7 @@ interface SessionTitleProviderResult {
   /** Proposed title text. */
   readonly title: string
   /** Exact seqs from `request.messages` used by this result. */
-  readonly messageSeqs: readonly number[]
+  readonly messageSeqs: readonly SessionSeq[]
   /** Auxiliary LLM route, when generation used a model. */
   readonly model?: SessionTitleModelProvenance
 }

+ 5 - 5
docs/subsystems/session-title.zh.md

@@ -46,7 +46,7 @@ interface SessionTitleEventData {
   /** Normalized non-empty title text. */
   readonly title: string
   /** Exact human `user/message` seqs used to derive this title; empty for an explicit user rename. */
-  readonly messageSeqs: number[]
+  readonly messageSeqs: SessionSeq[]
   /** Whether the built-in fallback, a registered provider, or the user supplied the title. */
   readonly source: SessionTitleSource
 }
@@ -56,7 +56,7 @@ interface SessionTitleEventData {
 /** Latest folded title plus the title event's durable envelope facts. */
 interface SessionTitleSnapshot extends SessionTitleEventData {
   /** Seq of the latest `session/title` event. */
-  readonly eventSeq: number
+  readonly eventSeq: SessionSeq
   /** Timestamp of the latest `session/title` event. */
   readonly updatedAt: number
 }
@@ -72,7 +72,7 @@ interface SessionTitleLlmRequestEventData {
   /** Registered title-provider identity responsible for the request. */
   readonly titleProvider: SessionTitleProviderId
   /** Exact human `user/message` seqs represented in `messages`. */
-  readonly messageSeqs: number[]
+  readonly messageSeqs: SessionSeq[]
   /** Exact auxiliary LLM route. */
   readonly route: SessionTitleModelProvenance
   /** Exact auxiliary system prompt. */
@@ -92,7 +92,7 @@ interface SessionTitleLlmRequestEventData {
 /** One eligible human text message exposed to title providers. */
 interface SessionTitleUserMessage {
   /** Source `user/message` event seq. */
-  readonly seq: number
+  readonly seq: SessionSeq
   /** Exact concatenated text-block content. */
   readonly text: string
 }
@@ -123,7 +123,7 @@ interface SessionTitleProviderResult {
   /** Proposed title text. */
   readonly title: string
   /** Exact seqs from `request.messages` used by this result. */
-  readonly messageSeqs: readonly number[]
+  readonly messageSeqs: readonly SessionSeq[]
   /** Auxiliary LLM route, when generation used a model. */
   readonly model?: SessionTitleModelProvenance
 }

+ 2 - 2
docs/subsystems/session.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/session.md
-session.md: fbeb501b9ba6283a41c108fc07fc8787ed9e68a4
-session.zh.md: daf25d74bc5bd039a7d8b1a1882c2ec197136ce3
+session.md: 40012d1fc7b5be602dd3783f1df24f8f61f8a969
+session.zh.md: 9d4f6c3ce90913e5236b011e025d8299b3192a57

+ 75 - 25
docs/subsystems/session.md

@@ -179,6 +179,28 @@ interface RequestContext {
 
 A proper discriminated union over `type` (not independent `type`/`data` unions), so `switch (event.type)` narrows `event.data` without casts. `seq` is the monotonic position in the log (`seq = log.length`); `time` is epoch ms.
 
+```ts type-equiv
+/** Sequence number of one existing event in a Session log. */
+type SessionSeq = BrandedNumber<'SessionSeq'>
+```
+
+```ts type-equiv
+/** A Session log gap, prefix length, or read offset, which may equal the event count. */
+type SessionLogOffset = BrandedNumber<'SessionLogOffset'>
+```
+
+```ts type-equiv
+/** Inclusive Session event watermark, or `-1` before any event exists. */
+type SessionSeqCursor = SessionSeq | -1
+```
+
+```ts type-equiv
+/** One existing Session event position, or explicit absence. */
+type OptionalSessionSeq = SessionSeq | null
+```
+
+`SessionSeq(value)` and `SessionLogOffset(value)` admit only non-negative safe integers and reject negative zero. They add compile-time brands without changing the serialized number; arithmetic returns an ordinary `number` that callers must admit again through the constructor for its intended role.
+
 ```ts type-equiv
 /**
  * One immutable entry in the session log.
@@ -197,7 +219,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
   [K in SessionEventType]: {
     type: K
     /** Monotonic sequence number within the session. */
-    seq: number
+    seq: SessionSeq
     /** Unix epoch milliseconds. */
     time: number
     data: SessionEventMap[K]
@@ -221,7 +243,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
      * provider stream; when the field is absent, the event does not record which
      * earlier events produced the message.
      */
-    sourceEventSeqs?: number[]
+    sourceEventSeqs?: SessionSeq[]
     /** How this event entered the surface; absent for non-surface events. */
     surfaceOp?: SurfaceOp
   } : object)
@@ -268,7 +290,7 @@ type SurfaceEventType =
  */
 type SurfaceOp =
   | 'append'
-  | { op: 'replace'; start: number; end: number }
+  | { op: 'replace'; start: SessionSeq; end: SessionSeq }
 ```
 
 `'append'` is the normal tail-append path. `replace` shadows surface entries from `start` through `end` inclusive (both must be valid surface seqs; `start === end` replaces a single entry) and inserts the new event in their place.
@@ -288,7 +310,7 @@ interface SurfaceIntent {
    * absent, the event does not record which earlier events produced the message.
    * Other surface events require a non-empty set when this field is present.
    */
-  sourceEventSeqs?: number[]
+  sourceEventSeqs?: SessionSeq[]
 }
 ```
 
@@ -306,7 +328,7 @@ Only `assistant/message` may carry a present empty `sourceEventSeqs`; when the f
 /** Readonly live projection of the message-producing session events. */
 interface SessionSurface {
   /** Current surface event sequences in model-visible order. */
-  readonly nodes: readonly number[]
+  readonly nodes: readonly SessionSeq[]
   /** Monotonic count of committed positional replacements. */
   readonly replaceGeneration: number
 }
@@ -320,13 +342,13 @@ interface SessionSurface {
 /** One replacement operation observed while folding a session surface. */
 interface SurfaceFoldReplacement {
   /** Seq of the event that replaced the prior surface range. */
-  seq: number
+  seq: SessionSeq
   /** Declared inclusive start seq of the replaced surface range. */
-  start: number
+  start: SessionSeq
   /** Declared inclusive end seq of the replaced surface range. */
-  end: number
+  end: SessionSeq
   /** Actual surface entries removed by the operation, in surface order. */
-  shadowedSeqs: number[]
+  shadowedSeqs: SessionSeq[]
 }
 ```
 
@@ -334,7 +356,7 @@ interface SurfaceFoldReplacement {
 /** Complete result of replaying the surface operations in a session log. */
 interface SurfaceFoldResult {
   /** Current surface event sequences in model-visible order. */
-  nodes: number[]
+  nodes: SessionSeq[]
   /** Replacement operations in event order. */
   replacements: SurfaceFoldReplacement[]
 }
@@ -358,13 +380,15 @@ declare class Session {
   get surface(): SessionSurface;
   /**
    * Detached, deep-frozen creation metadata (format version, cwd, lineage,
-   * seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
+   * and whether fork history exists). Supplied by the store via `ctx.sessions.create()`. When a
    * `Session` is created without a store-owned header, a minimal header is
    * synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
    * `session.header` is always present. Kept out of the event log — it is a
    * storage concern, not replayable conversation state.
    */
   readonly header: SessionHeader;
+  /** Number of leading events inherited from this Session's fork parent. */
+  readonly inheritedEventCount: SessionLogOffset;
   /** The session identity, derived from its durable header's single copy. */
   get id(): SessionId;
   /**
@@ -373,9 +397,9 @@ declare class Session {
    * construction — replay, fork, or resume — and were never published on the
    * `session/event` firehose (constructor seeds do not emit), so consumers
    * that replay the log as a publication substitute (telemetry adoption)
-   * start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
-   * boundary: a resumed session's constructor seed is its full stored log,
-   * while its header keeps the original fork value — this field is the
+   * start here. Distinct from {@link inheritedEventCount}, the DURABLE
+   * fork-lineage cut: a resumed session's constructor seed is its full stored
+   * log, while the inherited count keeps the original fork value — this field is the
    * in-process construction fact.
    *
    * Not persisted itself: a seeded session projects it into the log as the
@@ -389,16 +413,22 @@ declare class Session {
    * store attaches and therefore does not publish either. Otherwise this seq
    * holds an ordinary published write.
   */
-  readonly firstLiveSeq: number;
+  readonly firstLiveSeq: SessionLogOffset;
   /**
    * Create a detached session by validating and snapshotting borrowed seed
    * events and storage metadata.
    * @param id - session identity.
    * @param seed - optional borrowed replay or fork events.
    * @param header - optional borrowed storage metadata.
+   * @param inheritedEventCount - exact fork-inherited prefix length for a seeded header.
    * @returns a detached session.
    */
-  static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;
+  static create(
+    id: SessionId,
+    seed?: readonly SessionEvent[],
+    header?: SessionHeader,
+    inheritedEventCount?: SessionLogOffset,
+  ): Session;
   /**
    * Restore a detached session by taking ownership of fresh persistence values.
    * The storage format, event envelopes, sequence continuity, surface transitions,
@@ -406,15 +436,21 @@ declare class Session {
    * @param id - restored session identity.
    * @param seed - fresh detached events whose ownership is transferred.
    * @param header - fresh detached metadata whose ownership is transferred.
+   * @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
    * @returns a restored detached session.
    */
-  static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;
+  static fromRestore(
+    id: SessionId,
+    seed: readonly SessionEvent[],
+    header: SessionHeader,
+    inheritedEventCount: SessionLogOffset,
+  ): Session;
   /**
    * Return the immutable event stored at one exact sequence number.
    * @param seq - event sequence number.
    * @returns the accepted event, or undefined when the log does not contain it.
    */
-  eventAt(seq: number): SessionEvent | undefined;
+  eventAt(seq: SessionSeq): SessionEvent | undefined;
   /**
    * Materialize an immutable snapshot of a half-open event sequence range.
    * A full current snapshot is reused until the next append; every previously
@@ -423,9 +459,23 @@ declare class Session {
    * @param toSeqExclusive - non-negative exclusive sequence number; defaults to the current end.
    * @returns a frozen array of the selected deeply frozen events.
    */
-  snapshotEvents(fromSeq: number = 0, toSeqExclusive: number = this.log.length): readonly SessionEvent[];
+  snapshotEvents(
+    fromSeq: SessionLogOffset = SessionLogOffset(0),
+    toSeqExclusive: SessionLogOffset = this.seq,
+  ): readonly SessionEvent[];
+  /**
+   * Return this Session's events after its fork-inherited prefix.
+   * @returns a fresh array containing child-owned events in log order.
+   */
+  ownEvents(): readonly SessionEvent[];
+  /**
+   * Whether one existing event position is outside the fork-inherited prefix.
+   * @param seq - event position in this Session.
+   * @returns true when the event belongs to this Session rather than its parent.
+   */
+  isOwnSeq(seq: SessionSeq): boolean;
   /** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
-  get seq(): number;
+  get seq(): SessionLogOffset;
   /**
    * Append one typed event to the log and synchronously notify observers via
    * the store-owned, module-private publication hooks. The hot path never blocks
@@ -525,7 +575,7 @@ Everything else (`turn/*`, `step/*`, plugin-owned `llm/retry`) is structural and
 
 `ctx.sessions.create(id, { seed, meta })` is the low-level replay/fork primitive. For ordinary live-session forks, `SessionStore` exposes one policy API:
 
-- `fork(source, boundary?, childSessionId?)` accepts a live `Session` object or live `SessionId`, selects source events through the inclusive `boundary` seq (default: current last event), requires the selected prefix to end outside an open turn, then creates a live child session with deep-cloned seed events plus child metadata (`parentSession`, `seedLength`, and inherited `cwd`).
+- `fork(source, boundary?, childSessionId?)` accepts a live `Session` object or live `SessionId`, selects source events through the inclusive `SessionSeq` boundary (default: current last event), requires the selected prefix to end outside an open turn, then creates a live child session with deep-cloned seed events, `parentSession`, `isSeeded: true`, the exact `inheritedEventCount`, and inherited `cwd`.
 
 An explicit `boundary` lets callers fork from any stable between-turn position, including a previous `turn/end` or a later standalone log-only event, even if the source has newer events or an open current turn. The API rejects a prefix that ends inside an open turn instead of clipping silently. Broader execution-relation sanity stays in the existing `dsh-invariants` plugin and persistence repair path rather than being duplicated in `fork()`. `dsh-subagent-fork-in-process` keeps its completed-prefix clipping because tool-time delegation usually starts while the parent turn is open; ordinary session branching should make the requested boundary explicit.
 
@@ -574,7 +624,7 @@ The optional `dsh-session/invariant` companion enforces the relations owned by c
 
 ## The end-seed boundary: `session/end-seed`
 
-A seeded session — resume, fork, or replay — appends this log-only event immediately after its constructor seed, as its first live write. Events before it have smaller seq values and came from the seed. It is the durable projection of `firstLiveSeq`: that field answers where this lifecycle's writes start for a consumer holding the object, while the event answers the same question for one holding only stored bytes. The payload is empty, so position and `time` carry the whole meaning, and it produces no message. `Session`'s constructor is the only legitimate writer.
+A Session constructed with an explicit seed — restore, fork, or replay — appends this log-only event immediately after that constructor seed, as its first live write. Events before it have smaller seq values and came through construction. It is the durable projection of `firstLiveSeq`: that field answers where this lifecycle's writes start for a consumer holding the object, while the event answers the same question for one holding only stored bytes. It does not define fork ownership; `isSeeded` plus `inheritedEventCount` do. The payload is empty, so position and `time` carry the whole meaning, and it produces no message. `Session`'s constructor is the only legitimate writer.
 
 An explicitly supplied empty seed writes `session/end-seed` at seq 0, which distinguishes an empty resumed session from a fresh one. A seed already ending in `session/end-seed` is not re-marked, so reopening an untouched session does not grow its log per pickup. Locate the LAST `session/end-seed` in stored history rather than assuming one exists at `firstLiveSeq`: after a pickup with no work, the event has a smaller seq than the next lifecycle's `firstLiveSeq`.
 
@@ -630,7 +680,7 @@ resolveAgent(sessionId: SessionId): Promise<ApiSessionAgentResult>
  * @param signal - optional caller cancellation for persistence reads.
  * @returns the current attached state or persisted header and event prefix.
  */
-inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionHeader; events: readonly SessionEvent[] }>
+inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspection>
 
 /**
  * Read all visible Session rows without resuming an Agent.
@@ -750,7 +800,7 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
 @Remote({ mode: 'stream' }) control(signal: AbortSignal): AsyncIterable<SessionControlFrame>
 ```
 
-Types: [SessionHeader](persistence.md) · [SessionId](core.md) · [SessionSearchRequest](session-query.md)
+Types: [SessionId](core.md) · [SessionInspection](persistence.md) · [SessionSearchRequest](session-query.md)
 
 Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/session-controller/src/index.ts)
 
@@ -883,7 +933,7 @@ list(): Session[]
  *   `SessionStore`'s id policy.
  * @returns The created live child session.
  */
-fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
+fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
 ```
 
 Types: [CreateSessionOptions](persistence.md) · [PrepareSessionOptions](persistence.md) · [SessionId](core.md)

+ 75 - 25
docs/subsystems/session.zh.md

@@ -179,6 +179,28 @@ interface RequestContext {
 
 基于 `type` 的真正可辨识联合(而非独立的 `type`/`data` 联合),因此 `switch (event.type)` 能直接收窄 `event.data`,无需类型断言。`seq` 是日志中的单调递增位置(`seq = log.length`);`time` 为 epoch 毫秒。
 
+```ts type-equiv
+/** Sequence number of one existing event in a Session log. */
+type SessionSeq = BrandedNumber<'SessionSeq'>
+```
+
+```ts type-equiv
+/** A Session log gap, prefix length, or read offset, which may equal the event count. */
+type SessionLogOffset = BrandedNumber<'SessionLogOffset'>
+```
+
+```ts type-equiv
+/** Inclusive Session event watermark, or `-1` before any event exists. */
+type SessionSeqCursor = SessionSeq | -1
+```
+
+```ts type-equiv
+/** One existing Session event position, or explicit absence. */
+type OptionalSessionSeq = SessionSeq | null
+```
+
+`SessionSeq(value)` 与 `SessionLogOffset(value)` 只接纳非负安全整数,并拒绝负零。它们仅添加编译期品牌,不改变序列化后的数值;算术会返回普通 `number`,调用方必须按结果的预期角色通过对应构造器重新接纳。
+
 ```ts type-equiv
 /**
  * One immutable entry in the session log.
@@ -197,7 +219,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
   [K in SessionEventType]: {
     type: K
     /** Monotonic sequence number within the session. */
-    seq: number
+    seq: SessionSeq
     /** Unix epoch milliseconds. */
     time: number
     data: SessionEventMap[K]
@@ -221,7 +243,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
      * provider stream; when the field is absent, the event does not record which
      * earlier events produced the message.
      */
-    sourceEventSeqs?: number[]
+    sourceEventSeqs?: SessionSeq[]
     /** How this event entered the surface; absent for non-surface events. */
     surfaceOp?: SurfaceOp
   } : object)
@@ -270,7 +292,7 @@ type SurfaceEventType =
  */
 type SurfaceOp =
   | 'append'
-  | { op: 'replace'; start: number; end: number }
+  | { op: 'replace'; start: SessionSeq; end: SessionSeq }
 ```
 
 `'append'` 是常规的尾部追加路径。`replace` 会遮蔽从 `start` 到 `end`(含两端)的 surface 条目(两者都必须是有效的 surface seq;`start === end` 时仅替换单个条目),并在原位置插入新事件。
@@ -290,7 +312,7 @@ interface SurfaceIntent {
    * absent, the event does not record which earlier events produced the message.
    * Other surface events require a non-empty set when this field is present.
    */
-  sourceEventSeqs?: number[]
+  sourceEventSeqs?: SessionSeq[]
 }
 ```
 
@@ -308,7 +330,7 @@ interface SurfaceIntent {
 /** Readonly live projection of the message-producing session events. */
 interface SessionSurface {
   /** Current surface event sequences in model-visible order. */
-  readonly nodes: readonly number[]
+  readonly nodes: readonly SessionSeq[]
   /** Monotonic count of committed positional replacements. */
   readonly replaceGeneration: number
 }
@@ -322,13 +344,13 @@ interface SessionSurface {
 /** One replacement operation observed while folding a session surface. */
 interface SurfaceFoldReplacement {
   /** Seq of the event that replaced the prior surface range. */
-  seq: number
+  seq: SessionSeq
   /** Declared inclusive start seq of the replaced surface range. */
-  start: number
+  start: SessionSeq
   /** Declared inclusive end seq of the replaced surface range. */
-  end: number
+  end: SessionSeq
   /** Actual surface entries removed by the operation, in surface order. */
-  shadowedSeqs: number[]
+  shadowedSeqs: SessionSeq[]
 }
 ```
 
@@ -336,7 +358,7 @@ interface SurfaceFoldReplacement {
 /** Complete result of replaying the surface operations in a session log. */
 interface SurfaceFoldResult {
   /** Current surface event sequences in model-visible order. */
-  nodes: number[]
+  nodes: SessionSeq[]
   /** Replacement operations in event order. */
   replacements: SurfaceFoldReplacement[]
 }
@@ -360,13 +382,15 @@ declare class Session {
   get surface(): SessionSurface;
   /**
    * Detached, deep-frozen creation metadata (format version, cwd, lineage,
-   * seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
+   * and whether fork history exists). Supplied by the store via `ctx.sessions.create()`. When a
    * `Session` is created without a store-owned header, a minimal header is
    * synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
    * `session.header` is always present. Kept out of the event log — it is a
    * storage concern, not replayable conversation state.
    */
   readonly header: SessionHeader;
+  /** Number of leading events inherited from this Session's fork parent. */
+  readonly inheritedEventCount: SessionLogOffset;
   /** The session identity, derived from its durable header's single copy. */
   get id(): SessionId;
   /**
@@ -375,9 +399,9 @@ declare class Session {
    * construction — replay, fork, or resume — and were never published on the
    * `session/event` firehose (constructor seeds do not emit), so consumers
    * that replay the log as a publication substitute (telemetry adoption)
-   * start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
-   * boundary: a resumed session's constructor seed is its full stored log,
-   * while its header keeps the original fork value — this field is the
+   * start here. Distinct from {@link inheritedEventCount}, the DURABLE
+   * fork-lineage cut: a resumed session's constructor seed is its full stored
+   * log, while the inherited count keeps the original fork value — this field is the
    * in-process construction fact.
    *
    * Not persisted itself: a seeded session projects it into the log as the
@@ -391,16 +415,22 @@ declare class Session {
    * store attaches and therefore does not publish either. Otherwise this seq
    * holds an ordinary published write.
   */
-  readonly firstLiveSeq: number;
+  readonly firstLiveSeq: SessionLogOffset;
   /**
    * Create a detached session by validating and snapshotting borrowed seed
    * events and storage metadata.
    * @param id - session identity.
    * @param seed - optional borrowed replay or fork events.
    * @param header - optional borrowed storage metadata.
+   * @param inheritedEventCount - exact fork-inherited prefix length for a seeded header.
    * @returns a detached session.
    */
-  static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;
+  static create(
+    id: SessionId,
+    seed?: readonly SessionEvent[],
+    header?: SessionHeader,
+    inheritedEventCount?: SessionLogOffset,
+  ): Session;
   /**
    * Restore a detached session by taking ownership of fresh persistence values.
    * The storage format, event envelopes, sequence continuity, surface transitions,
@@ -408,15 +438,21 @@ declare class Session {
    * @param id - restored session identity.
    * @param seed - fresh detached events whose ownership is transferred.
    * @param header - fresh detached metadata whose ownership is transferred.
+   * @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
    * @returns a restored detached session.
    */
-  static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;
+  static fromRestore(
+    id: SessionId,
+    seed: readonly SessionEvent[],
+    header: SessionHeader,
+    inheritedEventCount: SessionLogOffset,
+  ): Session;
   /**
    * Return the immutable event stored at one exact sequence number.
    * @param seq - event sequence number.
    * @returns the accepted event, or undefined when the log does not contain it.
    */
-  eventAt(seq: number): SessionEvent | undefined;
+  eventAt(seq: SessionSeq): SessionEvent | undefined;
   /**
    * Materialize an immutable snapshot of a half-open event sequence range.
    * A full current snapshot is reused until the next append; every previously
@@ -425,9 +461,23 @@ declare class Session {
    * @param toSeqExclusive - non-negative exclusive sequence number; defaults to the current end.
    * @returns a frozen array of the selected deeply frozen events.
    */
-  snapshotEvents(fromSeq: number = 0, toSeqExclusive: number = this.log.length): readonly SessionEvent[];
+  snapshotEvents(
+    fromSeq: SessionLogOffset = SessionLogOffset(0),
+    toSeqExclusive: SessionLogOffset = this.seq,
+  ): readonly SessionEvent[];
+  /**
+   * Return this Session's events after its fork-inherited prefix.
+   * @returns a fresh array containing child-owned events in log order.
+   */
+  ownEvents(): readonly SessionEvent[];
+  /**
+   * Whether one existing event position is outside the fork-inherited prefix.
+   * @param seq - event position in this Session.
+   * @returns true when the event belongs to this Session rather than its parent.
+   */
+  isOwnSeq(seq: SessionSeq): boolean;
   /** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
-  get seq(): number;
+  get seq(): SessionLogOffset;
   /**
    * Append one typed event to the log and synchronously notify observers via
    * the store-owned, module-private publication hooks. The hot path never blocks
@@ -527,7 +577,7 @@ declare class Session {
 
 `ctx.sessions.create(id, { seed, meta })` 是底层的回放/fork 原语。对于普通的活跃会话 fork,`SessionStore` 暴露一个策略 API:
 
-- `fork(source, boundary?, childSessionId?)` 接受一个活跃的 `Session` 对象或活跃的 `SessionId`,选取到 `boundary` seq(含)为止的源事件(默认为当前最后一个事件),要求所选前缀结束时没有开放轮次,然后创建一个活跃的子会话,包含深克隆的种子事件和子会话元数据(`parentSession`、`seedLength` 及继承的 `cwd`)。
+- `fork(source, boundary?, childSessionId?)` 接受一个活跃的 `Session` 对象或活跃的 `SessionId`,选取到 `SessionSeq` boundary(含)为止的源事件(默认为当前最后一个事件),要求所选前缀结束时没有开放轮次,然后创建一个活跃的子会话,包含深克隆的 seed event、`parentSession`、`isSeeded: true`、精确 `inheritedEventCount` 及继承的 `cwd`。
 
 显式 `boundary` 允许调用者从任意稳定的轮次间位置 fork,包括之前的 `turn/end` 或更晚的独立纯日志事件,即使源会话有更新的事件或正在进行的轮次。API 拒绝结束于开放轮次内的前缀,而不是静默截断。更广泛的执行关系健全性检查留在既有的 `dsh-invariants` 插件和持久化修复路径中,不在 `fork()` 中重复。`dsh-subagent-fork-in-process` 保留其已完成前缀截断逻辑,因为工具调用时的委托通常在父轮次仍然打开时启动;普通的会话分支应显式指定请求的 boundary。
 
@@ -578,7 +628,7 @@ interface TurnEndReasonMap {
 
 ## 种子结束边界:`session/end-seed`
 
-带种子的会话(恢复、fork 或回放)紧接构造种子之后追加这个仅日志事件,作为自己的第一次实时写入。在它之前的事件具有更小的 seq,且来自种子。它是 `firstLiveSeq` 的持久投影:该字段为持有对象的消费方回答本生命周期的写入从哪里开始,该事件则为只持有存储字节的消费方回答同一问题。payload 为空,因此位置与 `time` 承载全部含义,且不产生任何消息。`Session` 的构造函数是唯一合法的写入方。
+用显式 seed 构造的 Session(restore、fork 或 replay)会紧接该 constructor seed 之后追加这个仅日志事件,作为自己的第一次实时写入。在它之前的事件具有更小的 seq,且经由构造进入。它是 `firstLiveSeq` 的持久投影:该字段为持有对象的 consumer 回答本 lifecycle 的写入从哪里开始,该事件则为只持有存储字节的 consumer 回答同一问题。它不定义 fork ownership;`isSeeded` 与 `inheritedEventCount` 才定义。payload 为空,因此位置与 `time` 承载全部含义,且不产生任何消息。`Session` 的构造函数是唯一合法的写入方。
 
 显式传入的空种子会在 seq 0 写入 `session/end-seed`,从而把从空日志恢复的会话与全新会话区分开来。种子本身已以 `session/end-seed` 结尾时不会重复标记,因此重新打开一个未被改动的会话不会每次拾起都增长日志。应定位存储历史中的最后一条 `session/end-seed`,而不是假定 `firstLiveSeq` 处一定有一条:在一次没有产生工作的拾起之后,该事件的 seq 会小于下一个生命周期的 `firstLiveSeq`。
 
@@ -634,7 +684,7 @@ resolveAgent(sessionId: SessionId): Promise<ApiSessionAgentResult>
  * @param signal - optional caller cancellation for persistence reads.
  * @returns the current attached state or persisted header and event prefix.
  */
-inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionHeader; events: readonly SessionEvent[] }>
+inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspection>
 
 /**
  * Read all visible Session rows without resuming an Agent.
@@ -754,7 +804,7 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
 @Remote({ mode: 'stream' }) control(signal: AbortSignal): AsyncIterable<SessionControlFrame>
 ```
 
-Types: [SessionHeader](persistence.zh.md) · [SessionId](core.zh.md) · [SessionSearchRequest](session-query.zh.md)
+Types: [SessionId](core.zh.md) · [SessionInspection](persistence.zh.md) · [SessionSearchRequest](session-query.zh.md)
 
 Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/session-controller/src/index.ts)
 
@@ -887,7 +937,7 @@ list(): Session[]
  *   `SessionStore`'s id policy.
  * @returns The created live child session.
  */
-fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
+fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
 ```
 
 Types: [CreateSessionOptions](persistence.zh.md) · [PrepareSessionOptions](persistence.zh.md) · [SessionId](core.zh.md)

+ 2 - 2
docs/subsystems/subagent.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/subagent.md
-subagent.md: 109f8569879c0578f804569d1105c03250ad3bf5
-subagent.zh.md: 649a3796f9f199306e45ea9653bb38df8683a518
+subagent.md: 5369ed85b5382dcb07df3e458a99124107ad5e51
+subagent.zh.md: 7495890531c6faa604a96edf443543840f69ae2c

+ 1 - 1
docs/subsystems/subagent.md

@@ -258,7 +258,7 @@ interface ContinuableCreateSpec {
 
 The descriptor (`SubagentDescriptorData` in [descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts)) is a mode-discriminated durable identity for every session-backed subagent. Both modes carry the provider name. A `one-shot` descriptor optionally carries a caller-owned display `label`; a `continuable` descriptor requires the delegation `description` as its durable creation label and additionally snapshots resolved child `agentOptions.provider`/`model`/`reasoningEffort` and optional `persona`/`toolFilter` for cold resume. It never snapshots the merge-extensible `AgentOptions` object, so an unrelated extension value cannot break continuation and a later composition input is a deliberate version change. It omits `subagentDepth` (cold resume trusts the persisted header's `delegationDepth` as the monotone floor) and `outputSchema` (one run or Activation's result contract, not durable identity).
 
-A local one-shot provider appends the descriptor inside the child's initial turn before its first request. The continuation manager appends the descriptor after any provider-supplied lineage and before the initial prompt is admitted; `header.seedLength` remains the fork-lineage boundary: resume-time descriptor authority reads the child's own suffix, while the list-serving identity projection folds `subagent/descriptor` last-wins so the child's own descriptor overrides a fork-seeded ancestor's. The event is log-only: no `surfaceOp`, never in model history, and retained across compaction by the append-only log. Malformed current-version descriptors are corrupt; unsupported versions cannot be classified by this runtime.
+A local one-shot provider appends the descriptor inside the child's initial turn before its first request. The continuation manager appends the descriptor after any provider-supplied lineage and before the initial prompt is admitted; `Session.inheritedEventCount` remains the fork-lineage boundary: resume-time descriptor authority reads the child's own suffix, while the list-serving identity projection folds `subagent/descriptor` last-wins so the child's own descriptor overrides a fork-seeded ancestor's. A seeded cold list skips a cache hint until an authoritative observation supplies that exact cut. The event is log-only: no `surfaceOp`, never in model history, and retained across compaction by the append-only log. Malformed current-version descriptors are corrupt; unsupported versions cannot be classified by this runtime.
 
 ## Durable enumeration: `listChildren()`, `listDescendants()`, and their entries
 

+ 1 - 1
docs/subsystems/subagent.zh.md

@@ -258,7 +258,7 @@ interface ContinuableCreateSpec {
 
 描述符([descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts) 中的 `SubagentDescriptorData`)是每个由会话支撑的 subagent 所使用、按模式判别的持久化身份。两种模式都携带提供方名称。`one-shot` 描述符可以携带调用方拥有的可选显示 `label`;`continuable` 描述符要求以委派 `description` 作为持久化创建标签,并另外对已解析的子 agent `agentOptions.provider`/`model`/`reasoningEffort` 与可选的 `persona`/`toolFilter` 建立快照,用于冷恢复。它绝不会对可合并扩展的 `AgentOptions` 对象建立快照,因此无关的扩展值不会破坏继续执行,后续新增组合配置输入则是一次有意的版本更改。描述符省略 `subagentDepth`(冷恢复以持久化 header 中的 `delegationDepth` 作为单调下界)和 `outputSchema`(单次运行或 Activation 的结果约定,而非持久化身份)。
 
-本地一次性提供方会在子 agent 的初始轮次内、首次请求前追加描述符。继续执行管理器会在任何提供方提供的谱系之后、初始提示词获准之前追加描述符;`header.seedLength` 仍是 fork 谱系边界:恢复时的描述符权威读取子 agent 自身的后缀,而供列表使用的身份投影以 last-wins 折叠 `subagent/descriptor`,子 agent 自己的描述符会覆盖 fork seed 中祖先的描述符。该事件只进入日志:不含 `surfaceOp`,绝不进入模型历史,并由仅追加日志跨压缩保留。格式错误的当前版本描述符属于损坏;本运行时无法对不受支持的版本进行分类。
+本地一次性提供方会在子 agent 的初始轮次内、首次请求前追加描述符。继续执行管理器会在任何提供方提供的谱系之后、初始提示词获准之前追加描述符;`Session.inheritedEventCount` 仍是 fork 谱系边界:恢复时的描述符权威读取子 agent 自身的后缀,而供列表使用的身份投影以 last-wins 折叠 `subagent/descriptor`,子 agent 自己的描述符会覆盖 fork seed 中祖先的描述符。seeded cold list 会跳过 cache hint,直到权威 observation 提供该精确 cut。该事件只进入日志:不含 `surfaceOp`,绝不进入模型历史,并由仅追加日志跨压缩保留。格式错误的当前版本描述符属于损坏;本运行时无法对不受支持的版本进行分类。
 
 ## 持久化枚举:`listChildren()`、`listDescendants()` 与其条目
 

+ 2 - 2
docs/subsystems/token-meter.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/token-meter.md
-token-meter.md: 9c4a1e4b95ffd84f65f7a73e208be245378a3301
-token-meter.zh.md: d9e2e7f773041ccb6d1e4c3cc4d81a342db0cc01
+token-meter.md: 2265f5073efbd2953e4f399ad58a026772b60006
+token-meter.zh.md: b1d9d66dc804fecace15489e61c1d080c096932d

+ 2 - 2
docs/subsystems/token-meter.md

@@ -12,7 +12,7 @@ Source: [`packages/llm/token-meter/src/types.ts`](../../packages/llm/token-meter
 /** Detached immutable request-pressure and surface snapshot at one consumed log revision. */
 interface TokenMeasurement {
   /** Number of durable events consumed; equal to the next unread event seq. */
-  readonly logRevision: number
+  readonly logRevision: SessionLogOffset
   /** Provider or heuristic anchor used for this measurement. */
   readonly baseline: TokenMeasurementBaseline
   /** Signed repricing of current surface content relative to the baseline anchor. */
@@ -34,7 +34,7 @@ Every measurement resolves the effective envelope's routed provider/model to tha
 /** One token-priced node in the current ordered session surface. */
 interface TokenSurfaceNode {
   /** Durable sequence number of the surface event. */
-  readonly seq: number
+  readonly seq: SessionSeq
   /**
    * Request-pressure tokens for the exact message projected by this node under
    * the measured route: image occurrences carry the route's declared visual

+ 2 - 2
docs/subsystems/token-meter.zh.md

@@ -12,7 +12,7 @@
 /** Detached immutable request-pressure and surface snapshot at one consumed log revision. */
 interface TokenMeasurement {
   /** Number of durable events consumed; equal to the next unread event seq. */
-  readonly logRevision: number
+  readonly logRevision: SessionLogOffset
   /** Provider or heuristic anchor used for this measurement. */
   readonly baseline: TokenMeasurementBaseline
   /** Signed repricing of current surface content relative to the baseline anchor. */
@@ -34,7 +34,7 @@ interface TokenMeasurement {
 /** One token-priced node in the current ordered session surface. */
 interface TokenSurfaceNode {
   /** Durable sequence number of the surface event. */
-  readonly seq: number
+  readonly seq: SessionSeq
   /**
    * Request-pressure tokens for the exact message projected by this node under
    * the measured route: image occurrences carry the route's declared visual

+ 9 - 8
packages/acp/acp/tests/bridge.spec.ts

@@ -254,6 +254,7 @@ describe('automation-only ACP bridge', () => {
       id: sessionId,
       createdAt: 1,
       cwd: process.cwd(),
+      isSeeded: false,
     }])
     const resume = vi.spyOn(harness.ctx.agents, 'resume')
 
@@ -363,14 +364,14 @@ describe('automation-only ACP bridge', () => {
     const active = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
     const persistence = harness.ctx.get('sessionPersistence')!
     vi.spyOn(persistence, 'list').mockResolvedValue([
-      { version: 0, id: SessionId(active.sessionId), createdAt: 9, cwd: process.cwd() },
-      { version: 0, id: SessionId('subagent'), createdAt: 8, cwd: '/missing/filter', origin: 'subagent' },
-      { version: 0, id: SessionId('fork'), createdAt: 7, cwd: '/missing/filter', parentSession: SessionId('parent') },
-      { version: 0, id: SessionId('no-cwd'), createdAt: 6 },
-      { version: 0, id: SessionId('relative'), createdAt: 5, cwd: 'relative' },
-      { version: 0, id: SessionId('other'), createdAt: 4, cwd: '/missing/other' },
-      { version: 0, id: SessionId('valid-b'), createdAt: 3, cwd: '/missing/filter' },
-      { version: 0, id: SessionId('valid-a'), createdAt: 3, cwd: '/missing/filter' },
+      { version: 0, id: SessionId(active.sessionId), createdAt: 9, cwd: process.cwd(), isSeeded: false },
+      { version: 0, id: SessionId('subagent'), createdAt: 8, cwd: '/missing/filter', isSeeded: false, origin: 'subagent' },
+      { version: 0, id: SessionId('fork'), createdAt: 7, cwd: '/missing/filter', isSeeded: true, parentSession: SessionId('parent') },
+      { version: 0, id: SessionId('no-cwd'), createdAt: 6, isSeeded: false },
+      { version: 0, id: SessionId('relative'), createdAt: 5, cwd: 'relative', isSeeded: false },
+      { version: 0, id: SessionId('other'), createdAt: 4, cwd: '/missing/other', isSeeded: false },
+      { version: 0, id: SessionId('valid-b'), createdAt: 3, cwd: '/missing/filter', isSeeded: false },
+      { version: 0, id: SessionId('valid-a'), createdAt: 3, cwd: '/missing/filter', isSeeded: false },
     ])
 
     await expect(harness.client.listSessions({ cwd: 'relative' })).rejects.toThrow(/absolute path/)

+ 4 - 4
packages/acp/acp/tests/updates.spec.ts

@@ -1,7 +1,7 @@
 import { describe, expect, it, vi } from 'vitest'
 import type { Context } from '@deepseek-ai/cordis'
 import { ToolCallId, MessageId } from '@deepseek-ai/dsh-llm'
-import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
+import { SessionSeq, type Session, type SessionEvent } from '@deepseek-ai/dsh-session'
 import { assistantUpdates, toolCallUpdate, toolResultUpdate } from '../src/updates.ts'
 
 /** Minimal committed assistant event for pure update projection tests. */
@@ -11,7 +11,7 @@ function assistantEvent(
 ): SessionEvent<'assistant/message'> {
   return {
     type: 'assistant/message',
-    seq: 0,
+    seq: SessionSeq(0),
     time: 0,
     data: {
       turn: 1,
@@ -57,13 +57,13 @@ describe('standard ACP update projection', () => {
   it('preserves malformed tool input and projects a failed result without hidden content', async () => {
     const call = toolCallUpdate({
       type: 'tool/call',
-      seq: 0,
+      seq: SessionSeq(0),
       time: 0,
       data: { turn: 1, step: 1, callId: ToolCallId('call-bad'), name: 'broken', arguments: '{' },
     })
     const result = await toolResultUpdate({ get: () => undefined } as unknown as Context, {
       type: 'tool/result',
-      seq: 0,
+      seq: SessionSeq(0),
       time: 0,
       data: {
         turn: 1,

+ 8 - 3
packages/api/session-controller/src/agent.ts

@@ -9,7 +9,8 @@ import type {
 import type {} from '@deepseek-ai/dsh-agent-default-model'
 import type {} from '@deepseek-ai/dsh-agent-presets'
 import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
-import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
+import type { Session, SessionId } from '@deepseek-ai/dsh-session'
+import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence'
 import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
 import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
 import type {} from '@deepseek-ai/dsh-typert-registry'
@@ -112,7 +113,7 @@ export async function inspectApiSession(
   ctx: Context,
   sessionId: SessionId,
   signal?: AbortSignal,
-): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
+): Promise<SessionInspection> {
   try {
     using observation = await ctx.sessionQuery.observeSession(sessionId, {
       ...(signal === undefined ? {} : { signal }),
@@ -121,7 +122,11 @@ export async function inspectApiSession(
     if (observation.header.cwd === undefined) {
       throw new ApiSessionNotFound(`session "${sessionId}" not found`)
     }
-    return { meta: observation.header, events: [...observation.events] }
+    return {
+      meta: observation.header,
+      inheritedEventCount: observation.inheritedEventCount,
+      events: [...observation.events],
+    }
   } catch (error: unknown) {
     if (error instanceof SessionQueryError
       && error.code === 'SESSION_QUERY_SESSION_NOT_FOUND') {

+ 3 - 3
packages/api/session-controller/src/client/contract/session.ts

@@ -9,7 +9,7 @@
  */
 import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
 import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
-import type { SessionId } from '@deepseek-ai/dsh-session/types'
+import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types'
 import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
 import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
 import type { PromptContentPart, QueueAction, SessionRequestId } from '../../types.ts'
@@ -113,7 +113,7 @@ export interface ISession {
    * @param title - raw title text (the host normalizes acceptance).
    * @returns the normalized accepted title and its event seq, or the business error.
    */
-  rename(title: string): Promise<RemoteResult<{ title: string; seq: number }>>
+  rename(title: string): Promise<RemoteResult<{ title: string; seq: SessionSeq }>>
   /**
    * Extend the history window backwards (older messages pagination).
    * @returns completion; failures land in snapshot.openState/loadingOlder.
@@ -127,7 +127,7 @@ export interface ISession {
    * @param seq - durable event seq the window must reach (a turn's `turn/start` seq).
    * @returns completion once covered, exhausted, superseded, or failed soft.
    */
-  loadThrough(seq: number): Promise<void>
+  loadThrough(seq: SessionSeq): Promise<void>
   /**
    * Execute one slash-command line against this session's agent — pure
    * admission semantics (the host executor durably logs the lifecycle).

+ 12 - 7
packages/api/session-controller/src/client/sessions/manager.ts

@@ -3,7 +3,7 @@
 // List data never enters zustand; React connects via subscribe/getListSnapshot.
 
 import type { SubagentAddress, SubagentCatalog } from '@deepseek-ai/dsh-subagent/client'
-import type { SessionId } from '@deepseek-ai/dsh-session/types'
+import { SessionSeq, type SessionId, type SessionSeqCursor } from '@deepseek-ai/dsh-session/types'
 import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
 import type {
   SessionControlBaseline,
@@ -26,6 +26,10 @@ import { ProjectionValueStore } from './projection-store.ts'
 import { Session } from './session.ts'
 import type { SessionRemotes } from './remotes.ts'
 
+function sessionSeqCursor(value: number): SessionSeqCursor {
+  return value === -1 ? -1 : SessionSeq(value)
+}
+
 /**
  * List arrival lifecycle, orthogonal to the pull-activity `state` axis:
  * `pending` (no successful pull yet — an empty items array means "nothing
@@ -498,7 +502,7 @@ export class SessionManager {
             if (block === undefined) continue
             const store = this.projectionStore(s.sessionId)
             const values = block.values as Record<string, unknown>
-            for (const key of Object.keys(values)) store.apply(key, values[key], block.asOfSeq)
+            for (const key of Object.keys(values)) store.apply(key, values[key], sessionSeqCursor(block.asOfSeq))
           }
         } else {
           this.listState = 'error'
@@ -590,7 +594,7 @@ export class SessionManager {
    * @returns the fork result (the child session id).
    */
   async fork(
-    opts: { sessionId: SessionId; atSeq?: number },
+    opts: { sessionId: SessionId; atSeq?: SessionSeq },
   ): Promise<RemoteResult<{ sessionId: SessionId }>> {
     const source = this.summaries.find(s => s.sessionId === opts.sessionId)
     const result = await this.remote.session.fork({
@@ -661,7 +665,7 @@ export class SessionManager {
       return
     }
     if (frame.type === 'projection') {
-      this.projectionStore(frame.sessionId).apply(frame.key, frame.value, frame.seq)
+      this.projectionStore(frame.sessionId).apply(frame.key, frame.value, SessionSeq(frame.seq))
       this.notifier.markDirty()
       return
     }
@@ -688,8 +692,9 @@ export class SessionManager {
 
     for (const [sessionId, block] of Object.entries(baseline.projections)) {
       const store = this.projectionStore(sessionId as SessionId)
-      store.truncate(block.asOfSeq)
-      store.seed(block)
+      const asOfSeq = sessionSeqCursor(block.asOfSeq)
+      store.truncate(asOfSeq)
+      store.seed({ ...block, asOfSeq })
     }
     for (const [sessionId, session] of this.sessions) {
       session.replaceControl(this.queues.get(sessionId) ?? [])
@@ -708,7 +713,7 @@ export class SessionManager {
     if (projections !== undefined) {
       const store = this.projectionStore(summary.sessionId)
       for (const [key, value] of Object.entries(projections.values)) {
-        store.apply(key, value, projections.asOfSeq)
+        store.apply(key, value, sessionSeqCursor(projections.asOfSeq))
       }
     }
     if (summary.origin === 'subagent' && summary.parentSessionId !== undefined) {

+ 5 - 4
packages/api/session-controller/src/client/sessions/projection-store.ts

@@ -9,6 +9,7 @@
  * bare observable faces feed `useProjection` (ui-renderer binds them).
  */
 import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
+import type { SessionSeqCursor } from '@deepseek-ai/dsh-session/types'
 import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
 import { Notifier } from './notifier.ts'
 
@@ -46,7 +47,7 @@ export type UseProjection = {
  */
 export interface ProjectionsBaseline {
   /** The consistent-cut seq (equals the window tail seq by construction). */
-  asOfSeq: number
+  asOfSeq: SessionSeqCursor
   /** Whole current values by key; a registered key absent here means the capability is absent. */
   values: Readonly<Record<string, unknown>>
 }
@@ -54,7 +55,7 @@ export interface ProjectionsBaseline {
 /** One key's row: the latest finished value and the seq it is consistent with. */
 interface Row {
   value: unknown
-  seq: number
+  seq: SessionSeqCursor
 }
 
 /** Per-key notification channel: the bare face plus its batching notifier. */
@@ -130,7 +131,7 @@ export class ProjectionValueStore {
    * @param value - whole value computed by the host unit.
    * @param seq - the unit's watermark at emission.
    */
-  apply(key: string, value: unknown, seq: number): void {
+  apply(key: string, value: unknown, seq: SessionSeqCursor): void {
     const row = this.rows.get(key)
     if (row !== undefined && seq <= row.seq) return // higher seq wins; replays and stale frames drop
     this.rows.set(key, { value, seq })
@@ -165,7 +166,7 @@ export class ProjectionValueStore {
    * baseline immediately afterward.
    * @param lastSeq - highest durable sequence reflected by the baseline.
    */
-  truncate(lastSeq: number): void {
+  truncate(lastSeq: SessionSeqCursor): void {
     for (const [key, row] of this.rows) {
       if (row.seq <= lastSeq) continue
       this.rows.delete(key)

+ 2 - 2
packages/api/session-controller/src/client/sessions/service.ts

@@ -16,7 +16,7 @@
  */
 import type { Context, Fiber } from '@deepseek-ai/cordis'
 import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
-import type { SessionId } from '@deepseek-ai/dsh-session/types'
+import { SessionSeq, type SessionId } from '@deepseek-ai/dsh-session/types'
 import { workspaceTitleOf } from '@deepseek-ai/dsh-util-workspace-path'
 import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
 import { SESSION_SEARCH_RESULT_LIMIT } from '../../types.ts'
@@ -438,7 +438,7 @@ export class ClientSessions implements ISessions {
       // Flooring lands inside the anchor's own turn (every turn opens with a
       // turn/start), so the host's first-turn/end-at-or-after cut still ends
       // on that turn — never clipped back to the previous one.
-      ...(opts.atSeq === undefined ? {} : { atSeq: Math.floor(opts.atSeq) }),
+      ...(opts.atSeq === undefined ? {} : { atSeq: SessionSeq(Math.floor(opts.atSeq)) }),
     })
     if (!result.ok) throw new SessionForkError(result.error, opts.sessionId)
     this.projectList()

+ 26 - 12
packages/api/session-controller/src/client/sessions/session.ts

@@ -5,7 +5,7 @@ import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
 import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
 import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client'
 import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
-import type { SessionId } from '@deepseek-ai/dsh-session/types'
+import { SessionLogOffset, SessionSeq, type SessionId } from '@deepseek-ai/dsh-session/types'
 import { SessionEventStream } from '../transport.ts'
 import type { SessionJournalChange } from '../transport.ts'
 import type {
@@ -13,6 +13,7 @@ import type {
   QueueAction,
   SessionAddress,
   SessionControlFrame,
+  SessionProjectionBaseline,
   SessionQueuedItem,
   SessionRequestId,
 } from '../../types.ts'
@@ -35,6 +36,13 @@ import type { ProjectionsBaseline } from './projection-store.ts'
 import { resolvedClientTimeZone } from '../time-zone.ts'
 import { SessionQueueMirror } from './queue-mirror.ts'
 
+function projectionsBaseline(value: SessionProjectionBaseline): ProjectionsBaseline {
+  return {
+    ...value,
+    asOfSeq: value.asOfSeq === -1 ? -1 : SessionSeq(value.asOfSeq),
+  }
+}
+
 /** Messages requested per history page. */
 export const PAGE_MESSAGES = 50
 
@@ -72,7 +80,7 @@ export interface SessionOptions {
  */
 export class Session implements SessionFace {
   // ---- Window and derived state (all private; the snapshot is the only read API) ----
-  private baseSeq = 0
+  private baseSeq = SessionLogOffset(0)
   private hasMore = false
   private openState: OpenState = 'cold'
   private openError: RemoteFailure | null = null
@@ -82,7 +90,7 @@ export class Session implements SessionFace {
   private openGeneration = 0
   private loadingOlder = false
   /** Shared low-water target of the running jump loop; null when no jump is paging. */
-  private jumpTargetSeq: number | null = null
+  private jumpTargetSeq: SessionSeq | null = null
   /** The running jump loop's completion, shared by retargeting callers. */
   private jumpPromise: Promise<void> | null = null
   /** Authoritative stream-only inbox snapshot; pending work never hits history. */
@@ -326,10 +334,12 @@ export class Session implements SessionFace {
    * @param title - raw title text (the host normalizes acceptance).
    * @returns the rename result (normalized accepted title + title event seq).
    */
-  async rename(title: string): Promise<RemoteResult<{ title: string; seq: number }>> {
+  async rename(title: string): Promise<RemoteResult<{ title: string; seq: SessionSeq }>> {
     const result = await this.remote.session.rename({ sessionId: this.sessionId, title })
-    if (result.ok) this.projections.apply('title', result.value.title, result.value.seq)
-    return result
+    if (!result.ok) return result
+    const seq = SessionSeq(result.value.seq)
+    this.projections.apply('title', result.value.title, seq)
+    return { ok: true, value: { title: result.value.title, seq } }
   }
 
   /**
@@ -377,11 +387,11 @@ export class Session implements SessionFace {
   }
 
   /** Jump loader: page backwards until the window covers seq (see ISession.loadThrough). */
-  loadThrough(seq: number): Promise<void> {
+  loadThrough(seq: SessionSeq): Promise<void> {
     if (this.openState !== 'open' || !this.hasMore || this.baseSeq <= seq) return Promise.resolve()
     if (this.jumpPromise !== null) {
       // Retarget the running loop to the lowest requested seq.
-      this.jumpTargetSeq = Math.min(this.jumpTargetSeq ?? seq, seq)
+      this.jumpTargetSeq = SessionSeq(Math.min(this.jumpTargetSeq ?? seq, seq))
       return this.jumpPromise
     }
     // A plain single-page pull owns the busy flag; the jump does not queue
@@ -434,7 +444,7 @@ export class Session implements SessionFace {
     this.openPromise = null
     this.openState = 'cold'
     this.openError = null
-    this.baseSeq = 0
+    this.baseSeq = SessionLogOffset(0)
     this.notifier.markDirty()
     await this.open()
   }
@@ -606,7 +616,11 @@ export class Session implements SessionFace {
   private acceptEventChange(change: SessionJournalChange): void {
     switch (change.type) {
       case 'replace':
-        this.installWindow(change.entries, change.hasMore, change.page.projections)
+        this.installWindow(
+          change.entries,
+          change.hasMore,
+          change.page.projections === undefined ? undefined : projectionsBaseline(change.page.projections),
+        )
         return
       case 'prepend':
         this.prependWindow(change.entries, change.hasMore)
@@ -618,7 +632,7 @@ export class Session implements SessionFace {
 
   /** Replace the complete contiguous window and apply page-owned projection metadata. */
   private installWindow(entries: readonly SessionEventLikeEntry[], hasMore: boolean, projections?: ProjectionsBaseline): void {
-    this.baseSeq = entries[0]?.event.seq ?? 0
+    this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0)
     this.hasMore = hasMore
     if (entries.some(entry => entry.event.type === 'turn/start')) this.firstPromptPendingTurn = false
     if (projections !== undefined) this.projections.seed(projections)
@@ -629,7 +643,7 @@ export class Session implements SessionFace {
 
   /** Prepend one stream-validated history page. */
   private prependWindow(entries: readonly SessionEventLikeEntry[], hasMore: boolean): void {
-    this.baseSeq = entries[0]?.event.seq ?? this.baseSeq
+    this.baseSeq = entries[0] === undefined ? this.baseSeq : SessionLogOffset(entries[0].event.seq)
     this.hasMore = hasMore
     this.eventSource.prepend(entries, hasMore)
   }

+ 12 - 7
packages/api/session-controller/src/commands.ts

@@ -10,6 +10,7 @@ import {
   ReasoningEffortId, createUserMessage, freezeMessage,
 } from '@deepseek-ai/dsh-llm'
 import type { MessageSource } from '@deepseek-ai/dsh-llm'
+import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
 import type { SessionEvent, SessionHeader, SessionId, UserMessage } from '@deepseek-ai/dsh-session'
 import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
 import { SessionTitleInvalidError } from '@deepseek-ai/dsh-session-title'
@@ -185,9 +186,11 @@ export class SessionCommandController {
    * @returns the new Session identity.
    */
   async fork(request: SessionForkRequest): Promise<SessionForkValue> {
-    if (request.atSeq !== undefined
-      && (!Number.isInteger(request.atSeq) || request.atSeq < 0)) {
-      throw new RemoteError('gateway/bad-request', 'atSeq must be a non-negative integer', {})
+    let atSeq: ReturnType<typeof SessionSeq> | undefined
+    try {
+      atSeq = request.atSeq === undefined ? undefined : SessionSeq(request.atSeq)
+    } catch {
+      throw new RemoteError('gateway/bad-request', 'atSeq must be a non-negative safe integer', {})
     }
     let observed: SessionObservation
     try {
@@ -207,7 +210,6 @@ export class SessionCommandController {
     }
     using source = observed
     const lastSeq = source.events.at(-1)?.seq ?? -1
-    const atSeq = request.atSeq
     const anchoredBoundary = atSeq === undefined
       ? undefined
       : source.events.find(event => event.type === 'turn/end' && event.seq >= atSeq)
@@ -224,8 +226,10 @@ export class SessionCommandController {
         { sessionId: request.sessionId },
       )
     }
-    let cut = boundary.seq + 1
-    while (cut < source.events.length && source.events[cut]?.type !== 'turn/start') cut++
+    let cut = SessionLogOffset(boundary.seq + 1)
+    while (cut < source.events.length && source.events[cut]?.type !== 'turn/start') {
+      cut = SessionLogOffset(cut + 1)
+    }
     let workspace: Workspace | undefined
     try {
       workspace = await this.forkWorkspace(source.header)
@@ -243,10 +247,11 @@ export class SessionCommandController {
       await this.ctx.agents.create({
         sessionId: childId,
         seed: source.events.slice(0, cut),
+        inheritedEventCount: cut,
         meta: {
           ...(source.header.cwd === undefined ? {} : { cwd: source.header.cwd }),
           parentSession: source.header.id,
-          seedLength: cut,
+          isSeeded: true,
           ...(composition.agentPreset === undefined
             ? {}
             : { agentPreset: composition.agentPreset }),

+ 70 - 28
packages/api/session-controller/src/history.ts

@@ -2,9 +2,19 @@
 
 import type { Context } from '@deepseek-ai/cordis'
 import { Deque } from '@deepseek-ai/dsh-deque'
-import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-session'
+import {
+  isAppendSurfaceEvent,
+  SessionLogOffset,
+  SessionSeq,
+} from '@deepseek-ai/dsh-session'
 import { isChunkRow, packChunkRuns, type ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
-import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
+import type {
+  SessionEvent,
+  SessionHeader,
+  SessionId,
+  SessionLogOffset as SessionLogOffsetType,
+  SessionSeqCursor,
+} from '@deepseek-ai/dsh-session'
 import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
 import type {} from '@deepseek-ai/dsh-subagent'
 import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
@@ -19,6 +29,7 @@ import type {
   SessionPageRequest,
   SessionProjectionBaseline,
   SessionProjectionValues,
+  SessionWireHeader,
   SessionWireEvent,
 } from './types.ts'
 
@@ -51,26 +62,32 @@ export class SessionHistoryController {
    */
   async page(request: SessionPageRequest, signal: AbortSignal): Promise<SessionPage> {
     validatePageRequest(request)
+    const throughSeq: SessionSeqCursor = request.throughSeq === -1
+      ? -1
+      : SessionSeq(request.throughSeq)
+    const beforeSeq = request.beforeSeq === undefined
+      ? undefined
+      : SessionLogOffset(request.beforeSeq)
     using source = await this.sourceFor(request.address, signal, false)
     signal.throwIfAborted()
     const sourceLog = source.events
-    const sourceCursor = sourceLog.at(-1)?.seq ?? -1
-    if (request.throughSeq > sourceCursor) {
+    const sourceCursor: SessionSeqCursor = sourceLog.at(-1)?.seq ?? -1
+    if (throughSeq > sourceCursor) {
       throw new RemoteError(
         'gateway/bad-request',
-        `session page through seq ${String(request.throughSeq)} is past cursor ${String(sourceCursor)}`,
+        `session page through seq ${String(throughSeq)} is past cursor ${String(sourceCursor)}`,
         {},
       )
     }
     /* v8 ignore next -- Session and persistence validation guarantee a dense zero-based event prefix. */
-    if (request.throughSeq >= 0 && sourceLog[request.throughSeq]?.seq !== request.throughSeq) {
-      throw new RemoteError('gateway/internal', `session log does not contain through seq ${String(request.throughSeq)}`, {})
+    if (throughSeq >= 0 && sourceLog[throughSeq]?.seq !== throughSeq) {
+      throw new RemoteError('gateway/internal', `session log does not contain through seq ${String(throughSeq)}`, {})
     }
     const page = paginate(
       sourceLog,
-      request.beforeSeq,
+      beforeSeq,
       request.maxMessages ?? DEFAULT_MAX_MESSAGES,
-      request.throughSeq,
+      throughSeq,
     )
     const records = pageRecords(page.events)
     return {
@@ -90,7 +107,7 @@ export class SessionHistoryController {
     const { address } = request
     const target = addressId(address)
     const buffered = new Deque<SessionEvent>()
-    let snapshotCursor: number | undefined
+    let snapshotCursor: SessionSeqCursor | undefined
     let wake: (() => void) | undefined
     const notify = (): void => {
       const resume = wake
@@ -115,7 +132,7 @@ export class SessionHistoryController {
       // opening observation, replay everything beyond that snapshot cursor.
       const suffix = session.snapshotEvents(snapshotCursor === undefined
         ? session.firstLiveSeq
-        : snapshotCursor + 1)
+        : SessionLogOffset(snapshotCursor + 1))
       for (let index = suffix.length - 1; index >= 0; index -= 1) {
         buffered.pushFront(suffix[index] as SessionEvent)
       }
@@ -132,7 +149,7 @@ export class SessionHistoryController {
       const page = paginate(events, undefined, request.maxMessages ?? DEFAULT_MAX_MESSAGES)
       yield {
         type: 'snapshot',
-        header: source.header,
+        header: wireHeader(source.header, source.inheritedEventCount),
         cursor,
         records: pageRecords(page.events),
         hasMore: page.hasMore,
@@ -149,18 +166,19 @@ export class SessionHistoryController {
           throw error
         }
       }
-      let nextSeq = cursor + 1
+      let nextOffset = SessionLogOffset(cursor + 1)
       while (!follower.closed && !signal.aborted) {
         const item = buffered.popFront()
         if (item === undefined) {
           await new Promise<void>((resolve) => { wake = resolve })
           continue
         }
-        if (item.seq < nextSeq) continue
-        if (item.seq !== nextSeq) {
-          throw new RemoteError('gateway/internal', `session event stream skipped seq ${String(nextSeq)}`, {})
+        const expectedSeq = SessionSeq(nextOffset)
+        if (item.seq < expectedSeq) continue
+        if (item.seq !== expectedSeq) {
+          throw new RemoteError('gateway/internal', `session event stream skipped seq ${String(expectedSeq)}`, {})
         }
-        nextSeq++
+        nextOffset = SessionLogOffset(nextOffset + 1)
         yield entryFor(item)
       }
     } finally {
@@ -187,7 +205,12 @@ export class SessionHistoryController {
         rejectNotFound(address)
       }
       try {
-        validateAddress(address, observation.header, observation.projections)
+        validateAddress(
+          address,
+          observation.header,
+          observation.inheritedEventCount,
+          observation.projections,
+        )
       } catch (error: unknown) {
         observation[Symbol.dispose]()
         throw error
@@ -213,11 +236,15 @@ function projectionBlock(
 }
 
 function validatePageRequest(request: SessionPageRequest): void {
-  if (!Number.isSafeInteger(request.throughSeq) || request.throughSeq < -1) {
+  if (!Number.isSafeInteger(request.throughSeq)
+    || request.throughSeq < -1
+    || Object.is(request.throughSeq, -0)) {
     throw new RemoteError('gateway/bad-request', 'throughSeq must be an integer greater than or equal to -1', {})
   }
   if (request.beforeSeq !== undefined
-    && (!Number.isSafeInteger(request.beforeSeq) || request.beforeSeq < 0)) {
+    && (!Number.isSafeInteger(request.beforeSeq)
+      || request.beforeSeq < 0
+      || Object.is(request.beforeSeq, -0))) {
     throw new RemoteError('gateway/bad-request', 'beforeSeq must be a non-negative safe integer', {})
   }
   if (request.maxMessages !== undefined
@@ -240,6 +267,7 @@ function addressId(address: SessionAddress): SessionId {
 function validateAddress(
   address: SessionAddress,
   header: SessionHeader,
+  inheritedEventCount: SessionLogOffsetType,
   projections: SessionObservation['projections'],
 ): void {
   if (address.kind === 'session') {
@@ -263,7 +291,7 @@ function validateAddress(
       reason: 'corrupt',
     })
   }
-  if (identity === undefined || identity.seq < (header.seedLength ?? 0)) {
+  if (identity === undefined || identity.seq < inheritedEventCount) {
     throw new RemoteError('subagent/catalog-diagnostic', 'subagent descriptor is unavailable', {
       parentSessionId: address.parentSessionId,
       childSessionId: address.childSessionId,
@@ -289,30 +317,44 @@ function rejectNotFound(address: SessionAddress): never {
 
 function paginate(
   events: readonly SessionEvent[],
-  beforeSeq: number | undefined,
+  beforeSeq: SessionLogOffsetType | undefined,
   maxMessages: number,
-  throughSeq = events.at(-1)?.seq ?? -1,
+  throughSeq: SessionSeqCursor = events.at(-1)?.seq ?? -1,
 ): { readonly events: SessionEvent[]; readonly hasMore: boolean } {
-  const end = Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1)
+  const end = SessionLogOffset(Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1))
   let count = 0
-  let cut = 0
+  let cut = SessionLogOffset(0)
   for (let index = end - 1; index >= 0; index--) {
     const event = events[index] as SessionEvent
     if (!MESSAGE_TYPES.has(event.type) || !isAppendSurfaceEvent(event)) continue
     count++
-    const sources = (event as { readonly sourceEventSeqs?: readonly number[] }).sourceEventSeqs
+    const sources = event.sourceEventSeqs
     let groupStart = event.seq
     if (sources !== undefined) {
-      for (const source of sources) groupStart = Math.min(groupStart, source)
+      for (const source of sources) {
+        if (source < groupStart) groupStart = source
+      }
     }
     if (count >= maxMessages) {
-      cut = groupStart
+      cut = SessionLogOffset(groupStart)
       break
     }
   }
   return { events: events.slice(cut, end), hasMore: cut > 0 }
 }
 
+/** Translate logical Session metadata to the unchanged v0 browser wire. */
+function wireHeader(
+  header: SessionHeader,
+  inheritedEventCount: SessionLogOffsetType,
+): SessionWireHeader {
+  const { isSeeded, ...wire } = header
+  return {
+    ...wire,
+    ...isSeeded ? { seedLength: inheritedEventCount } : {},
+  }
+}
+
 function entryFor(event: SessionEvent): SessionEventEntry {
   return {
     type: 'event',

+ 8 - 3
packages/api/session-controller/src/index.ts

@@ -4,7 +4,8 @@ import { Context } from '@deepseek-ai/cordis'
 import z from '@deepseek-ai/schemastery'
 import { errorChain } from '@deepseek-ai/dsh-llm'
 import { canOpenNativePath, openNativePath } from '@deepseek-ai/dsh-native-command'
-import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
+import type { SessionId } from '@deepseek-ai/dsh-session'
+import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence'
 import type { SessionObservation } from '@deepseek-ai/dsh-session-query'
 import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
 import {
@@ -191,10 +192,14 @@ export class SessionController extends TypertRemoteService {
   inspect(
     sessionId: SessionId,
     signal?: AbortSignal,
-  ): Promise<{ meta: SessionHeader; events: readonly SessionEvent[] }> {
+  ): Promise<SessionInspection> {
     const attached = this.ctx.sessions.get(sessionId)
     if (attached !== undefined) {
-      return Promise.resolve({ meta: attached.header, events: attached.snapshotEvents() })
+      return Promise.resolve({
+        meta: attached.header,
+        inheritedEventCount: attached.inheritedEventCount,
+        events: attached.snapshotEvents(),
+      })
     }
     return inspectApiSession(this.ctx, sessionId, signal)
   }

+ 4 - 1
packages/api/session-controller/src/list.ts

@@ -4,6 +4,7 @@ import { stat } from 'node:fs/promises'
 import type { Context } from '@deepseek-ai/cordis'
 import type {} from '@deepseek-ai/dsh-agent-presets'
 import type { ImageAttachmentLimits } from '@deepseek-ai/dsh-attachment'
+import { SessionLogOffset } from '@deepseek-ai/dsh-session'
 import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
 import type {} from '@deepseek-ai/dsh-session-projection'
 import type {} from '@deepseek-ai/dsh-session-projection-cache'
@@ -329,7 +330,9 @@ export class ApiSessionList {
   ): SessionProjectionHints | undefined {
     try {
       const block = session === undefined
-        ? this.ctx.get('sessionProjectionCache')?.cachedSnapshot(header)
+        ? header.isSeeded
+          ? undefined
+          : this.ctx.get('sessionProjectionCache')?.cachedSnapshot(header, SessionLogOffset(0))
         : this.ctx.sessionProjections.cachedSnapshot(session)
       return block !== undefined && Object.keys(block.values).length > 0
         ? {

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

@@ -7,7 +7,7 @@ import type { Branded } from '@deepseek-ai/dsh-brand'
 import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
 import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
 import type { ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
-import type { SessionHeader, SessionId, SurfaceOp } from '@deepseek-ai/dsh-session/types'
+import type { SessionId } from '@deepseek-ai/dsh-session/types'
 import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
 import type { JobId } from '@deepseek-ai/dsh-jobs/brand'
 import type { JsonValue } from '@deepseek-ai/dsh-util-values'
@@ -384,6 +384,25 @@ export interface SessionEventEntry {
   readonly event: SessionWireEvent
 }
 
+/** v0-compatible Session metadata carried on the browser wire. */
+export interface SessionWireHeader {
+  readonly version: number
+  readonly id: SessionId
+  readonly createdAt: number
+  readonly cwd?: string
+  readonly parentSession?: SessionId
+  /** Exact inherited prefix length; absent for an unseeded Session. */
+  readonly seedLength?: number
+  readonly origin?: 'subagent'
+  readonly delegationDepth?: number
+  readonly agentPreset?: string
+}
+
+/** Browser wire form of one Session surface operation. */
+export type SessionWireSurfaceOp =
+  | 'append'
+  | { readonly op: 'replace'; readonly start: number; readonly end: number }
+
 /** Event-shaped wire representation of one packed chunk row. */
 export type ChunkRowEvent = {
   [Kind in ChunkRow['type']]: {
@@ -411,7 +430,7 @@ export interface SessionWireEvent {
   readonly data: JsonValue
   readonly ignorable?: true
   readonly sourceEventSeqs?: number[]
-  readonly surfaceOp?: SurfaceOp
+  readonly surfaceOp?: SessionWireSurfaceOp
 }
 
 /** One message-aligned backwards-history request. */
@@ -439,7 +458,7 @@ export interface SessionPage {
 export type SessionFollowFrame =
   | {
     readonly type: 'snapshot'
-    readonly header: SessionHeader
+    readonly header: SessionWireHeader
     readonly cursor: number
     readonly records: readonly SessionHistoryRecord[]
     readonly hasMore: boolean

+ 31 - 19
packages/api/session-controller/tests/agent.host.spec.ts

@@ -5,9 +5,10 @@ import { Context } from '@deepseek-ai/cordis'
 import AgentRegistry from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-presets'
-import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
+import SessionStore, { SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
 import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
 import type { SessionObservation } from '@deepseek-ai/dsh-session-query'
+import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence'
 import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import {
@@ -47,10 +48,19 @@ function header(id: string, cwd: string | null = '/workspace'): SessionHeader {
     version: 0,
     id: SessionId(id),
     createdAt: 1,
+    isSeeded: false,
     ...(cwd === null ? {} : { cwd }),
   }
 }
 
+function unseededInspection(
+  meta: SessionHeader,
+  events: readonly SessionEvent[] = [],
+): SessionInspection {
+  if (meta.isSeeded) throw new Error('seeded inspection fixtures require an explicit inherited cut')
+  return { meta, inheritedEventCount: SessionLogOffset(0), events }
+}
+
 function providePersistence(ctx: Context, persistence: Record<string, unknown>): () => void {
   return ctx.provide('sessionPersistence', testSessionPersistence(ctx, persistence) as never)
 }
@@ -97,7 +107,7 @@ describe('ApiSession identity failures', () => {
     const listed = header('cwd-less-catalog', null)
     const disposeListed = providePersistence(ctx, {
       list: () => Promise.resolve([listed]),
-      inspect: () => Promise.resolve({ meta: listed, events: [] }),
+      inspect: () => Promise.resolve(unseededInspection(listed)),
     })
     await expect(inspectApiSession(ctx, listed.id)).rejects.toBeInstanceOf(ApiSessionNotFound)
     disposeListed()
@@ -106,7 +116,7 @@ describe('ApiSession identity failures', () => {
     const inspected = header('cwd-less-inspect', null)
     providePersistence(ctx, {
       list: () => Promise.resolve([catalog]),
-      inspect: () => Promise.resolve({ meta: inspected, events: [] }),
+      inspect: () => Promise.resolve(unseededInspection(inspected)),
     })
     await expect(inspectApiSession(ctx, catalog.id)).rejects.toBeInstanceOf(ApiSessionNotFound)
   })
@@ -117,11 +127,11 @@ describe('ApiSession identity failures', () => {
     await ctx.plugin(SessionStore)
     installSessionReadTestServices(ctx)
     const meta = header('signalled-inspection')
-    const inspect = vi.fn(() => Promise.resolve({ meta, events: [] }))
+    const inspect = vi.fn(() => Promise.resolve(unseededInspection(meta)))
     providePersistence(ctx, { inspect })
     const signal = new AbortController().signal
 
-    await expect(inspectApiSession(ctx, meta.id, signal)).resolves.toEqual({ meta, events: [] })
+    await expect(inspectApiSession(ctx, meta.id, signal)).resolves.toEqual(unseededInspection(meta))
     expect(inspect).toHaveBeenCalledWith(meta.id, signal)
   })
 })
@@ -138,6 +148,7 @@ describe('ApiSession Agent lookup and recovery', () => {
     const observed = {
       source: 'prepared',
       header: meta,
+      inheritedEventCount: SessionLogOffset(0),
       events: [],
       cursor: -1,
       projections: { asOfSeq: -1, values: {} },
@@ -177,7 +188,7 @@ describe('ApiSession Agent lookup and recovery', () => {
     const ordinaryMeta = header('ordinary-race')
     providePersistence(ordinary.ctx, {
       list: () => Promise.resolve([ordinaryMeta]),
-      inspect: () => Promise.resolve({ meta: ordinaryMeta, events: [] }),
+      inspect: () => Promise.resolve(unseededInspection(ordinaryMeta)),
     })
     const winner = agent(ordinary.ctx, ordinaryMeta)
     vi.spyOn(ordinary.ctx.agents, 'resume').mockImplementation(async () => {
@@ -190,7 +201,7 @@ describe('ApiSession Agent lookup and recovery', () => {
     const childMeta = header('child-race')
     providePersistence(child.ctx, {
       list: () => Promise.resolve([childMeta]),
-      inspect: () => Promise.resolve({ meta: childMeta, events: [] }),
+      inspect: () => Promise.resolve(unseededInspection(childMeta)),
     })
     vi.spyOn(child.ctx.agents, 'resume').mockImplementation(async () => {
       child.ctx.sessions.create(childMeta.id, {
@@ -217,7 +228,7 @@ describe('ApiSession Agent lookup and recovery', () => {
     const meta = header('failed')
     providePersistence(failed.ctx, {
       list: () => Promise.resolve([meta]),
-      inspect: () => Promise.resolve({ meta, events: [] }),
+      inspect: () => Promise.resolve(unseededInspection(meta)),
     })
     vi.spyOn(failed.ctx.agents, 'resume').mockRejectedValue(new Error('factory unavailable'))
     await expect(failed.agents.resolveAgent(meta.id)).resolves.toMatchObject({
@@ -231,6 +242,7 @@ describe('ApiSession Agent lookup and recovery', () => {
     const observed = {
       source: 'prepared',
       header: meta,
+      inheritedEventCount: SessionLogOffset(0),
       events: [],
       cursor: -1,
       retain: vi.fn(),
@@ -363,27 +375,27 @@ describe('ApiSession create or adoption', () => {
     const meta = { ...header('stored'), agentPreset: 'minimal' }
     const events = [{
       type: 'agent-preset/selected',
-      seq: 0,
+      seq: SessionSeq(0),
       time: 1,
       data: { agentPreset: 'minimal' },
     }] as SessionEvent[]
     providePersistence(ctx, {
       list: () => Promise.resolve([meta]),
-      inspect: () => Promise.resolve({ meta, events }),
+      inspect: () => Promise.resolve(unseededInspection(meta, events)),
     })
     ctx.provide('agentPresets', {
       resolve: (id?: string) => Promise.resolve({ id: id ?? 'minimal' }),
       mount: () => Promise.resolve(),
     } as never)
+    const resumedSession = ctx.sessions.prepare(meta.id, {
+      seed: structuredClone(events),
+      meta: structuredClone(meta),
+      inheritedEventCount: SessionLogOffset(0),
+      seedSource: 'persistence',
+    })
     const resumed = {
       id: meta.id,
-      session: {
-        id: meta.id,
-        header: meta,
-        snapshotEvents: () => events,
-        eventAt: (seq: number) => events[seq],
-        seq: events.length,
-      },
+      session: resumedSession,
       status: 'idle',
       ctx,
     } as unknown as Agent
@@ -401,7 +413,7 @@ describe('ApiSession create or adoption', () => {
     const childMeta = header('resume-child-race')
     providePersistence(child.ctx, {
       list: () => Promise.resolve([childMeta]),
-      inspect: () => Promise.resolve({ meta: childMeta, events: [] }),
+      inspect: () => Promise.resolve(unseededInspection(childMeta)),
     })
     child.ctx.provide('agentPresets', {
       resolve: () => {
@@ -420,7 +432,7 @@ describe('ApiSession create or adoption', () => {
     const stored = header('stored-cwd-conflict', '/stored')
     providePersistence(conflict.ctx, {
       list: () => Promise.resolve([stored]),
-      inspect: () => Promise.resolve({ meta: stored, events: [] }),
+      inspect: () => Promise.resolve(unseededInspection(stored)),
     })
     await expect(conflict.agents.ensureSession(stored.id, '/requested', true))
       .rejects.toBeInstanceOf(ApiSessionCwdConflict)

+ 32 - 8
packages/api/session-controller/tests/client-contract.client.spec.ts

@@ -1,11 +1,18 @@
 import { describe, expect, expectTypeOf, it, vi } from 'vitest'
 import type { PromptContentPart as AttachmentPromptContentPart } from '@deepseek-ai/dsh-attachment/types'
+import { SessionSeq, type SessionSeqCursor } from '@deepseek-ai/dsh-session/types'
 import {
   MutableSessionEventSource, type SessionLiveEventEntry,
 } from '../src/client/contract/events.ts'
-import type { PromptContentPart as SessionPromptContentPart } from '../src/types.ts'
+import type { ISession } from '../src/client/contract/session.ts'
+import type { ProjectionsBaseline } from '../src/client/sessions/projection-store.ts'
+import { ProjectionValueStore } from '../src/client/sessions/projection-store.ts'
+import type { PromptContentPart as SessionPromptContentPart, SessionPageRequest } from '../src/types.ts'
+import { ev, plainTurn } from './event-script.client.ts'
 
-function entry(seq: number): SessionLiveEventEntry {
+type RenameSuccess = Extract<Awaited<ReturnType<ISession['rename']>>, { readonly ok: true }>
+
+function entry(seq: SessionSeq): SessionLiveEventEntry {
   return {
     type: 'event',
     event: {
@@ -18,6 +25,23 @@ function entry(seq: number): SessionLiveEventEntry {
 }
 
 describe('Client Session contracts', () => {
+  it('requires branded Session positions at internal event fixture boundaries', () => {
+    expectTypeOf(entry).parameter(0).toEqualTypeOf<SessionSeq>()
+    expectTypeOf(ev.user).parameter(0).toEqualTypeOf<SessionSeq>()
+    expectTypeOf(ev.commandDone).parameter(4).toEqualTypeOf<SessionSeq | undefined>()
+    expectTypeOf(ev.compactSummary).parameter(2).toEqualTypeOf<SessionSeq>()
+    expectTypeOf(ev.compactCheckpoint).parameter(1).toEqualTypeOf<SessionSeq>()
+    expectTypeOf(plainTurn).parameter(0).toEqualTypeOf<SessionSeq>()
+  })
+
+  it('brands same-process Session event positions while keeping the API wire numeric', () => {
+    expectTypeOf<ISession['loadThrough']>().parameter(0).toEqualTypeOf<SessionSeq>()
+    expectTypeOf<RenameSuccess['value']['seq']>().toEqualTypeOf<SessionSeq>()
+    expectTypeOf<ProjectionValueStore['apply']>().parameter(2).toEqualTypeOf<SessionSeqCursor>()
+    expectTypeOf<ProjectionsBaseline['asOfSeq']>().toEqualTypeOf<SessionSeqCursor>()
+    expectTypeOf<SessionPageRequest['throughSeq']>().toEqualTypeOf<number>()
+  })
+
   it('keeps its catalog-visible prompt parts identical to attachment intake', () => {
     expectTypeOf<SessionPromptContentPart>().toEqualTypeOf<AttachmentPromptContentPart>()
   })
@@ -26,9 +50,9 @@ describe('Client Session contracts', () => {
     const feed = new MutableSessionEventSource()
     const listener = vi.fn()
     const dispose = feed.subscribe(listener)
-    const first = entry(1)
-    const older = entry(0)
-    const live = entry(2)
+    const first = entry(SessionSeq(1))
+    const older = entry(SessionSeq(0))
+    const live = entry(SessionSeq(2))
 
     feed.replace([first], true)
     expect(feed.getSnapshot()).toEqual({
@@ -56,13 +80,13 @@ describe('Client Session contracts', () => {
     expect(listener).toHaveBeenCalledTimes(3)
 
     dispose()
-    feed.append(entry(3))
+    feed.append(entry(SessionSeq(3)))
     expect(listener).toHaveBeenCalledTimes(3)
   })
 
   it('does not traverse the complete event window while appending', () => {
     const feed = new MutableSessionEventSource()
-    const first = entry(1)
+    const first = entry(SessionSeq(1))
     const base = [first]
     const iterate = vi.fn(Array.prototype[Symbol.iterator].bind(base))
     Object.defineProperty(base, Symbol.iterator, { value: iterate })
@@ -70,7 +94,7 @@ describe('Client Session contracts', () => {
     iterate.mockClear()
 
     const before = feed.getSnapshot()
-    const live = entry(2)
+    const live = entry(SessionSeq(2))
     feed.append(live)
     const after = feed.getSnapshot()
 

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä