Jelajahi Sumber

refactor(agent): remove inbox service

_Kerman 2 minggu lalu
induk
melakukan
bd3b651ea8
86 mengubah file dengan 160 tambahan dan 344 penghapusan
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.i18n.yaml
  2. 2 2
      .agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md
  3. 2 2
      .agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md
  4. 0 3
      apps/cli/composition.md
  5. 2 2
      apps/cli/tests/fixtures/dsh-badge/snapshot.ts
  6. 2 2
      docs/capability-seams.i18n.yaml
  7. 0 4
      docs/capability-seams.md
  8. 0 4
      docs/capability-seams.zh.md
  9. 2 2
      docs/config-catalog.i18n.yaml
  10. 2 2
      docs/config-catalog.md
  11. 2 2
      docs/config-catalog.zh.md
  12. 2 2
      docs/module-graph.i18n.yaml
  13. 2 1
      docs/module-graph.md
  14. 2 1
      docs/module-graph.zh.md
  15. 2 2
      docs/subsystems/core.i18n.yaml
  16. 2 19
      docs/subsystems/core.md
  17. 2 19
      docs/subsystems/core.zh.md
  18. 0 3
      examples/headless-agent/tests/code-mode.e2e.ts
  19. 2 2
      examples/headless-agent/tests/fixtures/e2b/e2b/bin.ts
  20. 0 3
      packages/bundle/base/cordis.patch.yml
  21. 2 4
      packages/bundle/headless/tests/headless.spec.ts
  22. 3 5
      packages/context/agent-instructions/tests/agent-instructions.spec.ts
  23. 1 1
      packages/context/time-context/tests/time-context.spec.ts
  24. 1 1
      packages/context/tmux-context/tests/tmux-context.spec.ts
  25. 1 0
      packages/core/agent-loop/package.json
  26. 2 3
      packages/core/agent-loop/src/agent.ts
  27. 1 1
      packages/core/agent-loop/src/index.ts
  28. 0 4
      packages/core/agent-loop/tests/agent-initiator.spec.ts
  29. 0 2
      packages/core/agent-loop/tests/agent.spec.ts
  30. 0 3
      packages/core/agent-loop/tests/cancel.spec.ts
  31. 0 8
      packages/core/agent-loop/tests/config-session-id.spec.ts
  32. 0 9
      packages/core/agent-loop/tests/contract-regressions.spec.ts
  33. 0 2
      packages/core/agent-loop/tests/coverage-edges.spec.ts
  34. 0 2
      packages/core/agent-loop/tests/interception.spec.ts
  35. 0 4
      packages/core/agent-loop/tests/loop.spec.ts
  36. 0 2
      packages/core/agent-loop/tests/properties.spec.ts
  37. 0 2
      packages/core/agent-loop/tests/request-cache.e2e.ts
  38. 0 2
      packages/core/agent-loop/tests/request-error.spec.ts
  39. 0 4
      packages/core/agent-loop/tests/request-reconstruction.spec.ts
  40. 0 11
      packages/core/agent-loop/tests/resume.spec.ts
  41. 0 2
      packages/core/agent-loop/tests/scope-lifecycle.spec.ts
  42. 0 2
      packages/core/agent-loop/tests/settings.spec.ts
  43. 0 5
      packages/core/agent-loop/tests/tool-calls.spec.ts
  44. 0 2
      packages/core/agent-loop/tests/tool-order.spec.ts
  45. 2 2
      packages/core/agent/README.i18n.yaml
  46. 1 1
      packages/core/agent/README.md
  47. 1 1
      packages/core/agent/README.zh.md
  48. 0 8
      packages/core/agent/package.json
  49. 25 0
      packages/core/agent/src/inbox-projection.ts
  50. 14 61
      packages/core/agent/src/inbox.ts
  51. 6 0
      packages/core/agent/src/index.ts
  52. 9 10
      packages/core/agent/tests/agent.spec.ts
  53. 2 2
      packages/e2b/e2b/tests/composition.e2e.ts
  54. 0 2
      packages/examples/agent-spine-demo/src/index.ts
  55. 9 14
      packages/extensions/tool-cordis/src/api-catalog.ts
  56. 2 2
      packages/feedback/command-feedback/tests/command-feedback.spec.ts
  57. 2 2
      packages/feedback/command-feedback/tests/loader-composition.spec.ts
  58. 2 2
      packages/fs/tool-str-replace-editor/tests/tools.spec.ts
  59. 2 2
      packages/goal/command-goal/tests/command-goal.spec.ts
  60. 3 7
      packages/goal/goal/tests/goal.spec.ts
  61. 3 5
      packages/goal/tool-goal/tests/tool-goal.spec.ts
  62. 2 5
      packages/host/apiproxy/tests/api-proxy-projections.spec.ts
  63. 2 2
      packages/host/apiproxy/tests/api-proxy-workspace.spec.ts
  64. 2 2
      packages/jobs/jobs-local/tests/jobs.spec.ts
  65. 0 3
      packages/llm/llm-retry/tests/loader-composition.spec.ts
  66. 0 2
      packages/plan/plan-mode/tests/integration.spec.ts
  67. 0 2
      packages/preset/agent-presets/tests/invariant.spec.ts
  68. 0 4
      packages/preset/agent-presets/tests/mount.spec.ts
  69. 0 2
      packages/preset/agent-presets/tests/settings.spec.ts
  70. 2 2
      packages/schedule/schedule/tests/runtime.spec.ts
  71. 2 2
      packages/schedule/schedule/tests/tools.spec.ts
  72. 2 2
      packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts
  73. 2 2
      packages/shell/tool-bash-persistent/tests/tools.spec.ts
  74. 3 2
      packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts
  75. 3 2
      packages/shell/tool-pwsh-persistent/tests/tools.spec.ts
  76. 2 2
      packages/skill/tool-skill/tests/tool-skill.spec.ts
  77. 4 4
      packages/terminal/terminal-bash/tests/index.spec.ts
  78. 2 2
      packages/terminal/terminal-bash/tests/local.spec.ts
  79. 2 2
      packages/terminal/terminal/tests/service.spec.ts
  80. 2 2
      packages/terminal/tool-terminal/tests/loader-composition.spec.ts
  81. 2 2
      packages/terminal/tool-terminal/tests/tools.spec.ts
  82. 0 2
      packages/test-support/agent-loop-testkit/src/index.ts
  83. 2 2
      packages/todo/tool-todo/tests/loader-composition.spec.ts
  84. 0 1
      scripts/gen-cordis-catalog.ts
  85. 0 8
      scripts/gen-doc-graphs.ts
  86. 0 2
      tsconfig.base.json

+ 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: c46c22cde3e96fb774233d9621acf1abf4ba0c33
-2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md: 51ada096a105a5c97cffd70c2d40782a71beddf7
+2026-07-31-claimed-pre-step-inbox-lifecycle.md: 8149c9df09ff69c017ba66a779d6fd3dfc314193
+2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md: e855b88b26a55dd9f5c33c87a4b59d67496d00ed

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

@@ -18,11 +18,11 @@ Before every proposed step, `Inbox.claim(target)` atomically removes the complet
 
 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`. `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.
+The two event surfaces have separate consumers. Observers following one message use `agent/inbox/inserted`, `claimed`, and `discarded`. `AgentRegistry` contributes the standard `inbox` projection over the durable `agent/inbox/spliced` stream whenever the projection registry is composed; 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 `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.
+The archived [addressable queue occurrence decision](../../archived/feature/2026-07-29-addressable-queue-operations.md) describes the superseded occurrence-wrapper design. `MessageId` owns addressability, while `AgentRegistry` contributes `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
 

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

@@ -18,11 +18,11 @@ Status: implemented
 
 持久 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`。`InboxService` 在持久 `agent/inbox/spliced` 流上注册标准 `inbox` 投影,供整体状态消费方与 live 恢复使用;UI 编辑与移除通过 Inbox 变更方法处理,从而让同一投影记录所有变化。
+两类事件接口服务不同消费方。跟踪单条消息的观察方使用 `agent/inbox/inserted`、`claimed` 与 `discarded`。`AgentRegistry` 会在投影注册表已组合时,在持久 `agent/inbox/spliced` 流上贡献标准 `inbox` 投影;UI 编辑与移除通过 Inbox 变更方法处理,从而让同一投影记录所有变化。
 
 必须对当前步骤进行原子改写的插件从 `agent/pre-step` 返回消息。只需要稍后上下文的插件可以直接修改 `agent.inbox`。Workspace context 同时使用两条路径:异步文件系统投影会暂存一条可替换的 `next-step` 消息,而下一次进入步骤的 pre-step 会把该消息或新组合的基线折入最终批次,并移除仍待处理的副本。reject 会让该条目继续排队。
 
-已归档的[可寻址队列项决策](../../archived/feature/2026-07-29-addressable-queue-operations.md)描述了已被取代的单次出现包装层设计。现在由 `MessageId` 负责寻址,而 `InboxService` 把 `inbox` 注册为持久 splice 上的标准会话投影。通用投影传输层会将该折叠结果用于实时更新、历史尾页的重连基线和冷进程重启恢复,无需 live Agent 镜像。
+已归档的[可寻址队列项决策](../../archived/feature/2026-07-29-addressable-queue-operations.md)描述了已被取代的单次出现包装层设计。`MessageId` 负责寻址,而 `AgentRegistry` 把 `inbox` 作为持久 splice 上的标准会话投影贡献给投影注册表。通用投影传输层会将该折叠结果用于实时更新、历史尾页的重连基线和冷进程重启恢复,无需 live Agent 镜像。
 
 ## 曾考虑的替代方案
 

+ 0 - 3
apps/cli/composition.md

@@ -30,8 +30,6 @@ 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"]
@@ -181,7 +179,6 @@ 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` |

+ 2 - 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, type Agent } from '@deepseek-ai/dsh-agent'
+import { agentEvents, Inbox, 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'
@@ -34,7 +34,7 @@ try {
     runMaintenance: job => job(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
-  Object.assign(agent, { inbox: ctx.inboxes.create(agent) })
+  Object.assign(agent, { inbox: new Inbox(ctx, agent.session, agentEvents(ctx, 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: 2f6bc45f9cb60809cedf31f86fcb68785ef80b9a
-capability-seams.zh.md: a9eeb06c063a86cdcd6ef3560677129d3adb2798
+capability-seams.md: a84a6d6e0836524e1f39f2fd067ae1f6570741a5
+capability-seams.zh.md: b6b0aba72729f9b9b752347ab00cd76a45c248d9

+ 0 - 4
docs/capability-seams.md

@@ -31,7 +31,6 @@ 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"]
@@ -202,7 +201,6 @@ 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
@@ -329,7 +327,6 @@ 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
@@ -429,7 +426,6 @@ 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. |

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

@@ -33,7 +33,6 @@ 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"]
@@ -204,7 +203,6 @@ 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
@@ -331,7 +329,6 @@ 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
@@ -431,7 +428,6 @@ 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: b35358c651454ed42a9c0cfd466f305c7d632e70
-config-catalog.zh.md: f6bb73d14e3da28e05628d7b1d7c29d053382f24
+config-catalog.md: 453065bec7ddc373627f9f632445db7e16b56f59
+config-catalog.zh.md: 2c2a9d84f051e79437493490af63cea70358c300

+ 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` · `inboxes` · `sessions` · `llm` · `tools` · `systemPrompt`
+Requires: `agents` · `sessionProjections` · `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:94`](../packages/examples/agent-spine-demo/src/index.ts)
+Source: [`packages/examples/agent-spine-demo/src/index.ts:93`](../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` · `inboxes` · `sessions` · `llm` · `tools` · `systemPrompt`
+需要:`agents` · `sessionProjections` · `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:94`](../packages/examples/agent-spine-demo/src/index.ts)
+来源:[`packages/examples/agent-spine-demo/src/index.ts:93`](../packages/examples/agent-spine-demo/src/index.ts)
 
 <a id="deepseek-aidsh-agent-tool-presentation"></a>
 

+ 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: 4fa25899f83f2140a8e28af123ed5ff202a437e4
-module-graph.zh.md: ba604a3ee7462584cd2d2324ee90936502ee35f9
+module-graph.md: c9b0d3dd7305bf0444c777f21468c3baa27d12d9
+module-graph.zh.md: b3eaf5a47cb5267fcbaa45017ea932948eaec68d

+ 2 - 1
docs/module-graph.md

@@ -759,6 +759,7 @@ flowchart TD
   pkg_agent_loop --> pkg_scope
   pkg_agent_loop --> pkg_session
   pkg_agent_loop --> pkg_session_persistence
+  pkg_agent_loop --> pkg_session_projection
   pkg_agent_loop --> pkg_settings
   pkg_agent_loop --> pkg_system_prompt
   pkg_agent_loop --> pkg_tools
@@ -1580,7 +1581,7 @@ flowchart TD
 | [`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-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), [`session-projection`](../packages/session/session-projection), [`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) |
 | [`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) |

+ 2 - 1
docs/module-graph.zh.md

@@ -760,6 +760,7 @@ flowchart TD
   pkg_agent_loop --> pkg_scope
   pkg_agent_loop --> pkg_session
   pkg_agent_loop --> pkg_session_persistence
+  pkg_agent_loop --> pkg_session_projection
   pkg_agent_loop --> pkg_settings
   pkg_agent_loop --> pkg_system_prompt
   pkg_agent_loop --> pkg_tools
@@ -1581,7 +1582,7 @@ flowchart TD
 | [`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-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), [`session-projection`](../packages/session/session-projection), [`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) |
 | [`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) |

+ 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: 6e919d02ac1c23cf4f49e92e5ce50a8983ac449d
-core.zh.md: 853d9500cd7e51b02ec2f9595bd7f294f7b1d66f
+core.md: b978cfdc102c93e0939dfc48c24947c45c96ee6f
+core.zh.md: 0dfef5b75ef9273884afe6e038a83e7e5785d9bc

+ 2 - 19
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, 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.
+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. `AgentRegistry` contributes the standard `inbox` projection whenever the projection registry is composed; its 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:
 
@@ -720,24 +720,7 @@ list(): Agent[]
 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)
+Source: [`packages/core/agent/src/index.ts:259`](../../packages/core/agent/src/index.ts)
 
 <a id="agent-events"></a>
 

+ 2 - 19
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 通知,随后由 Inbox 逐条发出 claimed 通知。`InboxService` 注册标准 `inbox` 投影;其注册表 cell 是唯一 live 状态,同一份折叠也服务于冷消费方。跟踪单条消息的消费方使用精确的 `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 通知。`AgentRegistry` 会在投影注册表已组合时贡献标准 `inbox` 投影;其 cell 是唯一 live 状态,同一份折叠也服务于冷消费方。跟踪单条消息的消费方使用精确的 `agent/inbox/inserted`、`claimed` 与 `discarded` 通知。
 
 取消:
 
@@ -728,24 +728,7 @@ list(): Agent[]
 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)
+Source: [`packages/core/agent/src/index.ts:259`](../../packages/core/agent/src/index.ts)
 
 <a id="agent-events"></a>
 

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

@@ -11,7 +11,6 @@ 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'
@@ -56,7 +55,6 @@ async function codeModeHarness(cwd: string): Promise<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)
@@ -75,7 +73,6 @@ async function workspaceCodeModeHarness(): Promise<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,7 @@
 import { readFile } from 'node:fs/promises'
 import { resolve } from 'node:path'
 import { boot } from '@deepseek-ai/dsh-app-boot'
-import type { Agent } from '@deepseek-ai/dsh-agent'
+import { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
 import type {} from '@deepseek-ai/dsh-fs-e2b'
 import type {} from '@deepseek-ai/dsh-bash-local'
@@ -30,7 +30,7 @@ const owner: Agent = {
   runMaintenance: task => task(new AbortController().signal),
   whenIdle: () => Promise.resolve(),
 }
-Object.assign(owner, { inbox: ctx.inboxes.create(owner) })
+Object.assign(owner, { inbox: new Inbox(ctx, owner.session, agentEvents(ctx, owner)) })
 const unregisterOwner = ctx.agents.register(owner)
 let terminalId: Awaited<ReturnType<typeof ctx.terminals.spawn>>['sessionId'] | undefined
 try {

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

@@ -58,9 +58,6 @@
     - 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

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

@@ -2,8 +2,7 @@
 
 import { afterEach, describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
-import AgentRegistry from '@deepseek-ai/dsh-agent'
-import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 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'
@@ -57,7 +56,6 @@ 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({
@@ -86,7 +84,7 @@ async function bench(script: Script): Promise<{
         inject: () => {},
         whenIdle: () => idle,
       } satisfies Partial<Agent>)
-      Object.assign(agent, { inbox: ctx.inboxes.create(agent) })
+      Object.assign(agent, { inbox: new Inbox(ctx, agent.session, agentEvents(ctx, agent)) })
       await options.setup?.(agentCtx)
       script.before?.(session)
       ctx.agents.register(agent)

+ 3 - 5
packages/context/agent-instructions/tests/agent-instructions.spec.ts

@@ -7,8 +7,7 @@ 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, { 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 AgentRegistry, { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent'
 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'
@@ -52,7 +51,7 @@ const testToolSignal = new AbortController().signal
 const isolatedInboxCtx = new Context()
 await isolatedInboxCtx.plugin(SessionStore)
 await isolatedInboxCtx.plugin(SessionProjectionRegistry)
-await isolatedInboxCtx.plugin(InboxService)
+await isolatedInboxCtx.plugin(AgentRegistry)
 let nextStubSession = 1
 
 async function tempRepo(): Promise<string> {
@@ -209,7 +208,7 @@ function stubAgent(cwd?: string, seed: SessionEvent[] = []): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
-  Object.assign(agent, { inbox: agentCtx.inboxes.create(agent) })
+  Object.assign(agent, { inbox: new Inbox(agentCtx, agent.session, agentEvents(agentCtx, agent)) })
   return agent
 }
 
@@ -2523,7 +2522,6 @@ 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)

+ 1 - 1
packages/context/time-context/tests/time-context.spec.ts

@@ -51,7 +51,7 @@ 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) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(agent.ctx, agent)) })
   return agent
 }
 

+ 1 - 1
packages/context/tmux-context/tests/tmux-context.spec.ts

@@ -107,7 +107,7 @@ 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) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(agent.ctx, agent)) })
   return agent
 }
 

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

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

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

@@ -11,12 +11,11 @@ import type {
   AgentOptions,
   AgentStatus,
   CancelOptions,
-  Inbox,
   InboxTarget,
   PreStepDecision,
   RequestErrorAction,
 } from '@deepseek-ai/dsh-agent'
-import { agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent'
+import { Inbox, agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent'
 import type { GenerateOptions, LlmCallConfig, Message, PreparedLlmCall } from '@deepseek-ai/dsh-llm'
 import {
   BlockAssembler,
@@ -85,7 +84,7 @@ export class ReactLoopAgent implements Agent {
     public readonly session: Session,
   ) {
     this.dispatch = agentEvents(loopCtx, this)
-    this.inbox = loopCtx.inboxes.create(this)
+    this.inbox = new Inbox(loopCtx, session, this.dispatch)
     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', 'inboxes', 'sessions', 'llm', 'tools', 'systemPrompt']
+  static inject = ['agents', 'sessionProjections', 'sessions', 'llm', 'tools', 'systemPrompt']
 
   /** Runtime schema for declarative agents. */
   static Config = z.object({

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

@@ -1,4 +1,3 @@
-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'
@@ -24,7 +23,6 @@ async function harness(adapter: LlmAdapter): Promise<Harness> {
   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)
@@ -126,7 +124,6 @@ describe('AgentLoop initiator scope', () => {
     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)
@@ -387,7 +384,6 @@ describe('AgentLoop initiator scope', () => {
     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)

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

@@ -1,4 +1,3 @@
-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'
@@ -16,7 +15,6 @@ async function harness(adapter: MockAdapter): Promise<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)

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

@@ -1,4 +1,3 @@
-import InboxService from '@deepseek-ai/dsh-agent/inbox'
 import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
 /**
@@ -28,7 +27,6 @@ async function harness(adapter: MockAdapter) {
   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)
@@ -674,7 +672,6 @@ describe('Agent.cancel()', () => {
     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)

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

@@ -1,4 +1,3 @@
-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'
@@ -32,7 +31,6 @@ async function makeCoreContext(): Promise<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)
@@ -332,7 +330,6 @@ describe('config-driven session id', () => {
     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)
@@ -359,7 +356,6 @@ describe('config-driven session id', () => {
     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)
@@ -380,7 +376,6 @@ describe('config-driven session id', () => {
     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)
@@ -406,7 +401,6 @@ describe('config-driven session id', () => {
     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)
@@ -424,7 +418,6 @@ describe('config-driven session id', () => {
     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)
@@ -451,7 +444,6 @@ describe('config-driven session id', () => {
     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)

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

@@ -1,4 +1,3 @@
-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'
@@ -33,7 +32,6 @@ async function harness(adapter: MockAdapter) {
   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)
@@ -530,7 +528,6 @@ describe('turn numbering continues across seeded sessions', () => {
     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)
@@ -683,7 +680,6 @@ describe('turn and step boundary recovery', () => {
     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)
@@ -1117,7 +1113,6 @@ describe('disposal and cancellation during pre-step assembly', () => {
     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)
@@ -1169,7 +1164,6 @@ describe('disposal and cancellation during pre-step assembly', () => {
     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)
@@ -1221,7 +1215,6 @@ describe('disposal and cancellation during pre-step assembly', () => {
     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)
@@ -1269,7 +1262,6 @@ describe('disposal and cancellation during pre-step assembly', () => {
     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)
@@ -1319,7 +1311,6 @@ describe('disposal and cancellation during pre-step assembly', () => {
     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)

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

@@ -1,4 +1,3 @@
-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'
@@ -21,7 +20,6 @@ async function harness(adapter: MockAdapter) {
   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)

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

@@ -1,4 +1,3 @@
-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'
@@ -34,7 +33,6 @@ async function harness(adapter: MockAdapter) {
   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)

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

@@ -1,4 +1,3 @@
-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'
@@ -20,7 +19,6 @@ async function harness(adapter: MockAdapter, persona = '') {
   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)
@@ -1454,7 +1452,6 @@ describe('agent loop', () => {
     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)
@@ -1480,7 +1477,6 @@ describe('agent loop', () => {
     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)

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

@@ -1,4 +1,3 @@
-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
@@ -42,7 +41,6 @@ async function harness() {
   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)

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

@@ -6,7 +6,6 @@ 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'
@@ -44,7 +43,6 @@ async function loopHarness(): Promise<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)

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

@@ -1,4 +1,3 @@
-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'
@@ -16,7 +15,6 @@ async function harness(adapter: MockAdapter): Promise<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)

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

@@ -1,4 +1,3 @@
-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
@@ -31,7 +30,6 @@ async function harnessRoutes(
   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)
@@ -260,7 +258,6 @@ describe('request stability across the loop', () => {
     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)
@@ -380,7 +377,6 @@ describe('request stability across the loop', () => {
     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)

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

@@ -1,4 +1,3 @@
-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'
@@ -31,7 +30,6 @@ async function mountPersistentHarness(root: string, adapter: MockAdapter): Promi
   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)
@@ -249,7 +247,6 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     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)
@@ -279,7 +276,6 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     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)
@@ -531,7 +527,6 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     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)
@@ -596,7 +591,6 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     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)
@@ -630,7 +624,6 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     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)
@@ -668,7 +661,6 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     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)
@@ -703,7 +695,6 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     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)
@@ -894,7 +885,6 @@ describe('configured-start failure edges', () => {
     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)
@@ -941,7 +931,6 @@ describe('configured-start failure edges', () => {
     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)

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

@@ -1,4 +1,3 @@
-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'
@@ -20,7 +19,6 @@ async function harnessWithLoop(adapter: MockAdapter = new MockAdapter([textRespo
   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)

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

@@ -1,4 +1,3 @@
-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. */
 
@@ -37,7 +36,6 @@ async function boot(): Promise<{ ctx: Context; settingsFiber: Fiber; loopFiber:
   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)

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

@@ -1,4 +1,3 @@
-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.
@@ -23,7 +22,6 @@ async function harness(adapter: MockAdapter, maxParallelToolCalls?: number) {
   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)
@@ -283,7 +281,6 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
     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)
@@ -353,7 +350,6 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
     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)
@@ -713,7 +709,6 @@ describe('code-mode native-tool denial through the agent loop', () => {
     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

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

@@ -1,4 +1,3 @@
-import InboxService from '@deepseek-ai/dsh-agent/inbox'
 import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
 /**
@@ -26,7 +25,6 @@ async function harness(adapter: MockAdapter, toolOrder?: SystemPromptConfig['too
   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: 6af7e57bdb0ae710a6d1e6e58ca2ac187a45eeb8
-README.zh.md: 1533f918e037c7dc7d131c7046d25fc259d33580
+README.md: aeddc6caf24289333a3e5b0f9b29124b1e591316
+README.zh.md: f815283ba638f0374ae30d8a8c3fe4ce6a66968b

+ 1 - 1
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.
 
-`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.
+`AgentRegistry` contributes the standard `inbox` session projection whenever the projection registry is composed. The 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 that unit 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).
 

+ 1 - 1
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 不会保留它们;领取后插入的消息仍等待后续边界。
 
-`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 在提交对应变更时自行发出这些通知,不引入另一层生命周期封套。
+`AgentRegistry` 会在投影注册表已组合时贡献标准 `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) 记录的完整流水线。
 

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

@@ -18,14 +18,6 @@
       "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"

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

@@ -1,6 +1,7 @@
 /** Inbox projection schema and its inferred wire value. */
 
 import type { UserMessage } from '@deepseek-ai/dsh-llm/types'
+import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
 import { z } from 'zod'
 
 /** Wire validation for pending agent input reconstructed from durable inbox splices. */
@@ -12,6 +13,30 @@ export const inboxProjectionSchema = z.object({
 /** Complete pending Inbox value reconstructed from durable splices. */
 export type InboxState = z.infer<typeof inboxProjectionSchema>
 
+/** Standard fold that reconstructs pending agent input from durable splices. */
+export const inboxProjectionDefinition = {
+  key: 'inbox',
+  stateSchema: inboxProjectionSchema,
+  init: (): InboxState => ({ 'next-turn': [], 'next-step': [] }),
+  apply(state: InboxState, 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 }
+  },
+  wire: {
+    viewSchema: inboxProjectionSchema,
+    view: (state: InboxState) => state,
+  },
+  stateVersion: 1,
+} satisfies ProjectionDefinition<'inbox', InboxState>
+
 declare module '@deepseek-ai/dsh-session-projection/types' {
   interface SessionProjectionMap {
     /** Pending agent input reconstructed from durable inbox splices. */

+ 14 - 61
packages/core/agent/src/inbox.ts

@@ -1,70 +1,25 @@
 /**
- * Incremental projection of durable agent inbox events.
+ * Command facade over the durable agent Inbox projection.
  *
  * @module @deepseek-ai/dsh-agent/inbox
  */
 
-import { Context, Service } from '@deepseek-ai/cordis'
+import type { Context } from '@deepseek-ai/cordis'
 import type { MessageId } from '@deepseek-ai/dsh-llm'
-import type { SessionEventMap, UserMessage } from '@deepseek-ai/dsh-session'
-import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
-import { agentEvents } from './dispatch.ts'
+// Type-only: resolves ctx.sessionProjections for the required Inbox projection.
+import type {} from '@deepseek-ai/dsh-session-projection'
+import type { Session, SessionEventMap, UserMessage } from '@deepseek-ai/dsh-session'
 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'
 
-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)
-  }
-}
-
 /** Agent-owned command facade over the standard durable Inbox projection. */
 export class Inbox {
-  private readonly dispatch: AgentEventDispatch
-
-  constructor(private readonly ctx: Context, private readonly agent: Agent) {
-    this.dispatch = agentEvents(ctx, agent)
-  }
+  constructor(
+    private readonly ctx: Context,
+    private readonly session: Session,
+    private readonly dispatch: AgentEventDispatch,
+  ) {}
 
   /** Prompts awaiting individual turns. */
   get nextTurn(): readonly UserMessage[] {
@@ -171,11 +126,11 @@ export class Inbox {
     return undefined
   }
 
-  /** Read the current durable projection value. */
+  /** Read the current durable projection state. */
   private current(): InboxState {
-    // InboxService registers this required projection before creating an Inbox.
+    // AgentLoop requires sessionProjections; AgentRegistry contributes this unit to it.
     // oxlint-disable-next-line typescript/no-non-null-assertion
-    return this.ctx.sessionProjections.snapshot(this.agent.session).values.inbox!
+    return this.ctx.sessionProjections.stateOf(this.session, 'inbox')!
   }
 
   /** Commit one normalized mutation and publish its live events. */
@@ -216,7 +171,7 @@ export class Inbox {
       ...(outcome === undefined ? {} : { outcome }),
     }
     const removed = inbox.slice(actualStart, actualStart + actualDeleteCount)
-    const event = this.agent.session.append('agent/inbox/spliced', splice)
+    const event = this.session.append('agent/inbox/spliced', splice)
     if (discardRemoved) {
       for (const message of removed) this.dispatch.emit('agent/inbox/discarded', { message })
     }
@@ -226,5 +181,3 @@ export class Inbox {
     return removed
   }
 }
-
-export default InboxService

+ 6 - 0
packages/core/agent/src/index.ts

@@ -12,7 +12,10 @@ import { isPromise } from 'node:util/types'
 import { scopeTarget } from '@deepseek-ai/dsh-scope'
 import type { Scoped } from '@deepseek-ai/dsh-scope'
 import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
+// Type-only: resolves ctx.sessionProjections for the optional Inbox projection contribution.
+import type {} from '@deepseek-ai/dsh-session-projection'
 import type { TypertContext, TypertLookup } from '@deepseek-ai/dsh-typert-protocol'
+import { inboxProjectionDefinition } from './inbox-projection.ts'
 import type { Agent, AgentOptions } from './runtime-types.ts'
 
 export * from './runtime-types.ts'
@@ -265,6 +268,9 @@ export class AgentRegistry extends Service {
 
   constructor(ctx: Context) {
     super(ctx, 'agents')
+    ctx.inject(['sessionProjections'], (projectionCtx) => {
+      projectionCtx.sessionProjections.register(inboxProjectionDefinition)
+    })
     ctx.inject(['typert'], (typeCtx) => {
       typeCtx.typert.lookups.register('agent', {
         parameter: 'agent',

+ 9 - 10
packages/core/agent/tests/agent.spec.ts

@@ -4,10 +4,9 @@ import { createUserMessage, freezeMessage } from '@deepseek-ai/dsh-llm'
 import SessionStore, { Session, SessionId, type UserMessage } from '@deepseek-ai/dsh-session'
 import AgentRegistry, {
   agentEvents,
-  InboxService,
+  Inbox,
 } 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 {
@@ -48,10 +47,10 @@ async function inboxAgent(rawId: string): Promise<{ ctx: Context; session: Sessi
   const ctx = new Context()
   await ctx.plugin(SessionStore)
   await ctx.plugin(SessionProjectionRegistry)
-  await ctx.plugin(InboxService)
+  await ctx.plugin(AgentRegistry)
   const session = ctx.sessions.create(SessionId(rawId))
   const agent = stubAgent(rawId, { ctx, session })
-  Object.assign(agent, { inbox: ctx.inboxes.create(agent) })
+  Object.assign(agent, { inbox: new Inbox(ctx, agent.session, agentEvents(ctx, agent)) })
   return { ctx, session, agent }
 }
 
@@ -65,7 +64,7 @@ describe('Inbox', () => {
     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) })
+    Object.assign(childAgent, { inbox: new Inbox(ctx, childAgent.session, agentEvents(ctx, childAgent)) })
 
     expect(child.header.seedLength).toBe(parent.events.length)
     expect(childAgent.inbox.nextTurn).toEqual([inherited])
@@ -169,15 +168,15 @@ describe('Inbox', () => {
     expect(session.events).toHaveLength(beforeClear + 2)
   })
 
-  it('registers the durable Inbox projection from the Inbox service', async () => {
+  it('registers the durable Inbox projection from the Agent registry', async () => {
     const ctx = new Context()
     await ctx.plugin(SessionStore)
     await ctx.plugin(SessionProjectionRegistry)
-    const inboxFiber = ctx.plugin(InboxService)
-    await inboxFiber
+    const agentFiber = ctx.plugin(AgentRegistry)
+    await agentFiber
     const session = ctx.sessions.create(SessionId('inbox-projection'))
     const agent = stubAgent('inbox-projection', { ctx, session })
-    Object.assign(agent, { inbox: ctx.inboxes.create(agent) })
+    Object.assign(agent, { inbox: new Inbox(ctx, agent.session, agentEvents(ctx, agent)) })
     const pending = createUserMessage({
       content: [{ type: 'text', text: 'pending' }],
       source: { kind: 'user' },
@@ -189,7 +188,7 @@ describe('Inbox', () => {
       'next-turn': [pending],
       'next-step': [],
     })
-    await inboxFiber.dispose()
+    await agentFiber.dispose()
     expect(ctx.sessionProjections.snapshot(session).values).toEqual({})
   })
 

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

@@ -3,7 +3,7 @@ import { join, posix } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { Context } from '@deepseek-ai/cordis'
 import { describe, expect, it } from 'vitest'
-import { Inbox } from '@deepseek-ai/dsh-agent'
+import { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import { runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
 import {
@@ -96,7 +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) })
+      Object.assign(owner, { inbox: new Inbox(owner.ctx, owner.session, agentEvents(owner.ctx, owner)) })
       const backend = new BashTerminalBackend(ctx, {
         backendType: 'shell', shellDialect: 'bash', shellPath: '/bin/bash', shellArgs: ['--noprofile', '--norc', '-i'],
         rows: 24, cols: 80,

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

@@ -20,7 +20,6 @@ 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'
@@ -238,7 +237,6 @@ 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 ?? {})

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

@@ -807,19 +807,6 @@ 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.',
@@ -2762,6 +2749,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
     name: 'AgentCancelCause',
     declaration: 'export type AgentCancelCause = {\n    readonly kind: \'user\';\n} | {\n    readonly kind: \'parent\';\n} | {\n    readonly kind: \'hook\';\n    readonly reason: string;\n} | {\n    readonly kind: \'disposed\';\n};',
   },
+  {
+    name: 'AgentEventDispatch',
+    declaration: 'export interface AgentEventDispatch {\n    emit<K extends AgentSubjectEvent>(name: K, payload: PayloadRest<K>): void;\n    serial<K extends AgentSubjectEvent>(name: K, payload: PayloadRest<K>): Promise<Awaited<Return<Events[K]>>>;\n    waterfall<K extends AgentSubjectEvent>(name: K, payload: PayloadRest<K>, ...rest: Tail<K>): Return<Events[K]>;\n}',
+  },
   {
     name: 'AgentFactory',
     declaration: 'export interface AgentFactory {\n    createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>;\n    resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>;\n}',
@@ -2790,6 +2781,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
     name: 'AgentStatus',
     declaration: 'export type AgentStatus = \'idle\' | \'running\';',
   },
+  {
+    name: 'AgentSubjectEvent',
+    declaration: 'export type AgentSubjectEvent = {\n    [K in keyof Events]: Events[K] extends (this: Scoped<Agent>, ...args: infer P) => unknown ? P extends [\n        infer Payload,\n        ...unknown[]\n    ] ? Payload extends {\n        agent: Agent;\n    } ? K : never : never : never;\n}[keyof Events];',
+  },
   {
     name: 'ApprovalOutcome',
     declaration: 'export type ApprovalOutcome = \'allowed-once\' | \'rejected\' | \'cancelled\' | \'unavailable\';',
@@ -3300,7 +3295,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'Inbox',
-    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}',
+    declaration: 'export class Inbox {\n    constructor(private readonly ctx: Context, private readonly session: Session, private readonly dispatch: AgentEventDispatch);\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

@@ -1,7 +1,7 @@
 import { beforeEach, describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import Loader from '@deepseek-ai/cordis-plugin-loader'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
 import CommandRuntime from '@deepseek-ai/dsh-commands'
 import SessionStore, { foldSurface, Session, SessionId } from '@deepseek-ai/dsh-session'
@@ -59,7 +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) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(agent.ctx, agent)) })
   return { agent, session }
 }
 

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

@@ -6,7 +6,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import Loader from '@deepseek-ai/cordis-plugin-loader'
 import Include from '@deepseek-ai/cordis-plugin-include'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
 import CommandRuntime from '@deepseek-ai/dsh-commands'
 import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
@@ -45,7 +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) })
+  Object.assign(value, { inbox: new Inbox(value.ctx, value.session, agentEvents(value.ctx, value)) })
   ctx.agents.register(value)
   return value
 }

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

@@ -6,7 +6,7 @@ import { Context } from '@deepseek-ai/cordis'
 import { FsVersion } from '@deepseek-ai/dsh-fs'
 import { CallId } from '@deepseek-ai/dsh-llm'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
 import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
@@ -44,7 +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) })
+  Object.assign(value, { inbox: new Inbox(value.ctx, value.session, agentEvents(value.ctx, value)) })
   ctx.agents.register(value)
   return value
 }

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

@@ -1,7 +1,7 @@
 import { describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import Loader from '@deepseek-ai/cordis-plugin-loader'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
 import CommandRuntime from '@deepseek-ai/dsh-commands'
 import GoalService from '@deepseek-ai/dsh-goal'
@@ -36,7 +36,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) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(agent.ctx, agent)) })
   return { agent, session }
 }
 

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

@@ -1,7 +1,6 @@
 import { describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
-import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent'
-import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 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'
@@ -22,7 +21,7 @@ interface StubAgent {
 const isolatedInboxCtx = new Context()
 await isolatedInboxCtx.plugin(SessionStore)
 await isolatedInboxCtx.plugin(SessionProjectionRegistry)
-await isolatedInboxCtx.plugin(InboxService)
+await isolatedInboxCtx.plugin(AgentRegistry)
 const sessionStubs = new WeakMap<Session, StubAgent>()
 
 /** Number the next balanced test-fixture turn. */
@@ -59,7 +58,7 @@ function stubAgentForSession(session: Session, suppliedCtx?: Context): StubAgent
     runMaintenance: task => task(new AbortController().signal),
     whenIdle() { return Promise.resolve() },
   }
-  Object.assign(agent, { inbox: agentCtx.inboxes.create(agent) })
+  Object.assign(agent, { inbox: new Inbox(agentCtx, agent.session, agentEvents(agentCtx, agent)) })
   const stub = {
     agent,
     session,
@@ -84,7 +83,6 @@ 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()}`)
@@ -195,7 +193,6 @@ describe('GoalService creation and replay', () => {
     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')), ctx)
@@ -451,7 +448,6 @@ describe('GoalService mutations', () => {
     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')), ctx)

+ 3 - 5
packages/goal/tool-goal/tests/tool-goal.spec.ts

@@ -1,8 +1,7 @@
 import { describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import Loader from '@deepseek-ai/cordis-plugin-loader'
-import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent'
-import InboxService from '@deepseek-ai/dsh-agent/inbox'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 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'
@@ -26,7 +25,7 @@ interface StubAgent {
 const isolatedInboxCtx = new Context()
 await isolatedInboxCtx.plugin(SessionStore)
 await isolatedInboxCtx.plugin(SessionProjectionRegistry)
-await isolatedInboxCtx.plugin(InboxService)
+await isolatedInboxCtx.plugin(AgentRegistry)
 
 /** Build one registry-compatible live agent whose injections enter the durable inbox. */
 function stubAgent(rawId: string, supplied?: Session, suppliedCtx?: Context): StubAgent {
@@ -55,7 +54,7 @@ function stubAgent(rawId: string, supplied?: Session, suppliedCtx?: Context): St
     runMaintenance: task => task(new AbortController().signal),
     whenIdle() { return Promise.resolve() },
   }
-  Object.assign(agent, { inbox: agentCtx.inboxes.create(agent) })
+  Object.assign(agent, { inbox: new Inbox(agentCtx, agent.session, agentEvents(agentCtx, agent)) })
   return { agent, session, setStatus(value) { status = value } }
 }
 
@@ -87,7 +86,6 @@ 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)

+ 2 - 5
packages/host/apiproxy/tests/api-proxy-projections.spec.ts

@@ -10,9 +10,7 @@
 import { describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import { z } from 'zod'
-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 AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import { AttachmentStore } from '@deepseek-ai/dsh-attachment'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
@@ -71,7 +69,6 @@ async function harness(withRegistry: boolean): Promise<{ ctx: Context; session:
   await ctx.plugin(AgentRegistry)
   if (withRegistry) {
     await ctx.plugin(SessionProjectionRegistry)
-    await ctx.plugin(InboxService)
   }
   const session = ctx.sessions.create()
   const agent = {
@@ -89,7 +86,7 @@ async function harness(withRegistry: boolean): Promise<{ ctx: Context; session:
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   } satisfies Agent
-  if (withRegistry) Object.assign(agent, { inbox: ctx.inboxes.create(agent) })
+  if (withRegistry) Object.assign(agent, { inbox: new Inbox(ctx, agent.session, agentEvents(ctx, agent)) })
   ctx.agents.register(agent)
   return { ctx, session }
 }

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

@@ -3,7 +3,7 @@ import { homedir, tmpdir } from 'node:os'
 import { join } from 'node:path'
 import { describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent, AgentFactory } from '@deepseek-ai/dsh-agent'
 import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
 import type { Session } from '@deepseek-ai/dsh-session'
@@ -55,7 +55,7 @@ function stubAgent(session: Session): Agent {
     runMaintenance: job => job(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
-  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(agent.ctx, agent)) })
   return agent
 }
 

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

@@ -1,7 +1,7 @@
 import { describe, expect, expectTypeOf, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import { bindScopeParent, createScope, scopeOf } from '@deepseek-ai/dsh-scope'
 import type { ScopeKey } from '@deepseek-ai/dsh-scope'
@@ -45,7 +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) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(agent.ctx, agent)) })
   agentScopeDisposers.set(agent, async () => { await scopeFiber.dispose() })
   return agent
 }

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

@@ -7,7 +7,6 @@ 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'
@@ -66,7 +65,6 @@ async function loadYaml(lines: readonly string[]): Promise<Context> {
     ['@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],
   ])
@@ -97,7 +95,6 @@ describe('real Loader composition', () => {
       "- 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'",
     ])

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

@@ -1,6 +1,5 @@
 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'
@@ -26,7 +25,6 @@ async function harness(adapter: MockAdapter): Promise<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)

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

@@ -1,7 +1,6 @@
 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'
@@ -30,7 +29,6 @@ async function harness(roster: Partial<Config> = {}): Promise<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)

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

@@ -3,7 +3,6 @@ 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'
@@ -48,7 +47,6 @@ async function harness(roster: Config = { default: 'standard', roots: ROOTS, inc
   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)
@@ -419,7 +417,6 @@ describe('the preset file is an input, never a persistence target', () => {
     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)
@@ -586,7 +583,6 @@ describe('replacing a composition', () => {
     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)

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

@@ -16,7 +16,6 @@ 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'
@@ -46,7 +45,6 @@ async function harness(
   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

@@ -1,6 +1,6 @@
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent, AgentCancelCause, InboxTarget } from '@deepseek-ai/dsh-agent'
 import type { UserMessage } from '@deepseek-ai/dsh-llm'
 import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
@@ -96,7 +96,7 @@ async function harness(): Promise<RuntimeHarness> {
     steer(_message: UserMessage) {},
     inject(_message: UserMessage) {},
   }
-  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(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')

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

@@ -1,6 +1,6 @@
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent, AgentCancelCause, InboxTarget } from '@deepseek-ai/dsh-agent'
 import { CallId } from '@deepseek-ai/dsh-llm'
 import type { UserMessage } from '@deepseek-ai/dsh-llm'
@@ -39,7 +39,7 @@ function stubAgent(ctx: Context, id: string): Agent {
     steer(_message: UserMessage) {},
     inject(_message: UserMessage) {},
   }
-  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(agent.ctx, agent)) })
   return agent
 }
 

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

@@ -8,7 +8,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
 import Include from '@deepseek-ai/cordis-plugin-include'
 import { CallId } from '@deepseek-ai/dsh-llm'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import TerminalSessionService from '@deepseek-ai/dsh-terminal'
 import * as TerminalLocal from '@deepseek-ai/dsh-terminal-bash'
@@ -55,7 +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) })
+  Object.assign(value, { inbox: new Inbox(value.ctx, value.session, agentEvents(value.ctx, value)) })
   ctx.agents.register(value)
   return value
 }

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

@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import { CallId } from '@deepseek-ai/dsh-llm'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import TerminalSessionService from '@deepseek-ai/dsh-terminal'
 import type {
@@ -50,7 +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) })
+  Object.assign(value, { inbox: new Inbox(value.ctx, value.session, agentEvents(value.ctx, value)) })
   ctx.agents.register(value)
   return value
 }

+ 3 - 2
packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts

@@ -9,7 +9,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
 import Include from '@deepseek-ai/cordis-plugin-include'
 import { CallId } from '@deepseek-ai/dsh-llm'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import TerminalSessionService from '@deepseek-ai/dsh-terminal'
 import * as TerminalBash from '@deepseek-ai/dsh-terminal-bash'
@@ -51,7 +51,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: () => {},
@@ -62,6 +62,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.session, agentEvents(value.ctx, value)) })
   ctx.agents.register(value)
   return value
 }

+ 3 - 2
packages/shell/tool-pwsh-persistent/tests/tools.spec.ts

@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import { CallId } from '@deepseek-ai/dsh-llm'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import TerminalSessionService from '@deepseek-ai/dsh-terminal'
 import type {
@@ -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.session, agentEvents(value.ctx, value)) })
   ctx.agents.register(value)
   return value
 }

+ 2 - 2
packages/skill/tool-skill/tests/tool-skill.spec.ts

@@ -54,7 +54,7 @@ function agentForCwd(cwd: string): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
-  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(agent.ctx, agent)) })
   return agent
 }
 
@@ -74,7 +74,7 @@ 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) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(agent.ctx, agent)) })
   return agent
 }
 

+ 4 - 4
packages/terminal/terminal-bash/tests/index.spec.ts

@@ -4,7 +4,7 @@ import { resolve } from 'node:path'
 import { Context } from '@deepseek-ai/cordis'
 import Loader from '@deepseek-ai/cordis-plugin-loader'
 import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { Inbox, type Agent } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent'
 import SandboxProvider from '@deepseek-ai/dsh-sandbox'
 import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
 import SandboxPolicyService, { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
@@ -59,7 +59,7 @@ function agent(ctx: Context, cwd?: string): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
-  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(agent.ctx, agent)) })
   return agent
 }
 
@@ -530,7 +530,7 @@ describe('terminal-bash plugin shape', () => {
       runMaintenance: task => task(new AbortController().signal),
       whenIdle: () => Promise.resolve(),
     }
-    Object.assign(owner, { inbox: new Inbox(owner.ctx, owner) })
+    Object.assign(owner, { inbox: new Inbox(owner.ctx, owner.session, agentEvents(owner.ctx, owner)) })
     ctx.agents.register(owner)
     const providerFiber = await registerStubLocalBackend(ctx, () => stubLocalSession())
     const created = await ctx.terminals.spawn(owner, { type: 'stub' })
@@ -580,7 +580,7 @@ describe('terminal-bash plugin shape', () => {
       runMaintenance: task => task(new AbortController().signal),
       whenIdle: () => Promise.resolve(),
     }
-    Object.assign(owner, { inbox: new Inbox(owner.ctx, owner) })
+    Object.assign(owner, { inbox: new Inbox(owner.ctx, owner.session, agentEvents(owner.ctx, owner)) })
     ctx.agents.register(owner)
     const gate = Promise.withResolvers<undefined>()
     await registerStubLocalBackend(ctx, () => stubLocalSession(() => gate.promise))

+ 2 - 2
packages/terminal/terminal-bash/tests/local.spec.ts

@@ -5,7 +5,7 @@ import { join } from 'node:path'
 import { afterEach, describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import TerminalSessionService from '@deepseek-ai/dsh-terminal'
 import type { TerminalSendOperation } from '@deepseek-ai/dsh-terminal'
@@ -46,7 +46,7 @@ function stubAgent(ctx: Context, rawId: string): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
-  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(agent.ctx, agent)) })
   return agent
 }
 

+ 2 - 2
packages/terminal/terminal/tests/service.spec.ts

@@ -1,7 +1,7 @@
 import { describe, expect, expectTypeOf, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import TerminalSessionService, { TerminalBackendCleanupError, TerminalError, TerminalSessionId } from '@deepseek-ai/dsh-terminal'
 import type {
@@ -37,7 +37,7 @@ function stubAgent(ctx: Context, rawId: string): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
-  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(agent.ctx, agent)) })
   agentScopeDisposers.set(agent, async () => { await scopeFiber.dispose() })
   return agent
 }

+ 2 - 2
packages/terminal/tool-terminal/tests/loader-composition.spec.ts

@@ -8,7 +8,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
 import Include from '@deepseek-ai/cordis-plugin-include'
 import { CallId } from '@deepseek-ai/dsh-llm'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime from '@deepseek-ai/dsh-tools'
@@ -49,7 +49,7 @@ function agent(ctx: Context): Agent {
     runMaintenance: job => job(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
-  Object.assign(value, { inbox: new Inbox(value.ctx, value) })
+  Object.assign(value, { inbox: new Inbox(value.ctx, value.session, agentEvents(value.ctx, value)) })
   ctx.agents.register(value)
   return value
 }

+ 2 - 2
packages/terminal/tool-terminal/tests/tools.spec.ts

@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import { CallId } from '@deepseek-ai/dsh-llm'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime, { renderToolsSdk } from '@deepseek-ai/dsh-tools'
@@ -26,7 +26,7 @@ function fakeAgent(ctx: Context, rawId: string): Agent {
     runMaintenance: job => job(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
-  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent) })
+  Object.assign(agent, { inbox: new Inbox(agent.ctx, agent.session, agentEvents(agent.ctx, agent)) })
   ctx.agents.register(agent)
   return agent
 }

+ 0 - 2
packages/test-support/agent-loop-testkit/src/index.ts

@@ -7,7 +7,6 @@
 
 import type { Context } from '@deepseek-ai/cordis'
 import AgentRegistry from '@deepseek-ai/dsh-agent'
-import InboxService from '@deepseek-ai/dsh-agent/inbox'
 import LlmRuntime from '@deepseek-ai/dsh-llm'
 import SessionStore from '@deepseek-ai/dsh-session'
 import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
@@ -43,7 +42,6 @@ export async function mountAgentLoopTestDependencies(
   await ctx.plugin(LlmRuntime)
   await ctx.plugin(SessionStore)
   await ctx.plugin(SessionProjectionRegistry)
-  await ctx.plugin(InboxService)
   await ctx.plugin(SystemPrompt, options.systemPrompt ?? {})
   await ctx.plugin(ToolRuntime, options.tools ?? {})
   await ctx.plugin(AgentRegistry)

+ 2 - 2
packages/todo/tool-todo/tests/loader-composition.spec.ts

@@ -11,7 +11,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
 import Include from '@deepseek-ai/cordis-plugin-include'
 import { CallId } from '@deepseek-ai/dsh-llm'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
+import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime from '@deepseek-ai/dsh-tools'
@@ -38,7 +38,7 @@ function agent(ctx: Context): Agent {
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
-  Object.assign(value, { inbox: new Inbox(value.ctx, value) })
+  Object.assign(value, { inbox: new Inbox(value.ctx, value.session, agentEvents(value.ctx, value)) })
   ctx.agents.register(value)
   return value
 }

+ 0 - 1
scripts/gen-cordis-catalog.ts

@@ -54,7 +54,6 @@ export const SERVICE_PAGE: Record<string, string> = {
   agentDefaultModel: 'core.md',
   agentPresets: 'core.md',
   agents: 'core.md',
-  inboxes: 'core.md',
   apiProxy: 'typert.md',
   approval: 'approval.md',
   attachments: 'attachment.md',

+ 0 - 8
scripts/gen-doc-graphs.ts

@@ -139,14 +139,6 @@ const SERVICE_ROLES: ServiceRole[] = [
     consumers: ['agent-loop', 'agent', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants', 'message-feedback'],
     note: 'Owns append-only Session instances and emits the durable session event feed.',
   },
-  {
-    key: 'inboxes',
-    pkg: 'agent',
-    title: 'Durable pending-input facade',
-    mode: 'core',
-    consumers: ['agent-loop'],
-    note: 'Registers the standard Inbox projection and creates command facades over its sole live state.',
-  },
   {
     key: 'invariants',
     pkg: 'invariants',

+ 0 - 2
tsconfig.base.json

@@ -98,8 +98,6 @@
       "@deepseek-ai/dsh-user-questions/types": ["./packages/interaction/user-questions/src/types.ts"],
       "@deepseek-ai/dsh-agent/types": ["./packages/core/agent/src/types.ts"],
       "@deepseek-ai/dsh-agent/brand": ["./packages/core/agent/src/brand.ts"],
-      "@deepseek-ai/dsh-agent/inbox": ["./packages/core/agent/src/inbox.ts"],
-      "@deepseek-ai/dsh-agent/inbox-projection": ["./packages/core/agent/src/inbox-projection.ts"],
       "@deepseek-ai/dsh-agent/invariant": ["./packages/core/agent/src/invariant.ts"],
       "@deepseek-ai/dsh-scope/invariant": ["./packages/core/scope/src/invariant.ts"],
       "@deepseek-ai/dsh-agent-loop/invariant": ["./packages/core/agent-loop/src/invariant.ts"],