Procházet zdrojové kódy

refactor(agent): back Inbox with a durable projection

_Kerman před 1 měsícem
rodič
revize
4b0af96838
100 změnil soubory, kde provedl 1210 přidání a 668 odebrání
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.i18n.yaml
  2. 4 4
      .agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md
  3. 4 4
      .agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md
  4. 2 2
      .agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.i18n.yaml
  5. 12 10
      .agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md
  6. 12 10
      .agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md
  7. 3 0
      apps/cli/composition.md
  8. 3 2
      apps/cli/tests/fixtures/dsh-badge/snapshot.ts
  9. 2 2
      docs/capability-seams.i18n.yaml
  10. 4 0
      docs/capability-seams.md
  11. 4 0
      docs/capability-seams.zh.md
  12. 2 2
      docs/config-catalog.i18n.yaml
  13. 2 2
      docs/config-catalog.md
  14. 2 2
      docs/config-catalog.zh.md
  15. 2 2
      docs/event-producer-consumer.i18n.yaml
  16. 3 3
      docs/event-producer-consumer.md
  17. 3 3
      docs/event-producer-consumer.zh.md
  18. 2 2
      docs/module-graph.i18n.yaml
  19. 162 159
      docs/module-graph.md
  20. 162 159
      docs/module-graph.zh.md
  21. 2 2
      docs/persistence-catalog.i18n.yaml
  22. 2 2
      docs/persistence-catalog.md
  23. 2 2
      docs/persistence-catalog.zh.md
  24. 2 2
      docs/subsystems/core.i18n.yaml
  25. 18 1
      docs/subsystems/core.md
  26. 18 1
      docs/subsystems/core.zh.md
  27. 2 2
      docs/subsystems/session-projection.i18n.yaml
  28. 4 4
      docs/subsystems/session-projection.md
  29. 4 4
      docs/subsystems/session-projection.zh.md
  30. 6 0
      examples/headless-agent/tests/code-mode.e2e.ts
  31. 2 2
      examples/headless-agent/tests/fixtures/e2b/e2b/bin.ts
  32. 3 0
      packages/bundle/base/cordis.patch.yml
  33. 7 2
      packages/bundle/headless/tests/headless.spec.ts
  34. 1 0
      packages/context/agent-instructions/package.json
  35. 5 10
      packages/context/agent-instructions/tests/agent-instructions.e2e.ts
  36. 22 7
      packages/context/agent-instructions/tests/agent-instructions.spec.ts
  37. 4 2
      packages/context/time-context/tests/time-context.spec.ts
  38. 4 2
      packages/context/tmux-context/tests/tmux-context.spec.ts
  39. 1 0
      packages/core/agent-loop/package.json
  40. 3 6
      packages/core/agent-loop/src/agent.ts
  41. 1 1
      packages/core/agent-loop/src/index.ts
  42. 8 0
      packages/core/agent-loop/tests/agent-initiator.spec.ts
  43. 4 0
      packages/core/agent-loop/tests/agent.spec.ts
  44. 6 0
      packages/core/agent-loop/tests/cancel.spec.ts
  45. 16 0
      packages/core/agent-loop/tests/config-session-id.spec.ts
  46. 18 0
      packages/core/agent-loop/tests/contract-regressions.spec.ts
  47. 4 0
      packages/core/agent-loop/tests/coverage-edges.spec.ts
  48. 4 0
      packages/core/agent-loop/tests/interception.spec.ts
  49. 24 0
      packages/core/agent-loop/tests/loop.spec.ts
  50. 4 0
      packages/core/agent-loop/tests/properties.spec.ts
  51. 4 0
      packages/core/agent-loop/tests/request-cache.e2e.ts
  52. 4 0
      packages/core/agent-loop/tests/request-error.spec.ts
  53. 8 0
      packages/core/agent-loop/tests/request-reconstruction.spec.ts
  54. 22 0
      packages/core/agent-loop/tests/resume.spec.ts
  55. 4 0
      packages/core/agent-loop/tests/scope-lifecycle.spec.ts
  56. 4 0
      packages/core/agent-loop/tests/settings.spec.ts
  57. 10 0
      packages/core/agent-loop/tests/tool-calls.spec.ts
  58. 4 0
      packages/core/agent-loop/tests/tool-order.spec.ts
  59. 2 2
      packages/core/agent/README.i18n.yaml
  60. 2 2
      packages/core/agent/README.md
  61. 2 2
      packages/core/agent/README.zh.md
  62. 13 0
      packages/core/agent/package.json
  63. 20 0
      packages/core/agent/src/inbox-projection.ts
  64. 95 85
      packages/core/agent/src/inbox.ts
  65. 2 2
      packages/core/agent/src/types.ts
  66. 106 32
      packages/core/agent/tests/agent.spec.ts
  67. 3 0
      packages/core/agent/tsconfig.json
  68. 2 1
      packages/e2b/e2b/tests/composition.e2e.ts
  69. 2 0
      packages/examples/agent-spine-demo/package.json
  70. 4 0
      packages/examples/agent-spine-demo/src/index.ts
  71. 15 6
      packages/extensions/tool-cordis/src/api-catalog.ts
  72. 2 2
      packages/feedback/command-feedback/tests/command-feedback.spec.ts
  73. 2 2
      packages/feedback/command-feedback/tests/loader-composition.spec.ts
  74. 2 1
      packages/fs/tool-str-replace-editor/tests/tools.spec.ts
  75. 3 3
      packages/goal/command-goal/tests/command-goal.spec.ts
  76. 50 19
      packages/goal/goal/tests/goal.spec.ts
  77. 11 8
      packages/goal/goal/tests/projection.spec.ts
  78. 25 8
      packages/goal/tool-goal/tests/tool-goal.spec.ts
  79. 5 11
      packages/host/apiproxy/src/api-proxy.ts
  80. 11 3
      packages/host/apiproxy/tests/api-proxy-jobs.spec.ts
  81. 96 4
      packages/host/apiproxy/tests/api-proxy-projections.spec.ts
  82. 4 2
      packages/host/apiproxy/tests/api-proxy-workspace.spec.ts
  83. 2 1
      packages/jobs/jobs-local/tests/jobs.spec.ts
  84. 1 0
      packages/llm/llm-retry/package.json
  85. 6 0
      packages/llm/llm-retry/tests/loader-composition.spec.ts
  86. 5 10
      packages/llm/llm-retry/tests/retry.spec.ts
  87. 4 0
      packages/plan/plan-mode/tests/integration.spec.ts
  88. 1 0
      packages/preset/agent-presets/package.json
  89. 4 0
      packages/preset/agent-presets/tests/invariant.spec.ts
  90. 8 0
      packages/preset/agent-presets/tests/mount.spec.ts
  91. 4 0
      packages/preset/agent-presets/tests/settings.spec.ts
  92. 2 2
      packages/schedule/schedule/tests/runtime.spec.ts
  93. 4 3
      packages/schedule/schedule/tests/tools.spec.ts
  94. 2 2
      packages/session/session-projection/README.i18n.yaml
  95. 6 6
      packages/session/session-projection/README.md
  96. 6 6
      packages/session/session-projection/README.zh.md
  97. 11 15
      packages/session/session-projection/src/index.ts
  98. 2 1
      packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts
  99. 2 1
      packages/shell/tool-bash-persistent/tests/tools.spec.ts
  100. 8 4
      packages/skill/tool-skill/tests/tool-skill.spec.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.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-31-claimed-pre-step-inbox-lifecycle.md
-2026-07-31-claimed-pre-step-inbox-lifecycle.md: 73768e1eee8957f8976d40812b0a31a2961f0825
-2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md: 343816abaf394b8f64924cf36b753c6b1b2e34ca
+2026-07-31-claimed-pre-step-inbox-lifecycle.md: c46c22cde3e96fb774233d9621acf1abf4ba0c33
+2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md: 51ada096a105a5c97cffd70c2d40782a71beddf7

+ 4 - 4
.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md

@@ -12,17 +12,17 @@ Occurrence-local inbox wrappers also duplicated the identity already carried by
 
 ## Decision
 
-Before every proposed step, `Inbox.claim(target)` atomically removes the complete batch: all `next-step` messages and, at a turn boundary, one `next-turn` message. At the initial boundary the loop first commits `turn/start`, so the claim and its single `agent/pre-step` decision have durable turn ownership. Claiming records normalized `agent/inbox/spliced` pure deletions with no outcome. The loop then emits `agent/inbox/claimed { message, turn }` once per claimed message and awaits the waterfall with that exclusive batch and `{ turn, step, signal }`.
+Before every proposed step, `Inbox.claim(target)` atomically removes the complete batch: all `next-step` messages and, at a turn boundary, one `next-turn` message. At the initial boundary the loop first commits `turn/start`, so the claim and its single `agent/pre-step` decision have durable turn ownership. Claiming records normalized `agent/inbox/spliced` pure deletions with no outcome, emits `agent/inbox/claimed { message, turn }` once per claimed message, and returns the exclusive batch for the loop's waterfall with `{ turn, step, signal }`.
 
 `PreStepDecision` is `{ kind: 'reject' } | { kind: 'enter'; messages: UserMessage[] }`. Reject opens no step, leaves the claimed batch removed, and closes the turn as blocked without any step events. Empty entry, cancellation, and failure before `step/start` likewise close a balanced no-step turn. Enter supplies the complete batch appended as `user/message` events after `step/start`. A listener wrapping `next()` preserves downstream changes unless it intentionally replaces them, so all message rewrites settle once in the final return value. There is no `agent/prompt-prepare`, `agent/prompt-submit`, or `agent/step` extension point.
 
-The durable inbox remains two `UserMessage[]` lists addressed by `MessageId`. `append`, `prepend`, and `splice` take a target, while `replace(messageId, newMessage)` and `remove(messageId)` locate the pending message across both lists before committing a normalized splice. Replacement may change identity and emits the old message as discarded followed by the new message as inserted. Every insertion emits `agent/inbox/inserted { message }`; an ordinary removal records `outcome: 'canceled'` and emits `agent/inbox/discarded { message }`. Claiming is the loop's internal step-boundary operation on the inbox and records pure deletions without notifications or an outcome, so the loop can publish claimed events itself. These live events add no placement, outcome, or batch fields.
+The durable inbox remains two `UserMessage[]` lists addressed by `MessageId`. `append`, `prepend`, and `splice` take a target, while `replace(messageId, newMessage)` and `remove(messageId)` locate the pending message across both lists before committing a normalized splice. Replacement may change identity and emits the old message as discarded followed by the new message as inserted. Every insertion emits `agent/inbox/inserted { message }`; an ordinary removal records `outcome: 'canceled'` and emits `agent/inbox/discarded { message }`. Claiming records pure deletions without an outcome and emits claimed events from Inbox itself. These live events add no placement, outcome, or batch fields.
 
-The two event surfaces have separate consumers. Observers following one message use `agent/inbox/inserted`, `claimed`, and `discarded`. Whole-queue consumers, including the Web queue projection and reconnect baseline, use the durable `agent/inbox/spliced` stream; UI edits and removals route through `Inbox.splice()` or another Inbox mutation method so the same projection records every change.
+The two event surfaces have separate consumers. Observers following one message use `agent/inbox/inserted`, `claimed`, and `discarded`. `InboxService` registers the standard `inbox` projection over the durable `agent/inbox/spliced` stream for whole-state consumers and live restoration; UI edits and removals route through an Inbox mutation method so the same projection records every change.
 
 Plugins that need current-step atomic rewriting return messages from `agent/pre-step`. Plugins that only need later context may mutate `agent.inbox` directly. Workspace context uses both paths: asynchronous filesystem projections stage one replaceable `next-step` item, while the next entering pre-step folds that item or a newly composed baseline into its final batch and removes the pending copy. Rejection keeps the item queued.
 
-The archived [addressable queue occurrence decision](../../archived/feature/2026-07-29-addressable-queue-operations.md) describes the superseded occurrence-wrapper design. `MessageId` now owns addressability, while the retained Host queue mirror derives its snapshots from the durable splice projection.
+The archived [addressable queue occurrence decision](../../archived/feature/2026-07-29-addressable-queue-operations.md) describes the superseded occurrence-wrapper design. `MessageId` now owns addressability, while `InboxService` registers `inbox` as the standard session projection over durable splices. The generic projection carrier serves that fold for live updates, history-tail reconnect baselines, and cold process-restart recovery without a live Agent mirror.
 
 ## Alternatives considered
 

+ 4 - 4
.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md

@@ -12,17 +12,17 @@ Status: implemented
 
 ## 决策
 
-每个拟议步骤之前,`Inbox.claim(target)` 会原子移除完整批次:全部 `next-step` 消息,以及轮次边界上的一条 `next-turn` 消息。在首次边界,循环会先提交 `turn/start`,使领取及其唯一一次 `agent/pre-step` 决策拥有持久轮次归属。领取会记录规范化、不带 outcome 的纯删除 `agent/inbox/spliced`。随后,循环针对每条已领取消息发出一次 `agent/inbox/claimed { message, turn }`,并用该独占批次与 `{ turn, step, signal }` 等待 waterfall(瀑布式事件)。
+每个拟议步骤之前,`Inbox.claim(target)` 会原子移除完整批次:全部 `next-step` 消息,以及轮次边界上的一条 `next-turn` 消息。在首次边界,循环会先提交 `turn/start`,使领取及其唯一一次 `agent/pre-step` 决策拥有持久轮次归属。领取会记录规范化、不带 outcome 的纯删除 `agent/inbox/spliced`,针对每条已领取消息发出一次 `agent/inbox/claimed { message, turn }`,并把独占批次返回给循环,由后者用 `{ turn, step, signal }` 等待 waterfall(瀑布式事件)。
 
 `PreStepDecision` 为 `{ kind: 'reject' } | { kind: 'enter'; messages: UserMessage[] }`。reject 不会打开步骤,会让已领取批次保持已删除,并将轮次关闭为 blocked,且不产生任何步骤事件。空的 enter、取消以及 `step/start` 前的失败同样会关闭一个边界平衡的无步骤轮次。enter 提供在 `step/start` 后以 `user/message` 追加的完整批次。包装 `next()` 的监听器会保留下游变更,除非有意替换,因此全部消息改写只在最终返回值中一次性结算。系统不再存在 `agent/prompt-prepare`、`agent/prompt-submit` 或 `agent/step` 扩展点。
 
-持久 inbox 仍是两份通过 `MessageId` 寻址的 `UserMessage[]` 列表。`append`、`prepend` 与 `splice` 接受 target;`replace(messageId, newMessage)` 与 `remove(messageId)` 则在提交规范化 splice 前,通过 `MessageId` 跨两份列表定位待处理消息。替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。每次插入发出 `agent/inbox/inserted { message }`;普通删除记录 `outcome: 'canceled'` 并发出 `agent/inbox/discarded { message }`。领取是循环在 inbox 上的内部步骤边界操作,记录不带通知或 outcome 的纯删除,因此循环可以自行发布 claimed 事件。这些实时事件不增加 placement、outcome 或批次字段。
+持久 inbox 仍是两份通过 `MessageId` 寻址的 `UserMessage[]` 列表。`append`、`prepend` 与 `splice` 接受 target;`replace(messageId, newMessage)` 与 `remove(messageId)` 则在提交规范化 splice 前,通过 `MessageId` 跨两份列表定位待处理消息。替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。每次插入发出 `agent/inbox/inserted { message }`;普通删除记录 `outcome: 'canceled'` 并发出 `agent/inbox/discarded { message }`。领取记录不带 outcome 的纯删除,并由 Inbox 自行发出 claimed 事件。这些实时事件不增加 placement、outcome 或批次字段。
 
-两类事件接口服务不同消费方。跟踪单条消息的观察方使用 `agent/inbox/inserted`、`claimed` 与 `discarded`。包括 Web 队列投影和重连基线在内的整体队列消费方使用持久 `agent/inbox/spliced` 流;UI 编辑与移除通过 `Inbox.splice()` 或其他 Inbox 变更方法处理,从而让同一投影记录所有变化。
+两类事件接口服务不同消费方。跟踪单条消息的观察方使用 `agent/inbox/inserted`、`claimed` 与 `discarded`。`InboxService` 在持久 `agent/inbox/spliced` 流上注册标准 `inbox` 投影,供整体状态消费方与 live 恢复使用;UI 编辑与移除通过 Inbox 变更方法处理,从而让同一投影记录所有变化。
 
 必须对当前步骤进行原子改写的插件从 `agent/pre-step` 返回消息。只需要稍后上下文的插件可以直接修改 `agent.inbox`。Workspace context 同时使用两条路径:异步文件系统投影会暂存一条可替换的 `next-step` 消息,而下一次进入步骤的 pre-step 会把该消息或新组合的基线折入最终批次,并移除仍待处理的副本。reject 会让该条目继续排队。
 
-已归档的[可寻址队列项决策](../../archived/feature/2026-07-29-addressable-queue-operations.md)描述了已被取代的单次出现包装层设计。现在由 `MessageId` 负责寻址,而保留的 Host 队列镜像根据持久 splice 投影派生快照。
+已归档的[可寻址队列项决策](../../archived/feature/2026-07-29-addressable-queue-operations.md)描述了已被取代的单次出现包装层设计。现在由 `MessageId` 负责寻址,而 `InboxService` 把 `inbox` 注册为持久 splice 上的标准会话投影。通用投影传输层会将该折叠结果用于实时更新、历史尾页的重连基线和冷进程重启恢复,无需 live Agent 镜像。
 
 ## 曾考虑的替代方案
 

+ 2 - 2
.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.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/proposed/architecture/2026-07-27-session-projection-and-command-log.md
-2026-07-27-session-projection-and-command-log.md: aa83ae4d0e96bc1681aec22da854ba258b9e19a9
-2026-07-27-session-projection-and-command-log.zh.md: 00349ac46dc300c60f725b257e0fff9bcdf3a586
+2026-07-27-session-projection-and-command-log.md: 779c7e7549fcbbae95b1a8da1ed1562c976f83ac
+2026-07-27-session-projection-and-command-log.zh.md: 8da3788cc019a2dd01a1b5e939f24b5e30b7f974

+ 12 - 10
.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md

@@ -18,9 +18,9 @@ The underlying gap is architectural: the client has no seam for a plugin to obse
 
 Four infrastructure pieces, then the domains become pure contributors.
 
-### Whole-value event rule
+### Whole-value projection rule
 
-A state-carrying log event MUST carry the complete post-change state, never a bare delta. All three domains already comply: `todo/write` is a whole-list snapshot, `plan/mode` a whole boolean, `goal/change` metadata a full `GoalSnapshot` (or a whole-value clear tombstone). The rule keeps every domain's transition trivially cheap (the framework drives it per event), keeps values self-describing on the wire, and lets any consumer treat the latest pushed value as final — out-of-order immunity by seq comparison, self-healing because a missed update is corrected by the next one.
+Every served projection value is complete and self-describing on the wire, so consumers can use the latest seq-qualified value as final. Source events need not repeat that aggregate: a domain may use whole-value events such as `todo/write` and `plan/mode`, or replayable operations such as `agent/inbox/spliced`. The unit's deterministic `apply`, versioned checkpoint state, and forward tail replay must produce the same whole value.
 
 ### Host projection registry (`dsh-session-projection`, new package)
 
@@ -50,15 +50,17 @@ declare module 'cordis' {
 ```
 
 - Values are wire JSON payloads; the same map typed end to end (host unit, wire block, React hook) via `import type` — no second DTO table, no separate client-side "views" map. How a value is *rendered* is the slot system's business, never the projection layer's.
-- **The host is the only place a projection is computed.** The framework drives every registered unit forward eagerly: each committed session event passes through `apply`; a unit uninterested in an event returns the same state reference, and an unchanged reference (`Object.is`) produces no downstream work. Clients never fold domain events — they receive finished values (baseline block + push frame below). This removes the double-implementation trap (plan's two-event fold written once, on the host) and any client-side domain code.
+- **The host is the only place a projection is computed.** The framework drives every registered unit forward: each committed session event passes through `apply`; a unit uninterested in an event returns the same state reference, and an unchanged reference (`Object.is`) produces no downstream work. Clients never fold domain events — they receive finished values (baseline block + push frame below). This removes the double-implementation trap (plan's two-event fold written once, on the host) and any client-side domain code.
 - **State is always computed, never logged.** The log holds events only; the unit's state lives in the framework's per-session watermark cache (`{state, observedSeq}` per unit) and, in a later phase, in a **persisted projection cache** on the domain-KV storage seam: rows of `(sessionId, key, ver, seq, val)` (`ver` = the unit's `stateVersion`, `seq` = the watermark, `val` = the state JSON). A row is never wrong, only possibly stale — its `seq` says exactly how stale. The one read recipe, cold and live alike: take the cached state (or `init()`), forward-apply only the events past its watermark, `view` the result. Cold listings (every session's title across all workspaces) become an index read plus, at worst, a short tail replay; the session-persistence seam grows a read-from-seq primitive for that tail in the same later phase. Write policy: throttled (count/interval, configurable) plus two mandatory points — `turn/end` and detach (the live-to-cold moment). A crash between writes costs a longer tail replay, never a wrong value.
 - A domain's input event set is its own choice: todos folds `todo/write` alone; plan folds `plan/mode` plus its own `/plan` `command/run` records (see the plan section); goal folds `goal/change` metadata; session title folds its title events (retiring the bespoke `session/title` frame and the client's title-snapshot map — the fourth hand-rolled projection this seam absorbs).
-- Registration is an effect (disposer with the fiber): an unloaded plugin's key disappears from subsequent responses and the client reads it as capability absence — HMR semantics for free. Duplicate keys throw. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected.
+- Registration is an effect (disposer with the fiber): the last unload removes the key from subsequent responses and the client reads it as capability absence. Same-key, same-version registrants share the unit. A domain that requires projection state declares the service dependency; optional contributors register under `ctx.inject(['sessionProjections'], …)`.
 - The package owns `./invariant` (every served key has a live registration).
 
-### Shipped consumer: the subagent identity unit
+### Shipped consumers
 
-The registry's two read faces already serve a shipped consumer beyond this RFC's wire plan: [subagent list identity via the projection unit](../../implemented/architecture/2026-08-06-subagent-list-identity-projection.md) registers a `subagent` unit — the durable mode/label identity folded last-wins from `subagent/descriptor` — and `SubagentRuntime.listChildren` reads it through `snapshot()` for a live child (the watermark cache, zero log reads) and `restore({}, events, 0)` over one persistence inspection for a cold one. The registry contract is unchanged: no failure channel and no new read face — a unit never throws, an absent value is the signal, and how absence renders is that consumer's decision.
+The registry's two read faces serve [subagent list identity via the projection unit](../../implemented/architecture/2026-08-06-subagent-list-identity-projection.md): a `subagent` unit folds durable mode/label identity last-wins from `subagent/descriptor`, and `SubagentRuntime.listChildren` reads it through `snapshot()` for a live child and `restore({}, events, 0)` over one persistence inspection for a cold one. An absent value remains that consumer's signal, and how absence renders is its own decision.
+
+Inbox's owning service registers its operation-folding unit. Like other units, it folds the complete session log, including inherited fork seed events, while preserving one projection state owner.
 
 ### Wire: projections block on the history tail page
 
@@ -153,7 +155,7 @@ Infrastructure first; the three in-flight PRs are left untouched and re-target a
 
 **Bounded reverse scan over the log tail (absorber declarations)** — rejected for now: nothing supports it today, it only serves domains whose every event carries the full folded state, and the persisted projection cache covers the same cold-read need uniformly (cache row + forward tail replay — the same recipe as the client's baseline + catch-up, and as paged loading). Revisit only if a real cold-read path emerges that checkpointing cannot serve.
 
-**An `invalidate`-style cell (mark dirty, refetch on domain events)** — rejected: it exists only to serve delta events. The whole-value rule makes every domain last-wins; goal's refetch loop, its coalescing, and its stale-read fence all disappear.
+**An `invalidate`-style cell (mark dirty, refetch on domain events)** — rejected: replayable operations already fold directly through `apply`. Refetching would reintroduce a second owner, a coalescing loop, and a stale-read fence instead of letting the domain's one deterministic transition produce the current whole value.
 
 **Hanging the registry off `ctx.apiProxy`** — rejected: session projections are not web-specific (TUI, ACP, headless are future consumers), and domain packages must not depend on the apiproxy package. The independent seam also deletes #587's type-only import edge from api-proxy into the plan package.
 
@@ -169,7 +171,7 @@ Infrastructure first; the three in-flight PRs are left untouched and re-target a
 
 ## Acceptance criteria
 
-- A domain plugin ships per-session log-derived state to React by writing only: the whole-value event declaration, one host unit `register`, its `SessionProjectionMap` merge, and inject callbacks — zero client-side code, no edits to the client `Session` class, `ConversationSnapshot`, api-proxy, or the wire schema files.
+- A domain plugin ships per-session log-derived state to React by writing only: its source event declaration, one host unit `register`, its `SessionProjectionMap` merge, and inject callbacks — zero client-side code, no edits to the client `Session` class, `ConversationSnapshot`, api-proxy, or the wire schema files.
 - The history tail page carries `projections` with `asOfSeq` equal to the window tail seq; loadOlder pages never carry it; a deployment without the registry serves histories without the block and clients treat every key as absent.
 - A stale baseline cannot overwrite a newer `session/projection` frame, and a replayed frame cannot regress the value store (higher-seq-wins tests on both paths).
 - A slash command executed on one tab renders a durable node in the flow on refresh, on a second tab, and after resume; unregistered commands render the generic card; the composer notice path for command outcomes is gone.
@@ -178,10 +180,10 @@ Infrastructure first; the three in-flight PRs are left untouched and re-target a
 
 ## Risks
 
-- **Whole-value rule is load-bearing**: a future domain logging bare deltas cannot serve consumers from its latest event and complicates its own unit. Mitigation: the rule is stated here and in the projection package README; the unit contract makes the full state explicit at every transition.
+- **Whole projection values are load-bearing**: every pushed or snapshotted value must stand alone at its seq. Source events may be whole values or replayable operations, but `apply`, checkpoint state, and tail replay must converge on that complete value.
 - **Synchronous unit discipline**: `init`/`apply`/`view` that await would tear the consistency cut. The registry documents and the invariant companion asserts synchronicity as far as practical; review owns the rest.
 - **Live registry churn is not pushed**: loading or unloading a domain plugin mid-session changes the key set, but no session event fires and no frame is pushed; open clients hold the stale key until the next tail pull (reconnect, gap repair, open). Accepted as a dev-only (HMR) staleness window — a registry-change push can be added to the change feed later without contract impact.
-- **Eager drive costs on busy sessions**: every committed event passes every registered unit's `apply`. Units are cheap per-event by construction (whole-value rule), non-matching events return the same reference, and the count of registered domains is small; if a hot path ever shows, per-unit event-type prefilters can be added without contract change.
+- **Eager drive costs on busy sessions**: every committed event passes every registered unit's `apply`. Units are cheap per event, non-matching events return the same reference, and the count of registered domains is small; if a hot path ever shows, per-unit event-type prefilters can be added without contract change.
 - **Projection payload growth**: every tail page carries every registered key. Payloads are whole values of UI-scale state (a todo list, a goal snapshot); if a future domain's value is large, per-key opt-out or lazy keys can be added to the request without changing the model.
 - **Command log volume**: two log-only events per slash command; bounded by human command frequency, negligible against chunk volume.
 - **Re-target churn**: three open PRs rebase onto a moved foundation. Accepted cost of infrastructure-first.

+ 12 - 10
.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md

@@ -18,9 +18,9 @@ Status: proposed
 
 先立四件基础设施,之后各领域都退化为纯贡献方。
 
-### 全量值事件规则
+### 全量投影值规则
 
-携带状态的日志事件必须携带变更后的完整状态,绝不携带裸增量。三个领域现状已然合规:`todo/write` 是整表快照,`plan/mode` 是一个完整布尔值,`goal/change` 元数据是完整的 `GoalSnapshot`(或一个全量值清除墓碑)。该规则让每个领域的状态转移始终足够廉价(框架逐事件驱动它),让值在协议层自描述,并让任何消费方都可以把最近推送的值当作最终值——靠 seq 比较获得乱序免疫,且自愈:漏掉的更新会被下一次更新纠正。
+每个对外投影值都完整且在线路上自描述,因此消费方可以把 seq 最新的值作为最终结果。源事件无需重复该聚合:领域可以使用 `todo/write`、`plan/mode` 等全量值事件,也可以使用 `agent/inbox/spliced` 等可回放操作。单元的确定性 `apply`、带版本 checkpoint state 与前向 tail replay 必须产生同一个完整值。
 
 ### host 侧投影注册表(`dsh-session-projection`,新包)
 
@@ -50,15 +50,17 @@ declare module 'cordis' {
 ```
 
 - 值就是协议层的 JSON 载荷;同一张类型表经 `import type` 端到端贯通(host 侧单元、协议块、React 钩子)——没有第二张 DTO 表,也没有独立的客户端「views」表。值如何*渲染*是 slot 体系的事,永远不归投影层管。
-- **host 是投影唯一的计算地点。** 框架主动驱动(eager drive)每个已注册的单元:每个已提交的会话事件都经过 `apply`;对某事件不感兴趣的单元返回同一个状态引用,而引用未变(`Object.is`)就不产生任何下游工作。客户端从不折叠领域事件——它们收到的是成品值(基线块 + 下文的推送帧)。这消除了双重实现陷阱(plan 的双事件折叠只在 host 写一遍),也消除了一切客户端侧领域代码。
+- **host 是投影唯一的计算地点。** 框架驱动每个已注册单元:每个已提交会话事件都会经过 `apply`;对某事件不感兴趣的单元返回同一个状态引用,而引用未变(`Object.is`)就不产生任何下游工作。客户端从不折叠领域事件——它们收到的是成品值(基线块 + 下文的推送帧)。这消除了双重实现陷阱(plan 的双事件折叠只在 host 写一遍),也消除了一切客户端侧领域代码。
 - **状态永远靠计算得出,绝不入日志。** 日志只存事件;单元的状态住在框架的按会话水位线缓存里(每单元一份 `{state, observedSeq}`),并在后续阶段进入 domain-KV 存储 seam 上的**持久投影缓存(persisted projection cache)**:形如 `(sessionId, key, ver, seq, val)` 的行(`ver` = 单元的 `stateVersion`,`seq` = 水位线,`val` = 状态 JSON)。一行永远不会是错的,至多是陈旧的——其 `seq` 精确说明陈旧到哪。冷读与活读共用同一套读取配方:取缓存状态(或 `init()`),只对超出其水位线的事件做正向 `apply`,再对结果做 `view`。冷列表(跨全部 workspace 列出每个会话的标题)变成一次索引读,至多外加一小段尾部回放;session-persistence seam 在同一后续阶段为这段尾部补一个按 seq 起读的原语。写入策略:节流(次数/间隔,可配置)外加两个强制点——`turn/end` 与 detach(由活转冷的时刻)。两次写入之间崩溃的代价是尾部回放更长一些,绝不会是值出错。
 - 领域的输入事件集由领域自己选择:todos 只折叠 `todo/write`;plan 折叠 `plan/mode` 外加它自己的 `/plan` `command/run` 记录(见 plan 一节);goal 折叠 `goal/change` 元数据;会话标题折叠其标题事件(顺带下线专设的 `session/title` 帧与客户端的标题快照表——这是该 seam 收编的第四个手工投影)。
-- 注册是 effect(disposer 随 fiber 走):插件卸载后其 key 从后续响应中消失,客户端将其读作能力缺失——HMR(热模块替换)语义随之自动成立。key 重复直接 throw。领域插件在 `ctx.inject(['sessionProjections'], …)` 下注册,因此不带注册表的 headless 组装完全不受影响。
+- 注册是 effect(disposer 随 fiber 走):最后一个注册方卸载后,key 会从后续响应中消失,客户端将其读作能力缺失。key 和版本相同的注册方共享该单元。必须使用投影状态的领域声明服务依赖;可选贡献方在 `ctx.inject(['sessionProjections'], …)` 下注册。
 - 该包拥有 `./invariant`(每个被服务的 key 都有一条存活的注册)。
 
-### 已交付的消费方:subagent 身份单元
+### 已交付的消费方
 
-注册表的两处既有读法已经服务于本 RFC 协议计划之外的一个已交付消费方:[subagent 列表经投影单元读取身份](../../implemented/architecture/2026-08-06-subagent-list-identity-projection.md)注册了 `subagent` 单元——从 `subagent/descriptor` 按 last-wins 折叠出的持久化 mode/label 身份——`SubagentRuntime.listChildren` 对 live child 经 `snapshot()` 读取(水位缓存,零日志读),对 cold child 则用一次持久化整读的结果调用 `restore({}, events, 0)` 读取。注册表约定不变:没有失败通道、没有新读法——单元永不抛错,值缺席本身就是信号,缺席如何呈现是该消费方自己的决定。
+注册表的两处读取接口服务于[通过投影单元读取 subagent 列表身份](../../implemented/architecture/2026-08-06-subagent-list-identity-projection.md):`subagent` 单元从 `subagent/descriptor` 按 last-wins 规则折叠持久化的 mode/label 身份,`SubagentRuntime.listChildren` 对 live child 通过 `snapshot()` 读取,对 cold child 则通过一次持久化检查结果调用 `restore({}, events, 0)` 读取。值缺席仍是该消费方的信号,如何呈现缺席仍由消费方自行决定。
+
+Inbox 的所属服务注册负责折叠操作的单元。它与其他单元一样折叠完整会话日志,包括 fork 继承的 seed 事件,同时维持唯一的投影状态所有者。
 
 ### 协议层:历史尾页上的 projections 块
 
@@ -153,7 +155,7 @@ host 侧命令执行器(`packages/interaction/commands`)在调用处理器
 
 **对日志尾部的有界反向扫描(absorber 声明)**——暂不采纳:今天没有任何东西支持它,它只服务于「每个事件都携带完整折叠状态」的领域,而持久投影缓存以统一方式覆盖同一冷读需求(缓存行 + 正向尾部回放——与客户端的基线 + 追赶、与分页加载是同一套配方)。只有当出现检查点机制服务不了的真实冷读路径时才重议。
 
-**`invalidate` 式 cell(标脏,遇领域事件就重取)**——不予采纳:它的存在只为伺候增量事件。全量值规则让每个领域都是 last-wins;goal 的重取循环、合并逻辑、陈旧读栅栏随之全部消失。
+**`invalidate` 式 cell(标脏,遇领域事件就重取)**——不予采纳:可回放操作已经可以直接经过 `apply` 折叠。重新抓取只会重新引入第二个所有者、合并循环与陈旧读栅栏,而不是让领域唯一的确定性 transition 产出当前完整值。
 
 **把注册表挂到 `ctx.apiProxy` 名下**——不予采纳:会话投影并非 web 专属(TUI、ACP(Agent Client Protocol)、headless 都是未来消费方),且领域包不得依赖 apiproxy 包。独立 seam 还顺带删掉了 #587 从 api-proxy 指向 plan 包的 type-only 导入边。
 
@@ -169,7 +171,7 @@ host 侧命令执行器(`packages/interaction/commands`)在调用处理器
 
 ## 验收标准
 
-- 领域插件把按会话的日志派生状态送达 React,只需写:全量值事件声明、一次 host 侧单元 `register`、自己那份 `SessionProjectionMap` merge、以及 inject 回调——零客户端侧代码,不改客户端 `Session` 类、`ConversationSnapshot`、api-proxy 或任何协议 schema 文件。
+- 领域插件把按会话的日志派生状态送达 React,只需写:源事件声明、一次 host 侧单元 `register`、自己那份 `SessionProjectionMap` merge、以及 inject 回调——零客户端侧代码,不改客户端 `Session` 类、`ConversationSnapshot`、api-proxy 或任何协议 schema 文件。
 - 历史尾页携带 `projections`,其 `asOfSeq` 等于窗口尾部 seq;loadOlder 页永不携带;未装注册表的部署照常返回不带该块的历史,客户端把所有 key 视为缺席。
 - 陈旧的基线不能覆盖更新的 `session/projection` 帧,重放的帧也不能让值仓倒退(两条路径都做 seq 高者胜测试)。
 - 在一个标签页执行的斜杠命令,刷新后、在第二个标签页上、恢复之后都在 flow 中渲染出持久节点;未注册的命令渲染通用卡片;命令结果的 composer 通知路径彻底移除。
@@ -178,10 +180,10 @@ host 侧命令执行器(`packages/interaction/commands`)在调用处理器
 
 ## 风险
 
-- **全量值规则是承重结构**:未来某个领域若只记裸增量,就无法凭其最新事件服务消费方,还会让自己的单元复杂化。缓解:该规则写明在本 Note 与投影包的 README 里;单元约定让完整状态在每次转移处都是显式的。
+- **全量投影值是承重结构**:每个推送或快照值都必须能在自己的 seq 上独立成立。源事件可以是全量值,也可以是可回放操作,但 `apply`、checkpoint state 与 tail replay 必须收敛到该完整值。
 - **单元的同步纪律**:`init`/`apply`/`view` 一旦 await 就会撕裂一致性切面。注册表在文档中申明这条纪律,invariant 配套在可行范围内断言同步性;其余由评审把关。
 - **注册表的实时增删不做推送**:会话中途加载或卸载领域插件会改变键集,但不会触发任何会话事件、也不会推任何帧;开着的客户端持有陈旧的 key 直到下次尾页拉取(重连、缺口修补、打开)。接受为仅开发期(HMR)的陈旧时窗——日后可以在变更流上加一个注册表变更推送,约定不受影响。
-- **忙碌会话上的主动驱动开销**:每个已提交事件都要过每个已注册单元的 `apply`。按构造,单元的逐事件开销很低(全量值规则),不匹配的事件返回同一引用,且已注册领域的数量很小;若真出现热点路径,可以加按单元的事件类型预过滤,约定不变。
+- **忙碌会话上的 eager 驱动开销**:每个已提交事件都要过每个已注册单元的 `apply`。单元的逐事件开销很低,不匹配的事件返回同一引用,且已注册领域的数量很小;若真出现热点路径,可以加按单元的事件类型预过滤,约定不变。
 - **投影载荷膨胀**:每个尾页携带每个已注册的 key。载荷是 UI 量级状态的全量值(一张 todo 清单、一份 goal 快照);将来若某领域的值很大,可以在请求上加逐 key 的 opt-out 或惰性 key,模型本身不用改。
 - **命令日志体量**:每条斜杠命令两个仅日志事件;上限由人敲命令的频率决定,相对分片体量可忽略不计。
 - **重新对接的返工**:三个未合入的 PR 要变基到挪动后的地基上。这是基础设施先行的既定代价。

+ 3 - 0
apps/cli/composition.md

@@ -30,6 +30,8 @@ flowchart LR
   cfg --> plugin_dsh_base_user_questions
   plugin_dsh_base_agent["agent<br/>@deepseek-ai/dsh-agent"]
   cfg --> plugin_dsh_base_agent
+  plugin_dsh_base_agent_inbox["agent-inbox<br/>@deepseek-ai/dsh-agent/inbox"]
+  cfg --> plugin_dsh_base_agent_inbox
   plugin_dsh_base_agent_default_model["agent-default-model<br/>@deepseek-ai/dsh-agent-default-model"]
   cfg --> plugin_dsh_base_agent_default_model
   plugin_dsh_base_jobs["jobs<br/>@deepseek-ai/dsh-jobs-local"]
@@ -179,6 +181,7 @@ flowchart LR
 | `session-title-llm` | `@deepseek-ai/dsh-session-title-first-prompt-llm` |
 | `user-questions` | `@deepseek-ai/dsh-user-questions` |
 | `agent` | `@deepseek-ai/dsh-agent` |
+| `agent-inbox` | `@deepseek-ai/dsh-agent/inbox` |
 | `agent-default-model` | `@deepseek-ai/dsh-agent-default-model` |
 | `jobs` | `@deepseek-ai/dsh-jobs-local` |
 | `llm-retry` | `@deepseek-ai/dsh-llm-retry` |

+ 3 - 2
apps/cli/tests/fixtures/dsh-badge/snapshot.ts

@@ -1,6 +1,6 @@
 import { fileURLToPath } from 'node:url'
 import { Context } from '@deepseek-ai/cordis'
-import { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent'
+import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
 import { CallId } from '@deepseek-ai/dsh-llm'
 import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
 import { SessionId } from '@deepseek-ai/dsh-session'
@@ -24,7 +24,7 @@ try {
     id: agentId,
     options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: undefined as never,
     status: 'idle',
     send: () => {},
     followup: () => {},
@@ -34,6 +34,7 @@ try {
     runMaintenance: job => job(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
+  Object.assign(agent, { inbox: ctx.inboxes.create(agent) })
   const decision = await agentEvents(ctx, agent).waterfall(
     'agent/pre-step',
     { messages: [], turn: 1, step: 1, signal: new AbortController().signal },

+ 2 - 2
docs/capability-seams.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/capability-seams.md
-capability-seams.md: 710c399510b6b123123e0a9586d86bfc3a96dff9
-capability-seams.zh.md: e0559d464ba1ecd2160eaba40c24bfd829e4a0d6
+capability-seams.md: 41ec9859d2dbf4c2c237059458803a5651bb9504
+capability-seams.zh.md: 100486445e55bb411f062637ec6cff636806cb97

+ 4 - 0
docs/capability-seams.md

@@ -31,6 +31,7 @@ flowchart LR
   pkg_subagent_inprocess["subagent-inprocess"]
   pkg_invariants["invariants"]
   pkg_message_feedback["message-feedback"]
+  svc_inboxes["ctx.inboxes<br/>Durable pending-input facade"]
   svc_invariants["ctx.invariants<br/>Package-owned invariant registry"]
   pkg_scope["scope"]
   pkg_typert_registry["typert-registry"]
@@ -198,6 +199,7 @@ flowchart LR
   svc_cordisInspect["ctx.cordisInspect<br/>Dynamic Cordis inspect registry"]
   pkg_acp --> svc_approval
   pkg_agent --> svc_agents
+  pkg_agent --> svc_inboxes
   pkg_agent_default_model --> svc_agentDefaultModel
   pkg_agent_loop --> svc_agentLoop
   pkg_agent_presets --> svc_agentPresets
@@ -321,6 +323,7 @@ flowchart LR
   svc_e2b --> pkg_fs_e2b
   svc_e2b --> pkg_subprocess_e2b
   svc_fs --> pkg_tool_fs
+  svc_inboxes --> pkg_agent_loop
   svc_invariants --> pkg_agent
   svc_invariants --> pkg_agent_loop
   svc_invariants --> pkg_scope
@@ -421,6 +424,7 @@ flowchart LR
 | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. |
 | `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
 | `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), `subagent-inprocess`, [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | Owns append-only Session instances and emits the durable session event feed. |
+| `ctx.inboxes` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop) | - | Registers the standard Inbox projection and creates command facades over its sole live state. |
 | `ctx.invariants` | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. |
 | `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges. |
 | `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. |

+ 4 - 0
docs/capability-seams.zh.md

@@ -33,6 +33,7 @@ flowchart LR
   pkg_subagent_inprocess["subagent-inprocess"]
   pkg_invariants["invariants"]
   pkg_message_feedback["message-feedback"]
+  svc_inboxes["ctx.inboxes<br/>Durable pending-input facade"]
   svc_invariants["ctx.invariants<br/>Package-owned invariant registry"]
   pkg_scope["scope"]
   pkg_typert_registry["typert-registry"]
@@ -200,6 +201,7 @@ flowchart LR
   svc_cordisInspect["ctx.cordisInspect<br/>Dynamic Cordis inspect registry"]
   pkg_acp --> svc_approval
   pkg_agent --> svc_agents
+  pkg_agent --> svc_inboxes
   pkg_agent_default_model --> svc_agentDefaultModel
   pkg_agent_loop --> svc_agentLoop
   pkg_agent_presets --> svc_agentPresets
@@ -323,6 +325,7 @@ flowchart LR
   svc_e2b --> pkg_fs_e2b
   svc_e2b --> pkg_subprocess_e2b
   svc_fs --> pkg_tool_fs
+  svc_inboxes --> pkg_agent_loop
   svc_invariants --> pkg_agent
   svc_invariants --> pkg_agent_loop
   svc_invariants --> pkg_scope
@@ -423,6 +426,7 @@ flowchart LR
 | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 拥有按会话隔离的回放折叠区;压力消费方共享不可变且带修订版本的测量结果。 |
 | `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 |
 | `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), `subagent-inprocess`, [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 |
+| `ctx.inboxes` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop) | - | 注册标准 Inbox 投影,并在其唯一 live 状态上创建命令 facade。 |
 | `ctx.invariants` | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | 配套子路径注册所属包本地的检查;该服务负责选择、唯一性、子 fiber,以及标明所属包的失败。 |
 | `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | 插件直接或通过 dsh-typert-loader 注册实时 zod 贡献;API 网关消费调用描述符和提供方,其他运行时消费方则在各自边界查询 schema 与反射元数据。 |
 | `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | 将生成的 Remote 描述符与实时 Cordis 服务关联,解析已注册的身份,并通过共享的 Connection RPC 载体提供一元调用。 |

+ 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: 54b966fb19de14637d8d6ba89b50d9e1e0e00762
-config-catalog.zh.md: 53c43c616a1d3fbb6b855bf5ebc69263023413ff
+config-catalog.md: 9c4d4fd1c4e198973940bf93eb5e66d9e7c47d04
+config-catalog.zh.md: 9a62d1d62703bba3ceb5a9a21752d86a3a72f2d6

+ 2 - 2
docs/config-catalog.md

@@ -136,7 +136,7 @@ Source: [`packages/context/agent-instructions/src/config.ts:18`](../packages/con
 
 ## `@deepseek-ai/dsh-agent-loop`
 
-Requires: `agents` · `sessions` · `llm` · `tools` · `systemPrompt`
+Requires: `agents` · `inboxes` · `sessions` · `llm` · `tools` · `systemPrompt`
 
 ```ts config-catalog
 /** Agent-loop plugin configuration. */
@@ -292,7 +292,7 @@ export interface GoalConfig {
 
 Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`JobsConfig`](#deepseek-aidsh-jobs-local) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillFileSystem`](../packages/skill/skill-filesystem/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/shell/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`toolJobs`](../packages/jobs/tool-jobs/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`workspaceContext`](../packages/context/agent-instructions/src/index.ts)
 
-Source: [`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts)
+Source: [`packages/examples/agent-spine-demo/src/index.ts:94`](../packages/examples/agent-spine-demo/src/index.ts)
 
 <a id="deepseek-aidsh-agent-tool-presentation"></a>
 

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

@@ -138,7 +138,7 @@ export interface Config {
 
 ## `@deepseek-ai/dsh-agent-loop`
 
-需要:`agents` · `sessions` · `llm` · `tools` · `systemPrompt`
+需要:`agents` · `inboxes` · `sessions` · `llm` · `tools` · `systemPrompt`
 
 ```ts config-catalog
 /** Agent-loop plugin configuration. */
@@ -294,7 +294,7 @@ export interface GoalConfig {
 
 依赖:[`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`JobsConfig`](#deepseek-aidsh-jobs-local) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillFileSystem`](../packages/skill/skill-filesystem/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/shell/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`toolJobs`](../packages/jobs/tool-jobs/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`workspaceContext`](../packages/context/agent-instructions/src/index.ts)
 
-来源:[`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts)
+来源:[`packages/examples/agent-spine-demo/src/index.ts:94`](../packages/examples/agent-spine-demo/src/index.ts)
 
 <a id="deepseek-aidsh-agent-tool-presentation"></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: c906ee6329fac66e7391c266213dd150dd5b8e09
-event-producer-consumer.zh.md: 77bf401b7215bd263c0d84f04e0eabe6b28b7915
+event-producer-consumer.md: e5d09f9ac963a401d267ac924e1a8aa681cc186b
+event-producer-consumer.zh.md: daf8690cff14771182cc903759c5137577607220

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

@@ -12,9 +12,9 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
 | `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
 | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
-| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
-| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
-| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
+| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
+| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
+| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
 | `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
 | `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
 | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |

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

@@ -14,9 +14,9 @@
 | `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
 | `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
 | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
-| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
-| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
-| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
+| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
+| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
+| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
 | `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
 | `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
 | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |

+ 2 - 2
docs/module-graph.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/module-graph.md
-module-graph.md: 0bd5f80534ba65e0d483bd04228cdac321e082bc
-module-graph.zh.md: 6239520b7d819eb14c2b859afa44a0c987545200
+module-graph.md: af584780da1a00ea515fb7ade824270cb660e231
+module-graph.zh.md: 0ccbf94e41d301793fb6e99b14883ce472f31674

+ 162 - 159
docs/module-graph.md

@@ -436,12 +436,6 @@ flowchart TD
   pkg_lsp --> pkg_brand
   pkg_lsp --> pkg_invariants
   pkg_lsp --> pkg_llm
-  pkg_agent --> pkg_invariants
-  pkg_agent --> pkg_llm
-  pkg_agent --> pkg_scope
-  pkg_agent --> pkg_session
-  pkg_agent --> pkg_system_prompt
-  pkg_agent --> pkg_typert_protocol
   pkg_skill_badge --> pkg_invariants
   pkg_skill_badge --> pkg_skill
   pkg_web_fetch_http --> pkg_invariants
@@ -478,6 +472,60 @@ flowchart TD
   pkg_session_projection --> pkg_session
   pkg_acp_snapshot --> pkg_invariants
   pkg_acp_snapshot --> pkg_session
+  pkg_agent --> pkg_invariants
+  pkg_agent --> pkg_llm
+  pkg_agent --> pkg_scope
+  pkg_agent --> pkg_session
+  pkg_agent --> pkg_session_projection
+  pkg_agent --> pkg_system_prompt
+  pkg_agent --> pkg_typert_protocol
+  pkg_fs --> pkg_brand
+  pkg_fs --> pkg_invariants
+  pkg_fs --> pkg_llm
+  pkg_fs --> pkg_sandbox
+  pkg_spill_local --> pkg_invariants
+  pkg_spill_local --> pkg_spill
+  pkg_message_feedback --> pkg_brand
+  pkg_message_feedback --> pkg_invariants
+  pkg_message_feedback --> pkg_llm
+  pkg_message_feedback --> pkg_session
+  pkg_message_feedback --> pkg_session_persistence
+  pkg_message_feedback --> pkg_storage_domain
+  pkg_message_feedback --> pkg_typert_protocol
+  pkg_sandbox_local --> pkg_invariants
+  pkg_sandbox_local --> pkg_llm
+  pkg_sandbox_local --> pkg_sandbox
+  pkg_sandbox_local --> pkg_session
+  pkg_session_persistence_jsonl --> pkg_invariants
+  pkg_session_persistence_jsonl --> pkg_session
+  pkg_session_persistence_jsonl --> pkg_session_persistence
+  pkg_session_persistence_sqlite --> pkg_invariants
+  pkg_session_persistence_sqlite --> pkg_session
+  pkg_session_persistence_sqlite --> pkg_session_persistence
+  pkg_session_projection_cache --> pkg_invariants
+  pkg_session_projection_cache --> pkg_session
+  pkg_session_projection_cache --> pkg_session_persistence
+  pkg_session_projection_cache --> pkg_session_projection
+  pkg_session_projection_cache --> pkg_storage_domain
+  pkg_session_stats --> pkg_invariants
+  pkg_session_stats --> pkg_llm
+  pkg_session_stats --> pkg_session
+  pkg_session_stats --> pkg_session_projection
+  pkg_session_title --> pkg_brand
+  pkg_session_title --> pkg_invariants
+  pkg_session_title --> pkg_llm
+  pkg_session_title --> pkg_session
+  pkg_session_title --> pkg_session_projection
+  pkg_shell --> pkg_invariants
+  pkg_shell --> pkg_sandbox
+  pkg_shell --> pkg_settings
+  pkg_shell --> pkg_subprocess
+  pkg_workspace --> pkg_brand
+  pkg_workspace --> pkg_invariants
+  pkg_workspace --> pkg_session
+  pkg_workspace --> pkg_session_persistence
+  pkg_workspace --> pkg_storage
+  pkg_workspace --> pkg_storage_domain
   pkg_llm_retry --> pkg_agent
   pkg_llm_retry --> pkg_brand
   pkg_llm_retry --> pkg_invariants
@@ -496,10 +544,14 @@ flowchart TD
   pkg_goal --> pkg_session
   pkg_goal --> pkg_session_projection
   pkg_goal --> pkg_typert_protocol
-  pkg_fs --> pkg_brand
-  pkg_fs --> pkg_invariants
-  pkg_fs --> pkg_llm
-  pkg_fs --> pkg_sandbox
+  pkg_fs_local --> pkg_fs
+  pkg_fs_local --> pkg_invariants
+  pkg_fs_observation_policy --> pkg_fs
+  pkg_fs_observation_policy --> pkg_invariants
+  pkg_skill_filesystem --> pkg_fs
+  pkg_skill_filesystem --> pkg_home_paths
+  pkg_skill_filesystem --> pkg_invariants
+  pkg_skill_filesystem --> pkg_skill
   pkg_web_search_deepseek --> pkg_agent
   pkg_web_search_deepseek --> pkg_credentials
   pkg_web_search_deepseek --> pkg_invariants
@@ -507,18 +559,25 @@ flowchart TD
   pkg_web_search_deepseek --> pkg_session
   pkg_web_search_deepseek --> pkg_settings
   pkg_web_search_deepseek --> pkg_web
-  pkg_spill_local --> pkg_invariants
-  pkg_spill_local --> pkg_spill
+  pkg_hook_protocol --> pkg_invariants
+  pkg_hook_protocol --> pkg_session
+  pkg_hook_protocol --> pkg_shell
+  pkg_session_query --> pkg_brand
+  pkg_session_query --> pkg_invariants
+  pkg_session_query --> pkg_llm
+  pkg_session_query --> pkg_session
+  pkg_session_query --> pkg_session_persistence
+  pkg_session_query --> pkg_session_title
   pkg_time_context --> pkg_agent
   pkg_time_context --> pkg_invariants
   pkg_time_context --> pkg_session
-  pkg_message_feedback --> pkg_brand
-  pkg_message_feedback --> pkg_invariants
-  pkg_message_feedback --> pkg_llm
-  pkg_message_feedback --> pkg_session
-  pkg_message_feedback --> pkg_session_persistence
-  pkg_message_feedback --> pkg_storage_domain
-  pkg_message_feedback --> pkg_typert_protocol
+  pkg_tmux_context --> pkg_agent
+  pkg_tmux_context --> pkg_invariants
+  pkg_tmux_context --> pkg_session
+  pkg_tmux_context --> pkg_shell
+  pkg_fs_e2b --> pkg_e2b
+  pkg_fs_e2b --> pkg_fs
+  pkg_fs_e2b --> pkg_invariants
   pkg_commands --> pkg_agent
   pkg_commands --> pkg_attachment
   pkg_commands --> pkg_brand
@@ -541,6 +600,13 @@ flowchart TD
   pkg_jobs --> pkg_brand
   pkg_jobs --> pkg_invariants
   pkg_jobs --> pkg_session
+  pkg_lsp_stdio --> pkg_brand
+  pkg_lsp_stdio --> pkg_fs
+  pkg_lsp_stdio --> pkg_invariants
+  pkg_lsp_stdio --> pkg_llm
+  pkg_lsp_stdio --> pkg_lsp
+  pkg_lsp_stdio --> pkg_subprocess
+  pkg_lsp_stdio --> pkg_timeout
   pkg_agent_presets --> pkg_agent
   pkg_agent_presets --> pkg_atomic_write
   pkg_agent_presets --> pkg_home_paths
@@ -549,42 +615,29 @@ flowchart TD
   pkg_agent_presets --> pkg_session
   pkg_agent_presets --> pkg_settings
   pkg_agent_presets --> pkg_system_prompt
-  pkg_sandbox_local --> pkg_invariants
-  pkg_sandbox_local --> pkg_llm
-  pkg_sandbox_local --> pkg_sandbox
-  pkg_sandbox_local --> pkg_session
   pkg_sandbox_policy --> pkg_agent
   pkg_sandbox_policy --> pkg_invariants
   pkg_sandbox_policy --> pkg_sandbox
   pkg_sandbox_policy --> pkg_session
   pkg_sandbox_policy --> pkg_system_prompt
-  pkg_session_persistence_jsonl --> pkg_invariants
-  pkg_session_persistence_jsonl --> pkg_session
-  pkg_session_persistence_jsonl --> pkg_session_persistence
-  pkg_session_persistence_sqlite --> pkg_invariants
-  pkg_session_persistence_sqlite --> pkg_session
-  pkg_session_persistence_sqlite --> pkg_session_persistence
-  pkg_session_projection_cache --> pkg_invariants
-  pkg_session_projection_cache --> pkg_session
-  pkg_session_projection_cache --> pkg_session_persistence
-  pkg_session_projection_cache --> pkg_session_projection
-  pkg_session_projection_cache --> pkg_storage_domain
-  pkg_session_stats --> pkg_invariants
-  pkg_session_stats --> pkg_llm
-  pkg_session_stats --> pkg_session
-  pkg_session_stats --> pkg_session_projection
   pkg_session_telemetry --> pkg_agent
   pkg_session_telemetry --> pkg_invariants
   pkg_session_telemetry --> pkg_session
-  pkg_session_title --> pkg_brand
-  pkg_session_title --> pkg_invariants
-  pkg_session_title --> pkg_llm
-  pkg_session_title --> pkg_session
-  pkg_session_title --> pkg_session_projection
-  pkg_shell --> pkg_invariants
-  pkg_shell --> pkg_sandbox
-  pkg_shell --> pkg_settings
-  pkg_shell --> pkg_subprocess
+  pkg_session_title_llm --> pkg_invariants
+  pkg_session_title_llm --> pkg_llm
+  pkg_session_title_llm --> pkg_session
+  pkg_session_title_llm --> pkg_session_title
+  pkg_session_title_llm --> pkg_timeout
+  pkg_bash_local --> pkg_invariants
+  pkg_bash_local --> pkg_settings
+  pkg_bash_local --> pkg_shell
+  pkg_bash_local --> pkg_subprocess
+  pkg_bash_local --> pkg_timeout
+  pkg_pwsh_local --> pkg_invariants
+  pkg_pwsh_local --> pkg_settings
+  pkg_pwsh_local --> pkg_shell
+  pkg_pwsh_local --> pkg_subprocess
+  pkg_pwsh_local --> pkg_timeout
   pkg_terminal --> pkg_agent
   pkg_terminal --> pkg_brand
   pkg_terminal --> pkg_invariants
@@ -597,12 +650,6 @@ flowchart TD
   pkg_workflow --> pkg_invariants
   pkg_workflow --> pkg_llm
   pkg_workflow --> pkg_session
-  pkg_workspace --> pkg_brand
-  pkg_workspace --> pkg_invariants
-  pkg_workspace --> pkg_session
-  pkg_workspace --> pkg_session_persistence
-  pkg_workspace --> pkg_storage
-  pkg_workspace --> pkg_storage_domain
   pkg_tools --> pkg_agent
   pkg_tools --> pkg_code_runtime
   pkg_tools --> pkg_invariants
@@ -620,23 +667,15 @@ flowchart TD
   pkg_goal_round_driver --> pkg_invariants
   pkg_goal_round_driver --> pkg_llm
   pkg_goal_round_driver --> pkg_session
-  pkg_fs_local --> pkg_fs
-  pkg_fs_local --> pkg_invariants
-  pkg_fs_observation_policy --> pkg_fs
-  pkg_fs_observation_policy --> pkg_invariants
-  pkg_skill_filesystem --> pkg_fs
-  pkg_skill_filesystem --> pkg_home_paths
-  pkg_skill_filesystem --> pkg_invariants
-  pkg_skill_filesystem --> pkg_skill
-  pkg_hook_protocol --> pkg_invariants
-  pkg_hook_protocol --> pkg_session
-  pkg_hook_protocol --> pkg_shell
-  pkg_session_query --> pkg_brand
-  pkg_session_query --> pkg_invariants
-  pkg_session_query --> pkg_llm
-  pkg_session_query --> pkg_session
-  pkg_session_query --> pkg_session_persistence
-  pkg_session_query --> pkg_session_title
+  pkg_fs_sandbox --> pkg_fs
+  pkg_fs_sandbox --> pkg_fs_local
+  pkg_fs_sandbox --> pkg_invariants
+  pkg_fs_sandbox --> pkg_sandbox
+  pkg_fs_sandbox --> pkg_sandbox_policy
+  pkg_session_query_sqlite --> pkg_invariants
+  pkg_session_query_sqlite --> pkg_session
+  pkg_session_query_sqlite --> pkg_session_persistence
+  pkg_session_query_sqlite --> pkg_session_query
   pkg_acp --> pkg_agent
   pkg_acp --> pkg_attachment
   pkg_acp --> pkg_invariants
@@ -653,13 +692,6 @@ flowchart TD
   pkg_compaction --> pkg_invariants
   pkg_compaction --> pkg_llm
   pkg_compaction --> pkg_session
-  pkg_tmux_context --> pkg_agent
-  pkg_tmux_context --> pkg_invariants
-  pkg_tmux_context --> pkg_session
-  pkg_tmux_context --> pkg_shell
-  pkg_fs_e2b --> pkg_e2b
-  pkg_fs_e2b --> pkg_fs
-  pkg_fs_e2b --> pkg_invariants
   pkg_command_feedback --> pkg_anonymous_user_id
   pkg_command_feedback --> pkg_commands
   pkg_command_feedback --> pkg_invariants
@@ -679,28 +711,26 @@ flowchart TD
   pkg_jobs_local --> pkg_jobs
   pkg_jobs_local --> pkg_scope
   pkg_jobs_local --> pkg_timeout
-  pkg_lsp_stdio --> pkg_brand
-  pkg_lsp_stdio --> pkg_fs
-  pkg_lsp_stdio --> pkg_invariants
-  pkg_lsp_stdio --> pkg_llm
-  pkg_lsp_stdio --> pkg_lsp
-  pkg_lsp_stdio --> pkg_subprocess
-  pkg_lsp_stdio --> pkg_timeout
-  pkg_session_title_llm --> pkg_invariants
-  pkg_session_title_llm --> pkg_llm
-  pkg_session_title_llm --> pkg_session
-  pkg_session_title_llm --> pkg_session_title
-  pkg_session_title_llm --> pkg_timeout
-  pkg_bash_local --> pkg_invariants
-  pkg_bash_local --> pkg_settings
-  pkg_bash_local --> pkg_shell
-  pkg_bash_local --> pkg_subprocess
-  pkg_bash_local --> pkg_timeout
-  pkg_pwsh_local --> pkg_invariants
-  pkg_pwsh_local --> pkg_settings
-  pkg_pwsh_local --> pkg_shell
-  pkg_pwsh_local --> pkg_subprocess
-  pkg_pwsh_local --> pkg_timeout
+  pkg_session_title_all_prompts_llm --> pkg_invariants
+  pkg_session_title_all_prompts_llm --> pkg_llm
+  pkg_session_title_all_prompts_llm --> pkg_session
+  pkg_session_title_all_prompts_llm --> pkg_session_title
+  pkg_session_title_all_prompts_llm --> pkg_session_title_llm
+  pkg_session_title_first_prompt_llm --> pkg_invariants
+  pkg_session_title_first_prompt_llm --> pkg_llm
+  pkg_session_title_first_prompt_llm --> pkg_session
+  pkg_session_title_first_prompt_llm --> pkg_session_title
+  pkg_session_title_first_prompt_llm --> pkg_session_title_llm
+  pkg_bash_sandbox --> pkg_bash_local
+  pkg_bash_sandbox --> pkg_invariants
+  pkg_bash_sandbox --> pkg_sandbox
+  pkg_bash_sandbox --> pkg_sandbox_policy
+  pkg_bash_sandbox --> pkg_shell
+  pkg_pwsh_sandbox --> pkg_invariants
+  pkg_pwsh_sandbox --> pkg_pwsh_local
+  pkg_pwsh_sandbox --> pkg_sandbox
+  pkg_pwsh_sandbox --> pkg_sandbox_policy
+  pkg_pwsh_sandbox --> pkg_shell
   pkg_terminal_bash --> pkg_agent
   pkg_terminal_bash --> pkg_invariants
   pkg_terminal_bash --> pkg_sandbox
@@ -731,11 +761,6 @@ flowchart TD
   pkg_tool_goal --> pkg_session
   pkg_tool_goal --> pkg_system_prompt
   pkg_tool_goal --> pkg_tools
-  pkg_fs_sandbox --> pkg_fs
-  pkg_fs_sandbox --> pkg_fs_local
-  pkg_fs_sandbox --> pkg_invariants
-  pkg_fs_sandbox --> pkg_sandbox
-  pkg_fs_sandbox --> pkg_sandbox_policy
   pkg_tool_fs --> pkg_attachment
   pkg_tool_fs --> pkg_fs
   pkg_tool_fs --> pkg_invariants
@@ -812,10 +837,6 @@ flowchart TD
   pkg_hooks_codex --> pkg_session
   pkg_hooks_codex --> pkg_session_persistence
   pkg_hooks_codex --> pkg_tools
-  pkg_session_query_sqlite --> pkg_invariants
-  pkg_session_query_sqlite --> pkg_session
-  pkg_session_query_sqlite --> pkg_session_persistence
-  pkg_session_query_sqlite --> pkg_session_query
   pkg_tool_session_query --> pkg_invariants
   pkg_tool_session_query --> pkg_llm
   pkg_tool_session_query --> pkg_session
@@ -897,26 +918,6 @@ flowchart TD
   pkg_session_telemetry_otel --> pkg_llm
   pkg_session_telemetry_otel --> pkg_session
   pkg_session_telemetry_otel --> pkg_session_telemetry
-  pkg_session_title_all_prompts_llm --> pkg_invariants
-  pkg_session_title_all_prompts_llm --> pkg_llm
-  pkg_session_title_all_prompts_llm --> pkg_session
-  pkg_session_title_all_prompts_llm --> pkg_session_title
-  pkg_session_title_all_prompts_llm --> pkg_session_title_llm
-  pkg_session_title_first_prompt_llm --> pkg_invariants
-  pkg_session_title_first_prompt_llm --> pkg_llm
-  pkg_session_title_first_prompt_llm --> pkg_session
-  pkg_session_title_first_prompt_llm --> pkg_session_title
-  pkg_session_title_first_prompt_llm --> pkg_session_title_llm
-  pkg_bash_sandbox --> pkg_bash_local
-  pkg_bash_sandbox --> pkg_invariants
-  pkg_bash_sandbox --> pkg_sandbox
-  pkg_bash_sandbox --> pkg_sandbox_policy
-  pkg_bash_sandbox --> pkg_shell
-  pkg_pwsh_sandbox --> pkg_invariants
-  pkg_pwsh_sandbox --> pkg_pwsh_local
-  pkg_pwsh_sandbox --> pkg_sandbox
-  pkg_pwsh_sandbox --> pkg_sandbox_policy
-  pkg_pwsh_sandbox --> pkg_shell
   pkg_shell_env --> pkg_home_paths
   pkg_shell_env --> pkg_invariants
   pkg_shell_env --> pkg_session_persistence
@@ -939,6 +940,7 @@ flowchart TD
   pkg_agent_loop_testkit --> pkg_invariants
   pkg_agent_loop_testkit --> pkg_llm
   pkg_agent_loop_testkit --> pkg_session
+  pkg_agent_loop_testkit --> pkg_session_projection
   pkg_agent_loop_testkit --> pkg_system_prompt
   pkg_agent_loop_testkit --> pkg_tools
   pkg_llm_replay --> pkg_compaction
@@ -1106,6 +1108,7 @@ flowchart TD
   pkg_agent_spine_demo --> pkg_llm_retry
   pkg_agent_spine_demo --> pkg_scope
   pkg_agent_spine_demo --> pkg_session
+  pkg_agent_spine_demo --> pkg_session_projection
   pkg_agent_spine_demo --> pkg_session_title
   pkg_agent_spine_demo --> pkg_shell_env
   pkg_agent_spine_demo --> pkg_skill
@@ -1470,7 +1473,6 @@ flowchart TD
 | [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
 | [`web`](../packages/web/web) | `web` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
-| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
 | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`skill`](../packages/skill/skill) |
 | [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`invariants`](../packages/runtime-diagnostics/invariants), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
 | [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
@@ -1483,58 +1485,64 @@ flowchart TD
 | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`acp-snapshot`](../packages/test-support/acp-snapshot) | `test-support` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
+| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
+| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
+| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
+| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
+| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
+| [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
+| [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
+| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) |
+| [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
+| [`session-title`](../packages/session/session-title) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
+| [`shell`](../packages/shell/shell) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
+| [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain) |
 | [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings) |
 | [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
-| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
+| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`fs-observation-policy`](../packages/fs/fs-observation-policy) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`skill`](../packages/skill/skill) |
 | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
-| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
+| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
+| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title) |
 | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
-| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
+| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
+| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
 | [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
+| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt) |
-| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
 | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
-| [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
-| [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
-| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) |
-| [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
 | [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
-| [`session-title`](../packages/session/session-title) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
-| [`shell`](../packages/shell/shell) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
+| [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) |
+| [`bash-local`](../packages/shell/bash-local) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain) |
 | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) |
 | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`goal-round-driver`](../packages/goal/goal-round-driver) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`fs-observation-policy`](../packages/fs/fs-observation-policy) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`skill`](../packages/skill/skill) |
-| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
-| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title) |
+| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
+| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`user-approval`](../packages/interaction/user-approval) |
 | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`compaction`](../packages/compaction/compaction) | `compaction` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
-| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
 | [`permission-presets`](../packages/interaction/permission-presets) | `interaction` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`user-approval`](../packages/interaction/user-approval) |
 | [`jobs-local`](../packages/jobs/jobs-local) | `jobs` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`scope`](../packages/core/scope), [`timeout`](../packages/util/timeout) |
-| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) |
-| [`bash-local`](../packages/shell/bash-local) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
+| [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
+| [`bash-sandbox`](../packages/shell/bash-sandbox) | `shell` | [`bash-local`](../packages/shell/bash-local), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) |
+| [`pwsh-sandbox`](../packages/shell/pwsh-sandbox) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`pwsh-local`](../packages/shell/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) |
 | [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal) |
 | [`token-meter`](../packages/llm/token-meter) | `llm` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
 | [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`agent-tool-presentation`](../packages/core/agent-tool-presentation) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) |
 | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
-| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
 | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
@@ -1545,7 +1553,6 @@ flowchart TD
 | [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
 | [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) |
 | [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
-| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
@@ -1560,14 +1567,10 @@ flowchart TD
 | [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
 | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
 | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
-| [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
-| [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
-| [`bash-sandbox`](../packages/shell/bash-sandbox) | `shell` | [`bash-local`](../packages/shell/bash-local), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) |
-| [`pwsh-sandbox`](../packages/shell/pwsh-sandbox) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`pwsh-local`](../packages/shell/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) |
 | [`shell-env`](../packages/shell/shell-env) | `shell` | [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-persistence`](../packages/session/session-persistence), [`shell`](../packages/shell/shell), [`tools`](../packages/core/tools) |
 | [`tool-bash-persistent`](../packages/shell/tool-bash-persistent) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`tool-terminal`](../packages/terminal/tool-terminal) | `terminal` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`terminal`](../packages/terminal/terminal), [`tools`](../packages/core/tools) |
-| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`llm-replay`](../packages/test-support/llm-replay) | `test-support` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
 | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
@@ -1592,7 +1595,7 @@ flowchart TD
 | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
 | [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
 | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
-| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
+| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
 | [`tool-team`](../packages/experimental/tool-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`team`](../packages/experimental/team), [`tools`](../packages/core/tools) |
 | [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
 | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |

+ 162 - 159
docs/module-graph.zh.md

@@ -438,12 +438,6 @@ flowchart TD
   pkg_lsp --> pkg_brand
   pkg_lsp --> pkg_invariants
   pkg_lsp --> pkg_llm
-  pkg_agent --> pkg_invariants
-  pkg_agent --> pkg_llm
-  pkg_agent --> pkg_scope
-  pkg_agent --> pkg_session
-  pkg_agent --> pkg_system_prompt
-  pkg_agent --> pkg_typert_protocol
   pkg_skill_badge --> pkg_invariants
   pkg_skill_badge --> pkg_skill
   pkg_web_fetch_http --> pkg_invariants
@@ -480,6 +474,60 @@ flowchart TD
   pkg_session_projection --> pkg_session
   pkg_acp_snapshot --> pkg_invariants
   pkg_acp_snapshot --> pkg_session
+  pkg_agent --> pkg_invariants
+  pkg_agent --> pkg_llm
+  pkg_agent --> pkg_scope
+  pkg_agent --> pkg_session
+  pkg_agent --> pkg_session_projection
+  pkg_agent --> pkg_system_prompt
+  pkg_agent --> pkg_typert_protocol
+  pkg_fs --> pkg_brand
+  pkg_fs --> pkg_invariants
+  pkg_fs --> pkg_llm
+  pkg_fs --> pkg_sandbox
+  pkg_spill_local --> pkg_invariants
+  pkg_spill_local --> pkg_spill
+  pkg_message_feedback --> pkg_brand
+  pkg_message_feedback --> pkg_invariants
+  pkg_message_feedback --> pkg_llm
+  pkg_message_feedback --> pkg_session
+  pkg_message_feedback --> pkg_session_persistence
+  pkg_message_feedback --> pkg_storage_domain
+  pkg_message_feedback --> pkg_typert_protocol
+  pkg_sandbox_local --> pkg_invariants
+  pkg_sandbox_local --> pkg_llm
+  pkg_sandbox_local --> pkg_sandbox
+  pkg_sandbox_local --> pkg_session
+  pkg_session_persistence_jsonl --> pkg_invariants
+  pkg_session_persistence_jsonl --> pkg_session
+  pkg_session_persistence_jsonl --> pkg_session_persistence
+  pkg_session_persistence_sqlite --> pkg_invariants
+  pkg_session_persistence_sqlite --> pkg_session
+  pkg_session_persistence_sqlite --> pkg_session_persistence
+  pkg_session_projection_cache --> pkg_invariants
+  pkg_session_projection_cache --> pkg_session
+  pkg_session_projection_cache --> pkg_session_persistence
+  pkg_session_projection_cache --> pkg_session_projection
+  pkg_session_projection_cache --> pkg_storage_domain
+  pkg_session_stats --> pkg_invariants
+  pkg_session_stats --> pkg_llm
+  pkg_session_stats --> pkg_session
+  pkg_session_stats --> pkg_session_projection
+  pkg_session_title --> pkg_brand
+  pkg_session_title --> pkg_invariants
+  pkg_session_title --> pkg_llm
+  pkg_session_title --> pkg_session
+  pkg_session_title --> pkg_session_projection
+  pkg_shell --> pkg_invariants
+  pkg_shell --> pkg_sandbox
+  pkg_shell --> pkg_settings
+  pkg_shell --> pkg_subprocess
+  pkg_workspace --> pkg_brand
+  pkg_workspace --> pkg_invariants
+  pkg_workspace --> pkg_session
+  pkg_workspace --> pkg_session_persistence
+  pkg_workspace --> pkg_storage
+  pkg_workspace --> pkg_storage_domain
   pkg_llm_retry --> pkg_agent
   pkg_llm_retry --> pkg_brand
   pkg_llm_retry --> pkg_invariants
@@ -498,10 +546,14 @@ flowchart TD
   pkg_goal --> pkg_session
   pkg_goal --> pkg_session_projection
   pkg_goal --> pkg_typert_protocol
-  pkg_fs --> pkg_brand
-  pkg_fs --> pkg_invariants
-  pkg_fs --> pkg_llm
-  pkg_fs --> pkg_sandbox
+  pkg_fs_local --> pkg_fs
+  pkg_fs_local --> pkg_invariants
+  pkg_fs_observation_policy --> pkg_fs
+  pkg_fs_observation_policy --> pkg_invariants
+  pkg_skill_filesystem --> pkg_fs
+  pkg_skill_filesystem --> pkg_home_paths
+  pkg_skill_filesystem --> pkg_invariants
+  pkg_skill_filesystem --> pkg_skill
   pkg_web_search_deepseek --> pkg_agent
   pkg_web_search_deepseek --> pkg_credentials
   pkg_web_search_deepseek --> pkg_invariants
@@ -509,18 +561,25 @@ flowchart TD
   pkg_web_search_deepseek --> pkg_session
   pkg_web_search_deepseek --> pkg_settings
   pkg_web_search_deepseek --> pkg_web
-  pkg_spill_local --> pkg_invariants
-  pkg_spill_local --> pkg_spill
+  pkg_hook_protocol --> pkg_invariants
+  pkg_hook_protocol --> pkg_session
+  pkg_hook_protocol --> pkg_shell
+  pkg_session_query --> pkg_brand
+  pkg_session_query --> pkg_invariants
+  pkg_session_query --> pkg_llm
+  pkg_session_query --> pkg_session
+  pkg_session_query --> pkg_session_persistence
+  pkg_session_query --> pkg_session_title
   pkg_time_context --> pkg_agent
   pkg_time_context --> pkg_invariants
   pkg_time_context --> pkg_session
-  pkg_message_feedback --> pkg_brand
-  pkg_message_feedback --> pkg_invariants
-  pkg_message_feedback --> pkg_llm
-  pkg_message_feedback --> pkg_session
-  pkg_message_feedback --> pkg_session_persistence
-  pkg_message_feedback --> pkg_storage_domain
-  pkg_message_feedback --> pkg_typert_protocol
+  pkg_tmux_context --> pkg_agent
+  pkg_tmux_context --> pkg_invariants
+  pkg_tmux_context --> pkg_session
+  pkg_tmux_context --> pkg_shell
+  pkg_fs_e2b --> pkg_e2b
+  pkg_fs_e2b --> pkg_fs
+  pkg_fs_e2b --> pkg_invariants
   pkg_commands --> pkg_agent
   pkg_commands --> pkg_attachment
   pkg_commands --> pkg_brand
@@ -543,6 +602,13 @@ flowchart TD
   pkg_jobs --> pkg_brand
   pkg_jobs --> pkg_invariants
   pkg_jobs --> pkg_session
+  pkg_lsp_stdio --> pkg_brand
+  pkg_lsp_stdio --> pkg_fs
+  pkg_lsp_stdio --> pkg_invariants
+  pkg_lsp_stdio --> pkg_llm
+  pkg_lsp_stdio --> pkg_lsp
+  pkg_lsp_stdio --> pkg_subprocess
+  pkg_lsp_stdio --> pkg_timeout
   pkg_agent_presets --> pkg_agent
   pkg_agent_presets --> pkg_atomic_write
   pkg_agent_presets --> pkg_home_paths
@@ -551,42 +617,29 @@ flowchart TD
   pkg_agent_presets --> pkg_session
   pkg_agent_presets --> pkg_settings
   pkg_agent_presets --> pkg_system_prompt
-  pkg_sandbox_local --> pkg_invariants
-  pkg_sandbox_local --> pkg_llm
-  pkg_sandbox_local --> pkg_sandbox
-  pkg_sandbox_local --> pkg_session
   pkg_sandbox_policy --> pkg_agent
   pkg_sandbox_policy --> pkg_invariants
   pkg_sandbox_policy --> pkg_sandbox
   pkg_sandbox_policy --> pkg_session
   pkg_sandbox_policy --> pkg_system_prompt
-  pkg_session_persistence_jsonl --> pkg_invariants
-  pkg_session_persistence_jsonl --> pkg_session
-  pkg_session_persistence_jsonl --> pkg_session_persistence
-  pkg_session_persistence_sqlite --> pkg_invariants
-  pkg_session_persistence_sqlite --> pkg_session
-  pkg_session_persistence_sqlite --> pkg_session_persistence
-  pkg_session_projection_cache --> pkg_invariants
-  pkg_session_projection_cache --> pkg_session
-  pkg_session_projection_cache --> pkg_session_persistence
-  pkg_session_projection_cache --> pkg_session_projection
-  pkg_session_projection_cache --> pkg_storage_domain
-  pkg_session_stats --> pkg_invariants
-  pkg_session_stats --> pkg_llm
-  pkg_session_stats --> pkg_session
-  pkg_session_stats --> pkg_session_projection
   pkg_session_telemetry --> pkg_agent
   pkg_session_telemetry --> pkg_invariants
   pkg_session_telemetry --> pkg_session
-  pkg_session_title --> pkg_brand
-  pkg_session_title --> pkg_invariants
-  pkg_session_title --> pkg_llm
-  pkg_session_title --> pkg_session
-  pkg_session_title --> pkg_session_projection
-  pkg_shell --> pkg_invariants
-  pkg_shell --> pkg_sandbox
-  pkg_shell --> pkg_settings
-  pkg_shell --> pkg_subprocess
+  pkg_session_title_llm --> pkg_invariants
+  pkg_session_title_llm --> pkg_llm
+  pkg_session_title_llm --> pkg_session
+  pkg_session_title_llm --> pkg_session_title
+  pkg_session_title_llm --> pkg_timeout
+  pkg_bash_local --> pkg_invariants
+  pkg_bash_local --> pkg_settings
+  pkg_bash_local --> pkg_shell
+  pkg_bash_local --> pkg_subprocess
+  pkg_bash_local --> pkg_timeout
+  pkg_pwsh_local --> pkg_invariants
+  pkg_pwsh_local --> pkg_settings
+  pkg_pwsh_local --> pkg_shell
+  pkg_pwsh_local --> pkg_subprocess
+  pkg_pwsh_local --> pkg_timeout
   pkg_terminal --> pkg_agent
   pkg_terminal --> pkg_brand
   pkg_terminal --> pkg_invariants
@@ -599,12 +652,6 @@ flowchart TD
   pkg_workflow --> pkg_invariants
   pkg_workflow --> pkg_llm
   pkg_workflow --> pkg_session
-  pkg_workspace --> pkg_brand
-  pkg_workspace --> pkg_invariants
-  pkg_workspace --> pkg_session
-  pkg_workspace --> pkg_session_persistence
-  pkg_workspace --> pkg_storage
-  pkg_workspace --> pkg_storage_domain
   pkg_tools --> pkg_agent
   pkg_tools --> pkg_code_runtime
   pkg_tools --> pkg_invariants
@@ -622,23 +669,15 @@ flowchart TD
   pkg_goal_round_driver --> pkg_invariants
   pkg_goal_round_driver --> pkg_llm
   pkg_goal_round_driver --> pkg_session
-  pkg_fs_local --> pkg_fs
-  pkg_fs_local --> pkg_invariants
-  pkg_fs_observation_policy --> pkg_fs
-  pkg_fs_observation_policy --> pkg_invariants
-  pkg_skill_filesystem --> pkg_fs
-  pkg_skill_filesystem --> pkg_home_paths
-  pkg_skill_filesystem --> pkg_invariants
-  pkg_skill_filesystem --> pkg_skill
-  pkg_hook_protocol --> pkg_invariants
-  pkg_hook_protocol --> pkg_session
-  pkg_hook_protocol --> pkg_shell
-  pkg_session_query --> pkg_brand
-  pkg_session_query --> pkg_invariants
-  pkg_session_query --> pkg_llm
-  pkg_session_query --> pkg_session
-  pkg_session_query --> pkg_session_persistence
-  pkg_session_query --> pkg_session_title
+  pkg_fs_sandbox --> pkg_fs
+  pkg_fs_sandbox --> pkg_fs_local
+  pkg_fs_sandbox --> pkg_invariants
+  pkg_fs_sandbox --> pkg_sandbox
+  pkg_fs_sandbox --> pkg_sandbox_policy
+  pkg_session_query_sqlite --> pkg_invariants
+  pkg_session_query_sqlite --> pkg_session
+  pkg_session_query_sqlite --> pkg_session_persistence
+  pkg_session_query_sqlite --> pkg_session_query
   pkg_acp --> pkg_agent
   pkg_acp --> pkg_attachment
   pkg_acp --> pkg_invariants
@@ -655,13 +694,6 @@ flowchart TD
   pkg_compaction --> pkg_invariants
   pkg_compaction --> pkg_llm
   pkg_compaction --> pkg_session
-  pkg_tmux_context --> pkg_agent
-  pkg_tmux_context --> pkg_invariants
-  pkg_tmux_context --> pkg_session
-  pkg_tmux_context --> pkg_shell
-  pkg_fs_e2b --> pkg_e2b
-  pkg_fs_e2b --> pkg_fs
-  pkg_fs_e2b --> pkg_invariants
   pkg_command_feedback --> pkg_anonymous_user_id
   pkg_command_feedback --> pkg_commands
   pkg_command_feedback --> pkg_invariants
@@ -681,28 +713,26 @@ flowchart TD
   pkg_jobs_local --> pkg_jobs
   pkg_jobs_local --> pkg_scope
   pkg_jobs_local --> pkg_timeout
-  pkg_lsp_stdio --> pkg_brand
-  pkg_lsp_stdio --> pkg_fs
-  pkg_lsp_stdio --> pkg_invariants
-  pkg_lsp_stdio --> pkg_llm
-  pkg_lsp_stdio --> pkg_lsp
-  pkg_lsp_stdio --> pkg_subprocess
-  pkg_lsp_stdio --> pkg_timeout
-  pkg_session_title_llm --> pkg_invariants
-  pkg_session_title_llm --> pkg_llm
-  pkg_session_title_llm --> pkg_session
-  pkg_session_title_llm --> pkg_session_title
-  pkg_session_title_llm --> pkg_timeout
-  pkg_bash_local --> pkg_invariants
-  pkg_bash_local --> pkg_settings
-  pkg_bash_local --> pkg_shell
-  pkg_bash_local --> pkg_subprocess
-  pkg_bash_local --> pkg_timeout
-  pkg_pwsh_local --> pkg_invariants
-  pkg_pwsh_local --> pkg_settings
-  pkg_pwsh_local --> pkg_shell
-  pkg_pwsh_local --> pkg_subprocess
-  pkg_pwsh_local --> pkg_timeout
+  pkg_session_title_all_prompts_llm --> pkg_invariants
+  pkg_session_title_all_prompts_llm --> pkg_llm
+  pkg_session_title_all_prompts_llm --> pkg_session
+  pkg_session_title_all_prompts_llm --> pkg_session_title
+  pkg_session_title_all_prompts_llm --> pkg_session_title_llm
+  pkg_session_title_first_prompt_llm --> pkg_invariants
+  pkg_session_title_first_prompt_llm --> pkg_llm
+  pkg_session_title_first_prompt_llm --> pkg_session
+  pkg_session_title_first_prompt_llm --> pkg_session_title
+  pkg_session_title_first_prompt_llm --> pkg_session_title_llm
+  pkg_bash_sandbox --> pkg_bash_local
+  pkg_bash_sandbox --> pkg_invariants
+  pkg_bash_sandbox --> pkg_sandbox
+  pkg_bash_sandbox --> pkg_sandbox_policy
+  pkg_bash_sandbox --> pkg_shell
+  pkg_pwsh_sandbox --> pkg_invariants
+  pkg_pwsh_sandbox --> pkg_pwsh_local
+  pkg_pwsh_sandbox --> pkg_sandbox
+  pkg_pwsh_sandbox --> pkg_sandbox_policy
+  pkg_pwsh_sandbox --> pkg_shell
   pkg_terminal_bash --> pkg_agent
   pkg_terminal_bash --> pkg_invariants
   pkg_terminal_bash --> pkg_sandbox
@@ -733,11 +763,6 @@ flowchart TD
   pkg_tool_goal --> pkg_session
   pkg_tool_goal --> pkg_system_prompt
   pkg_tool_goal --> pkg_tools
-  pkg_fs_sandbox --> pkg_fs
-  pkg_fs_sandbox --> pkg_fs_local
-  pkg_fs_sandbox --> pkg_invariants
-  pkg_fs_sandbox --> pkg_sandbox
-  pkg_fs_sandbox --> pkg_sandbox_policy
   pkg_tool_fs --> pkg_attachment
   pkg_tool_fs --> pkg_fs
   pkg_tool_fs --> pkg_invariants
@@ -814,10 +839,6 @@ flowchart TD
   pkg_hooks_codex --> pkg_session
   pkg_hooks_codex --> pkg_session_persistence
   pkg_hooks_codex --> pkg_tools
-  pkg_session_query_sqlite --> pkg_invariants
-  pkg_session_query_sqlite --> pkg_session
-  pkg_session_query_sqlite --> pkg_session_persistence
-  pkg_session_query_sqlite --> pkg_session_query
   pkg_tool_session_query --> pkg_invariants
   pkg_tool_session_query --> pkg_llm
   pkg_tool_session_query --> pkg_session
@@ -899,26 +920,6 @@ flowchart TD
   pkg_session_telemetry_otel --> pkg_llm
   pkg_session_telemetry_otel --> pkg_session
   pkg_session_telemetry_otel --> pkg_session_telemetry
-  pkg_session_title_all_prompts_llm --> pkg_invariants
-  pkg_session_title_all_prompts_llm --> pkg_llm
-  pkg_session_title_all_prompts_llm --> pkg_session
-  pkg_session_title_all_prompts_llm --> pkg_session_title
-  pkg_session_title_all_prompts_llm --> pkg_session_title_llm
-  pkg_session_title_first_prompt_llm --> pkg_invariants
-  pkg_session_title_first_prompt_llm --> pkg_llm
-  pkg_session_title_first_prompt_llm --> pkg_session
-  pkg_session_title_first_prompt_llm --> pkg_session_title
-  pkg_session_title_first_prompt_llm --> pkg_session_title_llm
-  pkg_bash_sandbox --> pkg_bash_local
-  pkg_bash_sandbox --> pkg_invariants
-  pkg_bash_sandbox --> pkg_sandbox
-  pkg_bash_sandbox --> pkg_sandbox_policy
-  pkg_bash_sandbox --> pkg_shell
-  pkg_pwsh_sandbox --> pkg_invariants
-  pkg_pwsh_sandbox --> pkg_pwsh_local
-  pkg_pwsh_sandbox --> pkg_sandbox
-  pkg_pwsh_sandbox --> pkg_sandbox_policy
-  pkg_pwsh_sandbox --> pkg_shell
   pkg_shell_env --> pkg_home_paths
   pkg_shell_env --> pkg_invariants
   pkg_shell_env --> pkg_session_persistence
@@ -941,6 +942,7 @@ flowchart TD
   pkg_agent_loop_testkit --> pkg_invariants
   pkg_agent_loop_testkit --> pkg_llm
   pkg_agent_loop_testkit --> pkg_session
+  pkg_agent_loop_testkit --> pkg_session_projection
   pkg_agent_loop_testkit --> pkg_system_prompt
   pkg_agent_loop_testkit --> pkg_tools
   pkg_llm_replay --> pkg_compaction
@@ -1108,6 +1110,7 @@ flowchart TD
   pkg_agent_spine_demo --> pkg_llm_retry
   pkg_agent_spine_demo --> pkg_scope
   pkg_agent_spine_demo --> pkg_session
+  pkg_agent_spine_demo --> pkg_session_projection
   pkg_agent_spine_demo --> pkg_session_title
   pkg_agent_spine_demo --> pkg_shell_env
   pkg_agent_spine_demo --> pkg_skill
@@ -1472,7 +1475,6 @@ flowchart TD
 | [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
 | [`web`](../packages/web/web) | `web` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
-| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
 | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`skill`](../packages/skill/skill) |
 | [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`invariants`](../packages/runtime-diagnostics/invariants), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
 | [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
@@ -1485,58 +1487,64 @@ flowchart TD
 | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`acp-snapshot`](../packages/test-support/acp-snapshot) | `test-support` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
+| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
+| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
+| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
+| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
+| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
+| [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
+| [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
+| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) |
+| [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
+| [`session-title`](../packages/session/session-title) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
+| [`shell`](../packages/shell/shell) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
+| [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain) |
 | [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings) |
 | [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
-| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
+| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`fs-observation-policy`](../packages/fs/fs-observation-policy) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`skill`](../packages/skill/skill) |
 | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
-| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
+| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
+| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title) |
 | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
-| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
+| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
+| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
 | [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
+| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt) |
-| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
 | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
-| [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
-| [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
-| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) |
-| [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
 | [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
-| [`session-title`](../packages/session/session-title) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
-| [`shell`](../packages/shell/shell) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
+| [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) |
+| [`bash-local`](../packages/shell/bash-local) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`workspace`](../packages/workspace/workspace) | `workspace` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain) |
 | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) |
 | [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`goal-round-driver`](../packages/goal/goal-round-driver) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`fs-observation-policy`](../packages/fs/fs-observation-policy) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`skill`](../packages/skill/skill) |
-| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
-| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title) |
+| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
+| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`user-approval`](../packages/interaction/user-approval) |
 | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`compaction`](../packages/compaction/compaction) | `compaction` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
-| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
 | [`permission-presets`](../packages/interaction/permission-presets) | `interaction` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`user-approval`](../packages/interaction/user-approval) |
 | [`jobs-local`](../packages/jobs/jobs-local) | `jobs` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`scope`](../packages/core/scope), [`timeout`](../packages/util/timeout) |
-| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) |
-| [`bash-local`](../packages/shell/bash-local) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
+| [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
+| [`bash-sandbox`](../packages/shell/bash-sandbox) | `shell` | [`bash-local`](../packages/shell/bash-local), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) |
+| [`pwsh-sandbox`](../packages/shell/pwsh-sandbox) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`pwsh-local`](../packages/shell/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) |
 | [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal) |
 | [`token-meter`](../packages/llm/token-meter) | `llm` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
 | [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`agent-tool-presentation`](../packages/core/agent-tool-presentation) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) |
 | [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
-| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
 | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
@@ -1547,7 +1555,6 @@ flowchart TD
 | [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
 | [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) |
 | [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
-| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
@@ -1562,14 +1569,10 @@ flowchart TD
 | [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
 | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
 | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
-| [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
-| [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
-| [`bash-sandbox`](../packages/shell/bash-sandbox) | `shell` | [`bash-local`](../packages/shell/bash-local), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) |
-| [`pwsh-sandbox`](../packages/shell/pwsh-sandbox) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`pwsh-local`](../packages/shell/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) |
 | [`shell-env`](../packages/shell/shell-env) | `shell` | [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-persistence`](../packages/session/session-persistence), [`shell`](../packages/shell/shell), [`tools`](../packages/core/tools) |
 | [`tool-bash-persistent`](../packages/shell/tool-bash-persistent) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`terminal`](../packages/terminal/terminal), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`tool-terminal`](../packages/terminal/tool-terminal) | `terminal` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`terminal`](../packages/terminal/terminal), [`tools`](../packages/core/tools) |
-| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`llm-replay`](../packages/test-support/llm-replay) | `test-support` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
 | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
@@ -1594,7 +1597,7 @@ flowchart TD
 | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
 | [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
 | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
-| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
+| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
 | [`tool-team`](../packages/experimental/tool-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`team`](../packages/experimental/team), [`tools`](../packages/core/tools) |
 | [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
 | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |

+ 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: b680bccf22f7840663e5268eb3feeb6b16f7fd42
-persistence-catalog.zh.md: 4b50582fa55dbdc672d8c45debe108b97a55f0e2
+persistence-catalog.md: eb035b2b2529c0b36e7d2ad55dbe7990995d9093
+persistence-catalog.zh.md: ad4c70b0e1d36beac173e19a309dadefb0a6c349

+ 2 - 2
docs/persistence-catalog.md

@@ -103,8 +103,8 @@ Sources: [`packages/core/session/src/types.ts:336`](../packages/core/session/src
 ```ts persistence-catalog
 /**
  * One normalized mutation of an agent's durable pending-message lists.
- * Live dispatch precedes projection mutation, so synchronous observers may
- * read the pre-splice inbox to recover the removed messages.
+ * The session-projection registry applies the committed event before
+ * `Session.append()` returns; Inbox live notifications follow that commit.
  */
 'agent/inbox/spliced': {
   target: InboxTarget

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

@@ -105,8 +105,8 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 ```ts persistence-catalog
 /**
  * One normalized mutation of an agent's durable pending-message lists.
- * Live dispatch precedes projection mutation, so synchronous observers may
- * read the pre-splice inbox to recover the removed messages.
+ * The session-projection registry applies the committed event before
+ * `Session.append()` returns; Inbox live notifications follow that commit.
  */
 'agent/inbox/spliced': {
   target: InboxTarget

+ 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: d14ad52e57572d5b0b110a0b16ff734e3499bdf5
-core.zh.md: 9ace28731f2532f571b66f2e7f6a3ea4a2c4e345
+core.md: 6e919d02ac1c23cf4f49e92e5ce50a8983ac449d
+core.zh.md: 853d9500cd7e51b02ec2f9595bd7f294f7b1d66f

+ 18 - 1
docs/subsystems/core.md

@@ -175,7 +175,7 @@ The inbox is the delivery vocabulary — two ordered pending-message lists the a
 type InboxTarget = 'next-turn' | 'next-step'
 ```
 
-Every pending occurrence is its `UserMessage`; `MessageId` is the sole identity. `Inbox.append`, `prepend`, `replace`, `remove`, `clear`, `splice`, and `claim` record normalized durable `agent/inbox/spliced` mutations and reject duplicate pending ids. `replace(messageId, newMessage)` and `remove(messageId)` locate the pending message across both lists; replacement may change identity and emits the old message as discarded followed by the new message as inserted. Ordinary removals and `clear()` are cancellations. `claim(target)` removes the proposed step batch — all `next-step` input plus, at a turn boundary, one `next-turn` message — through pure deletion splices without emitting discarded notifications, and the loop separately emits per-message claimed notifications. Whole-queue consumers such as UI projections reconstruct `nextTurn` and `nextStep` from the durable splices, while consumers following one message use the exact `agent/inbox/inserted`, `claimed`, and `discarded` notifications.
+Every pending occurrence is its `UserMessage`; `MessageId` is the sole identity. `Inbox.append`, `prepend`, `replace`, `remove`, `clear`, `splice`, and `claim` record normalized durable `agent/inbox/spliced` mutations and reject duplicate pending ids. `replace(messageId, newMessage)` and `remove(messageId)` locate the pending message across both lists; replacement may change identity and emits the old message as discarded followed by the new message as inserted. Ordinary removals and `clear()` are cancellations. `claim(target)` removes the proposed step batch — all `next-step` input plus, at a turn boundary, one `next-turn` message — through pure deletion splices without emitting discarded notifications, then Inbox emits per-message claimed notifications. `InboxService` registers the standard `inbox` projection; its registry cell is the sole live state and the same fold serves cold consumers. Consumers following one message use the exact `agent/inbox/inserted`, `claimed`, and `discarded` notifications.
 
 Cancellation:
 
@@ -722,6 +722,23 @@ roots(): Agent[]
 
 Source: [`packages/core/agent/src/index.ts:256`](../../packages/core/agent/src/index.ts)
 
+<a id="ctxinboxes--inboxservice"></a>
+
+### `ctx.inboxes` — `InboxService`
+
+Root Inbox service: creates live inboxes and owns their durable projection.
+
+```ts cordis-catalog
+/**
+ * Restore one live Inbox for an agent and publish its committed mutations.
+ * @param agent - agent that owns the durable session and live Inbox events.
+ * @returns the restored Inbox.
+ */
+create(agent: Agent): Inbox
+```
+
+Source: [`packages/core/agent/src/inbox.ts:25`](../../packages/core/agent/src/inbox.ts)
+
 <a id="agent-events"></a>
 
 ### `agent/*` events

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

@@ -179,7 +179,7 @@ inbox 即投递词汇——agent 以持久投影形式拥有的两条有序待
 type InboxTarget = 'next-turn' | 'next-step'
 ```
 
-每个待处理入队项就是其 `UserMessage`;`MessageId` 是唯一标识。`Inbox.append`、`prepend`、`replace`、`remove`、`clear`、`splice` 与 `claim` 会记录规范化的持久 `agent/inbox/spliced` 变更,并拒绝重复的待处理 id。`replace(messageId, newMessage)` 与 `remove(messageId)` 通过 `MessageId` 跨两份列表定位待处理消息;替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。普通删除和 `clear()` 都表示取消。`claim(target)` 通过纯删除 splice 移除拟进入步骤的批次——全部 `next-step` 输入,外加轮次边界上的一条 `next-turn` 消息——且不发出 discarded 通知;循环另行逐条发出 claimed 通知。UI 投影等整体队列消费方通过持久 splice 重建 `nextTurn` 与 `nextStep`,而跟踪单条消息的消费方使用精确的 `agent/inbox/inserted`、`claimed` 与 `discarded` 通知。
+每个待处理入队项就是其 `UserMessage`;`MessageId` 是唯一标识。`Inbox.append`、`prepend`、`replace`、`remove`、`clear`、`splice` 与 `claim` 会记录规范化的持久 `agent/inbox/spliced` 变更,并拒绝重复的待处理 id。`replace(messageId, newMessage)` 与 `remove(messageId)` 通过 `MessageId` 跨两份列表定位待处理消息;替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。普通删除和 `clear()` 都表示取消。`claim(target)` 通过纯删除 splice 移除拟进入步骤的批次——全部 `next-step` 输入,外加轮次边界上的一条 `next-turn` 消息——且不发出 discarded 通知,随后由 Inbox 逐条发出 claimed 通知。`InboxService` 注册标准 `inbox` 投影;其注册表 cell 是唯一 live 状态,同一份折叠也服务于冷消费方。跟踪单条消息的消费方使用精确的 `agent/inbox/inserted`、`claimed` 与 `discarded` 通知。
 
 取消:
 
@@ -730,6 +730,23 @@ roots(): Agent[]
 
 Source: [`packages/core/agent/src/index.ts:256`](../../packages/core/agent/src/index.ts)
 
+<a id="ctxinboxes--inboxservice"></a>
+
+### `ctx.inboxes` — `InboxService`
+
+Root Inbox service: creates live inboxes and owns their durable projection.
+
+```ts cordis-catalog
+/**
+ * Restore one live Inbox for an agent and publish its committed mutations.
+ * @param agent - agent that owns the durable session and live Inbox events.
+ * @returns the restored Inbox.
+ */
+create(agent: Agent): Inbox
+```
+
+Source: [`packages/core/agent/src/inbox.ts:25`](../../packages/core/agent/src/inbox.ts)
+
 <a id="agent-events"></a>
 
 ### `agent/*` events

+ 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: 281e7630eed6480de07a66bf7050798c83396f77
-session-projection.zh.md: bc1448e8baa0f38b3a9ffed1c6005e8905c93bcd
+session-projection.md: 59668a4026b90d869d73771bf9dadfc15bfaea5e
+session-projection.zh.md: 70960278e886c1a6f6b093f0c0a9f426f516e6a6

+ 4 - 4
docs/subsystems/session-projection.md

@@ -54,7 +54,7 @@ interface ProjectionDefinition<K extends keyof SessionProjectionMap, S> {
 }
 ```
 
-The whole-value event rule is load-bearing: a state-carrying log event carries the complete post-change state, never a bare delta — it keeps every transition trivially cheap and every served value self-describing (last-wins for consumers).
+Every served projection value is a complete read model. A source event may carry a whole value or a domain-owned operation; the unit's deterministic `apply` owns replay, and checkpoint plus forward tail replay reconstructs the same state.
 
 ## The snapshot and the change feed
 
@@ -90,7 +90,7 @@ type ProjectionChangeListener = (
 
 ## The registry: `ctx.sessionProjections`
 
-`SessionProjectionRegistry` ([signatures](#ctxsessionprojections--sessionprojectionregistry)) owns the drive: one `session/event` subscription, eager `apply` over every registered unit, and per-session per-unit watermark cells. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect whose disposer rides the calling fiber: an unloaded domain plugin's key (with its cached cells) disappears from subsequent drives and snapshots, and clients read that as capability absence; duplicate keys throw. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected.
+`SessionProjectionRegistry` ([signatures](#ctxsessionprojections--sessionprojectionregistry)) owns the drive through one `session/event` subscription and per-session per-unit watermark cells. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect whose disposer rides the calling fiber: an unloaded domain plugin's key (with its cached cells) disappears from subsequent drives and snapshots, and clients read that as capability absence; duplicate keys throw. Domain plugins declare `sessionProjections` as a dependency so assemblies without the registry stay unaffected.
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 
@@ -154,7 +154,7 @@ Source: [`packages/session/session-projection-cache/src/index.ts:71`](../../pack
 
 ### `ctx.sessionProjections` — `SessionProjectionRegistry`
 
-`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive), and a changed state reference notifies the change feed with the schema-validated view. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin's key disappears from snapshots and clients read it as capability absence. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads.
+`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive), and a changed state reference notifies the change feed with the schema-validated view. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the full in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin's key disappears from snapshots and clients read it as capability absence. A domain that requires this capability declares a Cordis service dependency; an optional contributor may register under `ctx.inject(['sessionProjections'], …)`. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads.
 
 ```ts cordis-catalog
 /**
@@ -258,5 +258,5 @@ restore(checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseS
 
 Types: [Session](session.md) · [SessionEvent](session.md)
 
-Source: [`packages/session/session-projection/src/index.ts:171`](../../packages/session/session-projection/src/index.ts)
+Source: [`packages/session/session-projection/src/index.ts:167`](../../packages/session/session-projection/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 4 - 4
docs/subsystems/session-projection.zh.md

@@ -54,7 +54,7 @@ interface ProjectionDefinition<K extends keyof SessionProjectionMap, S> {
 }
 ```
 
-全量值事件规则是承重结构:携带状态的日志事件携带的是变更后的完整状态,绝不是裸增量——这让每次状态转移始终足够廉价,也让每个被供给的值自描述(对消费方即 last-wins)。
+每个对外投影值都是完整读模型。源事件可以携带完整值,也可以携带领域拥有的操作;单元的确定性 `apply` 负责回放,checkpoint 加前向 tail replay 会重建出同一状态。
 
 ## 快照与变更流
 
@@ -90,7 +90,7 @@ type ProjectionChangeListener = (
 
 ## 注册表:`ctx.sessionProjections`
 
-`SessionProjectionRegistry`([签名](#ctxsessionprojections--sessionprojectionregistry))拥有驱动权:一份 `session/event` 订阅、对每个已注册单元即时调用 `apply`,以及每会话每单元的水位线(watermark)cell。cell 惰性构建:在事件流过之后才注册的单元,或比注册表更早的会话,都在首次触达(事件或读取)时从 `init` 出发在内存日志上折叠。注册是一个 effect,其 disposer 随调用方 fiber 走:领域插件卸载后,其 key(连同缓存的 cell)从后续驱动与快照中消失,客户端将其读作能力缺失;key 重复直接 throw。领域插件在 `ctx.inject(['sessionProjections'], …)` 下注册,因此不带注册表的 headless 组装完全不受影响。
+`SessionProjectionRegistry`([签名](#ctxsessionprojections--sessionprojectionregistry))通过一份 `session/event` 订阅以及每会话每单元的水位线(watermark)cell 拥有驱动权。cell 惰性构建:在事件流过之后才注册的单元,或比注册表更早的会话,都在首次触达(事件或读取)时从 `init` 出发在内存日志上折叠。注册是一个 effect,其 disposer 随调用方 fiber 走:领域插件卸载后,其 key(连同缓存的 cell)从后续驱动与快照中消失,客户端将其读作能力缺失;key 重复直接 throw。领域插件把 `sessionProjections` 声明为依赖,因此不含注册表的组装不受影响。
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 
@@ -154,7 +154,7 @@ Source: [`packages/session/session-projection-cache/src/index.ts:71`](../../pack
 
 ### `ctx.sessionProjections` — `SessionProjectionRegistry`
 
-`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive), and a changed state reference notifies the change feed with the schema-validated view. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin's key disappears from snapshots and clients read it as capability absence. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads.
+`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive), and a changed state reference notifies the change feed with the schema-validated view. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the full in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin's key disappears from snapshots and clients read it as capability absence. A domain that requires this capability declares a Cordis service dependency; an optional contributor may register under `ctx.inject(['sessionProjections'], …)`. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads.
 
 ```ts cordis-catalog
 /**
@@ -258,5 +258,5 @@ restore(checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseS
 
 Types: [Session](session.md) · [SessionEvent](session.md)
 
-Source: [`packages/session/session-projection/src/index.ts:171`](../../packages/session/session-projection/src/index.ts)
+Source: [`packages/session/session-projection/src/index.ts:167`](../../packages/session/session-projection/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 6 - 0
examples/headless-agent/tests/code-mode.e2e.ts

@@ -6,10 +6,12 @@ import { Context } from '@deepseek-ai/cordis'
 import LlmRuntime, { createUserMessage, CallId, HarnessError  } from '@deepseek-ai/dsh-llm'
 import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
 import type { SessionEvent } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime, { RUN_CODE_NAME, defineTool } from '@deepseek-ai/dsh-tools'
 import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools'
 import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
 
 import AgentLoop from '@deepseek-ai/dsh-agent-loop'
 import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
@@ -53,6 +55,8 @@ async function codeModeHarness(cwd: string): Promise<Context> {
   const harness = new Context()
   await harness.plugin(LlmRuntime)
   await harness.plugin(SessionStore)
+  await harness.plugin(SessionProjectionRegistry)
+  await harness.plugin(InboxService)
   await harness.plugin(SystemPrompt, { persona: PERSONA })
   await harness.plugin(ToolRuntime, { mode: 'code' })
   await harness.plugin(AgentRegistry)
@@ -70,6 +74,8 @@ async function workspaceCodeModeHarness(): Promise<Context> {
   const harness = new Context()
   await harness.plugin(LlmRuntime)
   await harness.plugin(SessionStore)
+  await harness.plugin(SessionProjectionRegistry)
+  await harness.plugin(InboxService)
   await harness.plugin(SystemPrompt, { persona: PERSONA })
   await harness.plugin(ToolRuntime, { mode: 'code' })
   await harness.plugin(AgentRegistry)

+ 2 - 2
examples/headless-agent/tests/fixtures/e2b/e2b/bin.ts

@@ -1,7 +1,6 @@
 import { readFile } from 'node:fs/promises'
 import { resolve } from 'node:path'
 import { boot } from '@deepseek-ai/dsh-app-boot'
-import { Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
 import type {} from '@deepseek-ai/dsh-fs-e2b'
@@ -20,7 +19,7 @@ const owner: Agent = {
   id: ownerId,
   options: {},
   session,
-  inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+  inbox: undefined as never,
   status: 'idle',
   ctx: ownerFiber.ctx,
   send() {},
@@ -31,6 +30,7 @@ const owner: Agent = {
   runMaintenance: task => task(new AbortController().signal),
   whenIdle: () => Promise.resolve(),
 }
+Object.assign(owner, { inbox: ctx.inboxes.create(owner) })
 const unregisterOwner = ctx.agents.register(owner)
 let terminalId: Awaited<ReturnType<typeof ctx.terminals.spawn>>['sessionId'] | undefined
 try {

+ 3 - 0
packages/bundle/base/cordis.patch.yml

@@ -58,6 +58,9 @@
     - id: agent
       name: '@deepseek-ai/dsh-agent'
 
+    - id: agent-inbox
+      name: '@deepseek-ai/dsh-agent/inbox'
+
     # The transport-independent default for Agents created by entry points.
     # Settings may supply a saved selection; consumers read it at creation time.
     - id: agent-default-model

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

@@ -2,11 +2,13 @@
 
 import { afterEach, describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry from '@deepseek-ai/dsh-agent'
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
 import type { Agent, AgentHandle, CreateAgentOptions } from '@deepseek-ai/dsh-agent'
 import AgentDefaultModelConfig from '@deepseek-ai/dsh-agent-default-model'
 import { createAssistantMessage } from '@deepseek-ai/dsh-llm'
 import SessionStore from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
 import { apply, Config, internals } from '../src/index.ts'
 
@@ -54,6 +56,8 @@ async function bench(script: Script): Promise<{
 }> {
   const ctx = new Context()
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(AgentDefaultModelConfig, { provider: 'test-provider', model: 'test-model' })
   ctx.agents.setFactory({
@@ -68,7 +72,7 @@ async function bench(script: Script): Promise<{
         id: session.id,
         options: options.agentOptions ?? {},
         session,
-        inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+        inbox: undefined as never,
         status: 'idle',
         ctx: agentCtx,
         cancel: () => {},
@@ -82,6 +86,7 @@ async function bench(script: Script): Promise<{
         inject: () => {},
         whenIdle: () => idle,
       } satisfies Partial<Agent>)
+      Object.assign(agent, { inbox: ctx.inboxes.create(agent) })
       await options.setup?.(agentCtx)
       script.before?.(session)
       ctx.agents.register(agent)

+ 1 - 0
packages/context/agent-instructions/package.json

@@ -48,6 +48,7 @@
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-agent-loop": "workspace:^",
+    "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
     "@deepseek-ai/dsh-fs": "workspace:^",
     "@deepseek-ai/dsh-fs-local": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",

+ 5 - 10
packages/context/agent-instructions/tests/agent-instructions.e2e.ts

@@ -4,13 +4,10 @@ import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import { afterEach, describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
-import LlmRuntime from '@deepseek-ai/dsh-llm'
-import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
-import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
-import ToolRuntime from '@deepseek-ai/dsh-tools'
-import AgentRegistry from '@deepseek-ai/dsh-agent'
+import { SessionId } from '@deepseek-ai/dsh-session'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
 import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
 import * as WorkspaceContext from '@deepseek-ai/dsh-agent-instructions'
 import { candidateScopeKey } from '../src/render.ts'
@@ -37,11 +34,9 @@ async function harness(): Promise<{ ctx: Context; agent: Agent }> {
   await mkdir(join(workdir, '.git'), { recursive: true })
   await writeFile(join(workdir, 'AGENTS.md'), `If the user asks for the workspace context handshake, reply with exactly this string and nothing else: ${PROBE}.\n`)
   ctx = new Context()
-  await ctx.plugin(LlmRuntime)
-  await ctx.plugin(SessionStore)
-  await ctx.plugin(SystemPrompt, { persona: 'Answer the user exactly and concisely.' })
-  await ctx.plugin(ToolRuntime)
-  await ctx.plugin(AgentRegistry)
+  await mountAgentLoopTestDependencies(ctx, {
+    systemPrompt: { persona: 'Answer the user exactly and concisely.' },
+  })
   await ctx.plugin(LocalFileSystem, { cwd: '/' })
   await ctx.plugin(ToolFs)
   await ctx.plugin(WorkspaceContext, { maxBytes: 65536 })

+ 22 - 7
packages/context/agent-instructions/tests/agent-instructions.spec.ts

@@ -6,9 +6,11 @@ import { Context } from '@deepseek-ai/cordis'
 import Loader from '@deepseek-ai/cordis-plugin-loader'
 import * as workspaceContext from '@deepseek-ai/dsh-agent-instructions'
 import LlmRuntime, { createUserMessage, CallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm'
-import SessionStore, { Session, SessionId, SESSION_FORMAT_VERSION, type SessionEvent, type UserMessage } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent'
+import SessionStore, { SessionId, type SessionEvent, type UserMessage } from '@deepseek-ai/dsh-session'
+import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
 import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { FileSystem, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
 import type {
   FsDirEntry,
@@ -47,6 +49,11 @@ import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent
 const sk = (directory: string, candidateName: string): string => candidateScopeKey(directory, candidateName)
 
 const testToolSignal = new AbortController().signal
+const isolatedInboxCtx = new Context()
+await isolatedInboxCtx.plugin(SessionStore)
+await isolatedInboxCtx.plugin(SessionProjectionRegistry)
+await isolatedInboxCtx.plugin(InboxService)
+let nextStubSession = 1
 
 async function tempRepo(): Promise<string> {
   return mkdtemp(join(tmpdir(), 'dsh-workspace-context-'))
@@ -181,14 +188,18 @@ async function mountFileToolsAndWorkspaceContext(ctx: Context, config: workspace
 }
 
 function stubAgent(cwd?: string, seed: SessionEvent[] = []): Agent {
-  const id = SessionId('s1')
-  const session = Session.create(id, seed, cwd === undefined ? undefined : { version: SESSION_FORMAT_VERSION, id, createdAt: 0, cwd })
-  return {
-    ctx: new Context(),
+  const id = SessionId(`agent-instructions-${String(nextStubSession++)}`)
+  const agentCtx = isolatedInboxCtx
+  const session = agentCtx.sessions.create(id, {
+    seed,
+    ...cwd === undefined ? {} : { meta: { createdAt: 0, cwd } },
+  })
+  const agent: Agent = {
+    ctx: agentCtx,
     id: SessionId('a1'),
     options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: undefined as never,
     status: 'idle',
     send: () => {},
     followup: () => {},
@@ -198,6 +209,8 @@ function stubAgent(cwd?: string, seed: SessionEvent[] = []): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
+  Object.assign(agent, { inbox: agentCtx.inboxes.create(agent) })
+  return agent
 }
 
 function stubToolExecution(
@@ -2509,6 +2522,8 @@ describe('dynamic nested workspace context injection', () => {
       ])
       await ctx.plugin(LlmRuntime)
       await ctx.plugin(SessionStore)
+      await ctx.plugin(SessionProjectionRegistry)
+      await ctx.plugin(InboxService)
       await ctx.plugin(SystemPrompt)
       await ctx.plugin(ToolRuntime)
       await ctx.plugin(AgentRegistry)

+ 4 - 2
packages/context/time-context/tests/time-context.spec.ts

@@ -36,11 +36,11 @@ async function mount(config: Config = {}) {
 }
 
 function sessionAgent(session: Session, id = 'agent'): Agent {
-  return {
+  const agent: Agent = {
     id: SessionId(id),
     options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: undefined as never,
     status: 'running',
     ctx: new Context(),
     send: () => {},
@@ -51,6 +51,8 @@ function sessionAgent(session: Session, id = 'agent'): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  return agent
 }
 
 function openMessageTurn(session: Session, turn: number, clientTimeZone?: string): void {

+ 4 - 2
packages/context/tmux-context/tests/tmux-context.spec.ts

@@ -92,11 +92,11 @@ async function mount(
 }
 
 function sessionAgent(session: Session, id = 'agent'): Agent {
-  return {
+  const agent: Agent = {
     id: SessionId(id),
     options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: undefined as never,
     status: 'running',
     ctx: new Context(),
     send: () => {},
@@ -107,6 +107,8 @@ function sessionAgent(session: Session, id = 'agent'): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  return agent
 }
 
 function openMessageTurn(session: Session, turn: number): void {

+ 1 - 0
packages/core/agent-loop/package.json

@@ -53,6 +53,7 @@
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-session-persistence": "workspace:^",
     "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
+    "@deepseek-ai/dsh-session-projection": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/cordis": "workspace:^",

+ 3 - 6
packages/core/agent-loop/src/agent.ts

@@ -11,11 +11,12 @@ import type {
   AgentOptions,
   AgentStatus,
   CancelOptions,
+  Inbox,
   InboxTarget,
   PreStepDecision,
   RequestErrorAction,
 } from '@deepseek-ai/dsh-agent'
-import { Inbox, agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent'
+import { agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent'
 import type { GenerateOptions, LlmCallConfig, Message, PreparedLlmCall } from '@deepseek-ai/dsh-llm'
 import {
   BlockAssembler,
@@ -84,11 +85,7 @@ export class ReactLoopAgent implements Agent {
     public readonly session: Session,
   ) {
     this.dispatch = agentEvents(loopCtx, this)
-    this.inbox = new Inbox(session, {
-      inserted: (message) => { this.dispatch.emit('agent/inbox/inserted', { message }) },
-      discarded: (message) => { this.dispatch.emit('agent/inbox/discarded', { message }) },
-      claimed: (message, turn) => { this.dispatch.emit('agent/inbox/claimed', { message, turn }) },
-    })
+    this.inbox = loopCtx.inboxes.create(this)
     const lastTurn = session.events.findLast(event => event.type === 'turn/start')?.data.turn ?? 0
     this.phase = { kind: 'idle', lastTurn }
     this.scope = createScope(loopCtx, this)

+ 1 - 1
packages/core/agent-loop/src/index.ts

@@ -294,7 +294,7 @@ function validateConfiguredAgents(agents: Config['agents']): void {
 
 /** Concrete agent factory and driver service. */
 export class AgentLoop extends Service implements AgentFactory {
-  static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt']
+  static inject = ['agents', 'inboxes', 'sessions', 'llm', 'tools', 'systemPrompt']
 
   /** Runtime schema for declarative agents. */
   static Config = z.object({

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

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { describe, expect, it } from 'vitest'
 import { Context, type Fiber } from '@deepseek-ai/cordis'
 import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
@@ -21,6 +23,8 @@ async function harness(adapter: LlmAdapter): Promise<Harness> {
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
   const agentsFiber = await ctx.plugin(AgentRegistry)
@@ -121,6 +125,8 @@ describe('AgentLoop initiator scope', () => {
     const adapter = new OverlapAdapter(ctx)
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
@@ -380,6 +386,8 @@ describe('AgentLoop initiator scope', () => {
     const adapter = new ReloadAdapter()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)

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

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
 import { describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
@@ -13,6 +15,8 @@ async function harness(adapter: MockAdapter): Promise<Context> {
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)

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

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
 /**
  * Tests for the queue-aware `Agent.cancel()` primitive. The default clears
@@ -25,6 +27,8 @@ async function harness(adapter: MockAdapter) {
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
@@ -514,6 +518,8 @@ describe('Agent.cancel()', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)

+ 16 - 0
packages/core/agent-loop/tests/config-session-id.spec.ts

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
@@ -29,6 +31,8 @@ async function makeCoreContext(): Promise<Context> {
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
@@ -327,6 +331,8 @@ describe('config-driven session id', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
@@ -352,6 +358,8 @@ describe('config-driven session id', () => {
     const ctx1 = new Context()
     await ctx1.plugin(LlmRuntime)
     await ctx1.plugin(SessionStore)
+    await ctx1.plugin(SessionProjectionRegistry)
+    await ctx1.plugin(InboxService)
     await ctx1.plugin(SystemPrompt)
     await ctx1.plugin(ToolRuntime)
     await ctx1.plugin(AgentRegistry)
@@ -371,6 +379,8 @@ describe('config-driven session id', () => {
     const ctx2 = new Context()
     await ctx2.plugin(LlmRuntime)
     await ctx2.plugin(SessionStore)
+    await ctx2.plugin(SessionProjectionRegistry)
+    await ctx2.plugin(InboxService)
     await ctx2.plugin(SystemPrompt)
     await ctx2.plugin(ToolRuntime)
     await ctx2.plugin(AgentRegistry)
@@ -395,6 +405,8 @@ describe('config-driven session id', () => {
     const ctx1 = new Context()
     await ctx1.plugin(LlmRuntime)
     await ctx1.plugin(SessionStore)
+    await ctx1.plugin(SessionProjectionRegistry)
+    await ctx1.plugin(InboxService)
     await ctx1.plugin(SystemPrompt)
     await ctx1.plugin(ToolRuntime)
     await ctx1.plugin(AgentRegistry)
@@ -411,6 +423,8 @@ describe('config-driven session id', () => {
     const ctx2 = new Context()
     await ctx2.plugin(LlmRuntime)
     await ctx2.plugin(SessionStore)
+    await ctx2.plugin(SessionProjectionRegistry)
+    await ctx2.plugin(InboxService)
     await ctx2.plugin(SystemPrompt)
     await ctx2.plugin(ToolRuntime)
     await ctx2.plugin(AgentRegistry)
@@ -436,6 +450,8 @@ describe('config-driven session id', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)

+ 18 - 0
packages/core/agent-loop/tests/contract-regressions.spec.ts

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import LlmRuntime, { createUserMessage, CallId, LlmError, MessageSource, ProviderRequestId, StreamChunk  } from '@deepseek-ai/dsh-llm'
@@ -30,6 +32,8 @@ async function harness(adapter: MockAdapter) {
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
@@ -525,6 +529,8 @@ describe('turn numbering continues across seeded sessions', () => {
     const ctx2 = new Context()
     await ctx2.plugin(LlmRuntime)
     await ctx2.plugin(SessionStore)
+    await ctx2.plugin(SessionProjectionRegistry)
+    await ctx2.plugin(InboxService)
     await ctx2.plugin(SystemPrompt)
     await ctx2.plugin(ToolRuntime)
     await ctx2.plugin(AgentRegistry)
@@ -676,6 +682,8 @@ describe('turn and step boundary recovery', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
@@ -1108,6 +1116,8 @@ describe('disposal and cancellation during pre-step assembly', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
@@ -1158,6 +1168,8 @@ describe('disposal and cancellation during pre-step assembly', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
@@ -1208,6 +1220,8 @@ describe('disposal and cancellation during pre-step assembly', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
@@ -1254,6 +1268,8 @@ describe('disposal and cancellation during pre-step assembly', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
@@ -1302,6 +1318,8 @@ describe('disposal and cancellation during pre-step assembly', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)

+ 4 - 0
packages/core/agent-loop/tests/coverage-edges.spec.ts

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import LlmRuntime, { createUserMessage, CallId, LlmError, StreamChunk, errorChain  } from '@deepseek-ai/dsh-llm'
@@ -18,6 +20,8 @@ async function harness(adapter: MockAdapter) {
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)

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

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import LlmRuntime, { createUserMessage, CallId  } from '@deepseek-ai/dsh-llm'
@@ -31,6 +33,8 @@ async function harness(adapter: MockAdapter) {
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)

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

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import LlmRuntime, { createUserMessage, CallId, LlmError, StreamChunk  } from '@deepseek-ai/dsh-llm'
@@ -17,6 +19,8 @@ async function harness(adapter: MockAdapter, persona = '') {
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt, { persona })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
@@ -132,6 +136,22 @@ describe('agent loop', () => {
     expect(adapter.requests).toHaveLength(1)
   })
 
+  it('rejects overlapping maintenance work', async () => {
+    const ctx = await harness(new MockAdapter([]))
+    const agent = ctx.agentLoop.create(SessionId('overlapping-maintenance'), {
+      provider: 'mock',
+      model: 'mock',
+    })
+    const finish = Promise.withResolvers<undefined>()
+    const maintenance = agent.runMaintenance(() => finish.promise)
+
+    expect(() => agent.runMaintenance(async () => undefined))
+      .toThrow('agent "overlapping-maintenance" already has active work')
+
+    finish.resolve(undefined)
+    await maintenance
+  })
+
   it('suppresses the replay when a latched maintenance wake is removed', async () => {
     const adapter = new MockAdapter([])
     const ctx = await harness(adapter)
@@ -1433,6 +1453,8 @@ describe('agent loop', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
@@ -1457,6 +1479,8 @@ describe('agent loop', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)

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

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 /**
  * Property-based tests for the agent loop's inbox/turn scheduling (the
  * property-testing Agent Note). Deterministic by construction: schedules are driven
@@ -39,6 +41,8 @@ async function harness() {
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)

+ 4 - 0
packages/core/agent-loop/tests/request-cache.e2e.ts

@@ -6,9 +6,11 @@ import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
 import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
 import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
 
 import AgentLoop from '@deepseek-ai/dsh-agent-loop'
 import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 
 /**
  * With-key proof that log-derived requests translate into real provider cache hits: a
@@ -41,6 +43,8 @@ async function loopHarness(): Promise<Context> {
   const created = new Context()
   await created.plugin(LlmRuntime)
   await created.plugin(SessionStore)
+  await created.plugin(SessionProjectionRegistry)
+  await created.plugin(InboxService)
   await created.plugin(SystemPrompt, { persona: SYSTEM })
   await created.plugin(ToolRuntime)
   await created.plugin(AgentRegistry)

+ 4 - 0
packages/core/agent-loop/tests/request-error.spec.ts

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import AgentRegistry from '@deepseek-ai/dsh-agent'
@@ -13,6 +15,8 @@ async function harness(adapter: MockAdapter): Promise<Context> {
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)

+ 8 - 0
packages/core/agent-loop/tests/request-reconstruction.spec.ts

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 /**
  * Loop-level reconstructability: every request the loop sends is a pure function of the
  * session log — messages derive at the step/start boundary and the header is the latest
@@ -28,6 +30,8 @@ async function harnessRoutes(
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt, { persona })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
@@ -255,6 +259,8 @@ describe('request stability across the loop', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt, { persona: 'stable base' })
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
@@ -373,6 +379,8 @@ describe('request stability across the loop', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt, { persona: 'stable base' })
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)

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

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
@@ -28,6 +30,8 @@ async function mountPersistentHarness(root: string, adapter: MockAdapter): Promi
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
@@ -244,6 +248,8 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     const ctx2 = new Context()
     await ctx2.plugin(LlmRuntime)
     await ctx2.plugin(SessionStore)
+    await ctx2.plugin(SessionProjectionRegistry)
+    await ctx2.plugin(InboxService)
     await ctx2.plugin(SystemPrompt)
     await ctx2.plugin(ToolRuntime)
     await ctx2.plugin(AgentRegistry)
@@ -272,6 +278,8 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     const ctx2 = new Context()
     await ctx2.plugin(LlmRuntime)
     await ctx2.plugin(SessionStore)
+    await ctx2.plugin(SessionProjectionRegistry)
+    await ctx2.plugin(InboxService)
     await ctx2.plugin(SystemPrompt)
     await ctx2.plugin(ToolRuntime)
     await ctx2.plugin(AgentRegistry)
@@ -522,6 +530,8 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
@@ -585,6 +595,8 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     const ctx2 = new Context()
     await ctx2.plugin(LlmRuntime)
     await ctx2.plugin(SessionStore)
+    await ctx2.plugin(SessionProjectionRegistry)
+    await ctx2.plugin(InboxService)
     await ctx2.plugin(SystemPrompt)
     await ctx2.plugin(ToolRuntime)
     await ctx2.plugin(AgentRegistry)
@@ -617,6 +629,8 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     const ctx2 = new Context()
     await ctx2.plugin(LlmRuntime)
     await ctx2.plugin(SessionStore)
+    await ctx2.plugin(SessionProjectionRegistry)
+    await ctx2.plugin(InboxService)
     await ctx2.plugin(SystemPrompt)
     await ctx2.plugin(ToolRuntime)
     await ctx2.plugin(AgentRegistry)
@@ -653,6 +667,8 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     const ctx2 = new Context()
     await ctx2.plugin(LlmRuntime)
     await ctx2.plugin(SessionStore)
+    await ctx2.plugin(SessionProjectionRegistry)
+    await ctx2.plugin(InboxService)
     await ctx2.plugin(SystemPrompt)
     await ctx2.plugin(ToolRuntime)
     await ctx2.plugin(AgentRegistry)
@@ -686,6 +702,8 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt)
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
@@ -875,6 +893,8 @@ describe('configured-start failure edges', () => {
     const configured = new Context()
     await configured.plugin(LlmRuntime)
     await configured.plugin(SessionStore)
+    await configured.plugin(SessionProjectionRegistry)
+    await configured.plugin(InboxService)
     await configured.plugin(SystemPrompt)
     await configured.plugin(ToolRuntime)
     await configured.plugin(AgentRegistry)
@@ -920,6 +940,8 @@ describe('configured-start failure edges', () => {
     const configured = new Context()
     await configured.plugin(LlmRuntime)
     await configured.plugin(SessionStore)
+    await configured.plugin(SessionProjectionRegistry)
+    await configured.plugin(InboxService)
     await configured.plugin(SystemPrompt)
     await configured.plugin(ToolRuntime)
     await configured.plugin(AgentRegistry)

+ 4 - 0
packages/core/agent-loop/tests/scope-lifecycle.spec.ts

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
 import { describe, expect, it } from 'vitest'
 import { Context, symbols, type EffectMeta, type Fiber } from '@deepseek-ai/cordis'
@@ -17,6 +19,8 @@ async function harnessWithLoop(adapter: MockAdapter = new MockAdapter([textRespo
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt, { persona: 'You are the deployment.' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)

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

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 /** The `agent-loop` settings section layered over the composition entry. */
 
 import { describe, expect, it } from 'vitest'
@@ -34,6 +36,8 @@ async function boot(): Promise<{ ctx: Context; settingsFiber: Fiber; loopFiber:
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)

+ 10 - 0
packages/core/agent-loop/tests/tool-calls.spec.ts

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 /**
  * Exercises scheduler ordering and cancellation with deterministic gated tools.
  * ACP expected outputs own transcript-facing coverage.
@@ -20,6 +22,8 @@ async function harness(adapter: MockAdapter, maxParallelToolCalls?: number) {
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt, { persona: '' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
@@ -278,6 +282,8 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt, { persona: '' })
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
@@ -346,6 +352,8 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt, { persona: '' })
     await ctx.plugin(ToolRuntime)
     await ctx.plugin(AgentRegistry)
@@ -704,6 +712,8 @@ describe('code-mode native-tool denial through the agent loop', () => {
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(SystemPrompt, { persona: '' })
     await ctx.plugin(ToolRuntime, { mode: 'code' })
     // eslint-disable-next-line @typescript-eslint/no-explicit-any -- FakeCodeRuntime is an internal test helper with an opaque type shape

+ 4 - 0
packages/core/agent-loop/tests/tool-order.spec.ts

@@ -1,3 +1,5 @@
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
 /**
  * Loop-level tool-order determinism: the request/header event — and therefore the frozen
@@ -23,6 +25,8 @@ async function harness(adapter: MockAdapter, toolOrder?: SystemPromptConfig['too
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt, { persona: 'stable base', ...toolOrder !== undefined ? { toolOrder } : {} })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/core/agent/README.md
-README.md: f6b698e93b254c97786155e7d2c7e81f07c0d981
-README.zh.md: 787c4f353909a1e2dbc60c33abb3c101e677e011
+README.md: 6af7e57bdb0ae710a6d1e6e58ca2ac187a45eeb8
+README.zh.md: 1533f918e037c7dc7d131c7046d25fc259d33580

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

@@ -54,7 +54,7 @@ Most interception points are cooperative waterfalls. `agent/pre-step` receives a
 
 `PreStepDecision` is either `{ kind: 'reject' }` or `{ kind: 'enter', messages }`. The enter branch is the complete identified, frozen batch for the proposed step. A listener that wraps downstream entry preserves that batch unless it intentionally replaces it; additions follow the waterfall's natural return order. Claiming already removed the offered messages from the inbox, so rejection does not retain them. Messages inserted after the claim remain pending for a later boundary.
 
-Inbox live notifications are deliberately per-message and minimal: `agent/inbox/inserted { message }`, `agent/inbox/claimed { message, turn }`, and `agent/inbox/discarded { message }`. They complement the durable `agent/inbox/spliced` projection without adding another lifecycle envelope.
+`InboxService` owns the standard `inbox` session projection. The projection registry folds durable `agent/inbox/spliced` events once and remains the sole owner of the live `{ 'next-turn', 'next-step' }` state; Inbox is a command facade that reads the registry snapshot rather than replaying or copying the fold. Inbox live notifications are deliberately per-message and minimal: `agent/inbox/inserted { message }`, `agent/inbox/claimed { message, turn }`, and `agent/inbox/discarded { message }`. Inbox emits them as it commits the corresponding mutation, without adding another lifecycle envelope.
 
 Turn and step boundaries and the model token stream are durable `session/event` facts rather than mirrored `agent/*` notifications. Consumers read `turn/*`, `step/*`, and `assistant/chunk` from the session feed; tool policy and outcome observation belong to the complete pipeline documented by [`dsh-tools`](../tools/README.md).
 
@@ -64,7 +64,7 @@ Turn and step boundaries and the model token stream are durable `session/event`
 
 The handle every plugin programs against:
 
-- `agent.inbox` — the agent-owned projection of durable `agent/inbox/spliced` events. `nextTurn` and `nextStep` expose pending `UserMessage` values. `append`, `prepend`, `replace`, `remove`, `clear`, `splice`, and `claim` mutate them; `replace(messageId, newMessage)` and `remove(messageId)` locate the pending message across both lists. Replacement may change identity and publishes the old message as discarded followed by the new message as inserted. Ordinary removals and `clear()` are durable cancellations and emit `agent/inbox/discarded`. `claim(target)` removes the next proposed batch with pure deletion splices; the loop then emits `agent/inbox/claimed`. `MessageId` is the only occurrence identity and must remain unique while pending.
+- `agent.inbox` — the live face of the standard durable `inbox` projection. `nextTurn` and `nextStep` expose pending `UserMessage` values. `append`, `prepend`, `replace`, `remove`, `clear`, `splice`, and `claim` mutate them; `replace(messageId, newMessage)` and `remove(messageId)` locate the pending message across both lists. Replacement may change identity and publishes the old message as discarded followed by the new message as inserted. Ordinary removals and `clear()` are durable cancellations and emit `agent/inbox/discarded`. `claim(target)` removes the next proposed batch with pure deletion splices and emits `agent/inbox/claimed` for each claimed message. `MessageId` is the only occurrence identity and must remain unique while pending.
 - `agent.followup(message)` — queue an ordinary `next-turn` message and wake the driver. It returns no completion handle; the message id identifies inbox insertion, claim, and discard facts, not a later output or `turn/end`.
 - `agent.steer(message)` — queue waking `next-step` input. An idle agent starts a turn synchronously; a running driver consumes later steering at its next step boundary.
 - `agent.inject(message)` — queue non-waking `next-step` context. A running driver claims it at the nearest later pre-step boundary; an idle driver leaves it pending until `followup()` or `steer()` wakes the driver. It may miss a request whose pre-step already claimed its batch.

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

@@ -54,7 +54,7 @@ Agent *创建* 由实现 `AgentFactory` 的插件(`dsh-agent-loop`)提供,
 
 `PreStepDecision` 要么是 `{ kind: 'reject' }`,要么是 `{ kind: 'enter', messages }`。enter 分支是拟进入步骤的完整、带标识且冻结的批次。包装下游 enter 的监听器会保留该批次,除非有意替换它;新增消息遵循 waterfall 的自然返回顺序。领取操作已经把候选消息从 inbox 删除,因此 reject 不会保留它们;领取后插入的消息仍等待后续边界。
 
-inbox 的实时通知刻意采用逐消息的最小载荷:`agent/inbox/inserted { message }`、`agent/inbox/claimed { message, turn }` 与 `agent/inbox/discarded { message }`。它们补充持久 `agent/inbox/spliced` 投影,但不引入另一层生命周期封套。
+`InboxService` 拥有标准 `inbox` 会话投影。投影注册表只折叠一次持久 `agent/inbox/spliced` 事件,并继续作为 live `{ 'next-turn', 'next-step' }` 状态的唯一所有者;Inbox 是读取注册表快照的命令 facade,不会重新回放或复制折叠结果。Inbox 的实时通知刻意采用逐消息的最小载荷:`agent/inbox/inserted { message }`、`agent/inbox/claimed { message, turn }` 与 `agent/inbox/discarded { message }`。Inbox 在提交对应变更时自行发出这些通知,不引入另一层生命周期封套。
 
 轮次和步骤边界以及模型 token 流是持久 `session/event` 事实,而不是镜像的 `agent/*` 通知。消费方从会话事件流读取 `turn/*`、`step/*` 和 `assistant/chunk`;工具策略与结果观测属于 [`dsh-tools`](../tools/README.md) 记录的完整流水线。
 
@@ -64,7 +64,7 @@ inbox 的实时通知刻意采用逐消息的最小载荷:`agent/inbox/inserte
 
 每个插件面向的 handle:
 
-- `agent.inbox`:agent 所拥有的持久 `agent/inbox/spliced` 事件投影。`nextTurn` 与 `nextStep` 暴露待处理的 `UserMessage` 值。`append`、`prepend`、`replace`、`remove`、`clear`、`splice` 与 `claim` 用于变更队列;`replace(messageId, newMessage)` 与 `remove(messageId)` 通过 `MessageId` 跨两份列表定位待处理消息。替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。普通删除和 `clear()` 都是持久取消,并发出 `agent/inbox/discarded`。`claim(target)` 通过纯删除 splice 移除下一个候选批次,随后由循环发出 `agent/inbox/claimed`。`MessageId` 是唯一的入队项标识,在消息待处理期间必须保持唯一。
+- `agent.inbox`:标准持久 `inbox` 投影的 live 接口。`nextTurn` 与 `nextStep` 暴露待处理的 `UserMessage` 值。`append`、`prepend`、`replace`、`remove`、`clear`、`splice` 与 `claim` 用于变更队列;`replace(messageId, newMessage)` 与 `remove(messageId)` 通过 `MessageId` 跨两份列表定位待处理消息。替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。普通删除和 `clear()` 都是持久取消,并发出 `agent/inbox/discarded`。`claim(target)` 通过纯删除 splice 移除下一个候选批次,并为每条已认领消息发出 `agent/inbox/claimed`。`MessageId` 是唯一的入队项标识,在消息待处理期间必须保持唯一。
 - `agent.followup(message)`:将一条普通 `next-turn` 消息排队并唤醒驱动器。它不返回完成 handle;消息 id 标识 inbox 的插入、领取与丢弃事实,而不标识之后的输出或 `turn/end`。
 - `agent.steer(message)`:将会唤醒的 `next-step` steering(中途引导)输入排队。agent 空闲时会同步启动一个轮次;驱动器运行期间收到的后续 steering 会在下一个步骤边界被消费。
 - `agent.inject(message)`:将不会唤醒的 `next-step` 上下文排队。运行中的驱动器会在最近的后续 pre-step 边界领取它;idle 驱动器则会让它保持待处理,直至 `followup()` 或 `steer()` 唤醒驱动器。若某次请求的 pre-step 已经领取完批次,它可能赶不上该请求。

+ 13 - 0
packages/core/agent/package.json

@@ -18,6 +18,14 @@
       "types": "./lib/types/index.d.ts",
       "default": "./lib/index.js"
     },
+    "./inbox": {
+      "types": "./lib/types/inbox.d.ts",
+      "default": "./lib/types/inbox.js"
+    },
+    "./inbox-projection": {
+      "types": "./lib/types/inbox-projection.d.ts",
+      "default": "./lib/types/inbox-projection.js"
+    },
     "./invariant": {
       "types": "./lib/types/invariant.d.ts",
       "default": "./lib/invariant.js"
@@ -41,15 +49,20 @@
     "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-scope": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-session-projection": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",
     "@deepseek-ai/cordis": "workspace:^"
   },
+  "dependencies": {
+    "zod": "^4.4.3"
+  },
   "devDependencies": {
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-scope": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-session-projection": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",
     "@deepseek-ai/dsh-typert-registry": "workspace:^",

+ 20 - 0
packages/core/agent/src/inbox-projection.ts

@@ -0,0 +1,20 @@
+/** Inbox projection schema and its inferred wire value. */
+
+import type { UserMessage } from '@deepseek-ai/dsh-llm/types'
+import { z } from 'zod'
+
+/** Wire validation for pending agent input reconstructed from durable inbox splices. */
+export const inboxProjectionSchema = z.object({
+  'next-turn': z.array(z.custom<UserMessage>()).readonly(),
+  'next-step': z.array(z.custom<UserMessage>()).readonly(),
+}).readonly()
+
+/** Complete pending Inbox value reconstructed from durable splices. */
+export type InboxState = z.infer<typeof inboxProjectionSchema>
+
+declare module '@deepseek-ai/dsh-session-projection/types' {
+  interface SessionProjectionMap {
+    /** Pending agent input reconstructed from durable inbox splices. */
+    inbox: InboxState
+  }
+}

+ 95 - 85
packages/core/agent/src/inbox.ts

@@ -4,54 +4,82 @@
  * @module @deepseek-ai/dsh-agent/inbox
  */
 
+import { Context, Service } from '@deepseek-ai/cordis'
 import type { MessageId } from '@deepseek-ai/dsh-llm'
-import type { Session, SessionEventMap, UserMessage } from '@deepseek-ai/dsh-session'
+import type { SessionEventMap, UserMessage } from '@deepseek-ai/dsh-session'
+import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
+import { agentEvents } from './dispatch.ts'
+import type { AgentEventDispatch } from './dispatch.ts'
+import { inboxProjectionSchema } from './inbox-projection.ts'
+import type { InboxState } from './inbox-projection.ts'
+import type { Agent } from './runtime-types.ts'
 import type { InboxTarget } from './types.ts'
 
-/** Mutable state privately owned by an {@link Inbox}. */
-type InboxState = Record<InboxTarget, UserMessage[]>
-
-/** Live notifications committed by inbox mutations. */
-export interface InboxNotifications {
-  /** Publish one inserted message. */
-  inserted(message: UserMessage): void
-  /** Publish one discarded message. */
-  discarded(message: UserMessage): void
-  /** Publish one claimed message inside its owning turn. */
-  claimed(message: UserMessage, turn: number): void
+declare module '@deepseek-ai/cordis' {
+  interface Context {
+    inboxes: InboxService
+  }
+}
+
+/** Root Inbox service: creates live inboxes and owns their durable projection. */
+export class InboxService extends Service {
+  static inject = ['sessionProjections']
+
+  constructor(ctx: Context) {
+    super(ctx, 'inboxes')
+    ctx.sessionProjections.register({
+      key: 'inbox',
+      schema: inboxProjectionSchema,
+      init: () => ({ 'next-turn': [], 'next-step': [] }),
+      apply(state, event) {
+        if (event.type !== 'agent/inbox/spliced') return state
+        const splice = event.data
+        const next = state[splice.target].toSpliced(
+          splice.start,
+          splice.removedCount ?? 0,
+          ...splice.inserted,
+        )
+        return splice.target === 'next-turn'
+          ? { 'next-turn': next, 'next-step': state['next-step'] }
+          : { 'next-turn': state['next-turn'], 'next-step': next }
+      },
+      view: state => state,
+      stateVersion: 1,
+    } satisfies ProjectionDefinition<'inbox', InboxState>)
+  }
+
+  /**
+   * Restore one live Inbox for an agent and publish its committed mutations.
+   * @param agent - agent that owns the durable session and live Inbox events.
+   * @returns the restored Inbox.
+   */
+  create(agent: Agent): Inbox {
+    return new Inbox(this.ctx, agent)
+  }
 }
 
-/** A replay-once projection that incrementally consumes later inbox splices. */
+/** Agent-owned command facade over the standard durable Inbox projection. */
 export class Inbox {
-  private readonly state: InboxState = { 'next-turn': [], 'next-step': [] }
-
-  constructor(
-    private readonly session: Session,
-    private readonly notifications: InboxNotifications,
-  ) {
-    for (const event of session.events.slice(session.header.seedLength ?? 0)) {
-      if (event.type !== 'agent/inbox/spliced') continue
-      try {
-        this.apply(event.data)
-      } catch (error: unknown) {
-        throw new Error(`invalid persisted inbox splice at session seq ${event.seq}`, { cause: error })
-      }
-    }
+  private readonly dispatch: AgentEventDispatch
+
+  constructor(private readonly ctx: Context, private readonly agent: Agent) {
+    this.dispatch = agentEvents(ctx, agent)
   }
 
   /** Prompts awaiting individual turns. */
   get nextTurn(): readonly UserMessage[] {
-    return this.state['next-turn']
+    return this.current()['next-turn']
   }
 
   /** Input awaiting the next step boundary. */
   get nextStep(): readonly UserMessage[] {
-    return this.state['next-step']
+    return this.current()['next-step']
   }
 
   /** Whether either pending-message list contains work. */
   get hasPending(): boolean {
-    return this.nextTurn.length > 0 || this.nextStep.length > 0
+    const state = this.current()
+    return state['next-turn'].length > 0 || state['next-step'].length > 0
   }
 
   /** Durably cancel all pending input, clearing next-step before next-turn. */
@@ -61,50 +89,41 @@ export class Inbox {
   }
 
   /**
-   * Remove and return the complete batch proposed for one step, publishing
-   * each claimed message. The durable splices are pure deletions.
+   * Remove and return the complete batch proposed for one step.
    * @param target - whether this boundary also consumes one queued turn.
    * @param turn - turn that will own the claimed batch.
    * @returns next-step input followed by the queued turn, when requested.
-   * @internal - The agent loop's step-boundary operation, not a plugin extension point.
    */
   claim(target: InboxTarget, turn: number): UserMessage[] {
     const claimed = this.mutate('next-step', 0, this.nextStep.length, [], false)
-    if (target === 'next-turn') {
-      claimed.push(...this.mutate('next-turn', 0, 1, [], false))
-    }
-    for (const message of claimed) this.notifications.claimed(message, turn)
+    if (target === 'next-turn') claimed.push(...this.mutate('next-turn', 0, 1, [], false))
+    for (const message of claimed) this.dispatch.emit('agent/inbox/claimed', { message, turn })
     return claimed
   }
 
   /**
-   * Append one message to a pending list and durably record the insertion.
+   * Append one message to a pending list.
    * @param target - pending list to extend.
    * @param message - message to append.
-   * @throws if the message identity is already pending.
    */
   append(target: InboxTarget, message: UserMessage): void {
-    this.splice(target, this.state[target].length, 0, [message])
+    this.splice(target, this.current()[target].length, 0, [message])
   }
 
   /**
-   * Prepend one message to a pending list and durably record the insertion.
+   * Prepend one message to a pending list.
    * @param target - pending list to extend.
    * @param message - message to prepend.
-   * @throws if the message identity is already pending.
    */
   prepend(target: InboxTarget, message: UserMessage): void {
     this.splice(target, 0, 0, [message])
   }
 
   /**
-   * Replace one pending message in place, possibly changing its identity. A
-   * successful replacement publishes the old message as discarded and the new
-   * message as inserted.
+   * Replace one pending message in place.
    * @param messageId - identity of the pending message to replace.
    * @param newMessage - replacement message.
    * @returns whether the message was still pending.
-   * @throws if the replacement duplicates another pending message identity.
    */
   replace(messageId: MessageId, newMessage: UserMessage): boolean {
     const location = this.locate(messageId)
@@ -114,7 +133,7 @@ export class Inbox {
   }
 
   /**
-   * Remove one pending message and durably record its cancellation.
+   * Remove one pending message.
    * @param messageId - identity of the pending message to remove.
    * @returns whether the message was still pending.
    */
@@ -127,9 +146,6 @@ export class Inbox {
 
   /**
    * Apply standard splice semantics and durably record the normalized result.
-   * The durable event commits before the live projection mutates, so synchronous
-   * `session/event` observers see the pre-splice lists and can reconstruct the
-   * removed messages from the normalized coordinates.
    * @param target - pending list to mutate.
    * @param start - splice position.
    * @param deleteCount - maximum number of messages to remove.
@@ -147,14 +163,22 @@ export class Inbox {
 
   /** Locate one pending identity across both owned lists. */
   private locate(messageId: MessageId): { target: InboxTarget; index: number } | undefined {
+    const state = this.current()
     for (const target of ['next-turn', 'next-step'] as const) {
-      const index = this.state[target].findIndex(message => message.id === messageId)
+      const index = state[target].findIndex(message => message.id === messageId)
       if (index >= 0) return { target, index }
     }
     return undefined
   }
 
-  /** Commit one normalized mutation and publish its live notifications. */
+  /** Read the current durable projection value. */
+  private current(): InboxState {
+    // InboxService registers this required projection before creating an Inbox.
+    // oxlint-disable-next-line typescript/no-non-null-assertion
+    return this.ctx.sessionProjections.snapshot(this.agent.session).values.inbox!
+  }
+
+  /** Commit one normalized mutation and publish its live events. */
   private mutate(
     target: InboxTarget,
     start: number,
@@ -162,7 +186,8 @@ export class Inbox {
     inserted: UserMessage[],
     discardRemoved: boolean,
   ): UserMessage[] {
-    const inbox = this.state[target]
+    const state = this.current()
+    const inbox = state[target]
     const truncatedStart = Math.trunc(start)
     const offset = Number.isNaN(truncatedStart) ? 0 : truncatedStart
     const actualStart = offset < 0
@@ -174,47 +199,32 @@ export class Inbox {
       inbox.length - actualStart,
     )
     if (actualDeleteCount === 0 && inserted.length === 0) return []
+    const candidate = inbox.toSpliced(actualStart, actualDeleteCount, ...inserted)
+    const ids = new Set<string>()
+    for (const message of target === 'next-turn'
+      ? [...candidate, ...state['next-step']]
+      : [...state['next-turn'], ...candidate]) {
+      if (ids.has(message.id)) throw new Error(`message "${message.id}" is already pending`)
+      ids.add(message.id)
+    }
     const outcome = discardRemoved && actualDeleteCount > 0 ? 'canceled' as const : undefined
-    const splice = {
+    const splice: SessionEventMap['agent/inbox/spliced'] = {
       target,
       start: actualStart,
       ...(actualDeleteCount === 0 ? {} : { removedCount: actualDeleteCount }),
       inserted,
       ...(outcome === undefined ? {} : { outcome }),
     }
-    this.validate(splice)
-    const event = this.session.append('agent/inbox/spliced', splice)
-    const removed = inbox.splice(actualStart, actualDeleteCount, ...event.data.inserted)
+    const removed = inbox.slice(actualStart, actualStart + actualDeleteCount)
+    const event = this.agent.session.append('agent/inbox/spliced', splice)
     if (discardRemoved) {
-      for (const message of removed) this.notifications.discarded(message)
-    }
-    for (const message of event.data.inserted) this.notifications.inserted(message)
-    return removed
-  }
-
-  /** Apply one normalized durable splice to the projection. */
-  private apply(splice: SessionEventMap['agent/inbox/spliced']): UserMessage[] {
-    this.validate(splice)
-    const inbox = this.state[splice.target]
-    return inbox.splice(splice.start, splice.removedCount ?? 0, ...splice.inserted)
-  }
-
-  /** Validate one normalized splice against the current projection. */
-  private validate(splice: SessionEventMap['agent/inbox/spliced']): void {
-    const inbox = this.state[splice.target]
-    const removedCount = splice.removedCount ?? 0
-    if (!Number.isSafeInteger(splice.start) || splice.start < 0 || splice.start > inbox.length
-      || !Number.isSafeInteger(removedCount) || removedCount < 0
-      || splice.start + removedCount > inbox.length) {
-      throw new Error('invalid inbox splice')
+      for (const message of removed) this.dispatch.emit('agent/inbox/discarded', { message })
     }
-    const candidate = inbox.toSpliced(splice.start, removedCount, ...splice.inserted)
-    const ids = new Set<string>()
-    for (const message of splice.target === 'next-turn'
-      ? [...candidate, ...this.nextStep]
-      : [...this.nextTurn, ...candidate]) {
-      if (ids.has(message.id)) throw new Error(`message "${message.id}" is already pending`)
-      ids.add(message.id)
+    for (const message of event.data.inserted) {
+      this.dispatch.emit('agent/inbox/inserted', { message })
     }
+    return removed
   }
 }
+
+export default InboxService

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

@@ -13,8 +13,8 @@ declare module '@deepseek-ai/dsh-session/types' {
   interface SessionEventMap {
     /**
      * One normalized mutation of an agent's durable pending-message lists.
-     * Live dispatch precedes projection mutation, so synchronous observers may
-     * read the pre-splice inbox to recover the removed messages.
+     * The session-projection registry applies the committed event before
+     * `Session.append()` returns; Inbox live notifications follow that commit.
      */
     'agent/inbox/spliced': {
       target: InboxTarget

+ 106 - 32
packages/core/agent/tests/agent.spec.ts

@@ -1,11 +1,13 @@
 import { describe, expect, expectTypeOf, it } from 'vitest'
 import { Context, Service, symbols } from '@deepseek-ai/cordis'
 import { createUserMessage, freezeMessage } from '@deepseek-ai/dsh-llm'
-import { Session, SessionId, type UserMessage } from '@deepseek-ai/dsh-session'
+import SessionStore, { Session, SessionId, type UserMessage } from '@deepseek-ai/dsh-session'
 import AgentRegistry, {
   agentEvents,
-  Inbox,
+  InboxService,
 } from '@deepseek-ai/dsh-agent'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
+import type {} from '@deepseek-ai/dsh-agent/inbox-projection'
 import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
 
 import type {
@@ -19,14 +21,17 @@ import type {
 
 function stubAgent(rawId: string, overrides: Partial<Agent> = {}): Agent {
   const id = SessionId(rawId)
-  const session = Session.create(id)
+  const session = overrides.session ?? Session.create(id)
+  const ctx = overrides.ctx ?? new Context()
   const agent: Agent = {
     id,
     options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: {
+      nextTurn: [], nextStep: [], hasPending: false,
+    } as never,
     status: 'idle',
-    ctx: new Context(),
+    ctx,
     send: () => {},
     followup: () => {},
     steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }),
@@ -34,32 +39,52 @@ function stubAgent(rawId: string, overrides: Partial<Agent> = {}): Agent {
     cancel() {},
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
+    ...overrides,
   }
-  return Object.assign(agent, overrides)
+  return agent
+}
+
+async function inboxAgent(rawId: string): Promise<{ ctx: Context; session: Session; agent: Agent }> {
+  const ctx = new Context()
+  await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
+  const session = ctx.sessions.create(SessionId(rawId))
+  const agent = stubAgent(rawId, { ctx, session })
+  Object.assign(agent, { inbox: ctx.inboxes.create(agent) })
+  return { ctx, session, agent }
 }
 
 describe('Inbox', () => {
-  it('rejects an invalid durable splice during reconstruction', () => {
-    const session = Session.create(SessionId('invalid-inbox-replay'))
-    session.append('agent/inbox/spliced', {
-      target: 'next-turn',
-      start: 1,
-      inserted: [],
+  it('projects inherited Inbox events in a forked session', async () => {
+    const { ctx, session: parent, agent: parentAgent } = await inboxAgent('inbox-fork-parent')
+    const inherited = createUserMessage({
+      content: [{ type: 'text', text: 'parent pending' }],
+      source: { kind: 'user' },
     })
-
-    expect(() => new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }))
-      .toThrow('invalid persisted inbox splice at session seq 0')
+    parentAgent.inbox.append('next-turn', inherited)
+    const child = ctx.sessions.fork(parent, undefined, SessionId('inbox-fork-child'))
+    const childAgent = stubAgent('inbox-fork-child', { ctx, session: child })
+    Object.assign(childAgent, { inbox: ctx.inboxes.create(childAgent) })
+
+    expect(child.header.seedLength).toBe(parent.events.length)
+    expect(childAgent.inbox.nextTurn).toEqual([inherited])
+    expect(childAgent.inbox.nextStep).toEqual([])
+    const own = createUserMessage({
+      content: [{ type: 'text', text: 'child pending' }],
+      source: { kind: 'user' },
+    })
+    childAgent.inbox.append('next-turn', own)
+    expect(childAgent.inbox.nextTurn).toEqual([inherited, own])
   })
 
-  it('replaces a pending message by identity across both lists', () => {
-    const session = Session.create(SessionId('replace-inbox'))
+  it('replaces a pending message by identity across both lists', async () => {
+    const { ctx, agent } = await inboxAgent('replace-inbox')
     const inserted: UserMessage[] = []
     const discarded: UserMessage[] = []
-    const inbox = new Inbox(session, {
-      claimed: () => {},
-      inserted: message => void inserted.push(message),
-      discarded: message => void discarded.push(message),
-    })
+    ctx.on('agent/inbox/inserted', ({ message }) => void inserted.push(message))
+    ctx.on('agent/inbox/discarded', ({ message }) => void discarded.push(message))
+    const { inbox } = agent
     const original = createUserMessage({
       content: [{ type: 'text', text: 'original' }],
       source: { kind: 'user' },
@@ -93,9 +118,9 @@ describe('Inbox', () => {
       .toThrow(`message "${replacement.id}" is already pending`)
   })
 
-  it('normalizes splice coordinates, rejects duplicate identities, and reports missing removals', () => {
-    const session = Session.create(SessionId('splice-inbox'))
-    const inbox = new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} })
+  it('normalizes splice coordinates, rejects duplicate identities, and reports missing removals', async () => {
+    const { agent } = await inboxAgent('splice-inbox')
+    const { inbox } = agent
     const first = createUserMessage({
       content: [{ type: 'text', text: 'first' }],
       source: { kind: 'user' },
@@ -104,22 +129,25 @@ describe('Inbox', () => {
       content: [{ type: 'text', text: 'second' }],
       source: { kind: 'user' },
     })
+    const prefixed = createUserMessage({
+      content: [{ type: 'text', text: 'prefixed' }],
+      source: { kind: 'user' },
+    })
 
     inbox.splice('next-turn', Number.NaN, Number.NaN, [first, second])
     expect(inbox.nextTurn).toEqual([first, second])
     expect(inbox.splice('next-turn', -1, 1, [])).toEqual([second])
+    inbox.prepend('next-turn', prefixed)
+    expect(inbox.nextTurn).toEqual([prefixed, first])
     expect(inbox.remove(second.id)).toBe(false)
     expect(() => { inbox.append('next-step', first) }).toThrow(`message "${first.id}" is already pending`)
   })
 
-  it('clears both pending lists as durable cancellations', () => {
-    const session = Session.create(SessionId('clear-inbox'))
+  it('clears both pending lists as durable cancellations', async () => {
+    const { ctx, session, agent } = await inboxAgent('clear-inbox')
     const discarded: UserMessage[] = []
-    const inbox = new Inbox(session, {
-      claimed: () => {},
-      inserted: () => {},
-      discarded: message => void discarded.push(message),
-    })
+    ctx.on('agent/inbox/discarded', ({ message }) => void discarded.push(message))
+    const { inbox } = agent
     const nextTurn = createUserMessage({ content: [{ type: 'text', text: 'turn' }], source: { kind: 'user' } })
     const nextStep = createUserMessage({ content: [{ type: 'text', text: 'step' }], source: { kind: 'user' } })
     inbox.append('next-turn', nextTurn)
@@ -140,6 +168,52 @@ describe('Inbox', () => {
     inbox.clear()
     expect(session.events).toHaveLength(beforeClear + 2)
   })
+
+  it('registers the durable Inbox projection from the Inbox service', async () => {
+    const ctx = new Context()
+    await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    const inboxFiber = ctx.plugin(InboxService)
+    await inboxFiber
+    const session = ctx.sessions.create(SessionId('inbox-projection'))
+    const agent = stubAgent('inbox-projection', { ctx, session })
+    Object.assign(agent, { inbox: ctx.inboxes.create(agent) })
+    const pending = createUserMessage({
+      content: [{ type: 'text', text: 'pending' }],
+      source: { kind: 'user' },
+    })
+
+    agent.inbox.append('next-turn', pending)
+
+    expect(ctx.sessionProjections.snapshot(session).values.inbox).toEqual({
+      'next-turn': [pending],
+      'next-step': [],
+    })
+    await inboxFiber.dispose()
+    expect(ctx.sessionProjections.snapshot(session).values).toEqual({})
+  })
+
+  it('uses the projection cell as the sole live state after direct durable appends', async () => {
+    const { ctx, session, agent } = await inboxAgent('inbox-live-projection')
+    const pending = createUserMessage({
+      content: [{ type: 'text', text: 'direct' }],
+      source: { kind: 'user' },
+    })
+    let observed: readonly UserMessage[] | undefined
+    ctx.on('session/event', (_session, event) => {
+      if (event.type === 'agent/inbox/spliced') observed = agent.inbox.nextTurn
+    })
+
+    session.append('agent/inbox/spliced', {
+      target: 'next-turn', start: 0, inserted: [pending],
+    })
+
+    expect(observed).toEqual([pending])
+    expect(agent.inbox.nextTurn).toEqual([pending])
+    expect(ctx.sessionProjections.snapshot(session).values.inbox).toEqual({
+      'next-turn': [pending], 'next-step': [],
+    })
+  })
 })
 
 describe('AgentRegistry', () => {

+ 3 - 0
packages/core/agent/tsconfig.json

@@ -23,6 +23,9 @@
     {
       "path": "../../core/session"
     },
+    {
+      "path": "../../session/session-projection"
+    },
     {
       "path": "../../core/system-prompt"
     },

+ 2 - 1
packages/e2b/e2b/tests/composition.e2e.ts

@@ -85,7 +85,7 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => {
         id: ownerId,
         options: {},
         session: ownerSession,
-        inbox: new Inbox(ownerSession, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+        inbox: undefined as never,
         status: 'idle',
         ctx,
         send() {},
@@ -96,6 +96,7 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => {
         runMaintenance: task => task(new AbortController().signal),
         whenIdle: () => Promise.resolve(),
       }
+      Object.assign(owner, { inbox: new Inbox(owner.ctx, owner) })
       const backend = new BashTerminalBackend(ctx, {
         backendType: 'shell', shellPath: '/bin/bash', shellArgs: ['--noprofile', '--norc', '-i'],
         rows: 24, cols: 80,

+ 2 - 0
packages/examples/agent-spine-demo/package.json

@@ -43,6 +43,7 @@
     "@deepseek-ai/dsh-llm-retry": "workspace:^",
     "@deepseek-ai/dsh-scope": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-session-projection": "workspace:^",
     "@deepseek-ai/dsh-session-title": "workspace:^",
     "@deepseek-ai/dsh-skill": "workspace:^",
     "@deepseek-ai/dsh-skill-filesystem": "workspace:^",
@@ -78,6 +79,7 @@
     "@deepseek-ai/dsh-sandbox-policy": "workspace:^",
     "@deepseek-ai/dsh-scope": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-session-projection": "workspace:^",
     "@deepseek-ai/dsh-session-title": "workspace:^",
     "@deepseek-ai/dsh-skill": "workspace:^",
     "@deepseek-ai/dsh-skill-filesystem": "workspace:^",

+ 4 - 0
packages/examples/agent-spine-demo/src/index.ts

@@ -13,12 +13,14 @@ import Timer from '@deepseek-ai/cordis-plugin-timer'
 import z from '@deepseek-ai/schemastery'
 import LlmRuntime from '@deepseek-ai/dsh-llm'
 import SessionStore from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import SessionTitleService, { type Config as SessionTitleConfig } from '@deepseek-ai/dsh-session-title'
 import SystemPrompt, { type Config as SystemPromptConfig } from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
 import SkillRegistry, { type Config as SkillRegistryConfig } from '@deepseek-ai/dsh-skill'
 import * as SkillFileSystem from '@deepseek-ai/dsh-skill-filesystem'
 import AgentRegistry from '@deepseek-ai/dsh-agent'
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
 import GoalService, { type Config as GoalDomainConfig } from '@deepseek-ai/dsh-goal'
 import * as goalSession from '@deepseek-ai/dsh-goal-round-driver'
 import * as toolGoal from '@deepseek-ai/dsh-tool-goal'
@@ -220,6 +222,7 @@ export function apply(ctx: Context, config: Config): void {
   ctx.plugin(Timer)
   ctx.plugin(LlmRuntime)
   ctx.plugin(SessionStore)
+  ctx.plugin(SessionProjectionRegistry)
   ctx.plugin(SessionTitleService, config.sessionTitle ?? EXAMPLE_SESSION_TITLE_CONFIG)
   // Owner schemas resolve defaults; forward toolOrder only when explicitly set.
   ctx.plugin(SystemPrompt, {
@@ -235,6 +238,7 @@ export function apply(ctx: Context, config: Config): void {
     ctx.plugin(SkillFileSystem, Object.assign({}, config.skills?.filesystem, { dshHome }))
   }
   ctx.plugin(AgentRegistry)
+  ctx.plugin(InboxService)
   ctx.plugin(llmRetry)
   if (config.goals !== undefined && config.goals !== false) {
     ctx.plugin(GoalService, config.goals.domain ?? {})

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

@@ -715,6 +715,19 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
       },
     ],
   },
+  {
+    key: 'inboxes',
+    summary: 'Root Inbox service: creates live inboxes and owns their durable projection.',
+    description: 'Root Inbox service: creates live inboxes and owns their durable projection.',
+    methods: [
+      {
+        signature: 'create(agent: Agent): Inbox',
+        description: 'Restore one live Inbox for an agent and publish its committed mutations.',
+        parameters: [{ name: 'agent', description: 'agent that owns the durable session and live Inbox events.' }],
+        returns: 'the restored Inbox.',
+      },
+    ],
+  },
   {
     key: 'invariants',
     summary: 'Package-owned invariant registry with global and regex-based selection.',
@@ -1110,7 +1123,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
   {
     key: 'sessionProjections',
     summary: '`ctx.sessionProjections`: the projection unit table and its drive.',
-    description: '`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit\'s `apply` (eager drive), and a changed state reference notifies the change feed with the schema-validated view. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin\'s key disappears from snapshots and clients read it as capability absence. Domain plugins register under `ctx.inject([\'sessionProjections\'], …)` so headless assemblies without the registry stay unaffected. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads.',
+    description: '`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit\'s `apply` (eager drive), and a changed state reference notifies the change feed with the schema-validated view. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the full in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin\'s key disappears from snapshots and clients read it as capability absence. A domain that requires this capability declares a Cordis service dependency; an optional contributor may register under `ctx.inject([\'sessionProjections\'], …)`. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads.',
     methods: [
       {
         signature: 'register<K extends keyof SessionProjectionMap, S>(definition: ProjectionDefinition<K, S>): () => void',
@@ -3241,11 +3254,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'Inbox',
-    declaration: 'export class Inbox {\n    constructor(private readonly session: Session, private readonly notifications: InboxNotifications);\n    get nextTurn(): readonly UserMessage[];\n    get nextStep(): readonly UserMessage[];\n    get hasPending(): boolean;\n    clear(): void;\n    claim(target: InboxTarget, turn: number): UserMessage[];\n    append(target: InboxTarget, message: UserMessage): void;\n    prepend(target: InboxTarget, message: UserMessage): void;\n    replace(messageId: MessageId, newMessage: UserMessage): boolean;\n    remove(messageId: MessageId): boolean;\n    splice(target: InboxTarget, start: number, deleteCount: number, inserted: UserMessage[]): UserMessage[];\n}',
-  },
-  {
-    name: 'InboxNotifications',
-    declaration: 'export interface InboxNotifications {\n    inserted(message: UserMessage): void;\n    discarded(message: UserMessage): void;\n    claimed(message: UserMessage, turn: number): void;\n}',
+    declaration: 'export class Inbox {\n    constructor(private readonly ctx: Context, private readonly agent: Agent);\n    get nextTurn(): readonly UserMessage[];\n    get nextStep(): readonly UserMessage[];\n    get hasPending(): boolean;\n    clear(): void;\n    claim(target: InboxTarget, turn: number): UserMessage[];\n    append(target: InboxTarget, message: UserMessage): void;\n    prepend(target: InboxTarget, message: UserMessage): void;\n    replace(messageId: MessageId, newMessage: UserMessage): boolean;\n    remove(messageId: MessageId): boolean;\n    splice(target: InboxTarget, start: number, deleteCount: number, inserted: UserMessage[]): UserMessage[];\n}',
   },
   {
     name: 'InboxTarget',

+ 2 - 2
packages/feedback/command-feedback/tests/command-feedback.spec.ts

@@ -43,13 +43,12 @@ class FakeTelemetry extends SessionTelemetryBackend {
 /** Build a live idle agent over a store-owned session, as an app's spine does. */
 function stubAgent(ctx: Context, id: string): { agent: Agent; session: Session } {
   const session = ctx.sessions.create(SessionId(id))
-  const inbox = new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} })
   let status: AgentStatus = 'idle'
   const agent: Agent = {
     id: session.id,
     options: {},
     session,
-    inbox,
+    inbox: undefined as never,
     ctx: new Context(),
     get status() { return status },
     send: () => {},
@@ -60,6 +59,7 @@ function stubAgent(ctx: Context, id: string): { agent: Agent; session: Session }
     runMaintenance: task => task(new AbortController().signal),
     whenIdle() { return Promise.resolve() },
   }
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
   return { agent, session }
 }
 

+ 2 - 2
packages/feedback/command-feedback/tests/loader-composition.spec.ts

@@ -29,13 +29,12 @@ function agent(ctx: Context): Agent {
   const scope = ctx.plugin(() => {})
   const id = SessionId('feedback-loader-agent')
   const session = ctx.sessions.create(id)
-  const inbox = new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} })
   let status: AgentStatus = 'idle'
   const value: Agent = {
     id,
     options: {},
     session,
-    inbox,
+    inbox: undefined as never,
     ctx: scope.ctx,
     get status() { return status },
     send: () => {},
@@ -46,6 +45,7 @@ function agent(ctx: Context): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
+  Object.assign(value, { inbox: new Inbox(value.ctx, value) })
   ctx.agents.register(value)
   return value
 }

+ 2 - 1
packages/fs/tool-str-replace-editor/tests/tools.spec.ts

@@ -33,7 +33,7 @@ function agent(ctx: Context, cwd: string): Agent {
     id,
     options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: undefined as never,
     status: 'idle',
     ctx: scope.ctx,
     send: () => {},
@@ -44,6 +44,7 @@ function agent(ctx: Context, cwd: string): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
+  Object.assign(value, { inbox: new Inbox(value.ctx, value) })
   ctx.agents.register(value)
   return value
 }

+ 3 - 3
packages/goal/command-goal/tests/command-goal.spec.ts

@@ -20,23 +20,23 @@ interface Harness {
 function stubAgent(ctx: Context, id: string): { agent: Agent; session: Session } {
   // Store-created: the command executor durably logs lifecycle events on it.
   const session = ctx.sessions.create(SessionId(id))
-  const inbox = new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} })
   let status: AgentStatus = 'idle'
   const agent: Agent = {
     id: session.id,
     options: {},
     session,
-    inbox,
+    inbox: undefined as never,
     ctx: new Context(),
     get status() { return status },
     send: () => {},
     followup: () => {},
     steer: () => {},
-    inject(input) { inbox.append('next-step', input) },
+    inject(input) { this.inbox.append('next-step', input) },
     cancel() { status = 'idle' },
     runMaintenance: task => task(new AbortController().signal),
     whenIdle() { return Promise.resolve() },
   }
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
   return { agent, session }
 }
 

+ 50 - 19
packages/goal/goal/tests/goal.spec.ts

@@ -1,9 +1,11 @@
 import { describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
-import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent'
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import { createUserMessage, HarnessError } from '@deepseek-ai/dsh-llm'
 import SessionStore, { Session, SessionId, type UserMessage } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import GoalService, {
   GoalError,
   GoalId,
@@ -17,6 +19,12 @@ interface StubAgent {
   session: Session
 }
 
+const isolatedInboxCtx = new Context()
+await isolatedInboxCtx.plugin(SessionStore)
+await isolatedInboxCtx.plugin(SessionProjectionRegistry)
+await isolatedInboxCtx.plugin(InboxService)
+const sessionStubs = new WeakMap<Session, StubAgent>()
+
 /** Number the next balanced test-fixture turn. */
 function nextTurn(session: Session): number {
   return session.events.reduce((max, event) => event.type === 'turn/start' ? Math.max(max, event.data.turn) : max, 0) + 1
@@ -24,41 +32,59 @@ function nextTurn(session: Session): number {
 
 /** Mirror the public Agent.inject contract for domain tests. */
 function appendInjection(session: Session, input: UserMessage): void {
-  new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }).append('next-step', input)
+  stubAgentForSession(session).agent.inbox.append('next-step', input)
 }
 
 /** Build a registry-compatible agent around one concrete session. */
-function stubAgentForSession(session: Session): StubAgent {
+function stubAgentForSession(session: Session, suppliedCtx?: Context): StubAgent {
+  const existing = sessionStubs.get(session)
+  if (existing !== undefined) return existing
   const id = session.id
-  const inbox = new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} })
+  const agentCtx = suppliedCtx ?? isolatedInboxCtx
+  if (suppliedCtx === undefined) {
+    agentCtx.sessions.enter(session)
+  }
   const agent: Agent = {
     id,
     options: {},
     session,
-    inbox,
-    ctx: new Context(),
+    inbox: undefined as never,
+    ctx: agentCtx,
     status: 'idle',
     send: () => {},
     followup: () => {},
     steer: () => {},
-    inject(input) { inbox.append('next-step', input) },
+    inject(input) { this.inbox.append('next-step', input) },
     cancel() {},
     runMaintenance: task => task(new AbortController().signal),
     whenIdle() { return Promise.resolve() },
   }
-  return {
+  Object.assign(agent, { inbox: agentCtx.inboxes.create(agent) })
+  const stub = {
     agent,
     session,
   }
+  sessionStubs.set(session, stub)
+  return stub
 }
 
 /** Build a registry-compatible agent around a fresh session. */
-function stubAgent(rawId: string, seed?: readonly import('@deepseek-ai/dsh-session').SessionEvent[]): StubAgent {
-  return stubAgentForSession(Session.create(SessionId(rawId), seed))
+function stubAgent(
+  rawId: string,
+  seed?: readonly import('@deepseek-ai/dsh-session').SessionEvent[],
+  ctx?: Context,
+): StubAgent {
+  const session = ctx === undefined
+    ? Session.create(SessionId(rawId), seed)
+    : ctx.sessions.create(SessionId(rawId), { ...(seed === undefined ? {} : { seed }) })
+  return stubAgentForSession(session, ctx)
 }
 
 async function harness(config: { defaultMaxGoalRounds?: number } = {}) {
   const ctx = new Context()
+  await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(GoalService, config)
   const stub = stubAgent(`goal-test-${Math.random()}`)
@@ -168,14 +194,16 @@ describe('GoalService creation and replay', () => {
   it('inherits the completed-turn goal prefix through SessionStore.fork with child activation disarmed', async () => {
     const ctx = new Context()
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(AgentRegistry)
     await ctx.plugin(GoalService)
-    const parent = stubAgentForSession(ctx.sessions.create(SessionId('goal-fork-parent')))
+    const parent = stubAgentForSession(ctx.sessions.create(SessionId('goal-fork-parent')), ctx)
     ctx.agents.register(parent.agent)
     const goal = ctx.goals.create(parent.agent, { objective: 'inherit through fork', maxGoalRounds: 5 })
     appendRound(parent.session, goal, 1)
 
-    const child = stubAgentForSession(ctx.sessions.fork(parent.session))
+    const child = stubAgentForSession(ctx.sessions.fork(parent.session), ctx)
     ctx.agents.register(child.agent)
     expect(ctx.goals.get(child.agent)).toMatchObject({
       id: goal.id,
@@ -235,7 +263,7 @@ describe('GoalService creation and replay', () => {
     const { ctx, agent } = await harness()
     // A same-id agent backed by a different session object — the live-instance
     // check must reject it even though the ids match.
-    const impostor = stubAgentForSession(Session.create(agent.id)).agent
+    const impostor = { ...agent, session: Session.create(agent.id) } as Agent
     expect(() => ctx.goals.get(impostor)).toThrow(expect.objectContaining({ code: 'GOAL_AGENT_NOT_LIVE' }))
     expect(() => ctx.goals.create(impostor, { objective: 'no' })).toThrow(expect.objectContaining({
       code: 'GOAL_AGENT_NOT_LIVE',
@@ -422,9 +450,11 @@ describe('GoalService mutations', () => {
   it('publishes a mutation consistently to a reentrant session observer', async () => {
     const ctx = new Context()
     await ctx.plugin(SessionStore)
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
     await ctx.plugin(AgentRegistry)
     await ctx.plugin(GoalService)
-    const stub = stubAgentForSession(ctx.sessions.create(SessionId('goal-reentrant-observer')))
+    const stub = stubAgentForSession(ctx.sessions.create(SessionId('goal-reentrant-observer')), ctx)
     ctx.agents.register(stub.agent)
     let observed: ReturnType<GoalService['get']>
     ctx.on('session/event', (session, event) => {
@@ -481,7 +511,7 @@ describe('GoalService mutations', () => {
     })
   })
 
-  it('reports the same corrupt unseen event after committing its valid prefix', async () => {
+  it('rejects a corrupt append while preserving the valid prefix', async () => {
     const { ctx, agent, session } = await harness()
     expect(ctx.goals.get(agent)).toBeUndefined()
     const change: GoalSnapshotChangeMeta = {
@@ -500,10 +530,11 @@ describe('GoalService mutations', () => {
       updatedAt: 12,
     }
     session.append('goal/change', change)
-    session.append('goal/change', { ...change, operation: 'edit', extra: true } as never)
+    expect(() => {
+      session.append('goal/change', { ...change, operation: 'edit', extra: true } as never)
+    }).toThrow('snapshot change must have exactly')
 
-    expect(() => ctx.goals.get(agent)).toThrow('snapshot change must have exactly')
-    expect(() => ctx.goals.get(agent)).toThrow('snapshot change must have exactly')
+    expect(ctx.goals.get(agent)).toMatchObject({ id: change.goal.id, objective: 'valid prefix' })
   })
 })
 
@@ -570,7 +601,7 @@ describe('goal replay validation', () => {
       content: [{ type: 'text', text: 'unrelated pending context' }],
       source: { kind: 'plugin', plugin: 'test' },
     })
-    const inbox = new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} })
+    const inbox = stubAgentForSession(session).agent.inbox
     inbox.append('next-step', message)
     expect(inbox.remove(message.id)).toBe(true)
     expect(foldGoal(session.events)).toMatchObject({ goal: { id: change.goal.id, revision: 1 } })

+ 11 - 8
packages/goal/goal/tests/projection.spec.ts

@@ -10,7 +10,7 @@
 
 import { describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry from '@deepseek-ai/dsh-agent'
 import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
 import type { UserMessage } from '@deepseek-ai/dsh-session'
@@ -31,19 +31,18 @@ interface Bench {
 /** Register a minimal registry-compatible live agent over a store session. */
 function liveAgent(ctx: Context, session: Session): Agent {
   const status: AgentStatus = 'idle'
-  const inbox = new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} })
   const agent: Agent = {
     id: session.id,
     options: {},
     session,
-    inbox,
+    inbox: { nextTurn: [], nextStep: [], hasPending: false } as never,
     ctx,
     get status() { return status },
     send: () => {},
     followup: () => {},
     steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }),
     inject(input: UserMessage) {
-      inbox.append('next-step', input)
+      this.inbox.append('next-step', input)
     },
     cancel() {},
     runMaintenance: task => task(new AbortController().signal),
@@ -130,10 +129,14 @@ describe('goal projection unit', () => {
     const created = bench.ctx.goals.create(bench.agent, { objective: 'stay cleared' })
     bench.ctx.goals.clear(bench.agent, created)
 
-    bench.agent.inbox.prepend('next-step', createUserMessage({
-      content: [{ type: 'text', text: 'unrelated pending context' }],
-      source: { kind: 'plugin', plugin: 'test' },
-    }))
+    bench.session.append('agent/inbox/spliced', {
+      target: 'next-step',
+      start: 0,
+      inserted: [createUserMessage({
+        content: [{ type: 'text', text: 'unrelated pending context' }],
+        source: { kind: 'plugin', plugin: 'test' },
+      })],
+    })
 
     expect(bench.tailValues().goal).toBeNull()
     expect(foldGoal(bench.session.events).goal).toBeUndefined()

+ 25 - 8
packages/goal/tool-goal/tests/tool-goal.spec.ts

@@ -1,13 +1,15 @@
 import { describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import Loader from '@deepseek-ai/cordis-plugin-loader'
-import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent'
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
 import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
 import GoalService, { GoalId } from '@deepseek-ai/dsh-goal'
 import type { GoalRef } from '@deepseek-ai/dsh-goal'
 import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
 import type { MessageSource } from '@deepseek-ai/dsh-llm'
-import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session'
+import SessionStore, { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime from '@deepseek-ai/dsh-tools'
 import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools'
@@ -21,17 +23,28 @@ interface StubAgent {
   setStatus(status: AgentStatus): void
 }
 
+const isolatedInboxCtx = new Context()
+await isolatedInboxCtx.plugin(SessionStore)
+await isolatedInboxCtx.plugin(SessionProjectionRegistry)
+await isolatedInboxCtx.plugin(InboxService)
+
 /** Build one registry-compatible live agent whose injections enter the durable inbox. */
-function stubAgent(rawId: string, supplied?: Session): StubAgent {
-  const session = supplied ?? Session.create(SessionId(rawId))
+function stubAgent(rawId: string, supplied?: Session, suppliedCtx?: Context): StubAgent {
+  const agentCtx = suppliedCtx ?? isolatedInboxCtx
+  const session = supplied ?? (suppliedCtx === undefined
+    ? agentCtx.sessions.create(SessionId(rawId))
+    : suppliedCtx.sessions.create(SessionId(rawId)))
+  if (suppliedCtx === undefined) {
+    if (agentCtx.sessions.get(session.id) !== session) agentCtx.sessions.enter(session)
+  }
   let status: AgentStatus = 'running'
   const agent: Agent = {
     id: session.id,
     options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: undefined as never,
     get status() { return status },
-    ctx: new Context(),
+    ctx: agentCtx,
     send: () => {},
     followup: () => {},
     steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }),
@@ -42,6 +55,7 @@ function stubAgent(rawId: string, supplied?: Session): StubAgent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle() { return Promise.resolve() },
   }
+  Object.assign(agent, { inbox: agentCtx.inboxes.create(agent) })
   return { agent, session, setStatus(value) { status = value } }
 }
 
@@ -71,12 +85,15 @@ function closeTurn(stub: StubAgent, turn: number): void {
 
 async function harness(config: toolGoal.Config = {}) {
   const ctx = new Context()
+  await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(AgentRegistry)
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(GoalService)
   const fiber = await ctx.plugin(toolGoal, config)
-  const root = stubAgent(`goal-tool-root-${Math.random()}`)
+  const root = stubAgent(`goal-tool-root-${Math.random()}`, undefined, ctx)
   ctx.agents.register(root.agent)
   return { ctx, fiber, root }
 }
@@ -243,7 +260,7 @@ describe('goal tool execution authority', () => {
     openTurn(root, { kind: 'user' })
     // A distinct agent object over root's exact session: same id, not the live
     // registered instance, so the executor must reject it.
-    const stale = stubAgent('goal-tool-stale', root.agent.session).agent
+    const stale = stubAgent('goal-tool-stale', root.agent.session, ctx).agent
     const staleResult = await execute(ctx, 'get_goal', {}, stale, stale)
     expect(staleResult.error?.info?.code).toBe('GOAL_TOOL_DRIVER_REQUIRED')
 

+ 5 - 11
packages/host/apiproxy/src/api-proxy.ts

@@ -16,7 +16,7 @@ import { contentHasImage, createUserMessage, freezeMessage, ReasoningEffortId }
 import { errorChain } from '@deepseek-ai/dsh-llm'
 import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
 import { isAppendSurfaceEvent, isJsonValue } from '@deepseek-ai/dsh-session'
-import type { JsonValue, Session, SessionEvent, SessionEventMap, SessionHeader, SessionId, UserMessage } from '@deepseek-ai/dsh-session'
+import type { JsonValue, Session, SessionEvent, SessionHeader, SessionId, UserMessage } from '@deepseek-ai/dsh-session'
 import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
 import { SessionQueryError, type SessionSearchCursor } from '@deepseek-ai/dsh-session-query'
 import { SubagentError } from '@deepseek-ai/dsh-subagent'
@@ -1267,16 +1267,10 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
     })
   })
 
-  /** Project both durable inbox lists, optionally including the splice currently being emitted. */
-  const queueItems = (
-    agent: Agent,
-    splice?: SessionEventMap['agent/inbox/spliced'],
-  ): QueuedInboxItem[] => {
+  /** Project both durable inbox lists from their committed state. */
+  const queueItems = (agent: Agent): QueuedInboxItem[] => {
     const project = (target: 'next-turn' | 'next-step'): readonly UserMessage[] => {
-      const messages = target === 'next-turn' ? agent.inbox.nextTurn : agent.inbox.nextStep
-      return splice?.target === target
-        ? messages.toSpliced(splice.start, splice.removedCount ?? 0, ...splice.inserted)
-        : messages
+      return target === 'next-turn' ? agent.inbox.nextTurn : agent.inbox.nextStep
     }
     return [
       ...project('next-turn').map(message => ({ id: message.id, placement: 'queued' as const, message })),
@@ -1295,7 +1289,7 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
     if (event.type !== 'agent/inbox/spliced') return
     const agent = ctx.agents.get(session.id)
     if (agent?.session !== session) return
-    broadcast({ type: 'session/queue', sessionId: session.id, items: queueItems(agent, event.data) })
+    broadcast({ type: 'session/queue', sessionId: session.id, items: queueItems(agent) })
   })
 
   /** Remove a wait before settling it: synchronous deletion makes the first claimant win. */

+ 11 - 3
packages/host/apiproxy/tests/api-proxy-jobs.spec.ts

@@ -9,7 +9,7 @@
 
 import { describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
 import type { Session } from '@deepseek-ai/dsh-session'
@@ -56,11 +56,19 @@ async function harness(withRegistry: boolean): Promise<{ ctx: Context; session:
   const session = ctx.sessions.create()
   const agent = {
     id: session.id,
+    options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: { nextTurn: [], nextStep: [], hasPending: false } as never,
     status: 'idle',
     ctx,
-  } as Agent
+    send() {},
+    followup() {},
+    steer() {},
+    inject() {},
+    cancel() {},
+    runMaintenance: task => task(new AbortController().signal),
+    whenIdle: () => Promise.resolve(),
+  } satisfies Agent
   ctx.agents.register(agent)
   return { ctx, session, agent }
 }

+ 96 - 4
packages/host/apiproxy/tests/api-proxy-projections.spec.ts

@@ -10,7 +10,9 @@
 import { describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import { z } from 'zod'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry from '@deepseek-ai/dsh-agent'
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import type {} from '@deepseek-ai/dsh-agent/inbox-projection'
 import { AttachmentStore } from '@deepseek-ai/dsh-attachment'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
@@ -52,10 +54,28 @@ async function harness(withRegistry: boolean): Promise<{ ctx: Context; session:
   await ctx.plugin(SessionStore)
   await ctx.plugin(UserQuestionService)
   await ctx.plugin(AgentRegistry)
-  if (withRegistry) await ctx.plugin(SessionProjectionRegistry)
+  if (withRegistry) {
+    await ctx.plugin(SessionProjectionRegistry)
+    await ctx.plugin(InboxService)
+  }
   const session = ctx.sessions.create()
-  // The gateway reads both the session and durable inbox baseline.
-  ctx.agents.register({ id: session.id, session, inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }), status: 'idle', ctx } as Agent)
+  const agent = {
+    id: session.id,
+    options: {},
+    session,
+    inbox: { nextTurn: [], nextStep: [], hasPending: false } as never,
+    status: 'idle',
+    ctx,
+    send() {},
+    followup() {},
+    steer() {},
+    inject() {},
+    cancel() {},
+    runMaintenance: task => task(new AbortController().signal),
+    whenIdle: () => Promise.resolve(),
+  } satisfies Agent
+  if (withRegistry) Object.assign(agent, { inbox: ctx.inboxes.create(agent) })
+  ctx.agents.register(agent)
   return { ctx, session }
 }
 
@@ -87,6 +107,78 @@ describe('session.history projections block', () => {
     expect(events.at(-1)?.event.seq).toBe(projections?.asOfSeq)
   })
 
+  it('reconstructs a cold persisted queue without publishing or resuming an Agent', async () => {
+    const { ctx } = await harness(true)
+    const coldId = SessionId('cold-persisted-queue')
+    const meta = { version: 0 as const, id: coldId, createdAt: 1, cwd: '/tmp' }
+    const message = createUserMessage({
+      content: [{ type: 'text', text: 'survive process restart' }],
+      source: { kind: 'user' },
+    })
+    const events = [{
+      type: 'agent/inbox/spliced',
+      seq: 0,
+      time: 2,
+      data: { target: 'next-turn', start: 0, inserted: [message] },
+    }] as const
+    ctx.provide('sessionPersistence', {
+      list: () => Promise.resolve([meta]),
+      inspect: () => Promise.resolve({ meta, events }),
+    } as never)
+    const response = await api(ctx).sessions.history(request({ sessionId: coldId }))
+    if (!response.result.ok) throw new Error('history failed')
+
+    expect(response.result.value.projections?.values.inbox).toEqual({
+      'next-turn': [message],
+      'next-step': [],
+    })
+    expect(ctx.agents.get(coldId)).toBeUndefined()
+    expect(ctx.sessions.get(coldId)).toBeUndefined()
+  })
+
+  it('removes claimed steering from the pending Inbox projection immediately', async () => {
+    const { ctx, session } = await harness(true)
+    const proxy = api(ctx)
+    const message = createUserMessage({
+      content: [{ type: 'text', text: 'apply this now' }],
+      source: { kind: 'user' },
+    })
+    const agent = ctx.agents.get(session.id)
+    if (agent === undefined) throw new Error('missing Agent')
+    agent.inbox.append('next-step', message)
+    agent.inbox.claim('next-step', 1)
+
+    const during = await proxy.sessions.history(request({ sessionId: session.id }))
+    if (!during.result.ok) throw new Error('history failed')
+    expect(during.result.value.projections?.values.inbox).toEqual({
+      'next-turn': [],
+      'next-step': [],
+    })
+
+    session.append('user/message', message, { surfaceOp: 'append' })
+    const settled = await proxy.sessions.history(request({ sessionId: session.id }))
+    if (!settled.result.ok) throw new Error('history failed')
+    expect(settled.result.value.projections?.values.inbox).toEqual({
+      'next-turn': [],
+      'next-step': [],
+    })
+
+    const rejected = createUserMessage({
+      content: [{ type: 'text', text: 'reject this pre-step' }],
+      source: { kind: 'user' },
+    })
+    session.append('turn/start', { turn: 1 })
+    agent.inbox.append('next-step', rejected)
+    agent.inbox.claim('next-step', 1)
+    session.append('turn/end', { turn: 1, reason: { kind: 'blocked' } })
+    const closed = await proxy.sessions.history(request({ sessionId: session.id }))
+    if (!closed.result.ok) throw new Error('history failed')
+    expect(closed.result.value.projections?.values.inbox).toEqual({
+      'next-turn': [],
+      'next-step': [],
+    })
+  })
+
   it('publishes the attachments imageLimits as a constant unit while both seams are composed', async () => {
     const { ctx, session } = await harness(true)
     const limits = {

+ 4 - 2
packages/host/apiproxy/tests/api-proxy-workspace.spec.ts

@@ -40,11 +40,11 @@ async function nextHostFrame(
 }
 
 function stubAgent(session: Session): Agent {
-  return {
+  const agent: Agent = {
     id: session.id,
     options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: undefined as never,
     status: 'idle',
     ctx: new Context(),
     send: () => {},
@@ -55,6 +55,8 @@ function stubAgent(session: Session): Agent {
     runMaintenance: job => job(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  return agent
 }
 
 /** Compose the API over real Session, Agent, Storage, Domain, and Workspace services. */

+ 2 - 1
packages/jobs/jobs-local/tests/jobs.spec.ts

@@ -34,7 +34,7 @@ function stubAgent(ctx: Context, rawId: string, presetScope?: ScopeKey): Agent {
     id,
     options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: undefined as never,
     status: 'idle' as const,
     ctx: agentCtx,
     send: () => {},
@@ -45,6 +45,7 @@ function stubAgent(ctx: Context, rawId: string, presetScope?: ScopeKey): Agent {
     runMaintenance: <T>(job: (signal: AbortSignal) => Promise<T>) => job(new AbortController().signal),
     whenIdle() { return Promise.resolve() },
   }
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
   agentScopeDisposers.set(agent, async () => { await scopeFiber.dispose() })
   return agent
 }

+ 1 - 0
packages/llm/llm-retry/package.json

@@ -59,6 +59,7 @@
     "@deepseek-ai/dsh-llm-deepseek": "workspace:^",
     "@deepseek-ai/dsh-llm-mock-server": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-session-projection": "workspace:^",
     "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
     "@deepseek-ai/dsh-session-persistence-sqlite": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",

+ 6 - 0
packages/llm/llm-retry/tests/loader-composition.spec.ts

@@ -7,10 +7,12 @@ import { Context } from '@deepseek-ai/cordis'
 import Loader from '@deepseek-ai/cordis-plugin-loader'
 import Include from '@deepseek-ai/cordis-plugin-include'
 import AgentRegistry from '@deepseek-ai/dsh-agent'
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
 import AgentLoop from '@deepseek-ai/dsh-agent-loop'
 import LlmRuntime, { createUserMessage, LlmAdapter, LlmError, resolveRetryPolicy  } from '@deepseek-ai/dsh-llm'
 import type { GenerateOptions, ResolvedRetryPolicy, StreamChunk } from '@deepseek-ai/dsh-llm'
 import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime from '@deepseek-ai/dsh-tools'
 import * as retry from '../src/index.ts'
@@ -60,9 +62,11 @@ async function loadYaml(lines: readonly string[]): Promise<Context> {
   const modules = new Map<string, unknown>([
     ['@deepseek-ai/dsh-llm', LlmRuntime],
     ['@deepseek-ai/dsh-session', SessionStore],
+    ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
     ['@deepseek-ai/dsh-system-prompt', SystemPrompt],
     ['@deepseek-ai/dsh-tools', ToolRuntime],
     ['@deepseek-ai/dsh-agent', AgentRegistry],
+    ['@deepseek-ai/dsh-agent/inbox', InboxService],
     ['@deepseek-ai/dsh-llm-retry', retry],
     ['@deepseek-ai/dsh-agent-loop', AgentLoop],
   ])
@@ -89,9 +93,11 @@ describe('real Loader composition', () => {
     const loaded = await loadYaml([
       "- name: '@deepseek-ai/dsh-llm'",
       "- name: '@deepseek-ai/dsh-session'",
+      "- name: '@deepseek-ai/dsh-session-projection'",
       "- name: '@deepseek-ai/dsh-system-prompt'",
       "- name: '@deepseek-ai/dsh-tools'",
       "- name: '@deepseek-ai/dsh-agent'",
+      "- name: '@deepseek-ai/dsh-agent/inbox'",
       "- name: '@deepseek-ai/dsh-llm-retry'",
       "- name: '@deepseek-ai/dsh-agent-loop'",
     ])

+ 5 - 10
packages/llm/llm-retry/tests/retry.spec.ts

@@ -1,7 +1,7 @@
 import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import type { Fiber } from '@deepseek-ai/cordis'
-import LlmRuntime, { createUserMessage, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, resolveRetryPolicy  } from '@deepseek-ai/dsh-llm'
+import { createUserMessage, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, resolveRetryPolicy  } from '@deepseek-ai/dsh-llm'
 import type {
   AlwaysRetryPolicyConfig,
   BackoffConfig,
@@ -11,14 +11,13 @@ import type {
   RetryPolicyConfig,
   StreamChunk,
 } from '@deepseek-ai/dsh-llm'
-import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
+import { SessionId } from '@deepseek-ai/dsh-session'
 import type { SessionEvent, SessionEventMap } from '@deepseek-ai/dsh-session'
 import type { LlmRetryEventData } from '@deepseek-ai/dsh-llm-retry/types'
-import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
-import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
-import AgentRegistry from '@deepseek-ai/dsh-agent'
+import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
 import type { Agent, RequestErrorAction } from '@deepseek-ai/dsh-agent'
 import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
 import * as retry from '../src/index.ts'
 
 type ScriptEntry = Error | Iterable<StreamChunk> | AsyncIterable<StreamChunk>
@@ -105,11 +104,7 @@ async function harness(
   internals: retry.RetryInternals = {},
 ): Promise<{ ctx: Context; retryFiber: Fiber; disposeAdapter: () => void }> {
   const ctx = new Context()
-  await ctx.plugin(LlmRuntime)
-  await ctx.plugin(SessionStore)
-  await ctx.plugin(SystemPrompt)
-  await ctx.plugin(ToolRuntime)
-  await ctx.plugin(AgentRegistry)
+  await mountAgentLoopTestDependencies(ctx)
   beforeRetry?.(ctx)
   adapter.configureRetryPolicies(policies)
   const retryFiber = await ctx.plugin(Object.assign((inner: Context) => {

+ 4 - 0
packages/plan/plan-mode/tests/integration.spec.ts

@@ -1,7 +1,9 @@
 import { describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
 import LlmRuntime, { createUserMessage, type StreamChunk  } from '@deepseek-ai/dsh-llm'
 import SessionStore, { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
 import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
@@ -23,6 +25,8 @@ async function harness(adapter: MockAdapter): Promise<Context> {
   const ctx = new Context()
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)

+ 1 - 0
packages/preset/agent-presets/package.json

@@ -64,6 +64,7 @@
     "@deepseek-ai/dsh-home-paths": "workspace:^",
     "@deepseek-ai/dsh-scope": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-session-projection": "workspace:^",
     "@deepseek-ai/dsh-settings": "workspace:^",
     "@deepseek-ai/dsh-settings-file": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",

+ 4 - 0
packages/preset/agent-presets/tests/invariant.spec.ts

@@ -1,10 +1,12 @@
 import { dirname, join } from 'node:path'
 import { fileURLToPath, pathToFileURL } from 'node:url'
 import { Context } from '@deepseek-ai/cordis'
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
 import Loader from '@deepseek-ai/cordis-plugin-loader'
 import Include from '@deepseek-ai/cordis-plugin-include'
 import LlmRuntime from '@deepseek-ai/dsh-llm'
 import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime from '@deepseek-ai/dsh-tools'
 import AgentRegistry, { assembleContextFor } from '@deepseek-ai/dsh-agent'
@@ -27,6 +29,8 @@ async function harness(roster: Partial<Config> = {}): Promise<Context> {
   ctx.loader.builtins.include = Include
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt, { persona: '' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)

+ 8 - 0
packages/preset/agent-presets/tests/mount.spec.ts

@@ -3,10 +3,12 @@ import { tmpdir } from 'node:os'
 import { dirname, join } from 'node:path'
 import { fileURLToPath, pathToFileURL } from 'node:url'
 import { Context } from '@deepseek-ai/cordis'
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
 import Loader from '@deepseek-ai/cordis-plugin-loader'
 import Include from '@deepseek-ai/cordis-plugin-include'
 import LlmRuntime from '@deepseek-ai/dsh-llm'
 import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime from '@deepseek-ai/dsh-tools'
 import AgentRegistry, { assembleContextFor, type Agent } from '@deepseek-ai/dsh-agent'
@@ -45,6 +47,8 @@ async function harness(roster: Config = { default: 'standard', roots: ROOTS, inc
   ctx.loader.builtins.include = Include
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt, { persona: '' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)
@@ -414,6 +418,8 @@ describe('the preset file is an input, never a persistence target', () => {
     scoped.loader.builtins.include = Include
     await scoped.plugin(LlmRuntime)
     await scoped.plugin(SessionStore)
+    await scoped.plugin(SessionProjectionRegistry)
+    await scoped.plugin(InboxService)
     await scoped.plugin(SystemPrompt, { persona: '' })
     await scoped.plugin(ToolRuntime)
     await scoped.plugin(AgentRegistry)
@@ -579,6 +585,8 @@ describe('replacing a composition', () => {
     scoped.loader.builtins.include = Include
     await scoped.plugin(LlmRuntime)
     await scoped.plugin(SessionStore)
+    await scoped.plugin(SessionProjectionRegistry)
+    await scoped.plugin(InboxService)
     await scoped.plugin(SystemPrompt, { persona: '' })
     await scoped.plugin(ToolRuntime)
     await scoped.plugin(AgentRegistry)

+ 4 - 0
packages/preset/agent-presets/tests/settings.spec.ts

@@ -16,7 +16,9 @@ import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
 import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime from '@deepseek-ai/dsh-tools'
 import AgentRegistry from '@deepseek-ai/dsh-agent'
+import InboxService from '@deepseek-ai/dsh-agent/inbox'
 import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import FileSettingsProvider from '@deepseek-ai/dsh-settings-file'
 import { settingsNamespace } from '@deepseek-ai/dsh-settings'
 import { describe, expect, it } from 'vitest'
@@ -43,6 +45,8 @@ async function harness(
   ctx.loader.builtins.include = Include
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt, { persona: '' })
   await ctx.plugin(ToolRuntime)
   await ctx.plugin(AgentRegistry)

+ 2 - 2
packages/schedule/schedule/tests/runtime.spec.ts

@@ -57,12 +57,11 @@ async function harness(): Promise<RuntimeHarness> {
     onFollowup: undefined as (() => void) | undefined,
     idle: Promise.withResolvers<undefined>(),
   }
-  const inbox = new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} })
   const agent: Agent = {
     id: session.id,
     options: {},
     session,
-    inbox,
+    inbox: undefined as never,
     status: 'idle',
     ctx: new Context(),
     send(_message: UserMessage, _target: InboxTarget, _wakeup: boolean) {},
@@ -97,6 +96,7 @@ async function harness(): Promise<RuntimeHarness> {
     steer(_message: UserMessage) {},
     inject(_message: UserMessage) {},
   }
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
   const disposeAgent = ctx.agents.register(agent)
   ctx.on('session/event', (_session, event) => {
     if (event.type === 'schedule/change' && event.data.operation === 'dispatch') order.push('dispatch')

+ 4 - 3
packages/schedule/schedule/tests/tools.spec.ts

@@ -24,12 +24,11 @@ interface ToolHarness {
 
 function stubAgent(ctx: Context, id: string): Agent {
   const session = ctx.sessions.create(SessionId(id))
-  const inbox = new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} })
-  return {
+  const agent: Agent = {
     id: session.id,
     options: {},
     session,
-    inbox,
+    inbox: undefined as never,
     status: 'idle',
     ctx: new Context(),
     send(_message: UserMessage, _target: InboxTarget, _wakeup: boolean) {},
@@ -40,6 +39,8 @@ function stubAgent(ctx: Context, id: string): Agent {
     steer(_message: UserMessage) {},
     inject(_message: UserMessage) {},
   }
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  return agent
 }
 
 async function harness(withPersistence = true): Promise<ToolHarness> {

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/session/session-projection/README.md
-README.md: 9018b133bb69ed4717fede14c9a2070a07c3fa62
-README.zh.md: 2a3af5620f84ff9697268101a6cfb894232b68b7
+README.md: 5adefbd34cb753d97559bee307f7e52e4864911c
+README.zh.md: d8f41d8f24ede743514a54c48ee7ea73c44f1ee4

+ 6 - 6
packages/session/session-projection/README.md

@@ -2,13 +2,13 @@
 
 English | [中文](README.zh.md)
 
-Session-projection Service Definition and drive registry. It owns `ctx.sessionProjections`, the registry that drives every registered projection unit over committed session events and serves finished whole values to carriers, currently the api-proxy history tail page and `session/projection` push frame. A domain registers pure mathematics; the framework owns the drive. The [session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md) records the design rationale.
+Session-projection Service Definition and drive registry. It owns `ctx.sessionProjections`, the registry that folds committed session events through every registered projection and serves finished whole values to carriers, currently the api-proxy history tail page and `session/projection` push frame. A domain registers pure mathematics; the framework owns the drive. The [session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md) records the design rationale.
 
 ## Service: `SessionProjectionRegistry` (ctx key: `sessionProjections`)
 
 ### Public API
 
-- `ctx.sessionProjections.register(definition): () => void` Register one domain's unit. Duplicate keys and invalid `stateVersion` throw; the registration is an effect on the calling fiber, so an unloaded domain plugin's key (with its cached cells) disappears from subsequent drives and snapshots — clients read that as capability absence.
+- `ctx.sessionProjections.register(definition): () => void` Register one domain's unit. Registrants with the same key and `stateVersion` share its cells; an incompatible version or invalid `stateVersion` throws. The registration is an effect on the calling fiber, so the last unload removes the key and its cached cells from subsequent drives and snapshots.
 - `ctx.sessionProjections.onChanged(listener): () => void` Subscribe to the change feed: one call per unit whose state reference changed, per committed event, carrying the schema-validated view and the causing seq. Effect-tied like `register`.
 - `ctx.sessionProjections.snapshot(session): ProjectionSnapshot` One consistent synchronous cut over every registered unit — `{ asOfSeq, values }` with `asOfSeq` = the seq of the last event every value reflects (`-1` for an empty log).
 
@@ -19,13 +19,13 @@ Session-projection Service Definition and drive registry. It owns `ctx.sessionPr
 
 ## Contract
 
-- **The framework drives, the domain computes.** The registry subscribes to `session/event` once; every committed event passes every unit's `apply` eagerly. Domains hold no subscriptions. Cells (`{state, observedSeq}` per unit per session, WeakMap-keyed) build lazily — a unit registered after events flowed, or a read of a session predating the registration, folds `init` over the in-memory log on first touch.
+- **The framework drives, the domain computes.** The registry subscribes to `session/event` once and runs every unit's `apply` over each committed event. Domains hold no subscriptions. Cells (`{state, observedSeq}` per unit per session, WeakMap-keyed) build lazily — a unit registered after events flowed, or a read of a session predating the registration, folds `init` over the in-memory log on first touch.
 - **Same-reference means no work.** `apply` MUST return the same state reference for events that do not concern the unit; the drive gates the change feed on `Object.is`, so non-matching events cost one call and nothing downstream.
-- **Whole-value event rule (load-bearing).** A state-carrying log event MUST carry the complete post-change state, never a bare delta — it keeps every transition trivially cheap and every served value self-describing (last-wins for consumers).
+- **Whole-value projection rule.** Every served projection value is a complete read model. Source events may carry whole values or domain-owned operations; the unit's deterministic `apply` owns replay, while checkpoint plus forward tail replay produces the same state without repeating aggregates in the log.
 - **Synchronous unit discipline.** `init`/`apply`/`view` MUST be synchronous; carriers read `snapshot()` in the same tick as their page slice, which is what makes `asOfSeq` one consistent cut. An accidentally-async `view` returns a Promise, which fails the boundary `schema.parse` loudly.
 - **State is plain JSON, `stateVersion` is its invalidation anchor.** The persisted projection cache stores `(sessionId, key, ver, seq, val)` rows; bump `stateVersion` whenever the state shape or the fold semantics change so stale rows are discarded instead of forward-applied into garbage.
 - **No wire vocabulary here.** The registry exposes only the change feed and the snapshot read face; carriers (api-proxy) mint their own frames (`session/projection`) and blocks from them.
-- **Optional capability.** Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected; carriers use `ctx.get('sessionProjections')` and omit their block/frames entirely when the registry is absent.
+- **Dependency follows use.** A domain that requires projection state declares `sessionProjections` as a Cordis service dependency; optional contributors may register under `ctx.inject(['sessionProjections'], …)`. Carriers use `ctx.get('sessionProjections')` and omit their block or frames when the registry is absent.
 
 ## Role
 
@@ -43,6 +43,6 @@ None; projections never assemble or send provider requests.
 
 - **Every tail page carries every registered key** — there is no per-key opt-out or lazy-key request shape yet; acceptable while values are UI-scale whole states (a todo list, a goal snapshot), revisit if a domain's value grows large.
 - **The unit table is process-wide, so key presence is not a per-session capability signal** — a key registered by ANY agent preset appears in every session's snapshot, including sessions whose own composition mounts nothing that produces it. A client must read the VALUE (`plan.active`, an empty todo list) rather than treat an absent key as absence of the feature; a unit whose empty value is indistinguishable from a real one belongs on the host plane instead, which is why `dsh-token-meter` sits there.
-- **Eager drive touches every unit per event** — cheap by construction (whole-value rule, same-reference gate), but a hot path would justify per-unit event-type prefilters, addable without contract change.
+- **Eager drive touches every unit per event** — bounded by each unit's transition and the same-reference gate, but a hot path would justify per-unit event-type prefilters, addable without contract change.
 - **Registry cells live in memory only** — a restart rebuilds by folding the log on first touch; compositions that mount `dsh-session-projection-cache` seed that fold from persisted rows instead.
 - **Synchronous unit discipline is only partially mechanical** — the boundary `schema.parse` rejects a Promise-returning `view`, but an `apply` that blocks or reads torn non-session state is a review concern; the invariant companion documents why no runtime check exists.

+ 6 - 6
packages/session/session-projection/README.zh.md

@@ -2,13 +2,13 @@
 
 [English](README.md) | 中文
 
-会话投影 Service Definition 与驱动注册表。它拥有 `ctx.sessionProjections`:该注册表在已提交的会话事件上驱动每个已注册的投影单元,并向载体提供完整的最终值,目前包括 api-proxy 历史尾页和 `session/projection` 推送帧。领域注册的只是纯数学;驱动权归框架。[session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md) 记录了设计理由。
+会话投影 Service Definition 与驱动注册表。它拥有 `ctx.sessionProjections`:该注册表把已提交的会话事件折叠进每个已注册投影,并向载体提供完整的最终值,目前包括 api-proxy 历史尾页和 `session/projection` 推送帧。领域注册的只是纯数学;驱动权归框架。[session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md) 记录了设计理由。
 
 ## 服务:`SessionProjectionRegistry`(ctx 键:`sessionProjections`)
 
 ### 公开 API
 
-- `ctx.sessionProjections.register(definition): () => void` 注册一个领域的单元。key 重复或 `stateVersion` 非法都会 throw;注册是挂在调用方 fiber 上的 effect,领域插件卸载后其 key(连同缓存的 cell)从后续驱动与快照中消失——客户端将其读作能力缺失。
+- `ctx.sessionProjections.register(definition): () => void` 注册一个领域单元。具有相同 key 和 `stateVersion` 的注册方共享其 cell;版本不兼容或 `stateVersion` 非法时会 throw。注册是挂在调用方 fiber 上的 effect,最后一个注册方卸载后,key 及其缓存 cell 会从后续驱动与快照中消失。
 - `ctx.sessionProjections.onChanged(listener): () => void` 订阅变更流:每个已提交事件、每个状态引用发生变化的单元各回调一次,携带经 schema 校验的 view 与致因 seq。与 `register` 一样绑定 effect。
 - `ctx.sessionProjections.snapshot(session): ProjectionSnapshot` 对全部已注册单元做一次一致的同步切面——`{ asOfSeq, values }`,其中 `asOfSeq` = 所有值共同反映到的最后一个事件的 seq(空日志为 `-1`)。
 
@@ -19,13 +19,13 @@
 
 ## 约定
 
-- **框架负责驱动,领域负责计算。** 注册表只订阅一次 `session/event`;每个已提交事件都会主动经过每个单元的 `apply`。领域不持有任何订阅。cell(每会话每单元一份 `{state, observedSeq}`,以 WeakMap 为键)惰性构建——在事件流过之后才注册的单元,或读取一个早于该注册的会话,都在首次触达时从 `init` 出发在内存日志上折叠。
+- **框架负责驱动,领域负责计算。** 注册表只订阅一次 `session/event`,并让每个单元的 `apply` 处理每个已提交事件。领域不持有任何订阅。cell(每会话每单元一份 `{state, observedSeq}`,以 WeakMap 为键)惰性构建——在事件流过之后才注册的单元,或读取一个早于该注册的会话,都在首次触达时从 `init` 出发在内存日志上折叠。
 - **同引用即无工作。** 对与单元无关的事件,`apply` 必须返回同一个状态引用;驱动以 `Object.is` 把守变更流,因此不匹配的事件只花一次调用,不产生任何下游工作。
-- **全量值事件规则(承重)。** 携带状态的日志事件必须携带变更后的完整状态,绝不携带裸增量——这让每次状态转移始终足够廉价,也让每个被供给的值自描述(对消费方即 last-wins)。
+- **全量投影值规则。** 每个对外投影值都是完整读模型。源事件可以携带完整值,也可以携带领域拥有的操作;单元的确定性 `apply` 负责回放,checkpoint 加前向 tail replay 会得到同一状态,无需在日志中重复聚合。
 - **单元的同步纪律。**`init`/`apply`/`view` 必须是同步的;载体在切出页面切片的同一 tick 内读取 `snapshot()`,`asOfSeq` 之所以是一个一致切面正系于此。误写成异步的 `view` 会返回 Promise,让边界的 `schema.parse` 当场大声失败。
 - **状态是纯 JSON,`stateVersion` 是其失效锚点。** 持久投影缓存(persisted projection cache)存储 `(sessionId, key, ver, seq, val)` 行;状态形状或折叠语义一旦变化就递增 `stateVersion`,使陈旧行被丢弃,而不是被正向 apply 成垃圾。
 - **本层没有协议词汇。** 注册表只暴露变更流与快照读取面;载体(api-proxy)据此自铸各自的帧(`session/projection`)与块。
-- **可选能力。** 领域插件在 `ctx.inject(['sessionProjections'], …)` 下注册,因此不带注册表的 headless 组装完全不受影响;载体使用 `ctx.get('sessionProjections')`,注册表缺席时完全省略自己的块与帧。
+- **依赖方式由用途决定。** 必须使用投影状态的领域把 `sessionProjections` 声明为 Cordis 服务依赖;可选贡献方可以在 `ctx.inject(['sessionProjections'], …)` 下注册。载体使用 `ctx.get('sessionProjections')`,注册表缺席时省略自己的块或帧。
 
 ## 职责
 
@@ -43,6 +43,6 @@
 
 - **每个尾页携带每个已注册的 key**——尚无逐 key 的 opt-out 或惰性 key 请求形状;在值都是 UI 量级的全量状态(一张 todo 清单、一份 goal 快照)时可以接受,若某领域的值变大再重议。
 - **单元表是进程级的,因此 key 是否存在不能当作逐会话的能力信号**——只要**任何**一个 agent preset 注册了某个 key,它就出现在每个会话的快照里,包括自身组装完全不产出该值的会话。客户端必须读**值**(`plan.active`、空的 todo 列表),不能把 key 缺席当作功能缺席;如果某个单元的空值与真实值无法区分,它就该待在宿主平面——`dsh-token-meter` 正因如此留在那里。
-- **主动驱动(eager drive)逐事件触达每个单元**——按构造开销很低(全量值规则、同引用闸门),但若出现热点路径,可加按单元的事件类型预过滤,约定不变。
+- **eager 驱动逐事件触达每个单元**——成本受每个单元的 transition 与同引用闸门约束;若出现热点路径,可加按单元的事件类型预过滤,约定不变。
 - **注册表 cell 只活在内存里**——重启后首次触达时靠折叠日志重建;挂载了 `dsh-session-projection-cache` 的组合改由持久行播种该折叠。
 - **单元同步纪律只有部分可机械把关**——边界 `schema.parse` 能拒绝返回 Promise 的 `view`,但阻塞的 `apply`、或读取撕裂的非会话状态的 `apply`,只能靠评审把关;invariant 配套项记载了为何不存在运行时检查。

+ 11 - 15
packages/session/session-projection/src/index.ts

@@ -5,14 +5,10 @@
  * forward eagerly over committed session events. Domain host plugins
  * contribute pure mathematics (init/apply/view); the framework owns the
  * subscription, the per-session watermark cache, and change notification;
- * carriers consume the snapshot read face and the change feed. Neither side
- * knows the other
- * (capability-seam three-way split). Design authority: the session-projection
- * RFC (.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md).
- *
- * Whole-value event rule (load-bearing): a state-carrying log event MUST
- * carry the complete post-change state, never a bare delta — it keeps every
- * unit's transition trivially cheap and every served value self-describing.
+ * carriers consume the snapshot read face and the change feed. Source events
+ * may carry whole values or domain operations; every `view` returns a complete
+ * current value. Design authority: the session-projection RFC
+ * (.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md).
  *
  * @module @deepseek-ai/dsh-session-projection
  */
@@ -158,15 +154,15 @@ interface Registration {
  * every registered unit's `apply` (eager drive), and a changed state
  * reference notifies the change feed with the schema-validated view.
  * Cells build lazily — a unit registered after events flowed, or a session
- * older than the registry, folds `init` over the in-memory log on first
+ * older than the registry, folds `init` over the full in-memory log on first
  * touch (event or read). Registration is an effect (disposer rides the
  * calling fiber): an unloaded domain plugin's key disappears from snapshots
- * and clients read it as capability absence. Domain
- * plugins register under `ctx.inject(['sessionProjections'], …)` so headless
- * assemblies without the registry stay unaffected. Registrants sharing a key
- * share one unit and are counted: the same tool package mounted in N agent
- * presets registers N times, and the key survives until the last one
- * unloads.
+ * and clients read it as capability absence. A domain that requires this
+ * capability declares a Cordis service dependency; an optional contributor
+ * may register under `ctx.inject(['sessionProjections'], …)`. Registrants
+ * sharing a key share one unit and are counted: the same tool package mounted
+ * in N agent presets registers N times, and the key survives until the last
+ * one unloads.
  */
 export class SessionProjectionRegistry extends Service {
   private readonly registrations = new Map<string, Registration>()

+ 2 - 1
packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts

@@ -44,7 +44,7 @@ function agent(ctx: Context, cwd: string): Agent {
     id,
     options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: undefined as never,
     status: 'idle',
     ctx: scope.ctx,
     send: () => {},
@@ -55,6 +55,7 @@ function agent(ctx: Context, cwd: string): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
+  Object.assign(value, { inbox: new Inbox(value.ctx, value) })
   ctx.agents.register(value)
   return value
 }

+ 2 - 1
packages/shell/tool-bash-persistent/tests/tools.spec.ts

@@ -39,7 +39,7 @@ function agent(ctx: Context, cwd: string | undefined): Agent {
     id,
     options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: undefined as never,
     status: 'idle',
     ctx: scope.ctx,
     send: () => {},
@@ -50,6 +50,7 @@ function agent(ctx: Context, cwd: string | undefined): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
+  Object.assign(value, { inbox: new Inbox(value.ctx, value) })
   ctx.agents.register(value)
   return value
 }

+ 8 - 4
packages/skill/tool-skill/tests/tool-skill.spec.ts

@@ -39,12 +39,12 @@ async function setup(home: string, config: toolSkill.Config = {}): Promise<Conte
 function agentForCwd(cwd: string): Agent {
   const id = SessionId(`tool-skill-${cwd}`)
   const session = Session.create(id, [], { version: 0, id, createdAt: 0, cwd })
-  return {
+  const agent: Agent = {
     ctx: new Context(),
     id,
     options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: undefined as never,
     status: 'idle',
     send: () => {},
     followup: () => {},
@@ -54,14 +54,16 @@ function agentForCwd(cwd: string): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  return agent
 }
 
 function sessionAgent(session: Session, id = 'tool-skill-agent'): Agent {
-  return {
+  const agent: Agent = {
     id: SessionId(id),
     options: {},
     session,
-    inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
+    inbox: undefined as never,
     status: 'running',
     ctx: new Context(),
     send: () => {},
@@ -72,6 +74,8 @@ function sessionAgent(session: Session, id = 'tool-skill-agent'): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  return agent
 }
 
 function openMessageTurn(session: Session, turn = 1): void {

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů