فهرست منبع

fix(session-controller): address review feedback on cold Inbox commands

- Map an absent persistence backend to queue-item-not-found for updateQueue.
- Re-fence the published Agent after shared cold resume settles.
- Document cold queue activation and regenerate its public API catalog.
- Supersede the attached-only claim in the Web subagent conversations note.
_Kerman 1 ماه پیش
والد
کامیت
6bfda7a0e5
26فایلهای تغییر یافته به همراه124 افزوده شده و 55 حذف شده
  1. 2 2
      .agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.i18n.yaml
  2. 2 1
      .agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md
  3. 2 1
      .agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.zh.md
  4. 2 2
      .agents/notes/implemented/bug-fix/2026-08-17-durable-web-queue-recovery.i18n.yaml
  5. 3 3
      .agents/notes/implemented/bug-fix/2026-08-17-durable-web-queue-recovery.md
  6. 3 3
      .agents/notes/implemented/bug-fix/2026-08-17-durable-web-queue-recovery.zh.md
  7. 2 2
      .agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml
  8. 1 1
      .agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md
  9. 1 1
      .agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md
  10. 2 2
      .agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.i18n.yaml
  11. 1 1
      .agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.md
  12. 1 1
      .agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.zh.md
  13. 2 2
      .agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml
  14. 2 2
      .agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md
  15. 2 2
      .agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md
  16. 2 2
      docs/subsystems/session.i18n.yaml
  17. 2 2
      docs/subsystems/session.md
  18. 2 2
      docs/subsystems/session.zh.md
  19. 2 2
      packages/api/session-controller/README.i18n.yaml
  20. 3 1
      packages/api/session-controller/README.md
  21. 3 1
      packages/api/session-controller/README.zh.md
  22. 5 1
      packages/api/session-controller/src/agent.ts
  23. 0 3
      packages/api/session-controller/src/commands.ts
  24. 15 7
      packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts
  25. 60 6
      packages/api/session-controller/tests/session-cold.host.spec.ts
  26. 2 2
      packages/extensions/tool-cordis/src/api-catalog.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md
-2026-08-18-session-history-and-event-transport.md: 808565ff7df60b8aa6aa3f18820c1139b8bf5362
-2026-08-18-session-history-and-event-transport.zh.md: 8bd00def4531afa9cdf77ae7f689f2e77908545e
+2026-08-18-session-history-and-event-transport.md: 2dc5eac282ac8a9902a1d27371e1ae55cae82751
+2026-08-18-session-history-and-event-transport.zh.md: fc80aa7279b5f7d7ce80c401e7eb80d2b288c889

+ 2 - 1
.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md

@@ -153,7 +153,8 @@ Each method explicitly selects a cold inspection, live-only lookup, or resume-ca
 | `session.follow(address)` | one live or prepared observation carrying the opening page and projections | Publishes the snapshot first, then promotes an ordinary cold Session once in the background |
 | `session.control()` | current attached Agents, pending registry, and process-local registries | Baseline and reconnect do not resume an Agent |
 | `session.attachment`, fork source read | authorized durable Session data | A read does not resume an Agent |
-| `session.updateQueue`, `cancel` | only the current live Agent | Does not resume vanished state |
+| `session.updateQueue` | live Agent or ordinary persisted Session | Resumes an ordinary cold Session before mutating its Inbox |
+| `session.cancel` | only the current live Agent | Does not resume vanished state |
 | `models`, `selectModel`, `rename`, `prompt` | command resolves the target Session | Resumes only when the method explicitly permits it |
 | `create` and fork target | new Session/Agent | The user command supplies creation authority |
 

+ 2 - 1
.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.zh.md

@@ -153,7 +153,8 @@ Session Remote 方法传递 `SessionId` 或 `SessionAddress`,不靠参数类
 | `session.follow(address)` | 一份携带 opening page 与 projection 的 live 或 prepared observation | 先发布 snapshot,再在后台把普通冷 Session 提升一次 |
 | `session.control()` | 当前 attached Agent、pending registry 与进程内 registry | baseline 与重连不恢复 Agent |
 | `session.attachment`、fork 源读取 | 已授权的持久 Session 数据 | 读取不恢复 Agent |
-| `session.updateQueue`、`cancel` | 仅命中当前 live Agent | 不为已消失状态恢复 Agent |
+| `session.updateQueue` | live Agent 或普通持久 Session | 修改 Inbox 前恢复普通冷 Session |
+| `session.cancel` | 仅命中当前 live Agent | 不为已消失状态恢复 Agent |
 | `models`、`selectModel`、`rename`、`prompt` | 命令解析目标 Session | 仅按方法约定显式恢复 |
 | `create` 与 fork 目标 | 新 Session/Agent | 用户命令提供创建授权 |
 

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-17-durable-web-queue-recovery.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/bug-fix/2026-08-17-durable-web-queue-recovery.md
-2026-08-17-durable-web-queue-recovery.md: 4b8f6a321be6550631763a99e0a130376ab4b081
-2026-08-17-durable-web-queue-recovery.zh.md: c482a97c55af74f90e3d9f6f4ab82bdda3489086
+2026-08-17-durable-web-queue-recovery.md: 8bfdeaa46d7b2854c3e78848216c05e0688f9d15
+2026-08-17-durable-web-queue-recovery.zh.md: d75789430abcc638a779db882d7491373805e583

+ 3 - 3
.agents/notes/implemented/bug-fix/2026-08-17-durable-web-queue-recovery.md

@@ -6,17 +6,17 @@ English | [中文](2026-08-17-durable-web-queue-recovery.zh.md)
 
 ## Problem
 
-Inbox state survives in the session log, but `session.updateQueue` previously looked up only a live Agent. After a Host restart, an ordinary persisted Session remains cold until an operation needs its Agent, so editing or removing a restored pending item incorrectly returned `queue-item-not-found`.
+Inbox state survives in the session log. An implementation that looks up only a live Agent cannot reach the restored pending rows of an ordinary persisted Session after a Host restart, so editing or removing one returns `queue-item-not-found` even though persistence still owns the queue occurrence.
 
 ## Decision
 
-`session.updateQueue` resolves an ordinary cold Session through the shared Agent resolver before reading or mutating its Inbox. A missing persisted Session still maps to `queue-item-not-found`, while other resume failures keep their existing error and subagent ownership keeps the same fence as other Agent operations.
+`session.updateQueue` resolves an ordinary cold Session through the shared Agent resolver before reading or mutating its Inbox. A missing persisted Session — including in a deployment that composes no persistence backend — still maps to `queue-item-not-found`, while other resume failures keep their existing error and subagent ownership keeps the same fence as other Agent operations.
 
 The resolved Agent constructs its Inbox from the registered durable projection. The command therefore reads the restored pending lists and records edits or removals through the existing normalized `agent/inbox/spliced` event. No new session event or on-disk format is introduced.
 
 ## Verification
 
-A cold-operation test provides a detached persisted Session with a pending Inbox splice, invokes `session.updateQueue`, and proves that the Session is resumed, the row is removed, and the durable removal splice is appended.
+A cold-operation test provides a detached persisted Session with a pending Inbox splice, invokes `session.updateQueue`, and proves that the Session is resumed, the row is removed, and the durable removal splice is appended. The shipped keyless Web queue-actions snapshot covers user-visible editing and removal through the real HTTP/SSE path; its output is unchanged, while the Host test isolates the cold lifecycle branch.
 
 ## Alternatives considered
 

+ 3 - 3
.agents/notes/implemented/bug-fix/2026-08-17-durable-web-queue-recovery.zh.md

@@ -6,17 +6,17 @@ Status: implemented
 
 ## 问题
 
-Inbox 状态保存在会话日志中,但 `session.updateQueue` 之前只查找 live Agent。Host 重启后,普通持久 Session 会保持冷状态,直到某项操作需要其 Agent,因此编辑或移除已恢复的待处理项会错误返回 `queue-item-not-found`。
+Inbox 状态保存在会话日志中。若实现只查找 live Agent,Host 重启后就无法访问普通持久 Session 中已恢复的待处理行,因此即使持久层仍拥有该 queue occurrence,编辑或移除操作也会返回 `queue-item-not-found`。
 
 ## 决策
 
-`session.updateQueue` 在读取或修改 Inbox 前,通过共享 Agent 解析器解析普通冷 Session。持久 Session 确实不存在时仍映射为 `queue-item-not-found`;其他恢复失败保留原有错误,subagent ownership 也保持与其他 Agent 操作相同的限制。
+`session.updateQueue` 在读取或修改 Inbox 前,通过共享 Agent 解析器解析普通冷 Session。持久 Session 确实不存在时(包括未组装持久化后端的部署)仍映射为 `queue-item-not-found`;其他恢复失败保留原有错误,subagent ownership 也保持与其他 Agent 操作相同的限制。
 
 解析出的 Agent 从已注册的持久投影构建 Inbox。因此,该命令会读取恢复出的待处理列表,并通过既有的规范化 `agent/inbox/spliced` 事件记录编辑或移除。系统不引入新的会话事件或磁盘格式。
 
 ## 验证
 
-冷操作测试提供一份带待处理 Inbox splice 的分离持久 Session,调用 `session.updateQueue`,并证明 Session 会被恢复、待处理项会被移除且持久删除 splice 会被追加。
+冷操作测试提供一份带待处理 Inbox splice 的分离持久 Session,调用 `session.updateQueue`,并证明 Session 会被恢复、待处理项会被移除且持久删除 splice 会被追加。既有 keyless Web queue-actions snapshot 通过真实 HTTP/SSE 路径覆盖用户可见的编辑与移除;其输出保持不变,Host 测试则隔离验证冷生命周期分支。
 
 ## 考虑过的替代方案
 

+ 2 - 2
.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md
-2026-07-27-web-subagent-conversations.md: c897d345d9749facfb2046104b7a95076cf2df95
-2026-07-27-web-subagent-conversations.zh.md: 0fdfd19b6dac56c275bb2d2306ed492a269e70d2
+2026-07-27-web-subagent-conversations.md: b65561bccd81d74609bcde89044b913695374a08
+2026-07-27-web-subagent-conversations.zh.md: 00b24ac1f2b0eccb7c872fcc82afc635f00c113c

+ 1 - 1
.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md

@@ -61,7 +61,7 @@ The gateway maps missing parent, missing or diagnostic catalog entries, not-resu
 
 Viewing persisted history creates no mux subscription by itself. When a follow-up materializes a cold child Activation, the existing Host and mux streams publish its lifecycle and events. Reconnect rebuilds the addressed window through `subagent.history`.
 
-The ordinary `session.history` route is likewise observation-only for both ordinary and subagent sessions, but it does not carry the catalog address or grant continuation authority. Every ordinary route that needs an Agent resolves through the shared ownership fence before cold resume; `session.cancel` and `session.updateQueue` apply the same check directly because they intentionally query only attached Agents.
+The ordinary `session.history` route is likewise observation-only for both ordinary and subagent sessions, but it does not carry the catalog address or grant continuation authority. Every ordinary route that needs an Agent resolves through the shared ownership fence before cold resume; `session.cancel` applies the check directly because it intentionally queries only attached Agents, while `session.updateQueue` resolves cold sessions through the same fence ([durable web queue recovery](../bug-fix/2026-08-17-durable-web-queue-recovery.md)).
 
 The adapter stays in `dsh-host-apiproxy`; `dsh-host-webserver` remains a carrier. Browser code imports the contract through the existing connection package and never reaches host `ctx`, preserving the [GUI RPC layering](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md).
 

+ 1 - 1
.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md

@@ -61,7 +61,7 @@ one-shot 行始终会用文案替代输入框,说明执行记录为只读。
 
 查看持久化历史本身不会创建 mux 订阅。当后续消息物化冷态 child Activation 时,现有 Host 与 mux 流会发布其生命周期与事件。重新连接时,系统通过 `subagent.history` 重建已寻址窗口。
 
-普通 `session.history` 路由对于普通会话和 subagent 会话同样只执行观察,但它既不携带目录地址,也不授予继续执行权限。每条需要 Agent 的普通路由都会在恢复冷会话前经过共享所有权栅栏;`session.cancel` 与 `session.updateQueue` 会直接执行同一检查,因为它们有意只查询已附加的 Agent。
+普通 `session.history` 路由对于普通会话和 subagent 会话同样只执行观察,但它既不携带目录地址,也不授予继续执行权限。每条需要 Agent 的普通路由都会在恢复冷会话前经过共享所有权栅栏;`session.cancel` 会直接执行同一检查,因为它有意只查询已附加的 Agent,而 `session.updateQueue` 会经由同一栅栏恢复冷会话([持久 Web 队列恢复](../bug-fix/2026-08-17-durable-web-queue-recovery.zh.md))。
 
 适配器仍位于 `dsh-host-apiproxy`;`dsh-host-webserver` 仍作为载体。浏览器代码通过现有连接包导入约定,绝不直接访问宿主 `ctx`,从而保持 [GUI RPC 分层](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md)。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.md
-2026-07-30-web-queue-steer-action.md: 2718c5b3cc95f1ab02db80230ba158d9b5c3b4e6
-2026-07-30-web-queue-steer-action.zh.md: 377117e2aa9c20b1c39d1fb7f450dbe729b00580
+2026-07-30-web-queue-steer-action.md: a112b875cd33422b5d22e3638afa8789155eb307
+2026-07-30-web-queue-steer-action.zh.md: ff0567e1b277df9658474b137f76c36c27d613ae

+ 1 - 1
.agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.md

@@ -32,7 +32,7 @@ The action does not run `agent/prompt-submit`: choosing steering intentionally c
 
 ### Host and client boundary
 
-`session.updateQueue` carries the `steer` action and maps the two negative outcomes to typed RPC errors. The conversion is one synchronous Agent operation; the Host never reconstructs it by combining remove and prompt calls.
+`session.updateQueue` carries the `steer` action and maps the two negative outcomes to typed RPC errors. The conversion is one Agent operation; the Host never reconstructs it by combining remove and prompt calls.
 
 The Host's existing `queuedMirror` remains the sole transient inbox authority. Its `session/queue` snapshot carries every live occurrence with `placement: 'queued' | 'steering'`: QueueDock renders only queued rows, while ChatView renders pending steering at the conversation tail after the `Deep diving...` running-status row, with Copy but without Fork, edit, or delete actions. Reconnect replays the same snapshot, so this visibility does not require client optimism or a second registry.
 

+ 1 - 1
.agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.zh.md

@@ -32,7 +32,7 @@ Composer 对新输入采用另一套尽力而为约定。所寻址会话空闲
 
 ### Host 与客户端边界
 
-`session.updateQueue` 会携带 `steer` 操作,并把两种负面结果映射为类型化 RPC 错误。这项转换是一次同步 Agent 操作;Host 绝不会通过组合移除和提示词调用来重建它。
+`session.updateQueue` 会携带 `steer` 操作,并把两种负面结果映射为类型化 RPC 错误。这项转换是一次 Agent 操作;Host 绝不会通过组合移除和提示词调用来重建它。
 
 Host 仍以现有 `queuedMirror` 作为唯一的瞬态 inbox 权威。`session/queue` 快照会携带所有存活单次入队项及其 `placement: 'queued' | 'steering'`:QueueDock 只渲染 queued 行,ChatView 则在会话流末尾、`Deep diving...` 运行状态行之后渲染待处理 steering,提供复制操作,但不提供 fork、编辑或删除操作。重连会重放同一份快照,因此这项可见性既不依赖客户端乐观展示,也不需要第二个注册表。
 

+ 2 - 2
.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md
-2026-08-10-unary-apiproxy-remote-migration.md: b63946b581d3a2afcd159e3b1c0ef44f824aa35c
-2026-08-10-unary-apiproxy-remote-migration.zh.md: 92f40fc79f2be44855620b6b6915798834284747
+2026-08-10-unary-apiproxy-remote-migration.md: 37e9ba636b03ca865bd8c48b0406e6b057b7d49a
+2026-08-10-unary-apiproxy-remote-migration.zh.md: d0060da0cd8399ffe2592ae7ef3ec83d9bf358c7

+ 2 - 2
.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md

@@ -44,7 +44,7 @@ The Remote API deliberately follows Service names rather than preserving dotted
 | Session Host lifecycle | `session.list`, `search`, `create`, `fork` | Cross-Agent persistence, Workspace assignment, preset composition, and creation policy. |
 | Session transcript | `session.history`, `attachment`, `subagent.history` | Cold/live logs, pagination, projections, presenters, and attachment authorization. |
 | Agent model selection | `session.models`, `selectModel` | Per-Agent state, model validation, and default persistence are BFF policy. |
-| Agent input and control | `session.prompt`, `updateQueue`, `cancel` | Image admission, Inbox mutation, and endpoint-specific live-only semantics. |
+| Agent input and control | `session.prompt`, `updateQueue`, `cancel` | Image admission, Inbox mutation, and endpoint-specific live-only (`cancel`) or cold-resume (`updateQueue`) semantics. |
 | Native settings document | `settings.openDocument` | Host path resolution, document preparation, and native opening remain product policy in API Proxy. |
 | Session skill catalog | `skill.list` | Cold Sessions must not resume; preset standing scope and presenter filtering are BFF joins. |
 | Host runtime information | `host.describe` | Version, cwd, default model, and attached count combine several Host owners. |
@@ -67,7 +67,7 @@ The migration must pin these outcomes with integration tests:
 - an id missing from durable persistence fails with `session-not-found`;
 - resolver failures keep their existing `RpcError` through `TypertLookupFailure`.
 
-Lookup policy is key-wide, not endpoint-specific. Methods such as prompt, queue editing, cancellation, model selection, and skill listing cannot use the shared `agent` or `session` lookup while retaining live-only or no-resume behavior, so they remain in the API Proxy until Typert supports an explicit per-endpoint policy.
+Lookup policy is key-wide, not endpoint-specific. Methods such as prompt, cancellation, model selection, and skill listing cannot use the shared `agent` or `session` lookup while retaining live-only or no-resume behavior; queue editing now resolves cold sessions through the shared resolver but keeps its Inbox-mutation and error-mapping policy local. These methods remain in the API Proxy until Typert supports an explicit per-endpoint policy.
 
 Methods whose signatures contain only branded ids do not invoke Typert object lookup. `subagents.interruptByParent()` must retain the existing process-local Activation lookup and parent-offline behavior: it does not call `agentFor`, read the catalog, inspect persistence, or cold-resume a parent or child.
 

+ 2 - 2
.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md

@@ -44,7 +44,7 @@ Remote API 有意采用服务名称,而不保留旧 RPC 的点分名称。例
 | Session Host 生命周期 | `session.list`、`search`、`create`、`fork` | 跨 Agent 持久化、Workspace 分配、preset 组合和创建策略。 |
 | Session transcript | `session.history`、`attachment`、`subagent.history` | cold/live 日志、分页、投影、呈现器和附件授权。 |
 | Agent 模型选择 | `session.models`、`selectModel` | 各 Agent 的状态、模型校验和默认值持久化属于 BFF 策略。 |
-| Agent 输入与控制 | `session.prompt`、`updateQueue`、`cancel` | 图片准入、Inbox 变更和端点特有的仅限 live 语义。 |
+| Agent 输入与控制 | `session.prompt`、`updateQueue`、`cancel` | 图片准入、Inbox 变更,以及端点特有的仅限 live(`cancel`)或恢复冷会话(`updateQueue`)语义。 |
 | 原生 settings 文档 | `settings.openDocument` | Host 路径解析、文档准备和原生打开仍属于 API Proxy 中的产品策略。 |
 | Session skill 目录 | `skill.list` | 不得恢复冷 Session;preset 的常驻 scope 和呈现器过滤属于 BFF 关联操作。 |
 | Host 运行时信息 | `host.describe` | 版本、cwd、默认模型和当前已附加的 Session 数量来自多个 Host 所有者。 |
@@ -67,7 +67,7 @@ Remote API 有意采用服务名称,而不保留旧 RPC 的点分名称。例
 - 持久化存储中不存在的 id 以 `session-not-found` 失败;
 - resolver 失败会保留现有的 `RpcError`,并通过 `TypertLookupFailure` 传递。
 
-Lookup 策略作用于整个 key,而非特定端点。提示词输入、队列编辑、取消、模型选择和 skill 列表等方法如果使用共享 `agent` 或 `session` lookup,就无法保留仅限 live 或禁止恢复的行为,因此在 Typert 支持显式的逐端点策略之前,这些方法仍留在 API Proxy 中。
+Lookup 策略作用于整个 key,而非特定端点。提示词输入、取消、模型选择和 skill 列表等方法如果使用共享 `agent` 或 `session` lookup,就无法保留仅限 live 或禁止恢复的行为;队列编辑目前已通过共享 resolver 恢复冷会话,但把 Inbox 变更和错误映射策略留在本地。在 Typert 支持显式的逐端点策略之前,这些方法仍留在 API Proxy 中。
 
 签名只包含 branded id 的方法不会调用 Typert 对象 lookup。`subagents.interruptByParent()` 必须保留现有的进程内 Activation lookup 和父级离线行为:它不会调用 `agentFor`、读取目录、检查持久化,也不会冷恢复父 Agent 或子 Agent。
 

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/session.md
-session.md: 87e8b33dec8eda6f7a716d71f5c4afa69a420d35
-session.zh.md: d30b7d24412a04521300c657c1b07daa1620ca5f
+session.md: 910f2d77a85ee1c03e07102a8bfbbbdd5eb1389a
+session.zh.md: 942241c3dda19955554d6fab1ae584d62fc0d87a

+ 2 - 2
docs/subsystems/session.md

@@ -667,11 +667,11 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
 @Remote('attachment') attachment(request: SessionAttachmentRequest): Promise<SessionAttachmentValue>
 
 /**
- * Mutate one still-pending queue occurrence on a live Agent.
+ * Mutate one still-pending queue occurrence, resuming a cold Agent first.
  * @param request - Session, queue item, and requested mutation.
  * @returns acknowledgement that the queue mutation was applied.
  */
-@Remote('updateQueue') updateQueue(request: SessionUpdateQueueRequest): SessionUpdateQueueValue
+@Remote('updateQueue') updateQueue(request: SessionUpdateQueueRequest): Promise<SessionUpdateQueueValue>
 
 /**
  * Cancel one active Agent turn without dropping its pending inbox.

+ 2 - 2
docs/subsystems/session.zh.md

@@ -671,11 +671,11 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
 @Remote('attachment') attachment(request: SessionAttachmentRequest): Promise<SessionAttachmentValue>
 
 /**
- * Mutate one still-pending queue occurrence on a live Agent.
+ * Mutate one still-pending queue occurrence, resuming a cold Agent first.
  * @param request - Session, queue item, and requested mutation.
  * @returns acknowledgement that the queue mutation was applied.
  */
-@Remote('updateQueue') updateQueue(request: SessionUpdateQueueRequest): SessionUpdateQueueValue
+@Remote('updateQueue') updateQueue(request: SessionUpdateQueueRequest): Promise<SessionUpdateQueueValue>
 
 /**
  * Cancel one active Agent turn without dropping its pending inbox.

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/api/session-controller/README.md
-README.md: 8f81caf78432f5c70586fd772a7db7d590035af2
-README.zh.md: 9932ed2a5b1995b3835243c7ffeb3c8c4767a523
+README.md: 6cae1d4a3a4be04aefd473f0af4cdf2bc86b3cbd
+README.zh.md: 48004caa160764a421ccc5d1d7c9122a2a722e86

+ 3 - 1
packages/api/session-controller/README.md

@@ -25,7 +25,9 @@ English | [中文](README.zh.md)
 
 History pages and follow opening snapshots carry a discriminated `SessionHistoryRecord`. Both variants use `{ type, event }`: `type: 'event'` carries one raw `SessionWireEvent`, while `type: 'chunks'` carries one lossless `ChunkRowEvent` for consecutive same-block `assistant/chunk` deltas. Both inner values expose `type`, `seq`, `time`, and `data`, so the Client retains each accepted record as one `SessionEventLikeEntry` without record-by-record conversion. A packed event's `seq` and `time` identify its first member, and `data` retains the fragment and timestamp-gap arrays. Live follow frames remain individual `event` records. Tool arguments, result content, failures, and `tool/result.data.meta` pass through unchanged; the controller does not resolve a Tool definition, run a presenter, or attach UI data.
 
-Each endpoint states its activation policy. List, search, attachment, history pages, and log following can inspect persistence without activating an Agent; queue mutation and cancellation require the corresponding live state; model, rename, and prompt commands may explicitly resume an ordinary Session. Create and fork are the only operations that create a new Agent. The service applies one preset-aware resume policy and subagent ownership fence to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces.
+Each endpoint states its activation policy. List, search, attachment, history pages, and log following can inspect persistence without activating an Agent; cancellation requires the corresponding live Agent; queue mutation, model, rename, and prompt commands may explicitly resume an ordinary Session. Create and fork are the only operations that create a new Agent. The service applies one preset-aware resume policy and subagent ownership fence to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces.
+
+`session.updateQueue` resolves the ordinary Session's Agent before locating the addressed `MessageId`, so an edit or removal can mutate a pending row reconstructed from the durable Inbox projection after a Host restart. A missing durable Session, including a Host without persistence, maps to `queue-item-not-found`; other resume failures retain their typed error, and subagent-owned identities remain fenced. Successful mutations append the standard `agent/inbox/spliced` event through the Agent's Inbox.
 
 The Client adapter exposes `SessionEventStream`, a Gateway `RemoteJournalStream` bound to one ordinary or direct-subagent address. It opens follow before the initial page, publishes only contiguous `replace`, `prepend`, and `append` changes, and repairs reconnect or sequence gaps through a tail page. Ordinary records cover `[event.seq, event.seq]`; packed rows cover `[event.seq, event.seq + memberCount - 1]`. A business, persistence, or unresolved continuity failure terminates the stream, while only physical carrier loss selects automatic resumption. `SessionControlStream` is a Gateway `RemoteSnapshotStream`; every generation opens with a complete process-local baseline, so reconnect replaces queue, jobs, and projection state instead of treating transient values as durable events. For each inbox change, the Host publishes the projection frame first and derives the queue replacement from that same validated post-fold value, so listener registration order cannot produce a stale queue frame.
 

+ 3 - 1
packages/api/session-controller/README.zh.md

@@ -25,7 +25,9 @@ kind: "package-reference"
 
 历史页与 follow opening snapshot 携带带判别字段的 `SessionHistoryRecord`。两个分支都使用 `{ type, event }`:`type: 'event'` 携带一个原始 `SessionWireEvent`,`type: 'chunks'` 则携带一个由连续且属于同一 block 的 `assistant/chunk` delta 组成的无损 `ChunkRowEvent`。两种内部值都公开 `type`、`seq`、`time` 与 `data`,因此 Client 无需逐 record 转换,就能把每条已接受 record 保留为一个 `SessionEventLikeEntry`。packed event 的 `seq` 与 `time` 表示首成员,`data` 保留 fragment 与 timestamp-gap 数组。实时 follow frame 继续携带单个 `event` record。工具参数、结果内容、失败信息和 `tool/result.data.meta` 原样通过;controller 不解析 Tool definition、不运行 presenter,也不附加 UI 数据。
 
-每个 endpoint 都声明自己的激活策略。列表、搜索、附件、历史页和日志跟随可以在不激活 Agent 的情况下检查 persistence;queue 变更和取消要求对应 live 状态仍然存在;模型、重命名和 prompt 命令可以显式恢复普通 Session。只有 create 和 fork 会创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。
+每个 endpoint 都声明自己的激活策略。列表、搜索、附件、历史页和日志跟随可以在不激活 Agent 的情况下检查 persistence;取消要求对应的 live Agent;queue 变更、模型、重命名和 prompt 命令可以显式恢复普通 Session。只有 create 和 fork 会创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。
+
+`session.updateQueue` 会先解析普通 Session 的 Agent,再定位被寻址的 `MessageId`,因此 Host 重启后,编辑或移除操作仍可修改从持久 Inbox 投影重建的待处理行。持久 Session 不存在时(包括 Host 未组装 persistence 后端)会映射为 `queue-item-not-found`;其他恢复失败保留其类型化错误,由 subagent 拥有的 identity 仍会被拒绝。成功的变更通过 Agent 的 Inbox 追加标准 `agent/inbox/spliced` 事件。
 
 Client adapter 提供 `SessionEventStream`,即绑定到一个普通 Session 或 direct subagent address 的 Gateway `RemoteJournalStream`。它在读取首个 page 前打开 follow,只发布连续的 `replace`、`prepend` 和 `append` 变更,并通过 tail page 修复重连或 seq 缺口。普通 record 覆盖 `[event.seq, event.seq]`,packed row 覆盖 `[event.seq, event.seq + memberCount - 1]`。业务、persistence 或无法恢复的连续性错误会终止 stream,只有物理载体断开才触发自动恢复。`SessionControlStream` 是 Gateway `RemoteSnapshotStream`;每代都以完整的进程本地 baseline 开始,因此重连会替换 queue、jobs 和 projection 状态,而不会把瞬态值当作 durable event。每次 inbox 变更时,Host 会先发布 projection frame,再从同一份已校验的折叠后值派生 queue replacement,因此监听器注册顺序不会产生陈旧的 queue frame。
 

+ 5 - 1
packages/api/session-controller/src/agent.ts

@@ -195,7 +195,11 @@ export class ApiSessionAgentController {
       this.resumes.set(sessionId, resume)
     }
     try {
-      return { agent: await resume }
+      const agent = await resume
+      // A shared resume can publish an identity that subagent routing adopts
+      // before every waiter observes it; apply the live ownership policy again.
+      const published = this.liveAgent(sessionId)
+      return published ?? { agent }
     } catch (error: unknown) {
       if (error instanceof ApiSessionNotFound) {
         return {

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

@@ -396,9 +396,6 @@ export class SessionCommandController {
       reject('queue-item-not-found', 'queued item is no longer pending', { itemId: request.itemId })
     }
     const { agent } = found
-    if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
-      rejectFailure(apiSessionSubagentOwnershipError(request.sessionId))
-    }
     const nextTurn = agent.inbox.nextTurn.find(message => message.id === request.itemId)
     const nextStep = agent.inbox.nextStep.find(message => message.id === request.itemId)
     const located = nextTurn === undefined

+ 15 - 7
packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts

@@ -49,7 +49,15 @@ async function commandHarness(): Promise<{
     assembled: undefined,
   }
   const agents = {
-    resolveAgent: () => Promise.resolve({ agent }),
+    resolveAgent: (sessionId: SessionId) => Promise.resolve(sessionId === agent.id
+      ? { agent }
+      : {
+        error: {
+          code: 'session-not-found' as const,
+          message: `session "${sessionId}" not found`,
+          details: { sessionId },
+        },
+      }),
     selectionFor: () => selection,
     serializeImageAdmission: <Value>(_agent: Agent, operation: () => Promise<Value>) => operation(),
     composeAgent: () => Promise.resolve({ setup: () => {} }),
@@ -96,22 +104,22 @@ describe('Session queue commands', () => {
     await expectFailure(Promise.resolve().then(() => controller.updateQueue({
       sessionId: agent.id, itemId: queued.id, action: { kind: 'steer' },
     })), 'steer-unavailable')
-    expect(controller.updateQueue({
+    await expect(controller.updateQueue({
       sessionId: agent.id,
       itemId: queued.id,
       action: { kind: 'edit', content: [{ type: 'text', text: 'edited' }] },
-    })).toEqual({ accepted: true })
+    })).resolves.toEqual({ accepted: true })
     expect(inbox.nextTurn[0]?.content).toEqual([{ type: 'text', text: 'edited' }])
-    expect(controller.updateQueue({
+    await expect(controller.updateQueue({
       sessionId: agent.id, itemId: nextStep.id, action: { kind: 'remove' },
-    })).toEqual({ accepted: true })
+    })).resolves.toEqual({ accepted: true })
 
     Object.assign(agent, { status: 'running' })
     const steered = inbox.nextTurn[0]
     if (steered === undefined) throw new Error('missing edited queue item')
-    expect(controller.updateQueue({
+    await expect(controller.updateQueue({
       sessionId: agent.id, itemId: steered.id, action: { kind: 'steer' },
-    })).toEqual({ accepted: true })
+    })).resolves.toEqual({ accepted: true })
     expect(steer).toHaveBeenCalledWith(steered)
 
     await expectFailure(Promise.resolve().then(() => controller.cancel({

+ 60 - 6
packages/api/session-controller/tests/session-cold.host.spec.ts

@@ -10,8 +10,7 @@ import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import { Context } from '@deepseek-ai/cordis'
 import SessionStore from '@deepseek-ai/dsh-session'
-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 { SessionHistoryController } from '@deepseek-ai/dsh-api-session-controller/src/history.ts'
 import { subagentIdentityProjectionDefinition } from '@deepseek-ai/dsh-subagent/src/projection.ts'
 import { TypertLookupFailure } from '@deepseek-ai/dsh-typert-protocol'
@@ -424,7 +423,6 @@ describe('Remote Agent and Session lookup policy', () => {
     const ctx = new Context()
     await ctx.plugin(SessionStore)
     await ctx.plugin(AgentRegistry)
-    await ctx.plugin(InboxService)
     const sessionId = sid('session-cold-queue-mutation')
     const meta = header(sessionId, 1000)
     const message = createUserMessage({
@@ -437,11 +435,11 @@ describe('Remote Agent and Session lookup policy', () => {
       time: 1001,
       data: { target: 'next-turn', start: 0, inserted: [message] },
     }] as SessionEvent[]
-    ctx.provide('sessionPersistence', {
+    providePersistence(ctx, {
       list: () => Promise.resolve([meta]),
       inspect: () => Promise.resolve({ meta, events }),
       locate: () => undefined,
-    } as never)
+    })
     let resumedAgent: Agent | undefined
     const resume = vi.spyOn(ctx.agents, 'resume').mockImplementation(async () => {
       const session = ctx.sessions.create(sessionId, {
@@ -463,7 +461,9 @@ describe('Remote Agent and Session lookup policy', () => {
         runMaintenance: task => task(new AbortController().signal),
         whenIdle: () => Promise.resolve(),
       } satisfies Agent
-      Object.assign(resumedAgent, { inbox: ctx.inboxes.create(resumedAgent) })
+      Object.assign(resumedAgent, {
+        inbox: new Inbox(ctx, resumedAgent.session, agentEvents(ctx, resumedAgent)),
+      })
       ctx.agents.register(resumedAgent)
       return { agent: resumedAgent, dispose: () => Promise.resolve() }
     })
@@ -487,6 +487,25 @@ describe('Remote Agent and Session lookup policy', () => {
     })
   })
 
+  it('keeps queue-item-not-found for a cold session when no persistence backend is composed', async () => {
+    const ctx = new Context()
+    await ctx.plugin(SessionStore)
+    await ctx.plugin(AgentRegistry)
+    const remote = createSessionTestRemote(ctx, {
+      defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
+      cwd: '/tmp',
+    })
+
+    const response = await remote.updateQueue(request({
+      sessionId: sid('session-no-persistence'),
+      itemId: MessageId('queued-item'),
+      action: { kind: 'remove' },
+    }))
+
+    expect(response.ok).toBe(false)
+    if (!response.ok) expect(response.error.code).toBe('queue-item-not-found')
+  })
+
   it('deduplicates a cold resume across Agent and Session parameters', async () => {
     const ctx = new Context()
     await ctx.plugin(TypertRegistry)
@@ -576,6 +595,41 @@ describe('Remote Agent and Session lookup policy', () => {
     expect(resume).not.toHaveBeenCalled()
     expect(inspect).toHaveBeenCalledOnce()
   })
+
+  it('reapplies the subagent ownership fence after a successful resume publishes the Agent', async () => {
+    const ctx = new Context()
+    await ctx.plugin(TypertRegistry)
+    await ctx.plugin(SessionStore)
+    await ctx.plugin(AgentRegistry)
+    const sessionId = sid('session-remote-resumed-child')
+    const meta = header(sessionId, 1000)
+    providePersistence(ctx, {
+      list: () => Promise.resolve([meta]),
+      inspect: () => Promise.resolve({ meta, events: [] as SessionEvent[] }),
+      locate: () => undefined,
+    })
+    vi.spyOn(ctx.agents, 'resume').mockImplementationOnce(async () => {
+      const session = ctx.sessions.create(sessionId, {
+        meta: { cwd: '/proj', origin: 'subagent' },
+      })
+      const published = { id: session.id, session, status: 'idle', ctx } as Agent
+      ctx.agents.register(published)
+      return { agent: published, dispose: () => Promise.resolve() }
+    })
+    const defaultLookup = ctx.typert.lookups.get('agent')
+    createSessionTestRemote(ctx, {
+      defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
+      cwd: '/tmp',
+    })
+    await vi.waitFor(() => { expect(ctx.typert.lookups.get('agent')).not.toBe(defaultLookup) })
+    const lookup = ctx.typert.lookups.get('agent')
+    if (lookup === undefined) throw new Error('Agent lookup provider was not mounted')
+
+    const resolution = lookup.resolve(sessionId)
+
+    await expect(resolution).rejects.toBeInstanceOf(TypertLookupFailure)
+    await expect(resolution).rejects.toMatchObject({ failure: { code: 'agent-busy' } })
+  })
 })
 
 describe('subagent ownership fence', () => {

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

@@ -1383,8 +1383,8 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         returns: 'the durable attachment reference and base64-encoded bytes.',
       },
       {
-        signature: '@Remote(\'updateQueue\') updateQueue(request: SessionUpdateQueueRequest): SessionUpdateQueueValue',
-        description: 'Mutate one still-pending queue occurrence on a live Agent.',
+        signature: '@Remote(\'updateQueue\') updateQueue(request: SessionUpdateQueueRequest): Promise<SessionUpdateQueueValue>',
+        description: 'Mutate one still-pending queue occurrence, resuming a cold Agent first.',
         parameters: [{ name: 'request', description: 'Session, queue item, and requested mutation.' }],
         returns: 'acknowledgement that the queue mutation was applied.',
       },