Răsfoiți Sursa

Merge master into session format migration

Tianyi Cui 3 săptămâni în urmă
părinte
comite
3bdc1b37e5
51 a modificat fișierele cu 563 adăugiri și 396 ștergeri
  1. 2 2
      .agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml
  2. 3 3
      .agents/notes/implemented/feature/2026-08-05-agent-teams.md
  3. 3 3
      .agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md
  4. 6 0
      .agents/notes/implemented/simplification/2026-08-30-team-send-message-steer.i18n.yaml
  5. 41 0
      .agents/notes/implemented/simplification/2026-08-30-team-send-message-steer.md
  6. 41 0
      .agents/notes/implemented/simplification/2026-08-30-team-send-message-steer.zh.md
  7. 6 0
      apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs
  8. 55 0
      apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts
  9. 2 2
      docs/config-catalog.i18n.yaml
  10. 1 1
      docs/config-catalog.md
  11. 1 1
      docs/config-catalog.zh.md
  12. 2 2
      docs/persistence-catalog.i18n.yaml
  13. 8 8
      docs/persistence-catalog.md
  14. 8 8
      docs/persistence-catalog.zh.md
  15. 2 2
      docs/subsystems/agent-team.i18n.yaml
  16. 4 3
      docs/subsystems/agent-team.md
  17. 4 3
      docs/subsystems/agent-team.zh.md
  18. 2 2
      docs/tool-catalog.i18n.yaml
  19. 3 29
      docs/tool-catalog.md
  20. 2 28
      docs/tool-catalog.zh.md
  21. 1 1
      packages/core/tools/tests/gen-tool-catalog.spec.ts
  22. 2 2
      packages/experimental/README.i18n.yaml
  23. 1 1
      packages/experimental/README.md
  24. 1 1
      packages/experimental/README.zh.md
  25. 2 2
      packages/experimental/agent-team/README.i18n.yaml
  26. 3 1
      packages/experimental/agent-team/README.md
  27. 3 1
      packages/experimental/agent-team/README.zh.md
  28. 1 1
      packages/experimental/agent-team/src/index.ts
  29. 31 41
      packages/experimental/agent-team/src/mailbox.ts
  30. 6 7
      packages/experimental/agent-team/src/projection.ts
  31. 3 3
      packages/experimental/agent-team/src/roster.ts
  32. 2 2
      packages/experimental/agent-team/src/task-board.ts
  33. 4 6
      packages/experimental/agent-team/src/types.ts
  34. 3 3
      packages/experimental/agent-team/tests/invariant.spec.ts
  35. 15 30
      packages/experimental/agent-team/tests/persistence.spec.ts
  36. 43 44
      packages/experimental/agent-team/tests/projection-events.spec.ts
  37. 116 80
      packages/experimental/agent-team/tests/team.spec.ts
  38. 2 2
      packages/experimental/tool-agent-team/README.i18n.yaml
  39. 8 8
      packages/experimental/tool-agent-team/README.md
  40. 9 9
      packages/experimental/tool-agent-team/README.zh.md
  41. 18 25
      packages/experimental/tool-agent-team/src/index.ts
  42. 5 7
      packages/experimental/tool-agent-team/tests/tool-team.spec.ts
  43. 2 2
      packages/extensions/tool-cordis/src/api-catalog.ts
  44. 2 2
      packages/subagent/subagent/README.i18n.yaml
  45. 1 1
      packages/subagent/subagent/README.md
  46. 1 1
      packages/subagent/subagent/README.zh.md
  47. 29 2
      packages/subagent/subagent/src/continuation.ts
  48. 10 5
      packages/subagent/subagent/src/index.ts
  49. 39 6
      packages/subagent/subagent/src/internal.ts
  50. 3 2
      packages/subagent/subagent/tests/control.spec.ts
  51. 1 1
      scripts/gen-tool-catalog.ts

+ 2 - 2
.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-agent-teams.md
-2026-08-05-agent-teams.md: fbcd8485a972323bc0f8ffb6a5cb7cca9a50044e
-2026-08-05-agent-teams.zh.md: 0a1a8a81c47abfca275f5fc3c60b8d514cdddd30
+2026-08-05-agent-teams.md: df84b2710d597330e9fdf6f8f83edb1441f100ec
+2026-08-05-agent-teams.zh.md: f1ef275610f6307578288d483eb561ee8d3c7531

+ 3 - 3
.agents/notes/implemented/feature/2026-08-05-agent-teams.md

@@ -32,7 +32,7 @@ Fresh children have no inherited conversation. Fork children capture the Lead's
 
 Peer communication is a Lead-log mailbox. `team/message/queued` is appended and flushed before delivery. The target message carries the stable message id and sender identity in both durable source metadata and a short model-visible prefix. A target receipt is acknowledged with `team/message/delivered` only after its pending inbox item or recorded user message is flushed. Immediate admission is serialized per target in queued-log order, recovery retries queued-minus-delivered in the same order, and delivery folds live or persisted target inbox/history state before cold resume. Every current-version Team payload is runtime-validated before entering replay state. The Team runtime tracks dispatch and asynchronous acknowledgement work from synchronous admission until settlement; disposal closes admission and awaits both before removing the service. Current waiters wake only after the owning Team event flush succeeds.
 
-Quiet `send_message` injects, flushes, and acknowledges immediately for a live target without waking it; an inactive target remains queued until another event materializes that teammate. Waking `followup_task` becomes the target's next FIFO turn and may cold-resume it. Success means the message is already durable even when immediate delivery is deferred. The mechanism provides process-local retry and target-Session de-duplication, not a cross-process exactly-once claim.
+`send_message` always attempts Steer delivery. A running target receives the message at the nearest step boundary, an idle target starts a turn, and an inactive teammate cold-resumes. Success means the message is already durable even when temporary delivery failure leaves it queued. The mechanism provides process-local retry and target-Session de-duplication, not a cross-process exactly-once claim. The [Team Steer messaging decision](../simplification/2026-08-30-team-send-message-steer.md) owns the single-tool scheduling rationale.
 
 Shared tasks are complete snapshots with Team-local ids and monotonic revisions. Every mutation carries `expectedRevision`. Any member creates, reads, or claims a ready unowned task; the owner or Lead edits and transitions it, while only the Lead assigns another member. Numeric task ids remain within the safe-integer allocation range, and exhaustion fails without reusing an id. Dependencies must name non-deleted tasks and form a complete DAG. Deleted tasks are retained tombstones. `writeScopes` are normalized path prefixes that produce overlap diagnostics but never block claim or authorize a write.
 
@@ -48,7 +48,7 @@ Worktree isolation is not a harness runtime behavior. A deployment or prompt may
 
 **Extend direct-child subagent tools with peer ids.** Rejected because parent/child authority and Team peer membership are different domains. Adding peer access to the continuation seam would weaken its exact-parent authorization and still leave roster and tasks without a persistence owner.
 
-**Store mail in each target Session before delivery.** Rejected because an inactive target is intentionally not materialized for quiet mail. The always-live Lead Session is the transaction home; target recording is the acknowledgement and de-duplication boundary.
+**Store mail in each target Session before Lead-log enqueue.** Rejected because target materialization and acceptance can fail after the Team has committed the send. The always-live Lead Session is the transaction home; target recording is the acknowledgement and de-duplication boundary.
 
 **Treat task ownership or write scopes as locks.** Rejected because external writers bypass them, crashed owners remain durable, and path-prefix overlap cannot prove semantic independence. False mutual exclusion is more dangerous than an explicit warning.
 
@@ -68,6 +68,6 @@ Package tests cover identity, name and authority checks, provider selection, res
 
 The Lead Session grows with whole task/member snapshots and mailbox acknowledgements. This favors independently inspectable recovery over compact deltas; configured task and pending-mail bounds cap active state, while deleted and delivered history remains append-only until broader Session retention applies.
 
-An active roster member can be non-resident, so `inactive` is not failure and a wakeup can incur cold-resume latency. A quiet message for an inactive target can remain pending indefinitely until the target is otherwise materialized. A failed member permanently consumes its name and member slot, making provisioning failures visible instead of silently recycling identity.
+An active roster member can be non-resident, so `inactive` is not failure and a send can incur cold-resume latency. Temporary inspection, resume, or inbox-admission failure can leave a durable message queued for recovery. A failed member permanently consumes its name and member slot, making provisioning failures visible instead of silently recycling identity.
 
 Coordination reduces likely checkout conflicts but cannot eliminate writes outside filesystem compare-and-set tools. The final diff and tests remain the Lead's integration boundary.

+ 3 - 3
.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md

@@ -32,7 +32,7 @@ fresh child 不继承对话。fork child 只捕获一次 Lead 已完成 turn 前
 
 Peer 通讯使用 Lead 日志 mailbox。投递前先追加并 flush `team/message/queued`。target message 会在持久 source metadata 与短模型可见前缀中同时携带稳定 message id 和 sender identity。只有 pending inbox 条目或已记录用户消息完成 flush,Lead 日志才写入 `team/message/delivered` acknowledgement。即时准入按 target 和 queued 日志顺序串行化,恢复按同一顺序重试 queued-minus-delivered,并在冷恢复前折叠 live 或 persisted target 的 inbox/历史状态。每个当前版本 Team payload 都会经过运行时验证后才进入 replay state。Team runtime 从同步准入到 settlement 全程跟踪 dispatch 与异步 acknowledgement 工作;dispose 会关闭准入,并在移除服务前等待两者。当前 waiter 只在所属 Team event flush 成功后被唤醒。
 
-对于 live target,quiet `send_message` 会立即注入、flush 并确认,但不会唤醒它;inactive target 会保持 queued,直到其他事件 materialize 该 teammate。waking `followup_task` 成为 target 的下一个 FIFO turn,并可冷恢复。即使即时投递被推迟,成功也表示消息已经持久化。该机制提供进程内重试与 target Session 去重,不宣称跨进程 exactly-once。
+`send_message` 始终尝试 Steer 投递。running target 在最近的步骤边界收到消息,idle target 启动一个轮次,inactive teammate 则冷恢复。即使临时投递失败让消息保持 queued,成功也表示消息已经持久化。该机制提供进程内重试与 target Session 去重,不宣称跨进程 exactly-once。[Team Steer 消息决策](../simplification/2026-08-30-team-send-message-steer.zh.md)负责单工具调度的理由。
 
 共享 task 是带 Team-local id 与单调 revision 的完整快照。每次变更都携带 `expectedRevision`。任意 member 可以创建、读取或 claim ready 且无 owner 的任务;Owner 或 Lead 可以编辑和转换;只有 Lead 可以分配给另一个 member。数字 task id 保持在安全整数分配范围内;该范围耗尽时会失败,不会复用 id。依赖必须指向未删除任务,并形成完整 DAG。删除任务保留为 tombstone。`writeScopes` 是规范化路径前缀,只产生重叠诊断,绝不会阻止 claim 或授予写权限。
 
@@ -48,7 +48,7 @@ Worktree isolation 不是 harness runtime 行为。deployment 或 prompt 可以
 
 **用 peer id 扩展 direct-child subagent tool。** 拒绝,因为 parent/child 权限与 Team peer membership 是不同领域。向 continuation seam 增加 peer access 会削弱 exact-parent authorization,仍无法为 roster 与 task 提供持久 owner。
 
-**投递前把 mail 存入每个 target Session。** 拒绝,因为 quiet mail 不会 materialize inactive target。始终 live 的 Lead Session 是事务 owner;target recording 是 acknowledgement 与去重边界。
+**在 Lead 日志入队前把 mail 存入每个 target Session。** 拒绝,因为 Team 提交发送之后,target materialization 与准入仍可能失败。始终 live 的 Lead Session 是事务 owner;target recording 是 acknowledgement 与去重边界。
 
 **把 task ownership 或 write scope 当作锁。** 拒绝,因为外部 writer 会绕过它们,崩溃 owner 会持久保留,而路径前缀重叠不能证明语义独立。虚假的互斥保证比明确 warning 更危险。
 
@@ -68,6 +68,6 @@ Package test 以逐文件 100% coverage 覆盖身份、名字与权限检查、p
 
 Lead Session 会随着完整 task/member 快照与 mailbox acknowledgement 增长。该设计用可独立检查的恢复能力换取更紧凑的 delta;配置的 task 与 pending-mail 限额限制 active state,而 deleted 与 delivered 历史会保持 append-only,直到更广泛的 Session retention 生效。
 
-active roster member 可以不驻留,因此 `inactive` 不表示失败,wakeup 可能产生 cold-resume 延迟。发往 inactive target 的 quiet message 可能无限等待,直到 target 因其他原因 materialize。failed member 会永久占用名字与 member slot,使 provisioning failure 保持可见而不是静默回收身份。
+active roster member 可以不驻留,因此 `inactive` 不表示失败,send 可能产生 cold-resume 延迟。临时 inspection、resume 或 inbox 准入失败可能留下持久 queued 消息等待恢复。failed member 会永久占用名字与 member slot,使 provisioning failure 保持可见而不是静默回收身份。
 
 协调可以降低 checkout 冲突概率,但无法消除文件系统 CAS 工具之外的写入。最终 diff 与测试仍是 Lead 的集成边界。

+ 6 - 0
.agents/notes/implemented/simplification/2026-08-30-team-send-message-steer.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-30-team-send-message-steer.md
+2026-08-30-team-send-message-steer.md: 9eb38ec96f51d074510bd918fc46bac9a61027d0
+2026-08-30-team-send-message-steer.zh.md: 44ed792a1c1a4e149c3309a538dfbe6418612b16

+ 41 - 0
.agents/notes/implemented/simplification/2026-08-30-team-send-message-steer.md

@@ -0,0 +1,41 @@
+# Agent Note: Team messaging uses one Steer send_message operation
+
+Status: implemented
+
+English | [中文](2026-08-30-team-send-message-steer.zh.md)
+
+## Problem
+
+Agent Teams exposed two model operations for one durable mailbox: quiet `send_message` injected into a live target without waking it, while `followup_task` queued a distinct waking turn and cold-resumed an inactive teammate. Models had to choose a scheduling policy instead of stating whom to message, and quiet messages could accumulate for an inactive teammate until unrelated work resumed it.
+
+The ordinary continuable-Agent controls already use one direction-neutral `send_message` with fixed Steer scheduling. Retaining separate Team names and delivery modes made equivalent model communication depend on whether the target happened to be a direct child or a Team peer.
+
+## Decision
+
+Every Team member receives one `send_message({ target, message })` tool. The Team tool set contains nine operations; `followup_task` and model-selectable quiet delivery are absent. The durable `TeamMessageSnapshot` stores sender, target, content, and message identity without a scheduling field.
+
+Every accepted Team message uses Steer. A running target receives it at the nearest step boundary, an idle target starts a turn, and an inactive teammate cold-resumes through the continuation lifecycle. A successful Team send remains durable before delivery starts. `accepted` means the target inbox accepted the message; `queued` means a temporary inspection, resume, or inbox-admission failure left it in the Team mailbox for recovery. Neither result means the target completed the requested work.
+
+The Lead receives the Team-attributed user message through `Agent.steer()`. A teammate receives it through a symbol-keyed host-only continuation adapter that authorizes the exact Lead-to-direct-child edge, preserves the original `TeamMessageSource`, and performs resident or cold-resume Steer admission. Sibling and teammate-to-Lead messages therefore retain the real sender; the Team runtime never calls public adjacent-Agent `sendMessage()` while impersonating the Lead.
+
+The Lead Session remains the mailbox transaction owner. It flushes `team/message/queued` before dispatch, serializes immediate admissions per target in Lead-log order, and records `team/message/delivered` only after the target Session durably contains the same Team message id. Recovery retries queued-minus-delivered records in order, and target-side source folding prevents duplicate acceptance across the crash window between inbox insertion and acknowledgement.
+
+## Alternatives considered
+
+**Keep quiet `send_message` and waking `followup_task`.** This preserves caller control over turn scheduling but makes the model choose an implementation policy, permits unread durable mail on inactive targets, and diverges from adjacent-Agent messaging.
+
+**Keep `followup_task` as an alias for Steer.** Two names for identical behavior would preserve the tool-selection failure without adding an observable capability.
+
+**Route siblings through public adjacent-Agent `sendMessage()`.** That operation authorizes only exact direct-parent or direct-child model senders and derives its own `AgentMessageSource`. Calling it with the Lead would misattribute sibling mail; widening it to Team membership would weaken its adjacency rule.
+
+**Drop the Team mailbox and deliver directly.** Direct delivery loses durable enqueue-before-admission, recovery after temporary failure, stable message ids, and target-side de-duplication.
+
+## Testing
+
+Package tests pin running, idle, inactive, Lead, sibling, and recovery delivery; target-local ordering; sender attribution; inbox/history de-duplication; temporary failure returning `queued`; and the nine-tool schema. The keyless Agent Teams profile snapshot drives a running implementer, steers a researcher message into its next step, and verifies that both teammates still complete their assigned tasks before the Lead aggregates the result.
+
+## Consequences
+
+Models have one Team communication choice and cannot park quiet information intentionally. A message may extend the target's current turn, so prompts and tests require teammates to integrate new messages without abandoning work already in progress.
+
+The host-only Steer adapter becomes part of the internal continuation integration used by Team delivery. Human browser prompts keep the separate Queue adapter and remain distinct turns. The broader [Agent Teams decision](../feature/2026-08-05-agent-teams.md) retains mailbox, roster, task, and shared-checkout ownership; the [adjacent-Agent messaging decision](../architecture/2026-08-27-adjacent-agent-steer-messaging.md) retains the public direct-edge authorization and model-message source.

+ 41 - 0
.agents/notes/implemented/simplification/2026-08-30-team-send-message-steer.zh.md

@@ -0,0 +1,41 @@
+# Agent Note: Team 消息使用单一 Steer send_message 操作
+
+Status: implemented
+
+[English](2026-08-30-team-send-message-steer.md) | 中文
+
+## 问题
+
+Agent Teams 为一个持久 mailbox 公开了两个模型操作:quiet `send_message` 注入 live target 而不唤醒它,`followup_task` 则排入一个独立 waking turn 并冷恢复 inactive teammate。模型必须选择调度策略,而不是只说明消息目标;quiet 消息可能为 inactive teammate 持续累积,直到无关工作恢复它。
+
+普通 continuable-Agent 控件已经使用一个方向无关、固定 Steer 调度的 `send_message`。保留独立的 Team 名称与投递模式,会让等价的模型通信因为 target 恰好是 direct child 还是 Team peer 而采用不同语义。
+
+## 决策
+
+每个 Team member 都会获得一个 `send_message({ target, message })` 工具。Team 工具集包含九个操作;不存在 `followup_task` 与模型可选的 quiet 投递。持久 `TeamMessageSnapshot` 存储 sender、target、content 与 message identity,不存储调度字段。
+
+每条已接受的 Team 消息都使用 Steer。running target 在最近的步骤边界收到消息,idle target 启动一个轮次,inactive teammate 则通过 continuation lifecycle 冷恢复。每次成功的 Team send 都会在开始投递前完成持久化。`accepted` 表示 target inbox 已接受消息;`queued` 表示临时 inspection、resume 或 inbox 准入失败让消息留在 Team mailbox 等待恢复。两种结果都不表示 target 已完成所请求的工作。
+
+Lead 通过 `Agent.steer()` 接收携带 Team 归因的用户消息。teammate 通过 symbol-keyed host-only continuation adapter 接收消息;该 adapter 会授权精确的 Lead-to-direct-child edge、保留原始 `TeamMessageSource`,并执行 resident 或 cold-resume Steer 准入。因此 sibling 与 teammate-to-Lead 消息保留真实 sender;Team 运行时绝不会伪装成 Lead 调用公开的相邻 Agent `sendMessage()`。
+
+Lead Session 继续作为 mailbox transaction owner。它在 dispatch 前 flush `team/message/queued`,按 Lead 日志顺序为每个 target 串行化即时准入,并且只有 target Session 持久包含相同 Team message id 后才记录 `team/message/delivered`。恢复按顺序重试 queued-minus-delivered 记录;target 侧 source 折叠会防止 inbox insertion 与 acknowledgement 之间的 crash window 导致重复准入。
+
+## 考虑过的替代方案
+
+**保留 quiet `send_message` 与 waking `followup_task`。** 这会保留调用方对 turn 调度的控制,但要求模型选择实现策略、允许 inactive target 存在未读持久 mail,并与相邻 Agent 消息语义分叉。
+
+**保留 `followup_task` 作为 Steer 别名。** 两个名字表达同一行为只会保留工具选择错误,不会增加可观察能力。
+
+**通过公开的相邻 Agent `sendMessage()` 路由 sibling。** 该操作只授权精确的 direct-parent 或 direct-child 模型 sender,并派生自己的 `AgentMessageSource`。以 Lead 身份调用会错误归因 sibling mail;把它扩展到 Team membership 则会削弱相邻关系规则。
+
+**删除 Team mailbox 并直接投递。** 直接投递会失去准入前持久入队、临时失败后的恢复、稳定 message id 与 target 侧去重。
+
+## 测试
+
+包测试固定 running、idle、inactive、Lead、sibling 与 recovery 投递,target-local ordering、sender attribution、inbox/history 去重、临时失败返回 `queued`,以及九工具 schema。无密钥 Agent Teams profile snapshot 驱动 running implementer,把 researcher 消息 Steer 到其下一步骤,并验证两个 teammate 都继续完成各自任务,之后 Lead 才汇总结果。
+
+## 后果
+
+模型只有一种 Team 通信选择,不能有意停放 quiet information。一条消息可能扩展 target 的当前 turn,因此提示词与测试要求 teammate 整合新消息,同时不放弃已经进行的工作。
+
+host-only Steer adapter 成为 Team 投递使用的内部 continuation 集成。人类浏览器 prompt 保留独立 Queue adapter,并继续形成不同 turn。更广泛的 [Agent Teams 决策](../feature/2026-08-05-agent-teams.zh.md)继续负责 mailbox、roster、task 与共享 checkout;[相邻 Agent 消息决策](../architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md)继续负责公开 direct-edge authorization 与 model-message source。

+ 6 - 0
apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs

@@ -88,6 +88,9 @@ function implementer(messages) {
   const names = calls(messages)
   const last = latestAssistantCalls(messages)
   const text = latestToolText(messages)
+  const userText = messages.flatMap(message => message.role === 'user'
+    ? message.content.filter(block => block.type === 'text').map(block => block.text)
+    : []).join('\n')
   if (!names.includes('team_task_create')) {
     if (last.includes('team_task_get') && text.includes('"subject":"Research"')) {
       return toolChunks([{ name: 'team_task_create', args: {
@@ -116,6 +119,9 @@ function implementer(messages) {
     }
     return toolChunks([{ name: 'team_task_get', args: { task_id: 'task-1' } }])
   }
+  if (!userText.includes('Research complete: use the deterministic finding.')) {
+    return toolChunks([{ name: 'wait_agent', args: { timeout_ms: 10000 } }])
+  }
   if (!names.includes('send_message')) {
     return toolChunks([
       { name: 'team_task_update', args: { task_id: 'task-2', expected_revision: 2, action: 'complete' } },

+ 55 - 0
apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts

@@ -558,6 +558,49 @@ describe('headless stream-json snapshots', () => {
         const tasks = rows.filter(row => row.type === 'team/task')
           .map(row => ((row.data as JsonObject).task as JsonObject))
         const latestTasks = Object.values(Object.fromEntries(tasks.map(task => [String(task.subject), task])))
+        const implementer = logs.find(log => typeof log.header.parentSession === 'string'
+          && parseJsonl(log.content).some((row) => {
+            if (row.type !== 'user/message') return false
+            const content: unknown = (row.data as JsonObject).content
+            return Array.isArray(content) && content.some((block: unknown) => (
+              typeof block === 'object' && block !== null && !Array.isArray(block)
+              && (block as JsonObject).type === 'text'
+              && typeof (block as JsonObject).text === 'string'
+              && ((block as JsonObject).text as string).includes('IMPLEMENTER_MARK')
+            ))
+          }))
+        if (implementer === undefined) throw new Error('Agent Teams snapshot did not persist the implementer')
+        const implementerRows = parseJsonl(implementer.content)
+        const steeredInboxIndex = implementerRows.findIndex((row) => {
+          if (row.type !== 'agent/inbox/spliced') return false
+          const data = row.data as JsonObject
+          const inserted: unknown = data.inserted
+          return data.target === 'next-step' && Array.isArray(inserted)
+            && inserted.some((message: unknown) => {
+              if (typeof message !== 'object' || message === null || Array.isArray(message)) return false
+              const source = (message as JsonObject).source
+              return typeof source === 'object' && source !== null && !Array.isArray(source)
+                && (source as JsonObject).kind === 'team-message'
+            })
+        })
+        const steeredMessageIndex = implementerRows.findIndex((row) => {
+          if (row.type !== 'user/message') return false
+          const source = (row.data as JsonObject).source
+          return typeof source === 'object' && source !== null && !Array.isArray(source)
+            && (source as JsonObject).kind === 'team-message'
+        })
+        const openTurnStart = implementerRows.findLastIndex((row, index) => (
+          index < steeredMessageIndex && row.type === 'turn/start'
+        ))
+        const openTurnEnd = implementerRows.findLastIndex((row, index) => (
+          index < steeredMessageIndex && row.type === 'turn/end'
+        ))
+        const completionAfterSteer = implementerRows.some((row, index) => {
+          if (index <= steeredMessageIndex || row.type !== 'tool/call') return false
+          const data = row.data as JsonObject
+          if (data.name !== 'team_task_update' || typeof data.arguments !== 'string') return false
+          return (JSON.parse(data.arguments) as JsonObject).action === 'complete'
+        })
         projection = {
           sessions: logs.length,
           memberEdges: members.length,
@@ -573,6 +616,12 @@ describe('headless stream-json snapshots', () => {
             && (row.data as JsonObject).name === 'wait_agent'),
           checkedRoster: rows.some(row => row.type === 'tool/call'
             && (row.data as JsonObject).name === 'list_agents'),
+          steerEvidence: {
+            nextStepInbox: steeredInboxIndex >= 0,
+            messageEntered: steeredMessageIndex > steeredInboxIndex,
+            enteredOpenTurn: openTurnStart > openTurnEnd,
+            completedAfterMessage: completionAfterSteer,
+          },
         }
       },
     })
@@ -592,6 +641,12 @@ describe('headless stream-json snapshots', () => {
         "memberEdges": 4,
         "queuedMessages": 2,
         "sessions": 3,
+        "steerEvidence": {
+          "completedAfterMessage": true,
+          "enteredOpenTurn": true,
+          "messageEntered": true,
+          "nextStepInbox": true,
+        },
         "tasks": [
           {
             "revision": 3,

+ 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: cedb4508fc474b4985be64765655708179f5336e
-config-catalog.zh.md: 8dc1c3befd39449fc7ddf8c767475d0db7ff52fb
+config-catalog.md: a91c4c4c1925e6e8a1ccc30f4eb5617298b67dad
+config-catalog.zh.md: 6b6637b1efa434d2091f1b3b66c96ff277b2d1eb

+ 1 - 1
docs/config-catalog.md

@@ -537,7 +537,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/experimental/agent-team/src/types.ts:131`](../packages/experimental/agent-team/src/types.ts)
+Source: [`packages/experimental/agent-team/src/types.ts:130`](../packages/experimental/agent-team/src/types.ts)
 
 <a id="deepseek-aidsh-experimental-code-runtime-python"></a>
 

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

@@ -539,7 +539,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/experimental/agent-team/src/types.ts:125`](../packages/experimental/agent-team/src/types.ts)
+来源:[`packages/experimental/agent-team/src/types.ts:124`](../packages/experimental/agent-team/src/types.ts)
 
 <a id="deepseek-aidsh-experimental-code-runtime-python"></a>
 

+ 2 - 2
docs/persistence-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/persistence-catalog.md
-persistence-catalog.md: ddc28d95741a06a714e7ed0c1f5d79207c637a2e
-persistence-catalog.zh.md: f37edda0ae6f96b1e10cb5ef376417abca1d4c15
+persistence-catalog.md: b8017a05f760180a90edce144e4e6d181c89bb82
+persistence-catalog.zh.md: a88cfca05b0a841d78673780937fa9e994877e38

+ 8 - 8
docs/persistence-catalog.md

@@ -778,12 +778,12 @@ Source: [`packages/subagent/tool-subagent/src/model-selection-state.ts:17`](../p
 
 ```ts persistence-catalog
 /** Whole teammate lifecycle value, stored only in the Team Lead Session. */
-'team/member': { version: 1; teamId: TeamId; member: TeamMemberSnapshot }
+'team/member': { version: 2; teamId: TeamId; member: TeamMemberSnapshot }
 ```
 
 Types: [TeamId](subsystems/agent-team.md) · [TeamMemberSnapshot](subsystems/agent-team.md)
 
-Source: [`packages/experimental/agent-team/src/types.ts:223`](../packages/experimental/agent-team/src/types.ts)
+Source: [`packages/experimental/agent-team/src/types.ts:221`](../packages/experimental/agent-team/src/types.ts)
 
 <a id="teammessagedelivered--log-only"></a>
 
@@ -792,7 +792,7 @@ Source: [`packages/experimental/agent-team/src/types.ts:223`](../packages/experi
 ```ts persistence-catalog
 /** Durable acknowledgement that the target Session recorded the message. */
 'team/message/delivered': {
-  version: 1
+  version: 2
   teamId: TeamId
   messageId: TeamMessageId
   targetId: SessionId
@@ -801,7 +801,7 @@ Source: [`packages/experimental/agent-team/src/types.ts:223`](../packages/experi
 
 Types: [TeamId](subsystems/agent-team.md) · [TeamMessageId](subsystems/agent-team.md)
 
-Source: [`packages/experimental/agent-team/src/types.ts:229`](../packages/experimental/agent-team/src/types.ts)
+Source: [`packages/experimental/agent-team/src/types.ts:227`](../packages/experimental/agent-team/src/types.ts)
 
 <a id="teammessagequeued--log-only"></a>
 
@@ -809,12 +809,12 @@ Source: [`packages/experimental/agent-team/src/types.ts:229`](../packages/experi
 
 ```ts persistence-catalog
 /** Durable mailbox enqueue, stored before delivery is attempted. */
-'team/message/queued': { version: 1; teamId: TeamId; message: TeamMessageSnapshot }
+'team/message/queued': { version: 2; teamId: TeamId; message: TeamMessageSnapshot }
 ```
 
 Types: [TeamId](subsystems/agent-team.md) · [TeamMessageSnapshot](subsystems/agent-team.md)
 
-Source: [`packages/experimental/agent-team/src/types.ts:227`](../packages/experimental/agent-team/src/types.ts)
+Source: [`packages/experimental/agent-team/src/types.ts:225`](../packages/experimental/agent-team/src/types.ts)
 
 <a id="teamtask--log-only"></a>
 
@@ -822,12 +822,12 @@ Source: [`packages/experimental/agent-team/src/types.ts:227`](../packages/experi
 
 ```ts persistence-catalog
 /** Whole shared-task value, stored only in the Team Lead Session. */
-'team/task': { version: 1; teamId: TeamId; task: TeamTaskSnapshot }
+'team/task': { version: 2; teamId: TeamId; task: TeamTaskSnapshot }
 ```
 
 Types: [TeamId](subsystems/agent-team.md) · [TeamTaskSnapshot](subsystems/agent-team.md)
 
-Source: [`packages/experimental/agent-team/src/types.ts:225`](../packages/experimental/agent-team/src/types.ts)
+Source: [`packages/experimental/agent-team/src/types.ts:223`](../packages/experimental/agent-team/src/types.ts)
 
 ### `todo/*`
 

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

@@ -780,12 +780,12 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 
 ```ts persistence-catalog
 /** Whole teammate lifecycle value, stored only in the Team Lead Session. */
-'team/member': { version: 1; teamId: TeamId; member: TeamMemberSnapshot }
+'team/member': { version: 2; teamId: TeamId; member: TeamMemberSnapshot }
 ```
 
 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamMemberSnapshot](subsystems/agent-team.zh.md)
 
-来源:[`packages/experimental/agent-team/src/types.ts:206`](../packages/experimental/agent-team/src/types.ts)
+来源:[`packages/experimental/agent-team/src/types.ts:204`](../packages/experimental/agent-team/src/types.ts)
 
 <a id="teammessagedelivered--log-only"></a>
 
@@ -794,7 +794,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 ```ts persistence-catalog
 /** Durable acknowledgement that the target Session recorded the message. */
 'team/message/delivered': {
-  version: 1
+  version: 2
   teamId: TeamId
   messageId: TeamMessageId
   targetId: SessionId
@@ -803,7 +803,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 
 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamMessageId](subsystems/agent-team.zh.md)
 
-来源:[`packages/experimental/agent-team/src/types.ts:212`](../packages/experimental/agent-team/src/types.ts)
+来源:[`packages/experimental/agent-team/src/types.ts:210`](../packages/experimental/agent-team/src/types.ts)
 
 <a id="teammessagequeued--log-only"></a>
 
@@ -811,12 +811,12 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 
 ```ts persistence-catalog
 /** Durable mailbox enqueue, stored before delivery is attempted. */
-'team/message/queued': { version: 1; teamId: TeamId; message: TeamMessageSnapshot }
+'team/message/queued': { version: 2; teamId: TeamId; message: TeamMessageSnapshot }
 ```
 
 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamMessageSnapshot](subsystems/agent-team.zh.md)
 
-来源:[`packages/experimental/agent-team/src/types.ts:210`](../packages/experimental/agent-team/src/types.ts)
+来源:[`packages/experimental/agent-team/src/types.ts:208`](../packages/experimental/agent-team/src/types.ts)
 
 <a id="teamtask--log-only"></a>
 
@@ -824,12 +824,12 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 
 ```ts persistence-catalog
 /** Whole shared-task value, stored only in the Team Lead Session. */
-'team/task': { version: 1; teamId: TeamId; task: TeamTaskSnapshot }
+'team/task': { version: 2; teamId: TeamId; task: TeamTaskSnapshot }
 ```
 
 类型:[TeamId](subsystems/agent-team.zh.md) · [TeamTaskSnapshot](subsystems/agent-team.zh.md)
 
-来源:[`packages/experimental/agent-team/src/types.ts:208`](../packages/experimental/agent-team/src/types.ts)
+来源:[`packages/experimental/agent-team/src/types.ts:206`](../packages/experimental/agent-team/src/types.ts)
 
 ### `todo/*`
 

+ 2 - 2
docs/subsystems/agent-team.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/agent-team.md
-agent-team.md: e3a746734b067bb7ef8f083c5c9af9cb3707dcf7
-agent-team.zh.md: 1f885a73861c091b50d27d15080ebc09f6d382ec
+agent-team.md: 201123ea94499ff20cae25d68b1f7a826439d143
+agent-team.zh.md: d6973ae6899c1d884dc51bd71afbd36a41895527

+ 4 - 3
docs/subsystems/agent-team.md

@@ -2,7 +2,7 @@
 
 English | [中文](agent-team.zh.md)
 
-Types shared by the experimental implicit-root Team domain, model tools, and host adapters. The [Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns identity, mailbox, task, and shared-checkout decisions; this page records the literal durable forms from [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts).
+Types shared by the experimental implicit-root Team domain, model tools, and host adapters. The [Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns identity, mailbox, task, and shared-checkout decisions; the [Team Steer messaging Agent Note](../../.agents/notes/implemented/simplification/2026-08-30-team-send-message-steer.md) owns message scheduling; this page records the literal durable forms from [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts).
 
 ## Identity and roster
 
@@ -34,11 +34,12 @@ interface TeamMessageSnapshot {
   readonly senderId: SessionId
   readonly senderName: string
   readonly targetId: SessionId
-  readonly delivery: 'quiet' | 'wakeup'
   readonly content: ContentBlock[]
 }
 ```
 
+Every message attempts Steer delivery. A running target receives it at the nearest step boundary, an idle target starts a turn, and an inactive teammate cold-resumes. Scheduling is not stored in the durable record because callers cannot select another mode.
+
 The target Session keeps message identity and sender attribution on both the pending inbox item and the eventual user message. Folding that source across inbox and history is the target-side de-duplication key; the model-visible framing repeats the id and sender.
 
 ```ts type-equiv
@@ -116,7 +117,7 @@ async spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise<Spawn
 /**
  * Queue one durable peer message, then attempt immediate delivery.
  * @param caller - exact live sending Team member.
- * @param request - target name, content, scheduling mode, and pre-queue cancellation.
+ * @param request - target name, content, and pre-queue cancellation.
  * @returns durable message identity and immediate-delivery observation.
  */
 async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise<SendTeamMessageResult>

+ 4 - 3
docs/subsystems/agent-team.zh.md

@@ -2,7 +2,7 @@
 
 [English](agent-team.md) | 中文
 
-实验性隐式 Root Team 领域、模型工具与宿主适配器共享的类型。[Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)负责身份、mailbox、task 与共享 checkout 决策;本页记录 [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts) 中的字面持久形式。
+实验性隐式 Root Team 领域、模型工具与宿主适配器共享的类型。[Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)负责身份、mailbox、task 与共享 checkout 决策;[Team Steer 消息 Agent Note](../../.agents/notes/implemented/simplification/2026-08-30-team-send-message-steer.zh.md)负责消息调度;本页记录 [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts) 中的字面持久形式。
 
 ## 身份与 roster
 
@@ -34,11 +34,12 @@ interface TeamMessageSnapshot {
   readonly senderId: SessionId
   readonly senderName: string
   readonly targetId: SessionId
-  readonly delivery: 'quiet' | 'wakeup'
   readonly content: ContentBlock[]
 }
 ```
 
+每条消息都会尝试 Steer 投递。running target 在最近的步骤边界收到消息,idle target 启动一个轮次,inactive teammate 则冷恢复。调用方不能选择其他模式,因此持久记录不存储调度方式。
+
 target Session 会在 pending inbox 条目和最终用户消息上保留消息身份与发送者归因。跨 inbox 与历史折叠该 source 构成 target 侧去重键;模型可见的 framing 会重复 id 和发送者。
 
 ```ts type-equiv
@@ -116,7 +117,7 @@ async spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise<Spawn
 /**
  * Queue one durable peer message, then attempt immediate delivery.
  * @param caller - exact live sending Team member.
- * @param request - target name, content, scheduling mode, and pre-queue cancellation.
+ * @param request - target name, content, and pre-queue cancellation.
  * @returns durable message identity and immediate-delivery observation.
  */
 async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise<SendTeamMessageResult>

+ 2 - 2
docs/tool-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/tool-catalog.md
-tool-catalog.md: f71c5dea97ca92fd816c45c6fbd816ee05309d63
-tool-catalog.zh.md: 3286da7015cfa793820f6107a96af1fc73472ddb
+tool-catalog.md: 416585106de28296a3105fafbbe6e4edc0d0f16b
+tool-catalog.zh.md: 89d2e467863c09b78805fd9db8ec785ad7936a9d

+ 3 - 29
docs/tool-catalog.md

@@ -36,7 +36,7 @@ This table connects model-visible tool names to the plugin package and service s
 | `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance independently controls whether it reads model-selection settings and its background behavior through `modelSelectionSettings`, `backgroundMode`, and `enableRunInBackground`. |
 | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). |
 | `@deepseek-ai/dsh-tool-jobs` | `job_kill`, `job_list`, `job_output` | `ctx.tools`, `ctx.jobs`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-job controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. |
-| `@deepseek-ai/dsh-experimental-tool-agent-team` | `followup_task`, `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/message/queued`, `team/message/delivered`, `team/task`, `tool/result` | - | All ten tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. |
+| `@deepseek-ai/dsh-experimental-tool-agent-team` | `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/message/queued`, `team/message/delivered`, `team/task`, `tool/result` | - | All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. |
 | `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task. |
 | `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`, `ctx.workflowEngine`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents the script children)` | `tool/call`, `tool/result` | - | - |
 | `@deepseek-ai/dsh-tool-web` | `web_fetch`, `web_search` | `ctx.tools`, `ctx.web`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | web_search and web_fetch keep provider selection behind ctx.web so model-visible schemas stay stable across backend swaps. |
@@ -1742,32 +1742,6 @@ The kind-agnostic background-job controller: background bash commands, PTY sends
 
 ## `@deepseek-ai/dsh-experimental-tool-agent-team`
 
-### `followup_task`
-
-Send a durable follow-up task to another Team member and start a turn when needed.
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "target": {
-      "type": "string",
-      "description": "Team member name, or lead."
-    },
-    "message": {
-      "type": "string",
-      "description": "Self-contained message for the target."
-    }
-  },
-  "required": [
-    "target",
-    "message"
-  ]
-}
-```
-
-Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts)
-
 ### `interrupt_agent`
 
 Interrupt one teammate's current turn while preserving its pending inbox. Team Lead only.
@@ -1804,7 +1778,7 @@ Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/exper
 
 ### `send_message`
 
-Send durable information to another Team member without starting an idle member.
+Send one durable message to another Team member. A running target receives it at the nearest step boundary; an idle target starts a turn; an inactive teammate cold-resumes.
 
 ```json
 {
@@ -2052,7 +2026,7 @@ Wait for the next teammate status, mailbox, or shared-task change after this cal
 
 Source: [`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts)
 
-All ten tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names.
+All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names.
 
 <a id="deepseek-aidsh-tool-todo"></a>
 

+ 2 - 28
docs/tool-catalog.zh.md

@@ -40,7 +40,7 @@
 | `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `modelSelectionSettings`、`backgroundMode` 与 `enableRunInBackground` 独立控制是否读取模型选择设置及其后台行为。 |
 | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是控制可继续后台 subagent 的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 |
 | `@deepseek-ai/dsh-tool-jobs` | `job_kill`、`job_list`、`job_output` | `ctx.tools`、`ctx.jobs`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`user/message via agent.inject() for background completion notices` | - | 与任务种类无关的后台任务控制器:后台 bash 命令、PTY 发送和 subagent 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 |
-| `@deepseek-ai/dsh-experimental-tool-agent-team` | `followup_task`、`interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/message/queued`、`team/message/delivered`、`team/task`、`tool/result` | - | 这 10 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 |
+| `@deepseek-ai/dsh-experimental-tool-agent-team` | `interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/message/queued`、`team/message/delivered`、`team/task`、`tool/result` | - | 这 9 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 |
 | `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`、`owning Agent session` | `tool/call`、`todo/write`、`tool/result` | - | todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为检查清单。`allowParallelInProgress` 是没有默认值的必填项,因此本目录明确选择 `true`,对应描述允许同时存在多个 `in_progress` 项。选择 `false` 的部署会获得同一工具,但描述会要求只能有 1 个活动任务。 |
 | `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`、`ctx.workflowEngine`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents the script children)` | `tool/call`、`tool/result` | - | - |
 | `@deepseek-ai/dsh-tool-web` | `web_fetch`、`web_search` | `ctx.tools`、`ctx.web`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | web_search 和 web_fetch 将提供方选择置于 ctx.web 之后,使模型可见 schema 在更换后端时保持稳定。 |
@@ -1748,32 +1748,6 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
 
 ## `@deepseek-ai/dsh-experimental-tool-agent-team`
 
-### `followup_task`
-
-向另一名 Team member 发送持久 follow-up task,并在需要时启动一个 turn。
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "target": {
-      "type": "string",
-      "description": "Team member name, or lead."
-    },
-    "message": {
-      "type": "string",
-      "description": "Self-contained message for the target."
-    }
-  },
-  "required": [
-    "target",
-    "message"
-  ]
-}
-```
-
-来源:[`packages/experimental/tool-agent-team/src/index.ts`](../packages/experimental/tool-agent-team/src/index.ts)
-
 ### `interrupt_agent`
 
 中断一名 teammate 的当前 turn,同时保留其待处理 inbox。仅 Team Lead 可用。
@@ -1810,7 +1784,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
 
 ### `send_message`
 
-向另一名 Team member 发送持久信息,但不启动 idle member。
+向另一名 Team member 发送一条持久消息。running target 会在最近的步骤边界收到消息;idle target 会启动一个 turn;inactive teammate 会冷恢复。
 
 ```json
 {

+ 1 - 1
packages/core/tools/tests/gen-tool-catalog.spec.ts

@@ -28,7 +28,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
     expect(names).toEqual([
       'ask_user_question', 'bash', 'bash', 'cordis_define', 'cordis_inspect_list',
       'cordis_inspect_query', 'cordis_inspect_self', 'cordis_run', 'cordis_stop',
-      'cordis_undefine', 'create_goal', 'edit', 'exit_plan_mode', 'followup_task', 'get_goal', 'glob', 'grep',
+      'cordis_undefine', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep',
       'interrupt_agent', 'interrupt_agent', 'job_kill', 'job_list', 'job_output',
       'list_agents', 'list_agents', 'list_subagent_models', 'lsp', 'pwsh', 'pwsh', 'ralph',
       'read', 'read_image', 'run_code', 'schedule_create', 'schedule_delete',

+ 2 - 2
packages/experimental/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/experimental/README.md
-README.md: 689739bc532ddde4c41c3fb550d098add0501f70
-README.zh.md: 18499a28849dd3f4671ef266f24818cdc30f15e8
+README.md: 1a5f60c84327f47d9edea7d6bccb87d710a766f7
+README.zh.md: b97fd66854bcf8ad435348ddcba605f44eeffe52

+ 1 - 1
packages/experimental/README.md

@@ -30,7 +30,7 @@ The experimental group contains prototype capabilities that are not part of any
 | [`client-ui-agent-team`](client-ui-agent-team/README.md) | Team roster, task board, and teammate navigation for Web | — |
 | [`code-runtime-python`](code-runtime-python/README.md) | CPython subprocess backend for the code-execution seam | `ctx.codeRuntime` |
 | [`inspector`](inspector/README.md) | Cross-realm CDP hub for Host debugging, Client Runtime inspection, network capture, and Cordis trees | `ctx.inspector` |
-| [`tool-agent-team`](tool-agent-team/README.md) | Ten tools that let the model create, message, and coordinate teammates | registers scoped tools on `ctx.tools` |
+| [`tool-agent-team`](tool-agent-team/README.md) | Nine tools that let the model create, message, and coordinate teammates | registers scoped tools on `ctx.tools` |
 | [`webworker-packer`](webworker-packer/README.md) | Builds the gzip-compressed VFS image consumed by the browser worker preview | library and CLI — no ctx key |
 | [`webworker-runtime`](webworker-runtime/README.md) | Runs the harness plugin tree inside a dedicated browser worker | library and worker entry — no ctx key |
 

+ 1 - 1
packages/experimental/README.zh.md

@@ -30,7 +30,7 @@ kind: "package-group"
 | [`client-ui-agent-team`](client-ui-agent-team/README.zh.md) | Web Team roster、任务板与 teammate 导航 | — |
 | [`code-runtime-python`](code-runtime-python/README.zh.md) | 代码执行 seam 的 CPython 子进程后端 | `ctx.codeRuntime` |
 | [`inspector`](inspector/README.zh.md) | 用于 Host 调试、Client Runtime 检查、网络采集与 Cordis 树的跨 realm CDP hub | `ctx.inspector` |
-| [`tool-agent-team`](tool-agent-team/README.zh.md) | 让模型创建、发消息与协调 teammate 的十个工具 | 按作用域注册工具到 `ctx.tools` |
+| [`tool-agent-team`](tool-agent-team/README.zh.md) | 让模型创建、发消息与协调 teammate 的九个工具 | 按作用域注册工具到 `ctx.tools` |
 | [`webworker-packer`](webworker-packer/README.zh.md) | 构建浏览器 worker 预览所消费的 gzip 压缩 VFS 镜像 | 库与 CLI,不使用 ctx key |
 | [`webworker-runtime`](webworker-runtime/README.zh.md) | 在专用浏览器 worker 中运行 harness 插件树 | 库与 worker 入口,不使用 ctx key |
 

+ 2 - 2
packages/experimental/agent-team/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/experimental/agent-team/README.md
-README.md: 0316834f566112b708f513b46cd0e5e3ab503061
-README.zh.md: 0aae14bccc06c57a9e1698f302cadd6b293baca8
+README.md: 1f9f7e98fdd2d8c32c6da2d514ff763367d6eb65
+README.zh.md: d6b57d35b6305c94fad966e3c9b3e20f6a3e81f5

+ 3 - 1
packages/experimental/agent-team/README.md

@@ -68,7 +68,7 @@ Only the Lead can create teammates or interrupt them.
 
 Any member can send a message to any other member or to the Lead. A live member receives it immediately; an offline member's messages queue and arrive when it resumes. Messages are never lost and never delivered twice.
 
-Two delivery modes cover the two common intents: a quiet message delivers information without starting an idle teammate (use it for updates that can wait), and a follow-up makes the message the recipient's next turn (use it to hand over work). The sender always sees the outcome — delivered now, or queued. A queued message is already safely stored, so it must not be resent.
+Every message uses Steer: a running target receives it at the nearest step boundary, an idle target starts a turn, and an inactive teammate cold-resumes. The sender always sees the outcome — accepted by the target inbox, or retained as queued when delivery is temporarily unavailable. A queued message is already safely stored, so it must not be resent.
 
 ### Shared task board
 
@@ -131,6 +131,8 @@ Every ordinary runtime root is the implicit Lead of a Team whose `TeamId` equals
 
 `sendMessage()` validates peer membership, appends `team/message/queued`, and flushes before attempting delivery. The target message begins with `Team message <id> from <name>:` and keeps the same id and sender in `TeamMessageSource`. A target receipt is acknowledged with `team/message/delivered` only after the target Session durably holds the message identity in its pending inbox or recorded history. Immediate admissions are serialized per target in durable queue order; recovery dispatches queued-minus-delivered records in the same order. Delivery folds both live and persisted target inbox/history state before retrying, so a crash between inbox acceptance and model claim does not duplicate the message. The guarantee is process-local retry plus target-Session de-duplication, not cross-process exactly-once delivery.
 
+Lead delivery calls `Agent.steer()` directly. Teammate delivery uses the continuation owner's host-only Steer path, which preserves the Team sender source while authorizing the Lead-to-child edge and cold-resuming inactive targets. Sibling messages never impersonate the Lead through the public adjacent-Agent messaging operation.
+
 ### Shared task board
 
 Tasks are complete versioned snapshots; every mutation carries `expectedRevision`, and a stale caller receives `TEAM_TASK_STALE_REVISION` instead of overwriting a newer value. Numeric `task-<n>` ids require a safe-integer suffix, and id-space exhaustion reports `TEAM_TASK_LIMIT` instead of reusing the final id. Deleted tasks remain tombstones for replay and id stability but do not consume `maxTasks` or appear in `listTasks()`. `writeScopes` are normalized workspace-relative prefixes; views warn on overlap with in-progress tasks but never block claim or authorize writes.

+ 3 - 1
packages/experimental/agent-team/README.zh.md

@@ -68,7 +68,7 @@ roster 显示每个成员的职责(`lead` 或 `teammate`)与当前状态:`
 
 任何成员都可以向任何其他成员或 Lead 发送消息。live 成员会立即收到;离线成员的消息会排队,并在其恢复后到达。消息不会丢失,也绝不会重复投递。
 
-两种投递模式覆盖两种常见意图:quiet 消息在不让 idle teammate 启动的情况下传达信息(用于可以等待的更新),follow-up 让消息成为接收方的下一个轮次(用于移交工作)。发送方始终能看到结果——已送达,或正在排队。排队的消息已经安全存储,因此绝不能重发。
+每条消息都使用 Steer:running target 在最近的步骤边界收到消息,idle target 启动一个轮次,inactive teammate 则冷恢复。发送方始终能看到结果——target inbox 已接受,或在投递暂时不可用时保留为 queued。排队的消息已经安全存储,因此绝不能重发。
 
 ### 共享任务板
 
@@ -131,6 +131,8 @@ Lead 可以停止 teammate 的当前轮次,而不会删除其排队的消息
 
 `sendMessage()` 校验 peer 成员关系,追加 `team/message/queued` 并在尝试投递前 flush。目标消息以 `Team message <id> from <name>:` 开头,并在 `TeamMessageSource` 中保留同一 id 与发送者。只有目标 Session 在 pending inbox 或已记录历史中持久持有消息身份后,才会以 `team/message/delivered` 确认投递。即时准入按目标与持久队列顺序串行化;恢复按同一顺序重新投递 queued-minus-delivered 记录。重试前会同时折叠 live 与持久目标 inbox/历史状态,因此 inbox 已接受但模型尚未 claim 时发生崩溃不会复制消息。该保证是进程内重试加 target Session 去重,而不是跨进程 exactly-once 投递。
 
+投递给 Lead 时直接调用 `Agent.steer()`。投递给 teammate 时使用 continuation owner 的 host-only Steer 路径;该路径会保留 Team 发送者 source,同时授权 Lead-to-child edge 并冷恢复 inactive target。sibling 消息绝不会通过公开的相邻 Agent 消息操作伪装成 Lead。
+
 ### 共享任务板
 
 任务是完整版本化快照;每次变更都携带 `expectedRevision`,陈旧调用方会收到 `TEAM_TASK_STALE_REVISION`,而不会覆盖更新的值。数字 `task-<n>` id 的后缀必须是安全整数,id 空间耗尽时报告 `TEAM_TASK_LIMIT`,而不是复用最后一个 id。已删除任务作为 tombstone 保留以供回放与维持 id 稳定,但不占用 `maxTasks`,也不出现在 `listTasks()` 中。`writeScopes` 是规范化后的 workspace 相对前缀;视图会对与 in-progress 任务的重叠发出警告,但绝不阻止 claim 或授予写权限。

+ 1 - 1
packages/experimental/agent-team/src/index.ts

@@ -157,7 +157,7 @@ export class TeamService extends TypertRemoteService {
   /**
    * Queue one durable peer message, then attempt immediate delivery.
    * @param caller - exact live sending Team member.
-   * @param request - target name, content, scheduling mode, and pre-queue cancellation.
+   * @param request - target name, content, and pre-queue cancellation.
    * @returns durable message identity and immediate-delivery observation.
    */
   async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise<SendTeamMessageResult> {

+ 31 - 41
packages/experimental/agent-team/src/mailbox.ts

@@ -8,7 +8,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
 import type { ContentBlock } from '@deepseek-ai/dsh-llm'
 import { SessionId } from '@deepseek-ai/dsh-session'
 import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
-import { queueHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal'
+import { steerHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal'
 import { errorMessage, TeamError } from './error.ts'
 import type { TeamJournal } from './journal.ts'
 import type { TeamRuntimeLifecycle } from './lifecycle.ts'
@@ -26,7 +26,6 @@ import type {
 /** Owns every process-local state transition for the durable Team mailbox. */
 export class TeamMailbox {
   private readonly dispatchTails = new Map<SessionId, Promise<void>>()
-  private readonly activeDispatches = new Map<SessionId, TeamMessageSnapshot>()
   private readonly inFlightMessages = new Set<TeamMessageId>()
   private readonly inFlightDispatches = new Set<Promise<unknown>>()
 
@@ -50,7 +49,7 @@ export class TeamMailbox {
   /**
    * Queue one durable peer message, then attempt immediate delivery.
    * @param caller - exact live sending Team member.
-   * @param request - target name, content, scheduling mode, and pre-queue cancellation.
+   * @param request - target name, content, and pre-queue cancellation.
    * @returns durable message identity and immediate-delivery observation.
    */
   async send(caller: Agent, request: SendTeamMessageRequest): Promise<SendTeamMessageResult> {
@@ -94,8 +93,6 @@ export class TeamMailbox {
       && (membership.role === 'lead' || message.targetId === agent.id))
     for (const message of messages) {
       signal.throwIfAborted()
-      if (membership.role === 'lead' && message.delivery === 'quiet'
-        && message.targetId !== membership.root.id && this.ctx.agents.get(message.targetId) === undefined) continue
       await this.tryDispatch(membership.root, message, signal)
     }
   }
@@ -135,14 +132,13 @@ export class TeamMailbox {
         senderId: caller.id,
         senderName: membership.name,
         targetId: target.id,
-        delivery: request.delivery,
         content,
       }
       if (Buffer.byteLength(JSON.stringify(this.deliveryContent(queued)), 'utf8') > this.maxMessageBytes) {
         throw new TeamError(`team message exceeds ${this.maxMessageBytes} bytes`, 'TEAM_MESSAGE_TOO_LARGE')
       }
       await this.journal.appendAndFlush(root, 'team/message/queued', {
-        version: 1,
+        version: 2,
         teamId: TeamId(root.id),
         message: queued,
       })
@@ -190,13 +186,7 @@ export class TeamMailbox {
     message: TeamMessageSnapshot,
     signal: AbortSignal,
   ): Promise<boolean> {
-    const active = this.activeDispatches.get(message.targetId)
-    const live = message.targetId === root.id ? root : this.ctx.agents.get(message.targetId)
-    if (active !== undefined && live !== undefined && message.delivery === 'quiet'
-      && this.messagePrecedes(root, message.id, active.id)) {
-      return await this.dispatchOnce(root, message, signal)
-    }
-    return await this.serializeDispatch(message, () => this.dispatchOnce(root, message, signal))
+    return await this.serializeDispatch(message, () => this.dispatchThrough(root, message, signal))
   }
 
   /** Serialize delivery admission for one durable target in queued order. */
@@ -206,16 +196,8 @@ export class TeamMailbox {
   ): Promise<boolean> {
     const targetId = message.targetId
     const prior = this.dispatchTails.get(targetId) ?? Promise.resolve()
-    const dispatch = async (): Promise<boolean> => {
-      this.activeDispatches.set(targetId, message)
-      try {
-        return await operation()
-      } finally {
-        this.activeDispatches.delete(targetId)
-      }
-    }
     /* v8 ignore next -- dispatch tails absorb rejection, so the recovery callback is a fail-safe backstop. */
-    const run = prior.then(dispatch, dispatch)
+    const run = prior.then(operation, operation)
     /* v8 ignore next -- dispatchOnce contains delivery failures and serializeDispatch itself does not throw. */
     const tail = run.then(() => undefined, () => undefined)
     this.dispatchTails.set(targetId, tail)
@@ -226,6 +208,29 @@ export class TeamMailbox {
     }
   }
 
+  /** Deliver every pending target message through `message` in durable queue order. */
+  private async dispatchThrough(
+    root: Agent,
+    message: TeamMessageSnapshot,
+    signal: AbortSignal,
+  ): Promise<boolean> {
+    const state = this.journal.state(root)
+    const pending = state.messages.filter(candidate =>
+      candidate.targetId === message.targetId && !state.delivered.includes(candidate.id))
+    const requested = pending.findIndex(candidate => candidate.id === message.id)
+    if (requested < 0) return state.delivered.includes(message.id)
+    for (const candidate of pending.slice(0, requested + 1)) {
+      const ownsInFlight = !this.inFlightMessages.has(candidate.id)
+      if (ownsInFlight) this.inFlightMessages.add(candidate.id)
+      try {
+        if (!await this.dispatchOnce(root, candidate, signal)) return false
+      } finally {
+        if (ownsInFlight) this.inFlightMessages.delete(candidate.id)
+      }
+    }
+    return true
+  }
+
   /** Attempt one queued delivery after target-local ordering admits it. */
   private async dispatchOnce(root: Agent, message: TeamMessageSnapshot, signal: AbortSignal): Promise<boolean> {
     try {
@@ -243,18 +248,9 @@ export class TeamMailbox {
       const content = this.deliveryContent(message)
       if (message.targetId === root.id) {
         const input = createUserMessage({ content, source })
-        if (message.delivery === 'wakeup') {
-          root.followup(input)
-          return await this.checkpointDelivered(root, root.session, message.id)
-        }
-        root.inject(input)
+        root.steer(input)
         return await this.checkpointDelivered(root, root.session, message.id)
       }
-      if (message.delivery === 'quiet') {
-        if (target === undefined) return false
-        target.inject(createUserMessage({ content, source }))
-        return await this.checkpointDelivered(root, target.session, message.id)
-      }
       if (target === undefined) {
         const recorded = await this.persistedTargetRecorded(message.targetId, message.id, signal)
         if (recorded === undefined) return false
@@ -263,7 +259,7 @@ export class TeamMailbox {
           return true
         }
       }
-      await queueHostSubagentPrompt(this.ctx.subagents, root, message.targetId, content, source, signal)
+      await steerHostSubagentPrompt(this.ctx.subagents, root, message.targetId, content, source, signal)
       return target === undefined
         ? true
         : await this.checkpointDelivered(root, target.session, message.id)
@@ -273,12 +269,6 @@ export class TeamMailbox {
     }
   }
 
-  /** Whether `left` was durably queued before `right` in one Lead log. */
-  private messagePrecedes(root: Agent, left: TeamMessageId, right: TeamMessageId): boolean {
-    const ids = this.journal.state(root).messages.map(message => message.id)
-    return ids.indexOf(left) < ids.indexOf(right)
-  }
-
   /** Flush one live target receipt before the Lead records its delivered edge. */
   private async checkpointDelivered(
     root: Agent,
@@ -299,7 +289,7 @@ export class TeamMailbox {
       const queued = state.messages.find(message => message.id === messageId)
       if (queued === undefined || queued.targetId !== targetId) return
       await this.journal.appendAndFlush(root, 'team/message/delivered', {
-        version: 1,
+        version: 2,
         teamId: TeamId(root.id),
         messageId,
         targetId,

+ 6 - 7
packages/experimental/agent-team/src/projection.ts

@@ -90,7 +90,6 @@ const teamMessageSnapshotSchema = z.object({
   senderId: sessionIdSchema,
   senderName: z.string(),
   targetId: sessionIdSchema,
-  delivery: z.enum(['quiet', 'wakeup']),
   content: z.array(contentBlockSchema),
 }).strict() as z.ZodType<TeamMessageSnapshot>
 
@@ -100,25 +99,25 @@ const teamEventSelectorSchema = z.object({
 }).loose()
 
 const teamMemberEventSchema = z.object({
-  version: z.literal(1),
+  version: z.literal(2),
   teamId: teamIdSchema,
   member: teamMemberSnapshotSchema,
 }).strict() as z.ZodType<SessionEventMap['team/member']>
 
 const teamTaskEventSchema = z.object({
-  version: z.literal(1),
+  version: z.literal(2),
   teamId: teamIdSchema,
   task: teamTaskSnapshotSchema,
 }).strict() as z.ZodType<SessionEventMap['team/task']>
 
 const teamMessageQueuedEventSchema = z.object({
-  version: z.literal(1),
+  version: z.literal(2),
   teamId: teamIdSchema,
   message: teamMessageSnapshotSchema,
 }).strict() as z.ZodType<SessionEventMap['team/message/queued']>
 
 const teamMessageDeliveredEventSchema = z.object({
-  version: z.literal(1),
+  version: z.literal(2),
   teamId: teamIdSchema,
   messageId: teamMessageIdSchema,
   targetId: sessionIdSchema,
@@ -225,7 +224,7 @@ function applyProjectionEvent(state: TeamProjectionState, event: SessionEvent):
   try {
     const selector = parsePersisted(event.type, teamEventSelectorSchema, event.data)
     if (selector.teamId !== state.id) return
-    if (selector.version !== 1) {
+    if (selector.version !== 2) {
       throw new Error(`unsupported Agent Teams event version ${String(selector.version)}`)
     }
     applyCurrentTeamEvent(state, parseCurrentTeamEvent(event))
@@ -307,7 +306,7 @@ function applyCurrentTeamEvent(state: TeamState, event: TeamSessionEvent): void
 /** Host-only Team projection selected by the projected Session identity. */
 export const teamProjectionDefinition = {
   key: 'agentTeam',
-  stateVersion: 2,
+  stateVersion: 3,
   stateSchema: teamProjectionEntrySchema,
   init: header => emptyTeamState(header.id),
   apply: (state, event) => {

+ 3 - 3
packages/experimental/agent-team/src/roster.ts

@@ -274,7 +274,7 @@ export class TeamRoster {
       if (state.members.length >= this.maxMembers) {
         throw new TeamError(`Team member limit ${this.maxMembers} reached`, 'TEAM_MEMBER_LIMIT')
       }
-      await this.journal.appendAndFlush(root, 'team/member', { version: 1, teamId: TeamId(root.id), member })
+      await this.journal.appendAndFlush(root, 'team/member', { version: 2, teamId: TeamId(root.id), member })
     })
 
     let started: ContinuableStart
@@ -424,7 +424,7 @@ export class TeamRoster {
           ...phase === 'failed' ? { error: failure } : {},
         }
         await this.journal.appendAndFlush(root, 'team/member', {
-          version: 1,
+          version: 2,
           teamId: TeamId(root.id),
           member: settled,
         })
@@ -472,7 +472,7 @@ export class TeamRoster {
       }
       if (current.phase !== 'provisioning') return current.phase
       await this.journal.appendAndFlush(root, 'team/member', {
-        version: 1,
+        version: 2,
         teamId: TeamId(root.id),
         member: terminal,
       })

+ 2 - 2
packages/experimental/agent-team/src/task-board.ts

@@ -67,7 +67,7 @@ export class TeamTaskBoard {
         writeScopes: this.writeScopes(request.writeScopes ?? []),
       }
       this.assertTaskGraph(state, task)
-      await this.journal.appendAndFlush(root, 'team/task', { version: 1, teamId: TeamId(root.id), task })
+      await this.journal.appendAndFlush(root, 'team/task', { version: 2, teamId: TeamId(root.id), task })
       return this.taskView(root, state, task)
     })
   }
@@ -209,7 +209,7 @@ export class TeamTaskBoard {
         revision: current.revision + 1,
       }
       this.assertTaskGraph(state, task)
-      await this.journal.appendAndFlush(root, 'team/task', { version: 1, teamId: TeamId(root.id), task })
+      await this.journal.appendAndFlush(root, 'team/task', { version: 2, teamId: TeamId(root.id), task })
       return this.taskView(root, state, task)
     })
   }

+ 4 - 6
packages/experimental/agent-team/src/types.ts

@@ -108,7 +108,6 @@ export interface TeamMessageSnapshot {
   readonly senderId: SessionId
   readonly senderName: string
   readonly targetId: SessionId
-  readonly delivery: 'quiet' | 'wakeup'
   readonly content: ContentBlock[]
 }
 
@@ -160,7 +159,6 @@ export interface SpawnTeammateResult {
 export interface SendTeamMessageRequest {
   readonly target: string
   readonly content: ContentBlock[]
-  readonly delivery: 'quiet' | 'wakeup'
   readonly signal: AbortSignal
 }
 
@@ -220,14 +218,14 @@ export interface TeamWaitResult {
 declare module '@deepseek-ai/dsh-session/types' {
   interface SessionEventMap {
     /** Whole teammate lifecycle value, stored only in the Team Lead Session. */
-    'team/member': { version: 1; teamId: TeamId; member: TeamMemberSnapshot }
+    'team/member': { version: 2; teamId: TeamId; member: TeamMemberSnapshot }
     /** Whole shared-task value, stored only in the Team Lead Session. */
-    'team/task': { version: 1; teamId: TeamId; task: TeamTaskSnapshot }
+    'team/task': { version: 2; teamId: TeamId; task: TeamTaskSnapshot }
     /** Durable mailbox enqueue, stored before delivery is attempted. */
-    'team/message/queued': { version: 1; teamId: TeamId; message: TeamMessageSnapshot }
+    'team/message/queued': { version: 2; teamId: TeamId; message: TeamMessageSnapshot }
     /** Durable acknowledgement that the target Session recorded the message. */
     'team/message/delivered': {
-      version: 1
+      version: 2
       teamId: TeamId
       messageId: TeamMessageId
       targetId: SessionId

+ 3 - 3
packages/experimental/agent-team/tests/invariant.spec.ts

@@ -30,13 +30,13 @@ describe('Agent Teams stream invariant', () => {
       phase: 'provisioning' as const,
     }
     expect(() => {
-      session.append('team/member', { version: 1, teamId: TeamId(session.id), member })
+      session.append('team/member', { version: 2, teamId: TeamId(session.id), member })
     }).not.toThrow()
 
     const invalid = ctx.sessions.create(SessionId('team-invariant-invalid'))
     expect(() => {
       invalid.append('team/member', {
-        version: 1,
+        version: 2,
         teamId: TeamId(invalid.id),
         member: { ...member, phase: 'active' },
       })
@@ -53,7 +53,7 @@ describe('Agent Teams stream invariant', () => {
 
     expect(() => {
       session.append('team/task', {
-        version: 1,
+        version: 2,
         teamId: TeamId(session.id),
         task: {
           id: TeamTaskId('task-1'),

+ 15 - 30
packages/experimental/agent-team/tests/persistence.spec.ts

@@ -178,12 +178,12 @@ for (const backend of backends) {
       await Promise.resolve()
 
       activeRoot.session.append('team/member', {
-        version: 1,
+        version: 2,
         teamId: TeamId(activeRoot.id),
         member: provisioning(childId, 'recoverable'),
       })
       failedRoot.session.append('team/member', {
-        version: 1,
+        version: 2,
         teamId: TeamId(failedRoot.id),
         member: provisioning(SessionId(`${backend.name}-missing`), 'missing'),
       })
@@ -225,7 +225,6 @@ for (const backend of backends) {
       const receipt = await second.ctx.agentTeams.sendMessage(activeHandle.agent, {
         target: 'recoverable',
         content: [{ type: 'text', text: 'resume after reconciliation' }],
-        delivery: 'wakeup',
         signal: SIGNAL,
       })
       expect(receipt.status).toBe('accepted')
@@ -249,7 +248,7 @@ for (const backend of backends) {
       await Promise.resolve()
       await Promise.resolve()
       root.session.append('team/member', {
-        version: 1,
+        version: 2,
         teamId: TeamId(root.id),
         member: provisioning(childId, 'pending-worker'),
       })
@@ -278,7 +277,7 @@ for (const backend of backends) {
       await second.dispose()
     })
 
-    it('replays queued-minus-delivered mail in FIFO order without waking for quiet mail', {
+    it('retries queued mail through cold-resume Steer after restart', {
       timeout: PERSISTENCE_TEST_TIMEOUT_MS,
     }, async () => {
       const storageRoot = mkdtempSync(join(tmpdir(), `dsh-team-mail-${backend.name.toLowerCase()}-`))
@@ -296,14 +295,15 @@ for (const backend of backends) {
         signal: SIGNAL,
       })
       await vi.waitFor(() => { expect(first.ctx.agents.get(started.member.id)).toBeUndefined() }, { timeout: 5_000 })
-      const quiet = await first.ctx.agentTeams.sendMessage(firstLead, {
+      vi.spyOn(first.ctx.sessionPersistence, 'open')
+        .mockRejectedValueOnce(new Error('temporary target read failure'))
+      const queued = await first.ctx.agentTeams.sendMessage(firstLead, {
         target: 'mail-worker',
-        content: [{ type: 'text', text: 'durable quiet context' }],
-        delivery: 'quiet',
+        content: [{ type: 'text', text: 'durable retry context' }],
         signal: SIGNAL,
       })
-      expect(quiet.status).toBe('queued')
-      expect(durable(firstLead).pendingMessages.map(message => message.id)).toEqual([quiet.messageId])
+      expect(queued.status).toBe('queued')
+      expect(durable(firstLead).pendingMessages.map(message => message.id)).toEqual([queued.messageId])
       await first.dispose()
 
       const second = await stack(backend, storageRoot, [textResponse('resumed teammate answer')])
@@ -311,19 +311,6 @@ for (const backend of backends) {
         resumeSessionId: rootId,
         agentOptions: { provider: 'mock', model: 'mock' },
       })
-      await vi.waitFor(() => {
-        expect(durable(rootHandle.agent).pendingMessages.map(message => message.id))
-          .toEqual([quiet.messageId])
-      })
-      expect(second.ctx.agents.get(started.member.id)).toBeUndefined()
-
-      const waking = await second.ctx.agentTeams.sendMessage(rootHandle.agent, {
-        target: 'mail-worker',
-        content: [{ type: 'text', text: 'resume after restart' }],
-        delivery: 'wakeup',
-        signal: SIGNAL,
-      })
-      expect(waking.status).toBe('accepted')
       await vi.waitFor(() => { expect(second.ctx.agents.get(started.member.id)).toBeUndefined() }, { timeout: 5_000 })
       await vi.waitFor(() => { expect(durable(rootHandle.agent).pendingMessages).toEqual([]) })
 
@@ -332,7 +319,7 @@ for (const backend of backends) {
         && event.data.source.kind === 'team-message'
         ? [event.data.source.messageId]
         : [])
-      expect(peerIds).toEqual([quiet.messageId, waking.messageId])
+      expect(peerIds).toEqual([queued.messageId])
 
       await rootHandle.dispose()
       await second.dispose()
@@ -386,11 +373,10 @@ for (const backend of backends) {
         senderId: rootId,
         senderName: 'lead',
         targetId: started.member.id,
-        delivery: 'wakeup',
         content: [{ type: 'text', text: 'already recorded before acknowledgement' }],
       }
       firstLead.session.append('team/message/queued', {
-        version: 1,
+        version: 2,
         teamId: TeamId(rootId),
         message: queued,
       })
@@ -439,21 +425,20 @@ for (const backend of backends) {
         senderId: rootId,
         senderName: 'lead',
         targetId: childId,
-        delivery: 'wakeup',
         content: [{ type: 'text', text: 'already durable in target inbox' }],
       }
       root.session.append('team/member', {
-        version: 1,
+        version: 2,
         teamId: TeamId(root.id),
         member: provisioned,
       })
       root.session.append('team/member', {
-        version: 1,
+        version: 2,
         teamId: TeamId(root.id),
         member: active,
       })
       root.session.append('team/message/queued', {
-        version: 1,
+        version: 2,
         teamId: TeamId(root.id),
         message: queued,
       })

+ 43 - 44
packages/experimental/agent-team/tests/projection-events.spec.ts

@@ -76,7 +76,6 @@ function message(overrides: Partial<TeamMessageSnapshot> = {}): TeamMessageSnaps
     senderId: ROOT,
     senderName: 'lead',
     targetId: CHILD,
-    delivery: 'quiet',
     content: [{ type: 'text', text: 'hello' }],
     ...overrides,
   }
@@ -85,15 +84,15 @@ function message(overrides: Partial<TeamMessageSnapshot> = {}): TeamMessageSnaps
 describe('Agent Teams projection events', () => {
   it('projects current-team records independently from inherited records', () => {
     const records: SessionEvent[] = [
-      event('team/member', { version: 1, teamId: TeamId('ancestor'), member: member() }, SessionSeq(0)),
-      event('team/member', { version: 1, teamId: TEAM, member: member() }, SessionSeq(1)),
+      event('team/member', { version: 2, teamId: TeamId('ancestor'), member: member() }, SessionSeq(0)),
+      event('team/member', { version: 2, teamId: TEAM, member: member() }, SessionSeq(1)),
       event('team/member', {
-        version: 1,
+        version: 2,
         teamId: TEAM,
         member: member({ phase: 'active' }),
       }, SessionSeq(2)),
-      event('team/task', { version: 1, teamId: TEAM, task: task({ id: TeamTaskId('task-7') }) }, SessionSeq(3)),
-      event('team/message/queued', { version: 1, teamId: TEAM, message: message() }, SessionSeq(4)),
+      event('team/task', { version: 2, teamId: TEAM, task: task({ id: TeamTaskId('task-7') }) }, SessionSeq(3)),
+      event('team/message/queued', { version: 2, teamId: TEAM, message: message() }, SessionSeq(4)),
     ]
     const projected = project(ROOT, records)
     const state = teamState(projected)
@@ -109,53 +108,53 @@ describe('Agent Teams projection events', () => {
   })
 
   it('enforces teammate identity and lifecycle', () => {
-    const base = event('team/member', { version: 1, teamId: TEAM, member: member() }, SessionSeq(0))
+    const base = event('team/member', { version: 2, teamId: TEAM, member: member() }, SessionSeq(0))
     expect(() => projectTeam(ROOT, [event('team/member', {
-      version: 1,
+      version: 2,
       teamId: TEAM,
       member: member({ phase: 'active' }),
     }, SessionSeq(0))])).toThrow(/must begin provisioning/)
     expect(() => projectTeam(ROOT, [base, event('team/member', {
-      version: 1,
+      version: 2,
       teamId: TEAM,
       member: member({ name: 'renamed', phase: 'active' }),
     }, SessionSeq(1))])).toThrow(/immutable identity/)
     expect(() => projectTeam(ROOT, [base, event('team/member', {
-      version: 1,
+      version: 2,
       teamId: TEAM,
       member: member({ phase: 'active' }),
     }, SessionSeq(1)), event('team/member', {
-      version: 1,
+      version: 2,
       teamId: TEAM,
       member: member({ phase: 'failed' }),
     }, SessionSeq(2))])).toThrow(/invalid active -> failed/)
 
     const duplicateName = member({ id: SessionId('child-b') })
     expect(() => projectTeam(ROOT, [base, event('team/member', {
-      version: 1,
+      version: 2,
       teamId: TEAM,
       member: duplicateName,
     }, SessionSeq(1))])).toThrow(/name .* reused/)
   })
 
   it('enforces task revision continuity', () => {
-    const first = event('team/task', { version: 1, teamId: TEAM, task: task() }, SessionSeq(0))
+    const first = event('team/task', { version: 2, teamId: TEAM, task: task() }, SessionSeq(0))
     expect(() => projectTeam(ROOT, [event('team/task', {
-      version: 1,
+      version: 2,
       teamId: TEAM,
       task: task({ revision: 2 }),
     }, SessionSeq(0))])).toThrow(/begin at revision 1/)
     expect(() => projectTeam(ROOT, [first, event('team/task', {
-      version: 1,
+      version: 2,
       teamId: TEAM,
       task: task({ revision: 3 }),
     }, SessionSeq(1))])).toThrow(/revision is not contiguous/)
   })
 
   it('rejects every invalid persisted task dependency relation', () => {
-    const first = event('team/task', { version: 1, teamId: TEAM, task: task() }, SessionSeq(0))
+    const first = event('team/task', { version: 2, teamId: TEAM, task: task() }, SessionSeq(0))
     const second = event('team/task', {
-      version: 1,
+      version: 2,
       teamId: TEAM,
       task: task({
         id: TeamTaskId('task-2'),
@@ -165,7 +164,7 @@ describe('Agent Teams projection events', () => {
     const invalid: Array<{ records: SessionEvent[]; message: RegExp }> = [
       {
         records: [event('team/task', {
-          version: 1,
+          version: 2,
           teamId: TEAM,
           task: task({ blockedBy: [TeamTaskId('missing')] }),
         }, SessionSeq(0))],
@@ -173,7 +172,7 @@ describe('Agent Teams projection events', () => {
       },
       {
         records: [event('team/task', {
-          version: 1,
+          version: 2,
           teamId: TEAM,
           task: task({ blockedBy: [TeamTaskId('task-1')] }),
         }, SessionSeq(0))],
@@ -188,7 +187,7 @@ describe('Agent Teams projection events', () => {
       },
       {
         records: [first, second, event('team/task', {
-          version: 1,
+          version: 2,
           teamId: TEAM,
           task: task({ revision: 2, blockedBy: [TeamTaskId('task-2')] }),
         }, SessionSeq(2))],
@@ -196,7 +195,7 @@ describe('Agent Teams projection events', () => {
       },
       {
         records: [first, second, event('team/task', {
-          version: 1,
+          version: 2,
           teamId: TEAM,
           task: task({ revision: 2, status: 'deleted' }),
         }, SessionSeq(2))],
@@ -211,7 +210,7 @@ describe('Agent Teams projection events', () => {
 
   it('leaves numeric allocation unchanged for a branded nonstandard task id', () => {
     const state = projectTeam(ROOT, [event('team/task', {
-      version: 1,
+      version: 2,
       teamId: TEAM,
       task: task({ id: TeamTaskId('external-task') }),
     }, SessionSeq(0))])
@@ -220,16 +219,16 @@ describe('Agent Teams projection events', () => {
 
   it('rejects a persisted numeric task id outside the safe integer range', () => {
     expect(() => projectTeam(ROOT, [event('team/task', {
-      version: 1,
+      version: 2,
       teamId: TEAM,
       task: task({ id: TeamTaskId('task-9007199254740992') }),
     }, SessionSeq(0))])).toThrow(/persisted Agent Teams team\/task payload is invalid/)
   })
 
   it('enforces mailbox queue and acknowledgement relations', () => {
-    const queued = event('team/message/queued', { version: 1, teamId: TEAM, message: message() }, SessionSeq(0))
+    const queued = event('team/message/queued', { version: 2, teamId: TEAM, message: message() }, SessionSeq(0))
     const delivered = event('team/message/delivered', {
-      version: 1,
+      version: 2,
       teamId: TEAM,
       messageId: TeamMessageId('message-1'),
       targetId: CHILD,
@@ -247,42 +246,42 @@ describe('Agent Teams projection events', () => {
   it('validates every current-version persisted payload before projecting it', () => {
     const malformed = [
       {
-        ...event('team/member', { version: 1, teamId: TEAM, member: member() }, SessionSeq(0)),
-        data: { version: 1, teamId: TEAM, member: { ...member(), name: 42 } },
+        ...event('team/member', { version: 2, teamId: TEAM, member: member() }, SessionSeq(0)),
+        data: { version: 2, teamId: TEAM, member: { ...member(), name: 42 } },
       },
       {
-        ...event('team/task', { version: 1, teamId: TEAM, task: task() }, SessionSeq(0)),
-        data: { version: 1, teamId: TEAM, task: { ...task(), blockedBy: [42] } },
+        ...event('team/task', { version: 2, teamId: TEAM, task: task() }, SessionSeq(0)),
+        data: { version: 2, teamId: TEAM, task: { ...task(), blockedBy: [42] } },
       },
       {
-        ...event('team/message/queued', { version: 1, teamId: TEAM, message: message() }, SessionSeq(0)),
+        ...event('team/message/queued', { version: 2, teamId: TEAM, message: message() }, SessionSeq(0)),
         data: {
-          version: 1,
+          version: 2,
           teamId: TEAM,
           message: { ...message(), content: [{ type: 'text', text: 42 }] },
         },
       },
       {
         ...event('team/message/delivered', {
-          version: 1,
+          version: 2,
           teamId: TEAM,
           messageId: TeamMessageId('message-1'),
           targetId: CHILD,
         }, SessionSeq(0)),
         data: {
-          version: 1,
+          version: 2,
           teamId: TEAM,
           messageId: TeamMessageId('message-1'),
           targetId: 42,
         },
       },
       {
-        ...event('team/member', { version: 1, teamId: TEAM, member: member() }, SessionSeq(0)),
-        data: { version: 1, teamId: TEAM, member: member(), unexpected: true },
+        ...event('team/member', { version: 2, teamId: TEAM, member: member() }, SessionSeq(0)),
+        data: { version: 2, teamId: TEAM, member: member(), unexpected: true },
       },
       {
-        ...event('team/task', { version: 1, teamId: TEAM, task: task() }, SessionSeq(0)),
-        data: { version: 1, teamId: 42, task: task() },
+        ...event('team/task', { version: 2, teamId: TEAM, task: task() }, SessionSeq(0)),
+        data: { version: 2, teamId: 42, task: task() },
       },
     ] as unknown as SessionEvent[]
 
@@ -295,7 +294,7 @@ describe('Agent Teams projection events', () => {
   it('retains merge-extensible content blocks while rejecting malformed core variants', () => {
     const extension = { type: 'plugin/custom', payload: { value: 1 } } as never
     const state = projectTeam(ROOT, [event('team/message/queued', {
-      version: 1,
+      version: 2,
       teamId: TEAM,
       message: message({ content: [extension] }),
     }, SessionSeq(0))])
@@ -304,23 +303,23 @@ describe('Agent Teams projection events', () => {
 
   it('records unsupported event versions without applying them', () => {
     const invalid = event('team/task', {
-      version: 2 as 1,
+      version: 1 as 2,
       teamId: TEAM,
       task: task(),
     }, SessionSeq(0))
     const later = event('team/task', {
-      version: 1,
+      version: 2,
       teamId: TEAM,
       task: task(),
     }, SessionSeq(1))
     const state = project(ROOT, [invalid, later])
-    expect(state.failure).toMatch(/unsupported Agent Teams event version 2/)
+    expect(state.failure).toMatch(/unsupported Agent Teams event version 1/)
     expect(isEmptyState(state)).toBe(true)
   })
 
   it('isolates unsupported inherited Team records from the current Team', () => {
     const inherited = event('team/task', {
-      version: 2 as 1,
+      version: 1 as 2,
       teamId: TeamId('ancestor'),
       task: task(),
     }, SessionSeq(0))
@@ -332,12 +331,12 @@ describe('Agent Teams projection events', () => {
   it('ignores malformed current-version records inherited from another Team', () => {
     const inherited = {
       ...event('team/task', {
-        version: 1,
+        version: 2,
         teamId: TeamId('ancestor'),
         task: task(),
       }, SessionSeq(0)),
       data: {
-        version: 1,
+        version: 2,
         teamId: TeamId('ancestor'),
         task: { ...task(), subject: 42 },
       },

+ 116 - 80
packages/experimental/agent-team/tests/team.spec.ts

@@ -11,7 +11,7 @@ import { SessionLogOffset, SessionId, type Session, type SessionEvent } from '@d
 import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
 import SubagentService from '@deepseek-ai/dsh-subagent'
-import { queueSubagentPrompt, type HostPromptQueue } from '@deepseek-ai/dsh-subagent/internal'
+import { deliverSubagentPrompt, type HostPromptDeliverer } from '@deepseek-ai/dsh-subagent/internal'
 import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
 import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
 import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -193,7 +193,7 @@ describe('Team identity and provisioning', () => {
       phase: 'provisioning' as const,
     }
     lead.session.append('team/member', {
-      version: 1,
+      version: 2,
       teamId: TeamId(lead.id),
       member: provisioning,
     })
@@ -346,7 +346,7 @@ describe('Team identity and provisioning', () => {
       diagnostics: ['string provider failure'],
     })
     await expect(first.ctx.agentTeams.sendMessage(first.lead, {
-      target: 'string-failure', content: content('cannot deliver'), delivery: 'quiet', signal: SIGNAL,
+      target: 'string-failure', content: content('cannot deliver'), signal: SIGNAL,
     })).rejects.toMatchObject({ code: 'TEAM_MEMBER_NOT_FOUND' })
 
     const second = await setup([])
@@ -354,7 +354,7 @@ describe('Team identity and provisioning', () => {
       const provisioning = durable(second.lead).members[0]
       if (provisioning === undefined) throw new Error('missing provisioning edge')
       second.lead.session.append('team/member', {
-        version: 1,
+        version: 2,
         teamId: TeamId(second.lead.id),
         member: { ...provisioning, phase: 'active' },
       })
@@ -555,7 +555,7 @@ describe('Team shared task DAG', () => {
     const { ctx, lead } = await setup([])
     const id = TeamTaskId(`task-${Number.MAX_SAFE_INTEGER}`)
     lead.session.append('team/task', {
-      version: 1,
+      version: 2,
       teamId: TeamId(lead.id),
       task: {
         id,
@@ -594,7 +594,7 @@ describe('Team shared task DAG', () => {
   })
 
   it('enforces CAS, ownership, dependencies, transitions, and write-scope warnings', async () => {
-    const { ctx, lead } = await setup(['hang', 'hang'])
+    const { ctx, lead } = await setup(['hang', 'hang', textResponse('beta integrated update')])
     const firstMember = await spawn(ctx, lead, 'alpha')
     const alpha = await waitRunning(ctx, firstMember.member.id)
     const secondMember = await spawn(ctx, lead, 'beta')
@@ -938,29 +938,31 @@ describe('Team Remote API', () => {
 })
 
 describe('Team mailbox and waiting', () => {
-  it('injects a quiet message addressed to the Lead and checkpoints its receipt', async () => {
-    const { ctx, lead } = await setup([])
+  it('steers a message addressed to the Lead and checkpoints its receipt', async () => {
+    const { ctx, lead } = await setup(['hang'])
     const message: TeamMessageSnapshot = {
-      id: TeamMessageId('quiet-lead-message'),
+      id: TeamMessageId('steer-lead-message'),
       senderId: SessionId('team-worker'),
       senderName: 'worker',
       targetId: lead.id,
-      delivery: 'quiet',
-      content: content('quiet report'),
+      content: content('progress report'),
     }
     lead.session.append('team/message/queued', {
-      version: 1,
+      version: 2,
       teamId: TeamId(lead.id),
       message,
     })
 
     await expect(teamInternals(ctx).mailbox.tryDispatch(lead, message, SIGNAL)).resolves.toBe(true)
-    expect(lead.inbox.nextStep.some(input => input.source.kind === 'team-message'
-      && input.source.messageId === message.id)).toBe(true)
+    expect(lead.session.snapshotEvents().some(event => event.type === 'agent/inbox/spliced'
+      && event.data.inserted.some(input => input.source.kind === 'team-message'
+        && input.source.messageId === message.id))).toBe(true)
     expect(durable(lead).pendingMessages).toEqual([])
+    lead.cancel({ kind: 'parent' })
+    await lead.whenIdle()
   })
 
-  it('acknowledges waking messages persisted by a busy Lead before model claim', async () => {
+  it('acknowledges steered messages persisted by a busy Lead before model claim', async () => {
     const { ctx, lead, teamFiber } = await setup(['hang', 'hang'], { maxPendingMessagesPerMember: 1 })
     const started = await spawn(ctx, lead, 'lead-reporter')
     const reporter = await waitRunning(ctx, started.member.id)
@@ -968,10 +970,10 @@ describe('Team mailbox and waiting', () => {
     await waitRunning(ctx, lead.id)
 
     const first = await ctx.agentTeams.sendMessage(reporter, {
-      target: 'lead', content: content('first wakeup report'), delivery: 'wakeup', signal: SIGNAL,
+      target: 'lead', content: content('first progress report'), signal: SIGNAL,
     })
     const second = await ctx.agentTeams.sendMessage(reporter, {
-      target: 'lead', content: content('second wakeup report'), delivery: 'wakeup', signal: SIGNAL,
+      target: 'lead', content: content('second progress report'), signal: SIGNAL,
     })
     expect([first.status, second.status]).toEqual(['accepted', 'accepted'])
     expect(lead.status).toBe('running')
@@ -1016,8 +1018,7 @@ describe('Team mailbox and waiting', () => {
     const target = await waitRunning(ctx, started.member.id)
     const immediate = await ctx.agentTeams.sendMessage(lead, {
       target: 'pending-target',
-      content: content('live quiet receipt'),
-      delivery: 'quiet',
+      content: content('live steer receipt'),
       signal: SIGNAL,
     })
     expect(immediate.status).toBe('accepted')
@@ -1030,11 +1031,10 @@ describe('Team mailbox and waiting', () => {
       senderId: lead.id,
       senderName: 'lead',
       targetId: target.id,
-      delivery: 'quiet',
       content: content('durable pending receipt'),
     }
     lead.session.append('team/message/queued', {
-      version: 1,
+      version: 2,
       teamId: TeamId(lead.id),
       message,
     })
@@ -1070,7 +1070,7 @@ describe('Team mailbox and waiting', () => {
       content: content('canceled before checkpoint'),
     }
     lead.session.append('team/message/queued', {
-      version: 1,
+      version: 2,
       teamId: TeamId(lead.id),
       message: disappearing,
     })
@@ -1098,7 +1098,7 @@ describe('Team mailbox and waiting', () => {
     await waitNoAgent(ctx, target.id)
   })
 
-  it('acknowledges waking messages accepted by a busy target inbox', async () => {
+  it('acknowledges steered messages accepted by a busy target inbox', async () => {
     const { ctx, lead } = await setup(['hang'], { maxPendingMessagesPerMember: 1 })
     const started = await spawn(ctx, lead, 'busy-target')
     const target = await waitRunning(ctx, started.member.id)
@@ -1110,24 +1110,24 @@ describe('Team mailbox and waiting', () => {
     })
 
     const first = await ctx.agentTeams.sendMessage(lead, {
-      target: 'busy-target', content: content('first waking message'), delivery: 'wakeup', signal: SIGNAL,
+      target: 'busy-target', content: content('first steered message'), signal: SIGNAL,
     })
 
     expect(first.status).toBe('accepted')
     expect(flushed).toEqual([lead.id, target.id, lead.id])
     expect(durable(lead).pendingMessages).toEqual([])
-    expect(target.inbox.nextTurn.some(message => message.source.kind === 'team-message'
+    expect(target.inbox.nextStep.some(message => message.source.kind === 'team-message'
       && message.source.messageId === first.messageId)).toBe(true)
 
     flushed.length = 0
     const second = await ctx.agentTeams.sendMessage(lead, {
-      target: 'busy-target', content: content('second waking message'), delivery: 'wakeup', signal: SIGNAL,
+      target: 'busy-target', content: content('second steered message'), signal: SIGNAL,
     })
 
     expect(second.status).toBe('accepted')
     expect(flushed).toEqual([lead.id, target.id, lead.id])
     expect(durable(lead).pendingMessages).toEqual([])
-    expect(target.inbox.nextTurn.filter(message => message.source.kind === 'team-message'
+    expect(target.inbox.nextStep.filter(message => message.source.kind === 'team-message'
       && (message.source.messageId === first.messageId || message.source.messageId === second.messageId)))
       .toHaveLength(2)
 
@@ -1136,35 +1136,37 @@ describe('Team mailbox and waiting', () => {
     await waitNoAgent(ctx, target.id)
   })
 
-  it('serializes concurrent waking delivery admission for one target', async () => {
-    const { ctx, lead } = await setup([textResponse('target initial')])
-    const target = await spawn(ctx, lead, 'ordered-target')
-    await waitNoAgent(ctx, target.member.id)
+  it('serializes concurrent Steer delivery admission for one target', async () => {
+    const { ctx, lead } = await setup(['hang'])
+    const started = await spawn(ctx, lead, 'ordered-target')
+    const target = await waitRunning(ctx, started.member.id)
     const entered = Promise.withResolvers<undefined>()
     const release = Promise.withResolvers<undefined>()
     const admitted: string[] = []
-    vi.spyOn(ctx.subagents as unknown as HostPromptQueue, queueSubagentPrompt)
-      .mockImplementation(async (_parent, _childId, blocks) => {
+    vi.spyOn(ctx.subagents as unknown as HostPromptDeliverer, deliverSubagentPrompt)
+      .mockImplementation(async (_parent, _childId, blocks, source) => {
         const last = blocks.at(-1)
         const text = last?.type === 'text' ? last.text : ''
         admitted.push(text)
-        if (text === 'first waking') {
+        if (text === 'first steer') {
           entered.resolve(undefined)
           await release.promise
         }
-        return createUserMessage({ content: blocks, source: { kind: 'user' } }).id
+        const input = createUserMessage({ content: blocks, source })
+        target.inject(input)
+        return input.id
       })
 
     const first = ctx.agentTeams.sendMessage(lead, {
-      target: 'ordered-target', content: content('first waking'), delivery: 'wakeup', signal: SIGNAL,
+      target: 'ordered-target', content: content('first steer'), signal: SIGNAL,
     })
     await entered.promise
     let secondSettled = false
     const second = ctx.agentTeams.sendMessage(lead, {
-      target: 'ordered-target', content: content('second waking'), delivery: 'wakeup', signal: SIGNAL,
+      target: 'ordered-target', content: content('second steer'), signal: SIGNAL,
     }).finally(() => { secondSettled = true })
-    await new Promise<void>((resolve) => { setTimeout(resolve, 0) })
-    expect(admitted).toEqual(['first waking'])
+    await vi.waitFor(() => { expect(durable(lead).pendingMessages).toHaveLength(2) })
+    expect(admitted).toEqual(['first steer'])
     expect(secondSettled).toBe(false)
 
     release.resolve(undefined)
@@ -1172,7 +1174,48 @@ describe('Team mailbox and waiting', () => {
       { status: 'accepted' },
       { status: 'accepted' },
     ])
-    expect(admitted).toEqual(['first waking', 'second waking'])
+    expect(admitted).toEqual(['first steer', 'second steer'])
+
+    ctx.agentTeams.interrupt(lead, 'ordered-target')
+    target.cancel({ kind: 'parent' })
+    await waitNoAgent(ctx, target.id)
+  })
+
+  it('delivers persisted mail before the later message that cold-resumes its target', async () => {
+    const { ctx, lead } = await setup([textResponse('target initial'), 'hang', 'hang'])
+    const started = await spawn(ctx, lead, 'reordered-target')
+    await waitNoAgent(ctx, started.member.id)
+    const earlier: TeamMessageSnapshot = {
+      id: TeamMessageId('earlier-message'),
+      senderId: lead.id,
+      senderName: 'lead',
+      targetId: started.member.id,
+      content: content('earlier steer'),
+    }
+    lead.session.append('team/message/queued', {
+      version: 2,
+      teamId: TeamId(lead.id),
+      message: earlier,
+    })
+    await ctx.sessions.flush(lead.session)
+
+    const later = await ctx.agentTeams.sendMessage(lead, {
+      target: 'reordered-target', content: content('later steer'), signal: SIGNAL,
+    })
+    expect(later.status).toBe('accepted')
+    const target = await waitRunning(ctx, started.member.id)
+    await vi.waitFor(() => {
+      const accepted = target.session.snapshotEvents().flatMap(event => event.type === 'agent/inbox/spliced'
+        ? event.data.inserted.flatMap(message => message.source.kind === 'team-message'
+          ? [message.source.messageId]
+          : [])
+        : [])
+      expect(accepted).toEqual([earlier.id, later.messageId])
+    })
+
+    ctx.agentTeams.interrupt(lead, 'reordered-target')
+    target.cancel({ kind: 'parent' })
+    await waitNoAgent(ctx, target.id)
   })
 
   it('deduplicates live target history and contains inspection and delivery failures', async () => {
@@ -1185,11 +1228,10 @@ describe('Team mailbox and waiting', () => {
       senderId: lead.id,
       senderName: 'lead',
       targetId: live.id,
-      delivery: 'wakeup',
       content: content('already in live history'),
     }
     lead.session.append('team/message/queued', {
-      version: 1, teamId: TeamId(lead.id), message,
+      version: 2, teamId: TeamId(lead.id), message,
     })
     await ctx.sessions.flush(lead.session)
     live.session.append('user/message', createUserMessage({
@@ -1214,13 +1256,14 @@ describe('Team mailbox and waiting', () => {
     }), { surfaceOp: 'append' })
     await expect(internal.tryDispatch(lead, message, SIGNAL)).resolves.toBe(true)
     await internal.markDelivered(lead, message.id, live.id)
+    await expect(internal.tryDispatch(lead, message, SIGNAL)).resolves.toBe(true)
 
     const wrongTarget: TeamMessageSnapshot = {
       ...message,
       id: TeamMessageId('wrong-target-message'),
     }
     lead.session.append('team/message/queued', {
-      version: 1, teamId: TeamId(lead.id), message: wrongTarget,
+      version: 2, teamId: TeamId(lead.id), message: wrongTarget,
     })
     await ctx.sessions.flush(lead.session)
     await internal.markDelivered(lead, wrongTarget.id, SessionId('wrong-target'))
@@ -1261,15 +1304,15 @@ describe('Team mailbox and waiting', () => {
     await waitNoAgent(ctx, inactiveStarted.member.id)
     const openRead = vi.spyOn(ctx.sessionPersistence, 'open').mockRejectedValueOnce(new Error('read unavailable'))
     const uncertain = await ctx.agentTeams.sendMessage(lead, {
-      target: 'inactive-target', content: content('inspection failure'), delivery: 'wakeup', signal: SIGNAL,
+      target: 'inactive-target', content: content('inspection failure'), signal: SIGNAL,
     })
     expect(uncertain.status).toBe('queued')
     openRead.mockRestore()
 
-    vi.spyOn(ctx.subagents as unknown as HostPromptQueue, queueSubagentPrompt)
+    vi.spyOn(ctx.subagents as unknown as HostPromptDeliverer, deliverSubagentPrompt)
       .mockRejectedValueOnce(new Error('delivery unavailable'))
     const failed = await ctx.agentTeams.sendMessage(lead, {
-      target: 'inactive-target', content: content('delivery failure'), delivery: 'wakeup', signal: SIGNAL,
+      target: 'inactive-target', content: content('delivery failure'), signal: SIGNAL,
     })
     expect(failed.status).toBe('queued')
     expect(warnings.some(warning => warning.includes('read unavailable'))).toBe(true)
@@ -1279,22 +1322,19 @@ describe('Team mailbox and waiting', () => {
     await waitNoAgent(ctx, live.id)
   })
 
-  it('keeps quiet mail dormant, wakes on follow-up, preserves FIFO, and de-duplicates delivery', async () => {
-    const { ctx, lead } = await setup(['hang', textResponse('beta first'), textResponse('beta resumed')])
+  it('cold-resumes an inactive sibling with sender attribution', async () => {
+    const { ctx, lead } = await setup(['hang', 'hang'])
     const alphaStarted = await spawn(ctx, lead, 'alpha')
     const alpha = await waitRunning(ctx, alphaStarted.member.id)
     const betaStarted = await spawn(ctx, lead, 'beta')
-    await waitNoAgent(ctx, betaStarted.member.id)
+    const beta = await waitRunning(ctx, betaStarted.member.id)
+    ctx.agentTeams.interrupt(lead, 'beta')
+    await waitNoAgent(ctx, beta.id)
 
-    const quiet = await ctx.agentTeams.sendMessage(alpha, {
-      target: 'beta', content: content('quiet info'), delivery: 'quiet', signal: SIGNAL,
-    })
-    expect(quiet.status).toBe('queued')
-    expect(ctx.agents.get(betaStarted.member.id)).toBeUndefined()
-    const waking = await ctx.agentTeams.sendMessage(alpha, {
-      target: 'beta', content: content('do another turn'), delivery: 'wakeup', signal: SIGNAL,
+    const first = await ctx.agentTeams.sendMessage(alpha, {
+      target: 'beta', content: content('first update'), signal: SIGNAL,
     })
-    expect(waking.status).toBe('accepted')
+    expect(first.status).toBe('accepted')
     await waitNoAgent(ctx, betaStarted.member.id)
     await vi.waitFor(() => { expect(durable(lead).pendingMessages).toEqual([]) })
 
@@ -1305,18 +1345,16 @@ describe('Team mailbox and waiting', () => {
       if (event.type !== 'user/message') return undefined
       const block = event.data.content.at(-1)
       return block?.type === 'text' ? block.text : undefined
-    })).toEqual(['quiet info', 'do another turn'])
+    })).toEqual(['first update'])
     expect(peerMessages.map(event => event.type === 'user/message'
       ? event.data.content[0]?.type === 'text' && event.data.content[0].text
       : undefined)).toEqual([
       expect.stringMatching(/^Team message .* from alpha:$/u),
-      expect.stringMatching(/^Team message .* from alpha:$/u),
     ])
     expect(peerMessages.map(event => event.type === 'user/message' && event.data.source.kind === 'team-message'
       ? [event.data.source.messageId, event.data.source.senderName]
       : undefined)).toEqual([
-      [quiet.messageId, 'alpha'],
-      [waking.messageId, 'alpha'],
+      [first.messageId, 'alpha'],
     ])
 
     ctx.agentTeams.interrupt(lead, 'alpha')
@@ -1331,25 +1369,26 @@ describe('Team mailbox and waiting', () => {
     const target = await spawn(ctx, lead, 'target')
     await waitNoAgent(ctx, target.member.id)
     await expect(ctx.agentTeams.sendMessage(lead, {
-      target: 'target', content: content('x'.repeat(300)), delivery: 'quiet', signal: SIGNAL,
+      target: 'target', content: content('x'.repeat(300)), signal: SIGNAL,
     })).rejects.toMatchObject({ code: 'TEAM_MESSAGE_TOO_LARGE' })
+    vi.spyOn(ctx.sessionPersistence, 'open').mockRejectedValueOnce(new Error('temporary read failure'))
     const queued = await ctx.agentTeams.sendMessage(lead, {
-      target: 'target', content: content('one'), delivery: 'quiet', signal: SIGNAL,
+      target: 'target', content: content('one'), signal: SIGNAL,
     })
     expect(queued.status).toBe('queued')
     await expect(ctx.agentTeams.sendMessage(lead, {
-      target: 'target', content: content('two'), delivery: 'quiet', signal: SIGNAL,
+      target: 'target', content: content('two'), signal: SIGNAL,
     })).rejects.toMatchObject({ code: 'TEAM_MAILBOX_FULL' })
     await expect(ctx.agentTeams.sendMessage(lead, {
-      target: 'lead', content: content('self'), delivery: 'quiet', signal: SIGNAL,
+      target: 'lead', content: content('self'), signal: SIGNAL,
     })).rejects.toMatchObject({ code: 'TEAM_SELF_MESSAGE' })
     await expect(ctx.agentTeams.sendMessage(lead, {
-      target: 'missing', content: content('unknown target'), delivery: 'quiet', signal: SIGNAL,
+      target: 'missing', content: content('unknown target'), signal: SIGNAL,
     })).rejects.toMatchObject({ code: 'TEAM_MEMBER_NOT_FOUND' })
     const controller = new AbortController()
     controller.abort(new TeamError('cancelled before queue', 'TEST_CANCELLED'))
     await expect(ctx.agentTeams.sendMessage(lead, {
-      target: 'target', content: content('cancelled'), delivery: 'quiet', signal: controller.signal,
+      target: 'target', content: content('cancelled'), signal: controller.signal,
     })).rejects.toMatchObject({ code: 'TEST_CANCELLED' })
   })
 
@@ -1358,12 +1397,12 @@ describe('Team mailbox and waiting', () => {
     const started = await spawn(ctx, lead, 'worker')
     const worker = await waitRunning(ctx, started.member.id)
     const followup = await ctx.agentTeams.sendMessage(lead, {
-      target: 'worker', content: content('retained follow-up'), delivery: 'wakeup', signal: SIGNAL,
+      target: 'worker', content: content('retained follow-up'), signal: SIGNAL,
     })
     expect(followup.status).toBe('accepted')
     expect(ctx.agentTeams.interrupt(lead, 'worker')).toEqual({ previousStatus: 'running' })
     await vi.waitFor(() => { expect(worker.status).toBe('idle') })
-    expect(worker.inbox.nextTurn.some(message => message.source.kind === 'team-message'
+    expect(worker.inbox.nextStep.some(message => message.source.kind === 'team-message'
       && message.source.messageId === followup.messageId)).toBe(true)
     worker.cancel({ kind: 'parent' })
     await waitNoAgent(ctx, worker.id)
@@ -1538,7 +1577,7 @@ describe('Team mailbox and waiting', () => {
       phase: 'provisioning' as const,
     }
     lead.session.append('team/member', {
-      version: 1,
+      version: 2,
       teamId: TeamId(lead.id),
       member,
     })
@@ -1551,7 +1590,7 @@ describe('Team mailbox and waiting', () => {
     })
     await waitRunning(ctx, childId)
     lead.session.append('team/member', {
-      version: 1,
+      version: 2,
       teamId: TeamId(lead.id),
       member: {
         ...member,
@@ -1574,7 +1613,7 @@ describe('Team mailbox and waiting', () => {
     const entered = Promise.withResolvers<undefined>()
     const aborted = Promise.withResolvers<undefined>()
     const release = Promise.withResolvers<undefined>()
-    vi.spyOn(ctx.subagents as unknown as HostPromptQueue, queueSubagentPrompt)
+    vi.spyOn(ctx.subagents as unknown as HostPromptDeliverer, deliverSubagentPrompt)
       .mockImplementation(async (_parent, _childId, _content, _source, signal) => {
         entered.resolve(undefined)
         return await new Promise<never>((_resolve, reject) => {
@@ -1591,7 +1630,6 @@ describe('Team mailbox and waiting', () => {
     const sending = ctx.agentTeams.sendMessage(lead, {
       target: 'mailbox-worker',
       content: content('resume during disposal'),
-      delivery: 'wakeup',
       signal: SIGNAL,
     })
     await entered.promise
@@ -1616,11 +1654,10 @@ describe('Team mailbox and waiting', () => {
       senderId: SessionId('sender'),
       senderName: 'sender',
       targetId: lead.id,
-      delivery: 'wakeup',
       content: content('acknowledge before disposal'),
     }
     lead.session.append('team/message/queued', {
-      version: 1,
+      version: 2,
       teamId: TeamId(lead.id),
       message,
     })
@@ -1695,14 +1732,13 @@ describe('Team mailbox and waiting', () => {
       signal: SIGNAL,
     })).rejects.toMatchObject({ code: 'TEAM_DISPOSED' })
     await expect(ctx.agentTeams.sendMessage(lead, {
-      target: 'nobody', content: content('must reject'), delivery: 'quiet', signal: SIGNAL,
+      target: 'nobody', content: content('must reject'), signal: SIGNAL,
     })).rejects.toMatchObject({ code: 'TEAM_DISPOSED' })
     await expect(internal.mailbox.tryDispatch(lead, {
       id: TeamMessageId('post-disposal-message'),
       senderId: lead.id,
       senderName: 'lead',
       targetId: lead.id,
-      delivery: 'quiet',
       content: content('must not dispatch'),
     }, SIGNAL)).resolves.toBe(false)
   })
@@ -1774,7 +1810,7 @@ describe('Team mailbox and waiting', () => {
       phase: 'provisioning' as const,
     }
     first.lead.session.append('team/member', {
-      version: 1, teamId: TeamId(first.lead.id), member: provisioning,
+      version: 2, teamId: TeamId(first.lead.id), member: provisioning,
     })
     const reconcileFirst = teamInternals(first.ctx).roster
     await reconcileFirst.reconcileProvisioning(first.lead, SIGNAL)
@@ -1794,7 +1830,7 @@ describe('Team mailbox and waiting', () => {
     const childId = SessionId('concurrently-settled-child')
     const member = { ...provisioning, id: childId, name: 'concurrent-child' }
     second.lead.session.append('team/member', {
-      version: 1, teamId: TeamId(second.lead.id), member,
+      version: 2, teamId: TeamId(second.lead.id), member,
     })
     const entered = Promise.withResolvers<undefined>()
     const release = Promise.withResolvers<undefined>()
@@ -1807,7 +1843,7 @@ describe('Team mailbox and waiting', () => {
     const reconciling = reconcileSecond.reconcileProvisioning(second.lead, SIGNAL)
     await entered.promise
     second.lead.session.append('team/member', {
-      version: 1,
+      version: 2,
       teamId: TeamId(second.lead.id),
       member: { ...member, phase: 'failed', error: 'settled elsewhere' },
     })

+ 2 - 2
packages/experimental/tool-agent-team/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/experimental/tool-agent-team/README.md
-README.md: b8474117d429d6f03562479834bb13daad3307b9
-README.zh.md: ca4dd17e0ccafef4112655f80543d539c9a11a4b
+README.md: 4f8d589189540c40560c9abd4e4db999c3d18542
+README.zh.md: 15a87f5640e7115d09792f138f1cc36f20d9eab0

+ 8 - 8
packages/experimental/tool-agent-team/README.md

@@ -1,5 +1,5 @@
 ---
-description: "Ten tools that let the model create, message, and coordinate teammates, for compositions mounting the experimental Team plugins."
+description: "Nine tools that let the model create, message, and coordinate teammates, for compositions mounting the experimental Team plugins."
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-experimental-tool-agent-team` gives the model a team toolset on top of the team domain package: create named teammates, send them messages or follow-up work, see who is available, wait for progress, interrupt a stuck teammate, and manage a shared task board — ten tools in total. A short policy section in every member's prompt teaches the model when to form a team (only when you ask for one) and how to coordinate on a shared workspace. Mounting it replaces legacy subagent controls with the same tool names, so a composition that wants both must disable the legacy definitions. It is experimental: excluded from official releases, carries no stability promise, and creates teammates only when you explicitly ask for a team.
+`dsh-experimental-tool-agent-team` gives the model a team toolset on top of the team domain package: create named teammates, steer messages to them, see who is available, wait for progress, interrupt a stuck teammate, and manage a shared task board — nine tools in total. A short policy section in every member's prompt teaches the model when to form a team (only when you ask for one) and how to coordinate on a shared workspace. Mounting it replaces legacy subagent controls with the same tool names, so a composition that wants both must disable the legacy definitions. It is experimental: excluded from official releases, carries no stability promise, and creates teammates only when you explicitly ask for a team.
 
 ## Table of Contents
 
@@ -25,7 +25,7 @@ English | [中文](README.zh.md)
 <a id="use-this-package"></a>
 ## Use this package
 
-Add this package on top of `@deepseek-ai/dsh-experimental-agent-team` when the model should run a team through tools. Once mounted, every team member — the Lead and each teammate — gets the same ten tools plus a policy paragraph that states its own role and name.
+Add this package on top of `@deepseek-ai/dsh-experimental-agent-team` when the model should run a team through tools. Once mounted, every team member — the Lead and each teammate — gets the same nine tools plus a policy paragraph that states its own role and name.
 
 ### When to choose it
 
@@ -54,10 +54,10 @@ Try it by asking the Lead model: "create a teammate named reviewer to check the
 
 ### What the model can do
 
-The ten tools group into four capabilities:
+The nine tools group into four capabilities:
 
 - **Create a teammate** — `spawn_teammate` takes a name, a description, and the initial task; only the Lead can call it.
-- **Send messages** — `send_message` delivers information without waking an idle teammate; `followup_task` makes the message the recipient's next turn and wakes it when needed.
+- **Send messages** — `send_message` steers a running member at its nearest step boundary, starts an idle member, and cold-resumes an inactive teammate.
 - **See and wait** — `list_agents` shows the roster with live status; `wait_agent` waits for the next team change; `interrupt_agent` stops a teammate's current turn (Lead only).
 - **Manage the task board** — `team_task_create`, `team_task_list`, `team_task_get`, and `team_task_update` add, browse, read, and update shared tasks.
 
@@ -91,12 +91,12 @@ The [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-
 
 | File | Role |
 |---|---|
-| [`src/index.ts`](src/index.ts) | Plugin entry: config, the fixed policy text, and the ten scoped tool registrations |
+| [`src/index.ts`](src/index.ts) | Plugin entry: config, the fixed policy text, and the nine scoped tool registrations |
 | — | No runtime invariant companion is published; the Team service owns durable and authorization relations. |
 
 ### Policy and tools
 
-One `team:policy` section on the member scope teaches each member its role and the coordination rules; the fixed text and the ten tool registrations are declared in [`src/index.ts`](src/index.ts). The ten tool schemas appear only in Team member scopes, so non-Team subagents keep the default catalog. Scoped registrations with the same names as the legacy global continuable-subagent controls shadow those globals for team members only.
+One `team:policy` section on the member scope teaches each member its role and the coordination rules; the fixed text and the nine tool registrations are declared in [`src/index.ts`](src/index.ts). The nine tool schemas appear only in Team member scopes, so non-Team subagents keep the default catalog. Scoped registrations with the same names as the legacy global continuable-subagent controls shadow those globals for team members only.
 
 ### Scoped registration and teardown
 
@@ -125,7 +125,7 @@ Read these pages when the package-level contract is not enough. They move from t
 
 #### What the model sees
 
-One stable policy section states the exact Team role/name/id, the explicit-delegation requirement, shared-cwd behavior, filesystem stale-version recovery, Bash/formatter/codegen risk, task and write-scope coordination, quiet versus waking delivery, the no-retry mailbox rule, and the Lead's duty to wait before answering. The ten Team schemas from `spawn_teammate` through `team_task_update` appear only in Team member scopes.
+One stable policy section states the exact Team role/name/id, the explicit-delegation requirement, shared-cwd behavior, filesystem stale-version recovery, Bash/formatter/codegen risk, task and write-scope coordination, Steer delivery, the no-retry mailbox rule, and the Lead's duty to wait before answering. The nine Team schemas from `spawn_teammate` through `team_task_update` appear only in Team member scopes.
 
 #### Token effect
 

+ 9 - 9
packages/experimental/tool-agent-team/README.zh.md

@@ -1,5 +1,5 @@
 ---
-description: "十个让模型创建、发消息与协调 teammate 的工具,供组合实验性 Team 插件的部署方阅读。"
+description: "九个让模型创建、发消息与协调 teammate 的工具,供组合实验性 Team 插件的部署方阅读。"
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-experimental-tool-agent-team` 在团队领域包之上给模型一套团队工具:创建具名 teammate、给它们发消息或后续任务、查看谁在线、等待进展、中断卡住的 teammate,以及管理共享任务板——共十个工具。每个成员的提示词中都有一段简短策略,教模型何时组建团队(只有你要求时)以及如何在共享工作区协作。挂载它会用同名的团队工具取代旧版 subagent 控件,因此想同时使用两者的组合必须禁用旧定义。它是实验性的:不进入正式发布、不承诺稳定性,并且只有你明确要求组建团队时才会创建 teammate。
+`dsh-experimental-tool-agent-team` 在团队领域包之上给模型一套团队工具:创建具名 teammate、向它们 Steer 消息、查看谁在线、等待进展、中断卡住的 teammate,以及管理共享任务板——共九个工具。每个成员的提示词中都有一段简短策略,教模型何时组建团队(只有你要求时)以及如何在共享工作区协作。挂载它会用同名的团队工具取代旧版 subagent 控件,因此想同时使用两者的组合必须禁用旧定义。它是实验性的:不进入正式发布、不承诺稳定性,并且只有你明确要求组建团队时才会创建 teammate。
 
 ## 目录
 
@@ -25,7 +25,7 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-当模型应该通过工具运行一支团队时,在 `@deepseek-ai/dsh-experimental-agent-team` 之上挂载本包。挂载后,每个团队成员——Lead 与每个 teammate——都会获得相同的十个工具,外加一段说明自身角色与名字的策略段落。
+当模型应该通过工具运行一支团队时,在 `@deepseek-ai/dsh-experimental-agent-team` 之上挂载本包。挂载后,每个团队成员——Lead 与每个 teammate——都会获得相同的九个工具,外加一段说明自身角色与名字的策略段落。
 
 ### 何时选择
 
@@ -54,10 +54,10 @@ kind: "package-reference"
 
 ### 模型能做什么
 
-十个工具分为四类能力:
+九个工具分为四类能力:
 
 - **创建 teammate**——`spawn_teammate` 接收名字、描述与初始任务;只有 Lead 可以调用它。
-- **发送消息**——`send_message` 在不唤醒 idle teammate 的情况下传达信息;`followup_task` 让消息成为接收方的下一个轮次,并在需要时唤醒它。
+- **发送消息**——`send_message` 在最近的步骤边界 Steer running member、启动 idle member,并冷恢复 inactive teammate。
 - **查看与等待**——`list_agents` 显示带实时状态的 roster;`wait_agent` 等待下一次团队变化;`interrupt_agent` 停止 teammate 的当前轮次(仅限 Lead)。
 - **管理任务板**——`team_task_create`、`team_task_list`、`team_task_get` 与 `team_task_update` 添加、浏览、读取与更新共享任务。
 
@@ -91,12 +91,12 @@ kind: "package-reference"
 
 | 文件 | 职责 |
 |---|---|
-| [`src/index.ts`](src/index.ts) | 插件入口:配置、固定策略文本与十个 scoped 工具注册 |
-| — | 不发布运行时不变式伴生入口;委托只能通过 `ctx.agentTeams` 观察。 |
+| [`src/index.ts`](src/index.ts) | 插件入口:配置、固定策略文本与九个 scoped 工具注册 |
+| — | 不发布运行时不变式伴生入口;Team 服务拥有持久化与授权关系。 |
 
 ### 策略与工具
 
-member scope 上的一个 `team:policy` 段落教每个成员自己的角色与协作规则;固定文本与十个工具注册都声明在 [`src/index.ts`](src/index.ts)。十个工具 schema 只出现在 Team member scope 中,因此非 Team subagent 保持默认目录。与旧全局 continuable-subagent 控件同名的 scoped 注册只会为团队成员覆盖这些全局控件。
+member scope 上的一个 `team:policy` 段落教每个成员自己的角色与协作规则;固定文本与九个工具注册都声明在 [`src/index.ts`](src/index.ts)。九个工具 schema 只出现在 Team member scope 中,因此非 Team subagent 保持默认目录。与旧全局 continuable-subagent 控件同名的 scoped 注册只会为团队成员覆盖这些全局控件。
 
 ### 按作用域注册与拆除
 
@@ -125,7 +125,7 @@ member scope 上的一个 `team:policy` 段落教每个成员自己的角色与
 
 #### 模型看到什么
 
-一段稳定策略会说明确切 Team role/name/id、显式 delegation 要求、共享 cwd 行为、文件 stale-version 恢复、Bash/formatter/codegen 风险、task/write-scope 协调、quiet 与 waking 投递区别、mailbox 不重试规则,以及 Lead 必须在回答前等待。`spawn_teammate` 到 `team_task_update` 的十个 Team schema 只出现在 Team member scope。
+一段稳定策略会说明确切 Team role/name/id、显式 delegation 要求、共享 cwd 行为、文件 stale-version 恢复、Bash/formatter/codegen 风险、task/write-scope 协调、Steer 投递、mailbox 不重试规则,以及 Lead 必须在回答前等待。`spawn_teammate` 到 `team_task_update` 的九个 Team schema 只出现在 Team member scope。
 
 #### Token 影响
 

+ 18 - 25
packages/experimental/tool-agent-team/src/index.ts

@@ -34,10 +34,10 @@ The Team Lead and all teammates share the same working directory and filesystem.
 
 Prefer read/edit/write for file changes. If a file operation returns FS_STALE_VERSION, read the current file, rebase your intended change onto the new content, and retry. Bash, formatters, code generators, and scripts are not fully protected by the filesystem version guard; coordinate them explicitly and have the Lead review the final diff and run tests.
 
-Use send_message for quiet information that must not start an idle teammate. Use followup_task when the target should run another turn. A delivered peer item starts with its stable message id and sender name. A successful send is already durable even when its result says queued; do not resend it. Shared-task workflow is list, get, claim with the current revision, perform the work, then complete. Task readiness never starts an owner. Before wait_agent, use list_agents and make sure another required member is running or provisioning; use followup_task first when the required member is inactive. wait_agent observes only changes after that call starts, never wakes a member, and returns noProgress immediately when no other member can produce a change. Re-list after wakeup or timeout. The Lead must wait for required teammates before giving the final answer.`
+send_message steers a running target at its nearest step boundary, starts an idle target, and cold-resumes an inactive teammate. A delivered peer item starts with its stable message id and sender name. A successful send is already durable even when its result says queued; do not resend it. Shared-task workflow is list, get, claim with the current revision, perform the work, then complete. Task readiness never starts an owner. Before wait_agent, use list_agents and make sure another required member is running or provisioning; use send_message first when the required member is inactive. wait_agent observes only changes after that call starts, never wakes a member, and returns noProgress immediately when no other member can produce a change. Re-list after wakeup or timeout. The Lead must wait for required teammates before giving the final answer.`
 
 const ACTIVE_WAIT_STATUSES: ReadonlySet<TeamMemberView['status']> = new Set(['running', 'provisioning'])
-const NO_ACTIVE_PEER_MESSAGE = 'No other Team member is running or provisioning. wait_agent cannot make progress or wake inactive teammates. Re-list with list_agents and team_task_list, then use followup_task to wake each required inactive teammate before waiting again.'
+const NO_ACTIVE_PEER_MESSAGE = 'No other Team member is running or provisioning. wait_agent cannot make progress or wake inactive teammates. Re-list with list_agents and team_task_list, then use send_message to wake each required inactive teammate before waiting again.'
 
 /**
  * One roster row, matching `TeamMemberView`. The Lead pseudo-row omits the
@@ -198,29 +198,22 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
       },
     })))
 
-    const messageTool = (toolName: 'send_message' | 'followup_task', delivery: 'quiet' | 'wakeup'): void => {
-      register(scoped.tools.register(defineTool({
-        name: toolName,
-        description: delivery === 'quiet'
-          ? 'Send durable information to another Team member without starting an idle member.'
-          : 'Send a durable follow-up task to another Team member and start a turn when needed.',
-        parameters: {
-          target: { type: 'string', required: true, description: 'Team member name, or lead.' },
-          message: { type: 'string', required: true, description: 'Self-contained message for the target.' },
-        },
-        output: jsonOutput(SEND_VALUE_SCHEMA),
-        execute(args, exec) {
-          return ctx.agentTeams.sendMessage(callingAgent(exec.agent, toolName), {
-            target: args.target,
-            content: [{ type: 'text', text: args.message }],
-            delivery,
-            signal: exec.signal,
-          })
-        },
-      })))
-    }
-    messageTool('send_message', 'quiet')
-    messageTool('followup_task', 'wakeup')
+    register(scoped.tools.register(defineTool({
+      name: 'send_message',
+      description: 'Send one durable message to another Team member. A running target receives it at the nearest step boundary; an idle target starts a turn; an inactive teammate cold-resumes.',
+      parameters: {
+        target: { type: 'string', required: true, description: 'Team member name, or lead.' },
+        message: { type: 'string', required: true, description: 'Self-contained message for the target.' },
+      },
+      output: jsonOutput(SEND_VALUE_SCHEMA),
+      execute(args, exec) {
+        return ctx.agentTeams.sendMessage(callingAgent(exec.agent, 'send_message'), {
+          target: args.target,
+          content: [{ type: 'text', text: args.message }],
+          signal: exec.signal,
+        })
+      },
+    })))
 
     register(scoped.tools.register(defineTool({
       name: 'list_agents',

+ 5 - 7
packages/experimental/tool-agent-team/tests/tool-team.spec.ts

@@ -26,7 +26,6 @@ const SIGNAL = new AbortController().signal
 const TOOL_NAMES = [
   'spawn_teammate',
   'send_message',
-  'followup_task',
   'list_agents',
   'wait_agent',
   'interrupt_agent',
@@ -179,7 +178,7 @@ describe('dsh-tool-team', () => {
       timedOut: false,
       noProgress: {
         reason: 'no-active-peer',
-        message: 'No other Team member is running or provisioning. wait_agent cannot make progress or wake inactive teammates. Re-list with list_agents and team_task_list, then use followup_task to wake each required inactive teammate before waiting again.',
+        message: 'No other Team member is running or provisioning. wait_agent cannot make progress or wake inactive teammates. Re-list with list_agents and team_task_list, then use send_message to wake each required inactive teammate before waiting again.',
       },
     })
     for (const timeout_ms of [9_999, 3_600_001, Number.MAX_SAFE_INTEGER + 1]) {
@@ -221,12 +220,12 @@ describe('dsh-tool-team', () => {
     // Every Team result reaches the model as compact JSON: indentation would
     // spend tokens on every roster, task, and receipt without adding meaning.
     expect(text(roster)).toBe(JSON.stringify(JSON.parse(text(roster))))
-    const peer = await execute(ctx, child, 'send_message', { target: 'lead', message: 'quiet report' })
+    const peer = await execute(ctx, child, 'send_message', { target: 'lead', message: 'progress report' })
     expect(peer.isError).toBe(false)
     expect(JSON.parse(text(peer))).toMatchObject({ status: 'accepted' })
-    const waking = await execute(ctx, child, 'followup_task', { target: 'lead', message: 'review the report' })
-    expect(waking.isError).toBe(false)
-    expect(JSON.parse(text(waking))).toMatchObject({ status: 'accepted' })
+    const followup = await execute(ctx, child, 'send_message', { target: 'lead', message: 'review the report' })
+    expect(followup.isError).toBe(false)
+    expect(JSON.parse(text(followup))).toMatchObject({ status: 'accepted' })
     await lead.whenIdle()
 
     const created = await execute(ctx, lead, 'team_task_create', {
@@ -425,7 +424,6 @@ describe('dsh-tool-team', () => {
     await ctx.agentTeams.sendMessage(lead, {
       target: 'cold-worker',
       content: [{ type: 'text', text: 'resume with Team scope' }],
-      delivery: 'wakeup',
       signal: SIGNAL,
     })
     const resumed = await waitRunning(ctx, childId)

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

@@ -370,7 +370,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
       {
         signature: 'async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise<SendTeamMessageResult>',
         description: 'Queue one durable peer message, then attempt immediate delivery.',
-        parameters: [{ name: 'caller', description: 'exact live sending Team member.' }, { name: 'request', description: 'target name, content, scheduling mode, and pre-queue cancellation.' }],
+        parameters: [{ name: 'caller', description: 'exact live sending Team member.' }, { name: 'request', description: 'target name, content, and pre-queue cancellation.' }],
         returns: 'durable message identity and immediate-delivery observation.',
       },
       {
@@ -4742,7 +4742,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'SendTeamMessageRequest',
-    declaration: 'export interface SendTeamMessageRequest {\n    readonly target: string;\n    readonly content: ContentBlock[];\n    readonly delivery: \'quiet\' | \'wakeup\';\n    readonly signal: AbortSignal;\n}',
+    declaration: 'export interface SendTeamMessageRequest {\n    readonly target: string;\n    readonly content: ContentBlock[];\n    readonly signal: AbortSignal;\n}',
   },
   {
     name: 'SendTeamMessageResult',

+ 2 - 2
packages/subagent/subagent/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/subagent/subagent/README.md
-README.md: 257a76a4ef84583d3337f697bc8024d72219c4d6
-README.zh.md: 135086b982df38089cdef0acf85535d35354a9d7
+README.md: 6712af94000d82878d8f7cea6194eeb0d2f92421
+README.zh.md: ee0cc8861c174939cc767ba6791c1c19c1d2bc8c

+ 1 - 1
packages/subagent/subagent/README.md

@@ -77,7 +77,7 @@ This section explains how the service is built and where the observable behavior
 |---|---|
 | [`src/index.ts`](src/index.ts) | Service entry: provider registry, start and continuation API, lifecycle events |
 | [`src/continuation.ts`](src/continuation.ts) | Continuable children: identity reservation, Activation residency, adjacent messaging, interrupt, settlement |
-| [`src/internal.ts`](src/internal.ts) | Host-only Queue adapter for browser and Team message protocols |
+| [`src/internal.ts`](src/internal.ts) | Host-only Queue and Steer adapters for browser and Team message protocols |
 | [`src/types.ts`](src/types.ts) | Public request, result, and provider contracts |
 | [`src/descriptor.ts`](src/descriptor.ts) | Versioned `subagent/descriptor` session-event vocabulary |
 | [`src/child-agent.ts`](src/child-agent.ts) | Child composition, delegated policy, depth helpers |

+ 1 - 1
packages/subagent/subagent/README.zh.md

@@ -77,7 +77,7 @@ kind: "package-reference"
 |---|---|
 | [`src/index.ts`](src/index.ts) | 服务入口:提供方注册表、启动与继续 API、生命周期事件 |
 | [`src/continuation.ts`](src/continuation.ts) | 可继续子级:身份预留、Activation 驻留、相邻消息、中断、结算 |
-| [`src/internal.ts`](src/internal.ts) | 供浏览器与 Team 消息协议使用的 host-only Queue 适配器 |
+| [`src/internal.ts`](src/internal.ts) | 供浏览器与 Team 消息协议使用的 host-only Queue 与 Steer 适配器 |
 | [`src/types.ts`](src/types.ts) | 公开的请求、结果与提供方约定 |
 | [`src/descriptor.ts`](src/descriptor.ts) | 版本化的 `subagent/descriptor` 会话事件词汇 |
 | [`src/child-agent.ts`](src/child-agent.ts) | 子级组装、委派策略、深度辅助函数 |

+ 29 - 2
packages/subagent/subagent/src/continuation.ts

@@ -134,7 +134,15 @@ export interface SubagentSendMessageOptions {
 
 /** Inputs shared by model steering and the human Queue adapter. */
 type ChildDeliveryOptions =
-  | { readonly delivery: 'steer'; readonly signal: AbortSignal }
+  | {
+    readonly delivery: 'steer'
+    /**
+     * A provided host source is preserved on the user message; omission attributes
+     * an adjacent-Agent message to the parent.
+     */
+    readonly source?: MessageSource
+    readonly signal: AbortSignal
+  }
   | { readonly delivery: 'queue'; readonly source: MessageSource; readonly signal: AbortSignal }
 
 /**
@@ -616,6 +624,25 @@ export class SubagentContinuationManager {
     return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'queue' })
   }
 
+  /**
+   * Steer one host-authored prompt to a direct continuable child.
+   * @param parent - exact live direct parent authorizing delivery.
+   * @param childId - durable direct-child session id.
+   * @param content - host-authored content to deliver.
+   * @param source - durable host-protocol provenance.
+   * @param signal - caller cancellation before inbox acceptance.
+   * @returns the accepted message's inbox id.
+   */
+  async steerPrompt(
+    parent: Agent,
+    childId: SessionId,
+    content: ContentBlock[],
+    source: MessageSource,
+    signal: AbortSignal,
+  ): Promise<MessageId> {
+    return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'steer' })
+  }
+
   /** Route one parent-originated delivery through residency and cold resume. */
   private async deliverToChild(
     parent: Agent,
@@ -1330,7 +1357,7 @@ export class SubagentContinuationManager {
     // Parent-originated delivery keeps the parent live through ownership, so
     // establish it before the message can enter the child's inbox.
     this.acquireOwnership(parent, activation.childId)
-    const message = options.delivery === 'steer'
+    const message = options.source === undefined
       ? agentMessage(parent, content)
       : createUserMessage({ content, source: options.source })
     const accepted = this.admitWaking(activation, message.id, () => {

+ 10 - 5
packages/subagent/subagent/src/index.ts

@@ -75,7 +75,7 @@ import { listChildren as listSubagentChildren, listDescendants as listSubagentDe
 import type { SubagentDescendantListEntry, SubagentListEntry } from './list-children.ts'
 import { snapshotSubagentDescriptor } from './descriptor.ts'
 import { subagentIdentityProjectionDefinition, subagentTimingProjectionDefinition } from './projection.ts'
-import { queueSubagentPrompt } from './internal.ts'
+import { deliverSubagentPrompt, type HostPromptDeliveryMode } from './internal.ts'
 
 export * from './out-of-process.ts'
 export { AssistantOutputFold, finalAssistantOutput } from './assistant-output.ts'
@@ -255,7 +255,7 @@ export class SubagentRuntime extends TypertRemoteService {
   }
 
   /**
-   * Queue one host-protocol message as a distinct direct-child turn.
+   * Deliver one host-protocol message to a direct continuable child.
    * Symbol-keyed so host adapters can preserve their own provenance without
    * widening the public Service Definition or impersonating an Agent sender.
    * @param parent - exact live direct parent authorizing delivery.
@@ -263,16 +263,20 @@ export class SubagentRuntime extends TypertRemoteService {
    * @param content - host-authored content to deliver.
    * @param source - durable host-protocol provenance.
    * @param signal - caller cancellation before inbox acceptance.
+   * @param delivery - Queue as a distinct turn or Steer at the nearest step.
    * @returns the accepted message's inbox id.
    */
-  private [queueSubagentPrompt](
+  private [deliverSubagentPrompt](
     parent: Agent,
     childId: SessionId,
     content: ContentBlock[],
     source: MessageSource,
     signal: AbortSignal,
+    delivery: HostPromptDeliveryMode,
   ): Promise<MessageId> {
-    return this.requireContinuations().queuePrompt(parent, childId, content, source, signal)
+    return delivery === 'steer'
+      ? this.requireContinuations().steerPrompt(parent, childId, content, source, signal)
+      : this.requireContinuations().queuePrompt(parent, childId, content, source, signal)
   }
 
   /**
@@ -445,12 +449,13 @@ export class SubagentRuntime extends TypertRemoteService {
         content = await admitPromptContent(attachments, request.content)
       }
       return {
-        messageId: await this[queueSubagentPrompt](
+        messageId: await this[deliverSubagentPrompt](
           parent,
           childSessionId,
           content,
           source,
           signal,
+          'queue',
         ),
       }
     } catch (error: unknown) {

+ 39 - 6
packages/subagent/subagent/src/internal.ts

@@ -34,20 +34,24 @@ export function isAdjacentAgentSendMessageTool(definition: ToolDefinition | unde
 }
 
 /**
- * Process-stable symbol-keyed Queue delivery shared by the bundled runtime
+ * Process-stable symbol-keyed host delivery shared by the bundled runtime
  * entry and this unbundled internal subpath.
  * @internal
  */
-export const queueSubagentPrompt = Symbol.for('dsh.subagent.queuePrompt')
+export const deliverSubagentPrompt = Symbol.for('dsh.subagent.deliverPrompt')
 
-/** Runtime face required by the host-only Queue adapter. */
-export interface HostPromptQueue {
-  [queueSubagentPrompt](
+/** Scheduling mode for one host-only direct-child prompt. */
+export type HostPromptDeliveryMode = 'queue' | 'steer'
+
+/** Runtime face required by the host-only prompt adapters. */
+export interface HostPromptDeliverer {
+  [deliverSubagentPrompt](
     parent: Agent,
     childId: SessionId,
     content: ContentBlock[],
     source: MessageSource,
     signal: AbortSignal,
+    delivery: HostPromptDeliveryMode,
   ): Promise<MessageId>
 }
 
@@ -69,11 +73,40 @@ export function queueHostSubagentPrompt(
   source: MessageSource,
   signal: AbortSignal,
 ): Promise<MessageId> {
-  return (runtime as unknown as HostPromptQueue)[queueSubagentPrompt](
+  return (runtime as unknown as HostPromptDeliverer)[deliverSubagentPrompt](
+    parent,
+    childId,
+    content,
+    source,
+    signal,
+    'queue',
+  )
+}
+
+/**
+ * Steer one host-protocol message without exposing another Service operation.
+ * @param runtime - subagent runtime owning continuation residency.
+ * @param parent - exact live direct parent authorizing delivery.
+ * @param childId - durable direct-child session id.
+ * @param content - host-authored content to deliver.
+ * @param source - durable host-protocol provenance.
+ * @param signal - caller cancellation before inbox acceptance.
+ * @returns the accepted message's inbox id.
+ */
+export function steerHostSubagentPrompt(
+  runtime: SubagentRuntime,
+  parent: Agent,
+  childId: SessionId,
+  content: ContentBlock[],
+  source: MessageSource,
+  signal: AbortSignal,
+): Promise<MessageId> {
+  return (runtime as unknown as HostPromptDeliverer)[deliverSubagentPrompt](
     parent,
     childId,
     content,
     source,
     signal,
+    'steer',
   )
 }

+ 3 - 2
packages/subagent/subagent/tests/control.spec.ts

@@ -13,7 +13,7 @@ import SubagentRuntime, {
   type SubagentListEntry,
   type SubagentPromptRequestId,
 } from '@deepseek-ai/dsh-subagent'
-import { queueSubagentPrompt, type HostPromptQueue } from '@deepseek-ai/dsh-subagent/internal'
+import { deliverSubagentPrompt, type HostPromptDeliverer } from '@deepseek-ai/dsh-subagent/internal'
 
 const PARENT = SessionId('parent')
 const CHILD = SessionId('child')
@@ -36,7 +36,7 @@ async function bench(live?: Record<string, { status: 'running' | 'idle' }>) {
 
 /** Spy on the private human-Queue adapter without widening the public service. */
 function promptDelivery(subagents: SubagentRuntime) {
-  return vi.spyOn(subagents as unknown as HostPromptQueue, queueSubagentPrompt)
+  return vi.spyOn(subagents as unknown as HostPromptDeliverer, deliverSubagentPrompt)
 }
 
 function childRow(id: SessionId, activity: 'running' | 'inactive'): SubagentListEntry {
@@ -254,6 +254,7 @@ describe('subagent prompt Remote', () => {
       [{ type: 'text', text: 'continue' }],
       { kind: 'user', rpcId: REQUEST_ID, clientTimeZone: 'Asia/Shanghai' },
       signal,
+      'queue',
     )
   })
 

+ 1 - 1
scripts/gen-tool-catalog.ts

@@ -543,7 +543,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
     },
     scope: ctx => catalogChildScopes.get(ctx) as Agent,
     note:
-      'All ten tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names.',
+      'All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names.',
   },
   {
     pkg: '@deepseek-ai/dsh-tool-todo',