Browse Source

Merge branch 'master' of github.com:deepseek-harness/deepseek-harness into fix/locale-default-english

Chinesezjc 2 months ago
parent
commit
e78fdf05fe
100 changed files with 2544 additions and 246 deletions
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml
  2. 0 0
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.md
  3. 0 0
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
  7. 3 3
      .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml
  8. 37 0
      .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md
  9. 37 0
      .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md
  10. 2 2
      .agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.i18n.yaml
  11. 1 1
      .agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md
  12. 1 1
      .agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.zh.md
  13. 6 0
      .agents/notes/implemented/bug-fix/2026-08-18-request-image-payload-bound.i18n.yaml
  14. 35 0
      .agents/notes/implemented/bug-fix/2026-08-18-request-image-payload-bound.md
  15. 35 0
      .agents/notes/implemented/bug-fix/2026-08-18-request-image-payload-bound.zh.md
  16. 2 2
      .agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml
  17. 2 2
      .agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md
  18. 2 2
      .agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md
  19. 2 2
      .agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.i18n.yaml
  20. 3 3
      .agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md
  21. 3 3
      .agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.zh.md
  22. 2 2
      .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.i18n.yaml
  23. 1 1
      .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.md
  24. 1 1
      .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md
  25. 2 2
      .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
  26. 12 12
      .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
  27. 12 12
      .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
  28. 6 0
      .agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml
  29. 73 0
      .agents/notes/implemented/feature/2026-08-05-agent-teams.md
  30. 73 0
      .agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md
  31. 2 2
      .agents/notes/implemented/feature/2026-08-11-workflow-run-status-driven-disclosure.i18n.yaml
  32. 16 14
      .agents/notes/implemented/feature/2026-08-11-workflow-run-status-driven-disclosure.md
  33. 16 14
      .agents/notes/implemented/feature/2026-08-11-workflow-run-status-driven-disclosure.zh.md
  34. 2 2
      .agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.i18n.yaml
  35. 6 6
      .agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.md
  36. 6 6
      .agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.zh.md
  37. 2 2
      .agents/notes/implemented/feature/2026-08-12-web-image-intake-and-limits-alignment.i18n.yaml
  38. 1 1
      .agents/notes/implemented/feature/2026-08-12-web-image-intake-and-limits-alignment.md
  39. 1 1
      .agents/notes/implemented/feature/2026-08-12-web-image-intake-and-limits-alignment.zh.md
  40. 6 0
      .agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.i18n.yaml
  41. 88 0
      .agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md
  42. 88 0
      .agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md
  43. 6 0
      .agents/notes/implemented/process/2026-08-17-readme-assets-on-cdn.i18n.yaml
  44. 35 0
      .agents/notes/implemented/process/2026-08-17-readme-assets-on-cdn.md
  45. 35 0
      .agents/notes/implemented/process/2026-08-17-readme-assets-on-cdn.zh.md
  46. 0 37
      .agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.md
  47. 0 37
      .agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.zh.md
  48. 6 0
      .agents/notes/implemented/simplification/2026-08-12-trim-agent-teams-read-and-lifecycle-surface.i18n.yaml
  49. 57 0
      .agents/notes/implemented/simplification/2026-08-12-trim-agent-teams-read-and-lifecycle-surface.md
  50. 57 0
      .agents/notes/implemented/simplification/2026-08-12-trim-agent-teams-read-and-lifecycle-surface.zh.md
  51. 1 0
      AGENTS.md
  52. 1 1
      README.i18n.yaml
  53. 3 3
      README.zh.md
  54. 6 2
      apps/web/tests/cordis-tool-round.e2e.ts
  55. 39 1
      apps/web/tests/models-settings.e2e.ts
  56. 18 0
      apps/web/tests/snapshots/models-settings/model-picker.expected.md
  57. 6 0
      apps/web/tests/snapshots/workflow-run/ui-live.expected.md
  58. 1 5
      apps/web/tests/snapshots/workflow-run/ui.expected.md
  59. 27 10
      apps/web/tests/workflow-run.e2e.ts
  60. BIN
      assets/community-wechat-official-account.png
  61. BIN
      assets/community-wecom-assistant.png
  62. 2 2
      docs/architecture.i18n.yaml
  63. 2 0
      docs/architecture.md
  64. 2 0
      docs/architecture.zh.md
  65. 2 2
      docs/capability-seams.i18n.yaml
  66. 6 0
      docs/capability-seams.md
  67. 6 0
      docs/capability-seams.zh.md
  68. 2 2
      docs/config-catalog.i18n.yaml
  69. 72 5
      docs/config-catalog.md
  70. 74 5
      docs/config-catalog.zh.md
  71. 2 2
      docs/event-producer-consumer.i18n.yaml
  72. 7 7
      docs/event-producer-consumer.md
  73. 7 7
      docs/event-producer-consumer.zh.md
  74. 2 2
      docs/module-graph.i18n.yaml
  75. 19 0
      docs/module-graph.md
  76. 19 0
      docs/module-graph.zh.md
  77. 2 2
      docs/persistence-catalog.i18n.yaml
  78. 59 0
      docs/persistence-catalog.md
  79. 59 0
      docs/persistence-catalog.zh.md
  80. 2 2
      docs/subsystems/README.i18n.yaml
  81. 1 0
      docs/subsystems/README.md
  82. 1 0
      docs/subsystems/README.zh.md
  83. 2 2
      docs/subsystems/subagent.i18n.yaml
  84. 20 1
      docs/subsystems/subagent.md
  85. 20 1
      docs/subsystems/subagent.zh.md
  86. 6 0
      docs/subsystems/team.i18n.yaml
  87. 183 0
      docs/subsystems/team.md
  88. 183 0
      docs/subsystems/team.zh.md
  89. 2 2
      docs/tool-catalog.i18n.yaml
  90. 317 0
      docs/tool-catalog.md
  91. 316 0
      docs/tool-catalog.zh.md
  92. 4 0
      examples/acp-agent/product-subagent-both.cordis.snapshot.yml
  93. 4 0
      examples/acp-agent/product-subagent-both.cordis.yml
  94. 2 0
      examples/acp-agent/product-subagent-codex.cordis.snapshot.yml
  95. 2 0
      examples/acp-agent/product-subagent-codex.cordis.yml
  96. 29 0
      examples/acp-agent/subagent-result-diagnostic.cordis.snapshot.yml
  97. 17 0
      examples/acp-agent/subagent-result-diagnostic.cordis.yml
  98. 107 1
      examples/acp-agent/tests/acp.snapshot.ts
  99. 41 0
      examples/acp-agent/tests/fixtures/image-offload.cordis.yml
  100. 50 0
      examples/acp-agent/tests/fixtures/subagent-result-diagnostic.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-web-config-plane.md
-2026-07-30-web-config-plane.md: 1326f50a792b6c6f791c9515ea03cc362d16fc7e
-2026-07-30-web-config-plane.zh.md: bea4bbc237f44864a5fa9bf677267ca0055c13ed
+2026-07-30-web-config-plane.md: ac989cb100190e9a41ebf04b5b2d80125d49e0cb
+2026-07-30-web-config-plane.zh.md: 26f11352b4362d3cbded3d1dc9205e289ee9b18c

File diff suppressed because it is too large
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md


File diff suppressed because it is too large
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md


+ 2 - 2
.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
-2026-08-10-product-subagent-providers-in-shared-host.md: a747d641ae112d114912958c289fe00b592e6ea5
-2026-08-10-product-subagent-providers-in-shared-host.zh.md: fef69e8a2d18135cbc9d5f0d80134fa5701bbbd0
+2026-08-10-product-subagent-providers-in-shared-host.md: 452ff1cca7e4e5f91f8c35092761ebe83f3ff174
+2026-08-10-product-subagent-providers-in-shared-host.zh.md: a62bf6faa3c9bba5326da1de20ecbc2946c02bcc

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md

@@ -16,7 +16,7 @@ Product providers remain process-scoped host-plane registrations. The [productio
 
 This note continues to own why a mounted product provider belongs on the host plane while its model-facing tool belongs to an Agent Preset. The production-install exclusion decision owns which Profiles install those optional packages. The provider-contract note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
 
-The providers use products already selected by the host environment. Codex starts `codex` from `PATH`; Claude Code resolves `claude` through the shared subprocess execution world and passes the exact path to the official SDK. Profile loading does not install a product, create product state, probe a version, test authentication, or add product-specific settings. Missing commands and product failures remain local to the attempted delegation.
+The providers use products already selected by the host environment. Codex starts `codex` from `PATH`; Claude Code resolves `claude` through the shared subprocess execution world and passes the exact path to the official SDK. Profile loading does not install a product, create product state, probe a version, or test authentication. It may supply each mounted Provider's deployment configuration, including the product-specific `permissionMode` values owned by the [non-interactive permissions decision](../feature/2026-08-15-product-subagent-noninteractive-permissions.md), without moving those choices into an Agent Preset or model-facing tool. Missing commands and product failures remain local to the attempted delegation.
 
 Only a Profile that selects the Claude Code provider carries the Claude Agent SDK's optional platform CLI payload. Production still resolves the host `claude`; the SDK payload remains provider-package installation cost rather than the production executable.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md

@@ -16,7 +16,7 @@ Status: implemented
 
 本说明继续负责解释为什么已经挂载的产品提供方属于 host plane,而面向模型的工具属于 Agent Preset。生产安装排除决策负责哪些 Profile 安装这些可选包。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)仍负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
 
-这些提供方使用宿主环境已经选定的产品。Codex 启动 `codex`,该命令从 `PATH` 解析;Claude Code 通过共享的子进程执行世界解析 `claude`,并把确切路径交给官方 SDK。加载 Profile 不会安装产品、创建产品状态、探测版本、测试身份验证,也不会新增产品专属设置。命令缺失和产品故障仍局限于发生问题的那次委派。
+这些提供方使用宿主环境已经选定的产品。Codex 启动 `codex`,该命令从 `PATH` 解析;Claude Code 通过共享的子进程执行世界解析 `claude`,并把确切路径交给官方 SDK。加载 Profile 不会安装产品、创建产品状态、探测版本或测试身份验证。它可以提供每个已挂载 Provider 的部署配置,包括由[非交互权限决策](../feature/2026-08-15-product-subagent-noninteractive-permissions.md)负责的产品专属 `permissionMode` 值,但不会把这些选择移入 Agent Preset 或面向模型的工具。命令缺失和产品故障仍局限于发生问题的那次委派。
 
 只有选择 Claude Code 提供方的 Profile 才会携带 Claude Agent SDK 的可选平台 CLI(命令行界面)载荷。生产环境仍解析宿主提供的 `claude`;这份 SDK 载荷是提供方包的安装成本,而不是生产可执行文件。
 

+ 3 - 3
.agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.i18n.yaml → .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml

@@ -1,6 +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-11-remove-empty-experimental-package-group.md
-2026-08-11-remove-empty-experimental-package-group.md: e5e81e3e3763f216921b3f3b74709b64be3dee37
-2026-08-11-remove-empty-experimental-package-group.zh.md: d44d0daaf346a5fea317f8c8c6a23f26eaa3cec0
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md
+2026-08-18-experimental-agent-teams-packages.md: cec73be48a5cde25fae570587d2ecd17a4a87d01
+2026-08-18-experimental-agent-teams-packages.zh.md: 71b702daa8694fddec6e2a0972704f19955cb509

+ 37 - 0
.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md

@@ -0,0 +1,37 @@
+# Agent Note: Incubate Agent Teams as private experimental packages
+
+Status: implemented
+
+English | [中文](2026-08-18-experimental-agent-teams-packages.zh.md)
+
+## Problem
+
+Agent Teams needs the real Session log, subagent lifecycle, tools, examples, snapshots, and repository checks while its service and tool contracts continue to change. Placing those packages in a product-role group makes them members of the dsh release family and gives them the same publication expectation as stable packages.
+
+An experimental directory without a current package previously imposed placement, dependency, promotion, and release rules on no consumer. Agent Teams supplies the concrete consumer, but the directory needs mechanical release exclusion and dependency isolation rather than a documentation-only status.
+
+## Decision
+
+`packages/experimental/team` and `packages/experimental/tool-team` are private workspace packages. Their npm names remain `@deepseek-ai/dsh-team` and `@deepseek-ai/dsh-tool-team`, so promotion changes paths and release metadata without renaming imports.
+
+The dsh release family and local baseline publisher exclude every manifest below `packages/experimental/`. Workspace constraints require each experimental package to set `private: true` and omit `publishConfig`. The same top-level check rejects `dependencies`, `optionalDependencies`, and `peerDependencies` from release packages, release apps, or the Python runtime to an experimental package. Experimental packages may depend on release packages and each other; tests may use them through `devDependencies`, and examples may load them explicitly.
+
+The generic caller-reserved continuable child identity and selective direct-child drain remain in the stable Subagent service. They own Subagent identity and Activation lifecycle without importing or naming Agent Teams; the experimental Team service consumes them in the permitted direction.
+
+Experimental status changes publication and compatibility expectations only. The packages retain the repository's ordinary documentation, invariant, lifecycle, security, unit, real-composition, and snapshot requirements. Promotion requires review of the public contracts, limitations, test evidence, release payload, runtime dependents, and a named owner accepting stable-package obligations.
+
+## Alternatives considered
+
+**Keep Agent Teams in a product-role group and describe it as opt-in.** Opt-in composition controls model behavior but does not exclude packages from publication or prevent stable packages from taking runtime dependencies on them.
+
+**Reserve an empty experimental group.** A directory without a current package has no owner or release mechanism to test. The group exists only while concrete packages need its enforced treatment.
+
+**Move the Subagent prerequisites into the experimental directory.** Child identity allocation and Activation teardown belong to the Subagent owner and contain no Team-specific contract. Moving or duplicating them would invert the dependency or split one lifecycle across packages.
+
+**Publish experimental packages under different npm names.** Experimental suffixes would make promotion rename every import and configuration row without strengthening release exclusion; private manifests and release-family filtering enforce the actual requirement.
+
+## Consequences
+
+Agent Teams can use the full repository graph and quality checks without entering official tarballs or becoming a supported runtime dependency. A release package cannot expose Team until the Team packages are promoted, so CLI and Web experiments use explicit example or experimental compositions instead of the shipped base bundles.
+
+The product-role grouping is less direct while the packages incubate, and promotion creates path churn. Keeping npm names stable limits that churn to repository paths, metadata, documentation, and composition.

+ 37 - 0
.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md

@@ -0,0 +1,37 @@
+# Agent Note: 将 Agent Teams 作为私有实验性包孵化
+
+Status: implemented
+
+[English](2026-08-18-experimental-agent-teams-packages.md) | 中文
+
+## 问题
+
+Agent Teams 的服务与工具约定仍在变化,但它需要使用真实 Session 日志、subagent 生命周期、工具、示例、快照和仓库检查。把这些包放在产品职责组会使其成为 dsh 发布系列成员,并获得与稳定包相同的发布预期。
+
+没有实际包的 experimental 目录曾经让没有消费方的放置、依赖、promotion 和发布规则长期存在。Agent Teams 提供了具体消费方,但该目录需要机械强制的发布排除与依赖隔离,不能只用文档标记状态。
+
+## 决策
+
+`packages/experimental/team` 与 `packages/experimental/tool-team` 是私有 workspace 包。其 npm 名仍为 `@deepseek-ai/dsh-team` 与 `@deepseek-ai/dsh-tool-team`,因此 promotion 只需修改路径与发布元数据,无需重命名 import。
+
+dsh 发布系列与本地 baseline 发布器均排除 `packages/experimental/` 下的所有 manifest。workspace 约束要求每个实验性包设置 `private: true` 并省略 `publishConfig`。同一个顶层检查会拒绝发布包、发布 app 或 Python runtime 通过 `dependencies`、`optionalDependencies` 或 `peerDependencies` 依赖实验性包。实验性包可以依赖发布包和其他实验性包;测试可以通过 `devDependencies` 使用它们,示例可以显式加载它们。
+
+通用的调用方预留 continuable child 身份和精确 direct-child drain 仍属于稳定 Subagent 服务。它们负责 Subagent 身份与 Activation 生命周期,不 import 或命名 Agent Teams;实验性 Team 服务沿允许的方向消费这些能力。
+
+实验性状态只改变发布与兼容性预期。这些包仍须满足仓库的一般文档、不变式、生命周期、安全、单元测试、真实组合测试和快照要求。promotion 前必须评审公开约定、限制、测试证据、发布 payload、运行时依赖方,并由一名具名 owner 接受稳定包义务。
+
+## 曾考虑的替代方案
+
+**把 Agent Teams 留在产品职责组,并标为显式启用。** 显式启用的组合可以控制模型行为,但不会阻止包发布,也不能阻止稳定包对其建立运行时依赖。
+
+**预留空的 experimental 组。** 没有实际包的目录没有 owner,也没有可供测试的发布机制。只有具体包需要这套强制处理时,该组才存在。
+
+**把 Subagent 前置能力移入 experimental 目录。** child 身份分配与 Activation teardown 属于 Subagent owner,且不包含 Team 专用约定。移动或复制这些能力会反转依赖方向,或把同一个生命周期拆到多个包中。
+
+**使用不同的 npm 名发布实验性包。** experimental 后缀会让 promotion 重命名所有 import 与配置项,却不能加强发布排除;私有 manifest 与发布系列过滤已经强制执行实际要求。
+
+## 后果
+
+Agent Teams 可以使用完整仓库依赖图与质量检查,而不进入正式 tarball,也不会成为受支持的运行时依赖。在 Team 包 promotion 前,发布包不能暴露 Team,因此 CLI 和 Web 实验使用显式示例或实验性组合,而不是已发布的基础组合包。
+
+孵化期间的产品职责分组不够直接,promotion 也会产生路径改动。保持 npm 名稳定后,改动范围仅包括仓库路径、元数据、文档和组合。

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md
-2026-08-10-subagent-empty-terminal-message-output.md: 693013f6810005ce02b08bd82f1f6a18511c40fb
-2026-08-10-subagent-empty-terminal-message-output.zh.md: 64d61af21f838ef3f515db8af116cbdd74e96179
+2026-08-10-subagent-empty-terminal-message-output.md: 24bab01ad844a5b48e0bf6fe0fc54df6403f4bb7
+2026-08-10-subagent-empty-terminal-message-output.zh.md: ab3488806a4f7c019d783e563c79e06aeeb86f33

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md

@@ -12,7 +12,7 @@ The agent loop appends an empty-content `assistant/message` when a `max-tokens`
 
 `dsh-subagent` owns one canonical selection rule in `src/assistant-output.ts`: select the last non-empty assistant message; without one, select the accumulated `text-delta` stream; ignore empty-content messages. The incremental `AssistantOutputFold` implements the rule through `push(event)` for session-event transports, `pushText(text)` for chunk-only transports, and `collect()` for selection. `finalAssistantOutput(events)` applies it to a complete event suffix for the in-process `readResult` and Activation capture. The SDK backend folds notification events; the ACP backend exposes no complete assistant messages and folds raw chunk text. `SubagentResult.output` defines the result contract, and `subagent/end.lastAssistantMessage` uses the same rule. When a child produces neither form of output, the lifecycle field is absent rather than an empty array for both one-shot and continuable runs. A `max-tokens` or `aborted` result retains its actual stop reason.
 
-The foreground delegation tool uses the same selection. A non-`completed` result remains an `isError` tool result, but its message appends the child's partial text after the stop-reason headline so the parent model receives both the failure and available output.
+The foreground delegation tool uses the same selection. A non-`completed` result remains an `isError` tool result, but its message presents the optional safe Provider diagnostic owned by the [non-interactive permissions decision](../feature/2026-08-15-product-subagent-noninteractive-permissions.md) after the stop-reason headline and appends the child's partial text afterward. The parent model receives the failure, separate infrastructure detail, and available assistant output without conflating them.
 
 ## Verification
 

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 `dsh-subagent` 在 `src/assistant-output.ts` 中拥有唯一的规范选取规则:选取最后一条非空 assistant 消息;没有时选取累积的 `text-delta` 流;忽略空内容消息。增量的 `AssistantOutputFold` 通过 `push(event)` 处理会话事件传输,通过 `pushText(text)` 处理仅分片传输,并通过 `collect()` 完成选取。`finalAssistantOutput(events)` 把规则应用于完整的事件后缀,供进程内 `readResult` 与 Activation capture 使用。SDK 后端折叠通知事件;ACP 后端不暴露完整的 assistant 消息,而是折叠原始分片文本。`SubagentResult.output` 定义结果约定,`subagent/end.lastAssistantMessage` 使用同一规则。子 agent 不产生这两种输出中的任何一种时,一次性与 continuable 运行的生命周期字段都会缺省,而不是空数组。`max-tokens` 或 `aborted` 结果保留实际的终止原因。
 
-前台委派工具使用同一选取规则。非 `completed` 的结果仍是 `isError` 工具结果,但其消息会在终止原因标题之后附上子 agent 的部分文本,让父模型同时接收失败信息与已有输出。
+前台委派工具使用同一选取规则。非 `completed` 的结果仍是 `isError` 工具结果,但其消息会在终止原因标题之后呈现由[非交互权限决策](../feature/2026-08-15-product-subagent-noninteractive-permissions.md)负责的可选安全提供方诊断,再附上子 agent 的部分文本。父模型会同时收到失败、独立的基础设施说明与已有 assistant 输出,而且不会把它们混为一体。
 
 ## 验证
 

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-08-18-request-image-payload-bound.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/bug-fix/2026-08-18-request-image-payload-bound.md
+2026-08-18-request-image-payload-bound.md: df10df39d18c7da4660b566e8f0b6a5a60ff8dc1
+2026-08-18-request-image-payload-bound.zh.md: 070f2d194f1459f3f2728fdf9d5d2db2c3a24385

+ 35 - 0
.agents/notes/implemented/bug-fix/2026-08-18-request-image-payload-bound.md

@@ -0,0 +1,35 @@
+# Agent Note: Request-level image payload bound
+
+Status: implemented
+
+English | [中文](2026-08-18-request-image-payload-bound.zh.md)
+
+## Problem
+
+Every image in session history is base64-inlined into every model request by the pi-ai adapter, so a long session's request body grows monotonically with each admitted image. Gateways cap request-body size; once the accumulated payload crossed such a cap the request was rejected with 413 (`Failed to buffer the request body: length limit exceeded`), and because nothing bounds or trims the assembled request, every retry resent the same oversized body. The session was permanently unusable, and the failure text matched no `classifyPiAiError` rule, so it surfaced as the generic `PI_AI_ERROR`. Admission bounds (per image, per message) cannot prevent this: each image is individually admissible, and the sum still grows without bound. Two screenshots were enough to trigger it in production.
+
+## Decision
+
+The pi-ai provider profile carries `maxRequestImageBytes` (default `DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20MiB`, a positive integer, per route, changeable from cordis.yml and the `llm-pi-ai` settings section). At request conversion, `toPiContext` sums the base64 length of every image in history (derived from `ImageAttachmentRef.bytes` without reading data) and, while the sum exceeds the bound, replaces the oldest images with a fixed model-facing placeholder. The placeholder tells the model to read the file again when a path is available or ask the user to attach the image again. The most recent images are omitted last; an image larger than the bound is itself omitted. Offload locations use message and nested block indexes rather than object identity, so replaying the same JSON log produces the same request. Offloaded images are never read from the attachment store. `classifyPiAiError` classifies 413 and specific request-body-cap wording as `INVALID_REQUEST` (resending the same body cannot succeed). Four images admitted at the attachment store's 3.5MiB raw-image default occupy at most 18.67MiB after base64 expansion. The 20MiB request-image default therefore retains four such images and reserves the rest of a 32MiB request for system prompts, history, tools, and JSON. Deployments behind stricter gateways lower the value per route.
+
+## Offload is conversion, not history
+
+The placeholder is model-visible but not logged as a session event. It stays within the model-visible ⟺ logged invariant the same way the adapter's other serialization does (`(no output)` fallbacks, text-only folding): the offload locations are a pure function of the logged history and the route configuration, so the exact request remains reconstructable from the session log plus the composition. A logged elision event becomes necessary only when offload decisions gain non-deterministic inputs (for example live gateway feedback), which belongs to the deferred capability-metadata design.
+
+## Alternatives considered
+
+- **Fail the request with a clear error instead of offloading.** Keeps the model informed but leaves the session wedged: the user cannot remove images from durable history, so a hard failure at the bound is permanent. Offload keeps the session serviceable, which is the point of the fix.
+- **Upload images once and reference them by URL / file id.** Removes the linear body growth entirely and is the right medium-term shape (providers and the internal gateway both document a Files path), but it introduces upload lifecycle management across providers and is far beyond a P0 hotfix.
+- **Count the full request body, not only images.** Text and tools contribute little and their sizes are only known after full serialization per protocol; bounding the dominant term with explicit headroom is accurate enough for the failure being fixed and much simpler. Revisit inside the route-capability design.
+- **Trim at admission instead.** Admission cannot see future accumulation; only the assembled request knows its total. Admission-side bounds (per-side dimension, bytes) remain as the first layer and are owned by [the dimension-limit note](2026-08-17-image-dimension-admission-limit.md).
+
+## Related
+
+- [Per-side image dimension admission limit](2026-08-17-image-dimension-admission-limit.md) — the admission-layer companion fix; together they close the two observed session-poisoning failures (400 dimension, 413 body size).
+
+## Consequences
+
+- An image-heavy long session keeps completing requests. The oldest images are omitted first; the most recent image is omitted only when it cannot fit within the bound.
+- Crossing the bound rewrites an early message, so the provider prompt-cache prefix ends at the newly offloaded image until the offloaded prefix stabilizes.
+- The bound counts base64 image payload only; deployments must keep it below their gateway's request-body cap with headroom, and the shipped default cannot know a private gateway's cap.
+- Route capability metadata driving admission and assembly together (image count, per-image size, request size, provider token formulas) remains deferred design work tracked outside this fix.

+ 35 - 0
.agents/notes/implemented/bug-fix/2026-08-18-request-image-payload-bound.zh.md

@@ -0,0 +1,35 @@
+# Agent Note: 请求级图片载荷上限
+
+Status: implemented
+
+[English](2026-08-18-request-image-payload-bound.md) | 中文
+
+## Problem
+
+pi-ai 适配器把会话历史中的每张图片 base64 内联进每一个模型请求,长会话的请求体随每张入库图片单调增长。网关对请求体大小设有上限;累积载荷一旦越线,请求被以 413 拒绝(`Failed to buffer the request body: length limit exceeded`),而组装层没有任何约束或裁剪,每次重试都会原样重发同一个超限请求体,会话永久不可用。该报错文本不匹配 `classifyPiAiError` 的任何规则,只能落进笼统的 `PI_AI_ERROR`。准入上限(单图、单消息)无法阻止这一点:每张图片单独看都合规,总和仍然无界增长。线上两张截图即可触发。
+
+## Decision
+
+pi-ai provider profile 增加 `maxRequestImageBytes`(默认 `DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20MiB`,正整数,按路由生效,可从 cordis.yml 与 `llm-pi-ai` settings 段修改)。请求转换时,`toPiContext` 由 `ImageAttachmentRef.bytes` 推算每张历史图片的 base64 长度(无需读取数据)求和,总和超过上限时从最老的图片开始替换为一段固定的模型可见占位文本。占位文本要求模型在有路径时重新读取文件,否则请用户重新附上图片。越新的图片越晚被省略;单张图片本身超过上限时也会被省略。offload 位置用消息与嵌套块的索引表示,不依赖对象身份,因此重放同一份 JSON 日志会产生相同请求。被 offload 的图片不会从附件存储读取。`classifyPiAiError` 把 413 与明确的请求体上限措辞归类为 `INVALID_REQUEST`(原样重发不可能成功)。四张按附件存储默认上限准入的 3.5MiB 原始图片,经 base64 膨胀后最多占 18.67MiB。20MiB 请求图片默认上限因此可保留四张这样的图片,并在 32MiB 请求内为系统提示词、历史、工具与 JSON 保留其余容量。网关更严格的部署按路由调低该值。
+
+## offload 是转换而非历史
+
+占位文本模型可见,但不记录为会话事件。它与适配器的其他序列化(`(no output)` 回退、纯文本折叠)以同样的方式满足「模型可见 ⟺ 已记录」不变量:offload 位置是已记录历史与路由配置的纯函数,确切请求仍可由会话日志加组合配置重建。只有当 offload 决策引入非确定性输入(例如网关的实时反馈)时才需要记录省略事件,那属于暂缓的能力元数据设计。
+
+## Alternatives considered
+
+- **在上限处直接报错而不 offload。** 模型知情,但会话仍然卡死:用户无法从持久历史中删除图片,越线即永久失败。offload 让会话保持可用,这正是本修复的目标。
+- **图片上传一次、按 URL / file id 引用。** 从结构上消除请求体线性增长,是正确的中期形态(各提供方与内部网关都有 Files 路径),但要跨提供方管理上传生命周期,远超 P0 热修复范围。
+- **统计完整请求体而非只统计图片。** 文本与工具占比很小,且其大小要到按协议完整序列化后才可知;对主导项设上限并留出显式余量,对所修故障足够精确且简单得多。留到路由能力设计中再议。
+- **改在准入侧裁剪。** 准入看不到未来的累积,只有组装后的请求知道自己的总量。准入侧上限(单边尺寸、字节)作为第一层保留,归[尺寸上限笔记](2026-08-17-image-dimension-admission-limit.md)所有。
+
+## Related
+
+- [图片单边尺寸准入上限](2026-08-17-image-dimension-admission-limit.md),准入层的配套修复;两者合起来封住已观测到的两类会话毒化故障(400 尺寸、413 请求体)。
+
+## Consequences
+
+- 图片较多的长会话持续可用。最老的图片优先省略;仅当最新图片本身无法装进上限时才会省略它。
+- 越过上限会改写较早的一条消息,提供方 prompt cache 前缀在新被 offload 的图片处截止,直到被 offload 的前缀稳定。
+- 上限只统计 base64 图片载荷;部署必须让它低于自家网关的请求体上限并留出余量,发行默认值无法预知私有网关的上限。
+- 由路由能力元数据同时驱动准入与组装(图片数量、单图大小、请求大小、提供方 token 公式)的设计仍为暂缓工作,在本修复之外跟踪。

+ 2 - 2
.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md
-2026-06-21-subagent-capability-seam.md: fa3b4f570bfccdc849a38b3eda16c1c8dd7b1827
-2026-06-21-subagent-capability-seam.zh.md: b25fe64377f98af92dbccb87f755627926975ef2
+2026-06-21-subagent-capability-seam.md: bc84d88d701a5f3018bf00f0ecf8b60750917407
+2026-06-21-subagent-capability-seam.zh.md: baec829a1a01018b490992fa0982bb23e43ba740

+ 2 - 2
.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md

@@ -54,11 +54,11 @@ Fresh and forked children are separate providers, not a request flag. `dsh-subag
 
 ### Child isolation and the parent log
 
-Each in-process subagent runs in its **own `Session`** (own id, `parentSession` lineage), persisted independently. Remote ACP and one-shot product providers instead mint a parent-scoped lifecycle id and expose no local `Agent` or child `Session`; their internal state remains in the remote process. Across both forms, the parent's log records only the spawn `tool/call` and its `tool/result` (the child's final output), while child steps and tool calls remain outside the parent log.
+Each in-process subagent runs in its **own `Session`** (own id, `parentSession` lineage), persisted independently. Remote ACP and one-shot product providers instead mint a parent-scoped lifecycle id and expose no local `Agent` or child `Session`; their internal state remains in the remote process. Across both forms, the parent's log records only the spawn `tool/call` and its `tool/result` (the child's final output, or a failed result with optional provider diagnostic), while child steps and tool calls remain outside the parent log.
 
 ### Synchronous collect (first cut)
 
-`dsh-tool-subagent` passes its execution signal to `start()`, awaits the child result, and disposes the run before reporting. Non-completed outcomes become error results rather than successful partial output, and independent result and disposal rejections retain both diagnostics.
+`dsh-tool-subagent` passes its execution signal to `start()`, awaits the child result, and disposes the run before reporting. Non-completed outcomes become error results rather than successful partial output; they present the optional safe diagnostic owned by the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) separately from partial assistant text. Independent result and disposal rejections remain independently observable.
 
 ### Provider selection is config, not model-facing
 

+ 2 - 2
.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md

@@ -54,11 +54,11 @@ bash seam([能力 seam](../architecture/2026-06-13-capability-seams.md))在
 
 ### 子 agent 隔离与父日志
 
-每个进程内 subagent 运行在**自己的 `Session`** 中(独立 id、`parentSession` 谱系),独立持久化。远端 ACP 和一次性产品提供方则会生成一个父级作用域的生命周期 id,且不暴露本地 `Agent` 或子 `Session`;其内部状态留在远端进程中。两种形式下,父日志都仅记录 spawn `tool/call` 及其 `tool/result`(子 agent 的最终输出),而子 agent 的步骤和工具调用均留在父日志之外。
+每个进程内 subagent 运行在**自己的 `Session`** 中(独立 id、`parentSession` 谱系),独立持久化。远端 ACP 和一次性产品提供方则会生成一个父级作用域的生命周期 id,且不暴露本地 `Agent` 或子 `Session`;其内部状态留在远端进程中。两种形式下,父日志都仅记录 spawn `tool/call` 及其 `tool/result`(子 agent 的最终输出,或带可选提供方诊断的失败结果),而子 agent 的步骤和工具调用均留在父日志之外。
 
 ### 同步收集(首版)
 
-`dsh-tool-subagent` 将其执行信号传给 `start()`,等待子 agent 结果,并在报告前 dispose 该 run。非完成态的结果变为错误结果,而非成功的部分输出;结果与 dispose 的拒绝相互独立,且两项诊断信息都会保留。
+`dsh-tool-subagent` 将其执行信号传给 `start()`,等待子 agent 结果,并在报告前 dispose 该 run。非完成态的结果变为错误结果,而非成功的部分输出;它会把由[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.md)负责的可选安全诊断与部分 assistant 文本分开呈现。结果与 dispose 的拒绝仍可彼此独立地观察。
 
 ### 提供方选择是配置,不面向模型
 

+ 2 - 2
.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md
-2026-07-08-background-subagent-tasks.md: 412ec61dcdecae1a273c5993d25a4a099a22e864
-2026-07-08-background-subagent-tasks.zh.md: 9d108440c992e150ed62edaef8bf860813a471b8
+2026-07-08-background-subagent-tasks.md: 4dcd961ee5a5db925f8f6ad83e97890eaedb8e63
+2026-07-08-background-subagent-tasks.zh.md: dd8ba47e018bdeeaffccca1fdaf52df75728057b

+ 3 - 3
.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md

@@ -8,7 +8,7 @@ English | [中文](2026-07-08-background-subagent-tasks.zh.md)
 
 The [subagent seam](2026-06-21-subagent-capability-seam.md) returns a `SubagentRun`, but the model-facing tool originally collected every run synchronously. Independent, slow delegations therefore held the parent call open or ran serially.
 
-Subagents need the same start, collect, list, stop, ownership, notification, and cleanup behavior as other long-running tools without adopting process-stream semantics. The child session remains the detailed trace; the parent needs the final answer and job status. A background child also outlives its starting tool call, so its cancellation and owner-disposal contracts must be explicit.
+Subagents need the same start, collect, list, stop, ownership, notification, and cleanup behavior as other long-running tools without adopting process-stream semantics. The child session remains the detailed trace; the parent needs the final answer or safe failure detail plus job status. A background child also outlives its starting tool call, so its cancellation and owner-disposal contracts must be explicit.
 
 ## Decision
 
@@ -16,7 +16,7 @@ Each `dsh-tool-subagent` instance may expose `run_in_background`, controlled by
 
 Background subagents use the [generic background job runtime](../architecture/2026-06-20-generic-long-running-tool-runtime.md). Collection, listing, cancellation, completion notices, and prompt guidance come from `job_output`, `job_list`, and `job_kill`; there are no subagent-specific companion tools.
 
-Foreground calls retain their synchronous contract: await provider startup and `run.result`, return final text only for `completed`, map other terminal reasons to an errored tool result, and always dispose the run before returning.
+Foreground calls retain their synchronous contract: await provider startup and `run.result`, return final text only for `completed`, map other terminal reasons to an errored tool result with the optional safe diagnostic described by the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md), and always dispose the run before returning.
 
 For a background call, the tool validates the parent and refuses an already-aborted execution signal before calling `ctx.jobs.start()`. The job runtime preflights the control API and owner cleanup before invoking the producer starter. That starter creates an independent `AbortController` and begins `ctx.subagents.start()`; after the id is returned, the tool-call signal no longer owns the child.
 
@@ -24,7 +24,7 @@ The task registration maps the subagent seam as follows:
 
 - `kind` is `subagent`, `label` is the model-supplied description, and `owner` is the parent agent.
 - `cancel(reason?)` aborts the task-owned controller. The same signal covers pending provider startup and the published run's remaining work.
-- `done` awaits provider startup, the child result, and `run.dispose()`. Completed runs return final text, aborted runs become `killed`, and other stop reasons become `failed`. Startup, result, and disposal failures become failed outcomes rather than rejected task promises.
+- `done` awaits provider startup, the child result, and `run.dispose()`. Completed runs return final text, aborted runs become `killed`, and other stop reasons become `failed` with the Provider diagnostic when present. Startup, result, and disposal failures become failed outcomes rather than rejected task promises.
 - `readOutput` is absent. While live, `job_output` returns status only; after settlement, it returns final output idempotently. Intermediate child activity remains in the child session.
 
 ## Lifecycle

+ 3 - 3
.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.zh.md

@@ -8,7 +8,7 @@ Status: implemented
 
 [subagent seam](2026-06-21-subagent-capability-seam.md) 会返回 `SubagentRun`,但原先面向模型的工具会同步收集每一次运行。因此,各自独立的慢速委派要么一直占用父调用,要么按串行方式运行。
 
-subagent 需要与其他长时间运行的工具相同的启动、收集、列出、停止、归属、通知和清理行为,但不应采用进程流语义。子会话仍是详细记录;父级只需最终答案和任务状态。后台子级的存活时间还会超过启动它的工具调用,因此必须明确其取消和拥有者资源释放约定。
+subagent 需要与其他长时间运行的工具相同的启动、收集、列出、停止、归属、通知和清理行为,但不应采用进程流语义。子会话仍是详细记录;父级只需最终答案或安全失败说明,以及任务状态。后台子级的存活时间还会超过启动它的工具调用,因此必须明确其取消和拥有者资源释放约定。
 
 ## 决策
 
@@ -16,7 +16,7 @@ subagent 需要与其他长时间运行的工具相同的启动、收集、列
 
 后台 subagent 使用[通用后台任务运行时](../architecture/2026-06-20-generic-long-running-tool-runtime.md)。`job_output`、`job_list` 和 `job_kill` 负责收集、列出、取消、完成通知和提示词引导;系统不提供 subagent 专用的配套工具。
 
-前台调用保留其同步约定:等待提供方启动和 `run.result`;仅当状态为 `completed` 时返回最终文本;将其他终止原因映射为出错的工具结果;并且始终在返回前释放该运行。
+前台调用保留其同步约定:等待提供方启动和 `run.result`;仅当状态为 `completed` 时返回最终文本;将其他终止原因映射为出错的工具结果,并在存在时附上由[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.md)描述的可选安全诊断;而且始终在返回前释放该运行。
 
 对于后台调用,工具会验证父级,并在调用 `ctx.jobs.start()` 前拒绝已中止的执行信号。任务运行时会在调用生产者启动器前,预检控制 API 和拥有者清理。该启动器创建独立的 `AbortController` 并启动 `ctx.subagents.start()`;返回 id 之后,工具调用的信号不再拥有该子级。
 
@@ -24,7 +24,7 @@ subagent 需要与其他长时间运行的工具相同的启动、收集、列
 
 - `kind` 为 `subagent`,`label` 为模型提供的描述,`owner` 为父 agent(智能体)。
 - `cancel(reason?)` 中止任务自有的控制器。同一个信号同时覆盖尚未完成的提供方启动和已发布 run 的剩余工作。
-- `done` 等待提供方启动、子级结果和 `run.dispose()`。已完成的运行返回最终文本,已中止的运行变为 `killed`,其他停止原因变为 `failed`。启动、结果和资源释放失败会转换为失败结果,而不是被拒绝的任务 Promise。
+- `done` 等待提供方启动、子级结果和 `run.dispose()`。已完成的运行返回最终文本,已中止的运行变为 `killed`,其他停止原因变为 `failed`,并在存在时携带提供方诊断。启动、结果和资源释放失败会转换为失败结果,而不是被拒绝的任务 Promise。
 - `readOutput` 不存在。任务存活期间,`job_output` 只返回状态;结算后,它以幂等方式返回最终输出。中间的子级活动仍保留在子会话中。
 
 ## 生命周期

+ 2 - 2
.agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.md
-2026-07-22-web-multimodal-image-input-and-durable-attachments.md: 8c07b8b786aeeb87a4c2db7c0e6e49928b0ddf2c
-2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md: 5d2d2963d6bee729362852701ea58f191f9721b2
+2026-07-22-web-multimodal-image-input-and-durable-attachments.md: 6a12380992c3205d6bb3f5701f093b3335fae2f9
+2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md: a8e94154e435a5359e7928d3b11eba7d1aee92f7

+ 1 - 1
.agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.md

@@ -140,7 +140,7 @@ Composer thumbnails and each `MessageImage` own ephemeral original-preview state
 
 ### Limits and trust boundaries
 
-Version one accepts PNG, JPEG, WebP, and GIF only. SVG and remote URLs are excluded. Default limits are 5 MiB per image, 20 images and 100 MiB aggregate image bytes per message, 40 million intrinsic pixels per image, and 2000 pixels on either side. These deployment-varying limits are validated backend configuration and enforced by the host before persistence. The client connection carrier has an independent configurable `maxRequestBodyBytes` cap (160 MiB by default) for every API request and fails load if it cannot hold the attachment service's aggregate image limit after base64 and envelope expansion; lowering image policy therefore never silently lowers the carrier limit for valid text or other RPCs. A body without a declared length is rejected the moment it crosses the cap rather than drained to its end.
+Version one accepts PNG, JPEG, WebP, and GIF only. SVG and remote URLs are excluded. Default limits are 3.5 MiB per image, 20 images and 100 MiB aggregate image bytes per message, 40 million intrinsic pixels per image, and 2000 pixels on either side. These deployment-varying limits are validated backend configuration and enforced by the host before persistence. The client connection carrier has an independent configurable `maxRequestBodyBytes` cap (160 MiB by default) for every API request and fails load if it cannot hold the attachment service's aggregate image limit after base64 and envelope expansion; lowering image policy therefore never silently lowers the carrier limit for valid text or other RPCs. A body without a declared length is rejected the moment it crosses the cap rather than drained to its end.
 
 Malformed base64, unsupported or mismatched media, truncated image payloads, excess bytes, excess image count, excess pixels, excess per-side dimensions, missing objects, and integrity mismatches return stable structured failures. Original filenames are reduced to a display basename, control characters are removed, and no local path is logged or returned to the browser.
 

+ 1 - 1
.agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md

@@ -140,7 +140,7 @@ Pi-AI 适配器是首条视觉输入路径:它在请求时解析 `ctx.attachme
 
 ### 限制与信任边界
 
-第一版仅接受 PNG、JPEG、WebP 和 GIF。不接受 SVG 和远程 URL。默认限制为每张图片 5 MiB、每条消息 20 张图片和 100 MiB 图片总字节数、每张图片 4,000 万个固有像素,以及任一边 2,000 像素。这些随部署变化的限制属于经过校验的后端配置,并由宿主在持久化前强制执行。客户端连接载体为每个 API 请求设置独立且可配置的 `maxRequestBodyBytes` 上限(默认 160 MiB);如果该上限无法容纳附件服务的图片总量限制经 base64 和请求封装膨胀后的大小,加载就会失败。因此,降低图片策略绝不会静默降低有效文本或其他 RPC 的载体上限。未声明长度的请求体在越过上限的瞬间即被拒绝,而不是先读完再拒。
+第一版仅接受 PNG、JPEG、WebP 和 GIF。不接受 SVG 和远程 URL。默认限制为每张图片 3.5 MiB、每条消息 20 张图片和 100 MiB 图片总字节数、每张图片 4,000 万个固有像素,以及任一边 2,000 像素。这些随部署变化的限制属于经过校验的后端配置,并由宿主在持久化前强制执行。客户端连接载体为每个 API 请求设置独立且可配置的 `maxRequestBodyBytes` 上限(默认 160 MiB);如果该上限无法容纳附件服务的图片总量限制经 base64 和请求封装膨胀后的大小,加载就会失败。因此,降低图片策略绝不会静默降低有效文本或其他 RPC 的载体上限。未声明长度的请求体在越过上限的瞬间即被拒绝,而不是先读完再拒。
 
 格式错误的 base64、不支持或不匹配的媒体、截断的图片数据、超出字节限制、超出图片数量、超出像素限制、超出单边尺寸限制、对象缺失和完整性不匹配都会返回稳定的结构化错误。原始文件名只保留用于显示的末段,控制字符会被移除,并且任何本地路径都不会写入日志或返回浏览器。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.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-04-claude-code-and-codex-subagent-backends.md
-2026-08-04-claude-code-and-codex-subagent-backends.md: 666945c4d8039874729a7f9da34d9cf82bfd479d
-2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 35b8dd51f1c45200a495c68f19f055a224123263
+2026-08-04-claude-code-and-codex-subagent-backends.md: f65c0626ad22db8f3e7d2a543c7aa87e58df54d4
+2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 97ac527b8e89cc07d65aa28102ba43d648b1b64c

+ 12 - 12
.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md

@@ -12,7 +12,7 @@ The product integrations must not become second owners for task text, cwd, cance
 
 ## Decision
 
-The harness publishes two sibling one-shot provider packages: `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their explicit Profile installation and host-plane placement, and the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice. Loading either provider starts no product process, and each tool accepts only a standalone text task; product selection remains deployment configuration.
+The harness publishes two sibling one-shot provider packages: `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their explicit Profile installation and host-plane placement, the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice, and the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) owns each product Provider's Profile-selected mode and diagnostic production. Loading either provider starts no product process, and each tool accepts only a standalone text task; product selection remains deployment configuration.
 
 Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools use `backgroundMode: 'one-shot'` and `maxDepth: 'provider-managed'`: the consumer keeps foreground collection as the default and may place the same run in the generic Job runtime, while recursion policy stays with the out-of-process product. Every call creates a fresh product process and a non-resumable product conversation. `ctx.subagents` owns named-request resolution and paired lifecycle events; `dsh-tool-subagent` owns model-visible scheduling and foreground-versus-Job adaptation; `ctx.jobs` and `dsh-tool-jobs` own Job ids, state, output, controls, notices, and parent-owner cancellation; each product provider owns native result mapping, while `dsh-subprocess` owns credential scrubbing, process-tree termination, and whole-tree exit observation.
 
@@ -34,15 +34,15 @@ fixed tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> product
 
 ## Codex provider
 
-`@deepseek-ai/dsh-subagent-codex` registers the fixed `codex` provider and starts `codex app-server --stdio` from `PATH`. Its public configuration contains only an explicit `env` overlay and a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`. Installation, login, `CODEX_HOME`, model selection, base URL, sandbox, approval policy, and product-session settings remain native Codex or deployment responsibilities.
+`@deepseek-ai/dsh-subagent-codex` registers the fixed `codex` provider and starts `codex app-server --stdio` from `PATH`. Its public configuration contains an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a three-value native `permissionMode` that defaults to `never`. Installation, login, `CODEX_HOME`, model selection, base URL, and product-session settings remain native Codex or deployment responsibilities; the selected mode owns only the thread approval/reviewer/sandbox fields described by the non-interactive permissions decision.
 
-Before publication, the provider validates a non-empty text-only task, starts the managed app-server in the parent workspace, completes `initialize` → `initialized`, and creates an `ephemeral: true` thread. The published run owns exactly one `turn/start`; its thread and turn ids remain private and are never persisted in the parent Session.
+Before publication, the provider validates a non-empty text-only task, starts the managed app-server in the parent workspace, completes `initialize` → `initialized`, maps the resolved mode into official `thread/start` fields, and creates an `ephemeral: true` thread. The fixed app-server argv contains no mode or task text. The published run owns exactly one `turn/start`; its thread and turn ids remain private and are never persisted in the parent Session.
 
-`turn/completed` is the authoritative remote terminal fact. The latest `agentMessage` with `phase: "final_answer"` wins, and that selected message must contain nonblank text. When the product emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback and must likewise be nonblank; commentary never replaces either answer. A failed turn with `error.codexErrorInfo: "contextWindowExceeded"` becomes `max-tokens`. A completed turn without an answer, every other failed or interrupted remote turn, malformed required fields in a recognized app-server frame, protocol closure, early process exit, or unknown server request becomes `error`; this version has no native refusal terminal and therefore produces no `refusal`. Local cancellation wins its race and remains `aborted`.
+`turn/completed` is the authoritative remote terminal fact. The latest `agentMessage` with `phase: "final_answer"` wins, and that selected message must contain nonblank text. When the product emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback and must likewise be nonblank; commentary never replaces either answer. A failed turn with `error.codexErrorInfo: "contextWindowExceeded"` becomes `max-tokens`. A completed turn without an answer, every other failed or interrupted remote turn, malformed required fields in a recognized app-server frame, protocol closure, early process exit, or unknown server request becomes `error`; a permission-related error may additionally carry the shared safe diagnostic. This version has no native refusal terminal and therefore produces no `refusal`. Local cancellation wins its race and remains `aborted` without permission detail.
 
-For command and file approvals, the unattended wire selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.147.0 request shape without an offered-decision list falls back to `decline`. It grants no requested permissions for the turn, answers user-input requests with no answers, and declines MCP elicitation. A request with no legal unattended response, or any unknown server request, fails the run instead of waiting for a user interface the provider does not supply.
+For command and file approvals, the unattended wire selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.147.0 request shape without an offered-decision list falls back to `decline`. It grants no requested permissions for the turn, answers user-input requests with no answers, and declines MCP elicitation. It records safe categories for those requests, declined command/file items, and `sandboxError`. Codex emits some early `never` rejections and sandbox violations only on structured stderr, so the Provider pipes and forwards stderr unchanged while matching two fixed signatures in a bounded per-run tail; raw stderr never enters the diagnostic. A request with no legal unattended response, or any unknown server request, fails the run instead of waiting for a user interface the provider does not supply.
 
-An unpublished startup failure closes the wire, terminates the acquired process tree, waits for exit, and then rejects `start()`. Published disposal best-effort interrupts a known turn, closes the wire, ends stdin, invokes the shared termination escalation, and waits for whole-tree exit. Result failure and teardown failure stay independently observable.
+An unpublished startup failure closes the wire, terminates the acquired process tree, waits for exit, detaches the stderr observer, and then rejects `start()`. Published disposal best-effort interrupts a known turn, closes the wire, ends stdin, invokes the shared termination escalation, waits for whole-tree exit, and detaches the observer. Result failure and teardown failure stay independently observable.
 
 Codex 0.147.0 speaks the Responses protocol, while DeepSeek's public OpenAI-compatible endpoint speaks Chat Completions. The credentialed Codex e2e therefore uses a loopback-only, test-private bridge for one no-tool nonce request: real Codex sends Responses to the bridge, the bridge forwards the received bearer credential and extracted task to the fixed official DeepSeek endpoint, and it wraps the real text in the minimal Responses SSE lifecycle. The bridge is neither a production proxy nor evidence that Codex connects to DeepSeek Chat Completions natively.
 
@@ -50,9 +50,9 @@ Codex 0.147.0 speaks the Responses protocol, while DeepSeek's public OpenAI-comp
 
 `@deepseek-ai/dsh-subagent-claude-code` registers the fixed `claude-code` provider and invokes `@anthropic-ai/claude-agent-sdk@0.3.220`. Before each run, the provider resolves the fixed `claude` name through the host subprocess execution world and passes that exact path as `pathToClaudeCodeExecutable`; the SDK therefore uses the native product that launched DSH rather than selecting its platform `optionalDependency`. A Windows `.cmd` or `.bat` path crosses `cmd.exe /v:off` as a quoted per-spawn environment expansion, so percent, ampersand, and exclamation path components remain data without changing the shared subprocess contract. The provider uses the official `query()` entrypoint and passes the SDK's `spawnClaudeCodeProcess` arguments, cwd, environment, and forwarded signal to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
 
-The public configuration contains the same two deployment-owned values as the Codex sibling: an explicit `env` overlay and a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`. Each run creates its own `AbortController`, sets `persistSession: false`, and disables `AskUserQuestion`. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. It supplies no `canUseTool`, elicitation, or dialog callback, so unattended interactions fail through the SDK rather than waiting for a user interface the provider does not own.
+The public configuration contains an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a five-value native `permissionMode` that defaults to `dontAsk`. Each run creates its own `AbortController`, sets `persistSession: false`, disables `AskUserQuestion`, and passes the resolved mode to the SDK; only `bypassPermissions` receives the SDK's explicit dangerous confirmation. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. Remaining permission prompts are denied, MCP elicitation is declined, and blocking dialogs fail closed instead of waiting for a user interface the provider does not own.
 
-The provider publishes only after both the SDK `Query` and a live managed CLI handle exist. It consumes the complete SDK stream and completes only when a `result` message has `subtype: "success"`, `is_error: false`, and a nonblank `result`, and the iterator then ends normally. Every SDK error subtype, an error-marked success, a missing result, iterator failure, protocol failure, or process failure becomes `error`. SDK turn, budget, and structured-output limits are not token-window facts, and the SDK exposes no native refusal terminal, so this provider produces neither `max-tokens` nor `refusal`. Local cancellation wins and becomes `aborted`.
+The provider publishes only after both the SDK `Query` and a live managed CLI handle exist. It consumes the complete SDK stream and completes only when a `result` message has `subtype: "success"`, `is_error: false`, and a nonblank `result`, and the iterator then ends normally. Every SDK error subtype, an error-marked success, a missing result, iterator failure, protocol failure, or process failure becomes `error`. When a permission denial or unattended callback contributes to that failure, the result may additionally carry the bounded, non-assistant diagnostic owned by the non-interactive permissions decision. SDK turn, budget, and structured-output limits are not token-window facts, and the SDK exposes no native refusal terminal, so this provider produces neither `max-tokens` nor `refusal`. Local cancellation wins and becomes `aborted` without permission detail.
 
 Startup rollback and published disposal close the SDK query, abort the per-run controller, invoke shared process-tree termination, and wait for whole-tree exit. `Query.close()` expresses graceful protocol intent but does not replace the subprocess owner's exit proof. Query-close failure, process failure, and teardown failure remain independently observable.
 
@@ -62,11 +62,11 @@ The credentialed Claude Code e2e uses the official DeepSeek Claude Code contract
 
 Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The Loader tier boots the README-shaped explicit Profile configuration, verifies both fixed one-shot tools expose optional background scheduling alongside generic Job controls, and starts neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret.
 
-The Codex evidence pins `@openai/codex@0.147.0` and `codex-cli 0.147.0`. Its real-product spec observes the exact Bearer key, original task, byte-exact final answer, unattended command rejection with no file side effect, local cancellation, and whole-tree exit. Production still supplies `codex` on `PATH`.
+The Codex evidence pins `@openai/codex@0.147.0` and `codex-cli 0.147.0`. Its real-product spec observes the exact Bearer key, original task, byte-exact final answer, thread-level `never` overriding ambient `on-request`, automatic-review startup, unattended command rejection with safe diagnostic and no file side effect, explicit dangerous-bypass writing in suite-owned temporary storage, local cancellation, and whole-tree exit. Production still supplies `codex` on `PATH`.
 
 The Codex credentialed e2e registers the production provider, starts the same real app-server, and requests one random nonce through the test-private bridge described above. It fixes the external endpoint and model, stores no credential or request payload, requires exactly one completed upstream response, compares the trimmed product answer byte-for-byte with the nonce, and waits for every managed handle to exit.
 
-The Claude Code evidence pins Agent SDK 0.3.220 and uses its platform-distributed Claude Code 2.1.220 CLI as the deterministic compatibility fixture, routed through the same native executable-resolution path production uses. Its real-product spec observes the exact `x-api-key`, original task, byte-exact final answer, inherited temporary host-setting marker, process failure, local cancellation, whole-tree exit, and a real Windows batch shim under a path containing percent, ampersand, and exclamation metacharacters. This evidence proves the official SDK/CLI integration path, not compatibility with every independently installed product version. The Loader and shipped-profile evidence resolve both product packages by name while starting neither product, and the provider suite proves that the SDK receives the executable resolved from the host `PATH`.
+The Claude Code evidence pins Agent SDK 0.3.220 and uses its platform-distributed Claude Code 2.1.220 CLI as the deterministic compatibility fixture, routed through the same native executable-resolution path production uses. Its real-product spec observes the exact `x-api-key`, original task, byte-exact final answer, an inherited interactive host setting overridden by the safe Provider mode, denied and bypassed writes in suite-owned temporary directories, safe permission diagnostics, process failure, local cancellation, whole-tree exit, and a real Windows batch shim under a path containing percent, ampersand, and exclamation metacharacters. This evidence proves the official SDK/CLI integration path, not compatibility with every independently installed product version. The Loader and shipped-profile evidence resolve both product packages by name while starting neither product, and the provider suite proves that the SDK receives the executable resolved from the host `PATH`.
 
 The Claude Code credentialed e2e maps the key and fixed official endpoint only in the provider's in-memory environment, uses the documented `deepseek-v4-pro[1m]` and `deepseek-v4-flash` model variables, and traverses the production provider, official SDK, and real CLI. It compares the trimmed result with a random nonce and proves whole-tree exit without calling the Messages API directly from the test.
 
@@ -82,7 +82,7 @@ The project owner's distribution authorization is scoped to the official `@anthr
 
 **Product doubles as required evidence.** Doubles cover exhaustive private protocol branches but do not prove package exports, official distributions, authentication, or real process behavior. Required evidence drives each official product against a loopback model fixture.
 
-**Plugin-managed login, product home, models, settings, or permissions.** Those choices would create another authority beside each product's native configuration and enlarge a one-shot provider into account management. The providers expose only an explicit environment overlay and teardown grace; unattended interaction fails closed.
+**Plugin-managed login, product home, models, settings, sandbox rules, or fine-grained permission policy.** Those choices would create another authority beside each product's native configuration and enlarge a one-shot provider into account management. Each product exposes only one native non-interactive mode choice in addition to environment and teardown configuration; neither Provider mirrors product rules or adds a human interaction channel.
 
 **Continuation, progress, product-native background state, and shared parent context.** The provider payload remains one final answer for one self-contained task. The generic Job layer may add its id, status, notice, collection, and cancellation results, but product sessions, resume, follow-up, intermediate messages, parent transcript transfer, structured output, and provider-specific background state need separate user contracts and are not prebuilt.
 
@@ -90,6 +90,6 @@ The project owner's distribution authorization is scoped to the official `@anthr
 
 Users delegate through two stable one-shot tools backed by the official product integrations. Explicit Profile installation and host-plane provider placement are owned by the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md); per-Preset tool exposure and foreground-default optional Job scheduling are owned by the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md). This note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of job settlement and process-tree quiescence.
 
-Every delegation pays for a fresh product process and independent model context. The product payload reaching the parent is final text only; background scheduling additionally exposes generic Job ids, status, completion notices, and collection or cancellation results. Product-native configuration makes behavior depend on the deployment's installed product, account state, and workspace settings. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.
+Every delegation pays for a fresh product process and independent model context. Successful product payload remains final assistant text; a failed product run may separately expose the shared safe diagnostic. Background scheduling additionally exposes generic Job ids, status, completion notices, and collection or cancellation results. Product-native configuration makes behavior depend on the deployment's installed product, account state, workspace settings, and selected Provider mode. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.
 
 Compatibility is pinned by package-level unit coverage, keyless real-product loopback tests, credentialed DeepSeek nonce tests, public Loader composition, built-package and NodeNext consumer checks, generated documentation and notices, and the repository CI matrix. A supported product or DeepSeek endpoint/model baseline change must refresh those facts; production performs no separate runtime version probe.

+ 12 - 12
.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 ## 决策
 
-harness 交付两个同级的一次性提供方包:`codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责显式 Profile 安装与 host plane(宿主平面)放置,[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责模型可见的调度选择。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择仍属于部署配置。
+harness 交付两个同级的一次性提供方包:`codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责显式 Profile 安装与 host plane(宿主平面)放置,[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责模型可见的调度选择,[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.md)则负责各产品提供方的 Profile 模式选择与诊断生产。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择仍属于部署配置。
 
 这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动能力,并传递父会话 cwd,但不会复制父级对话。文档所示的工具使用 `backgroundMode: 'one-shot'` 与 `maxDepth: 'provider-managed'`:消费方默认在前台收集结果,也可把同一次运行放入通用 Job 运行时,而递归策略仍由进程外产品负责。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。`ctx.subagents` 负责具名请求解析与成对生命周期事件;`dsh-tool-subagent` 负责模型可见的调度以及前台与 Job 适配;`ctx.jobs` 和 `dsh-tool-jobs` 负责 Job id、状态、输出、控制、通知与父级 owner 取消;各产品提供方负责原生结果映射,`dsh-subprocess` 则负责凭证清洗、进程树终止以及整棵进程树的退出观测。
 
@@ -34,15 +34,15 @@ fixed tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> product
 
 ## Codex 提供方
 
-`@deepseek-ai/dsh-subagent-codex` 注册固定的 `codex` 提供方,并启动 `codex app-server --stdio`,该命令从 `PATH` 解析。其公开配置仅包含显式的 `env` 覆盖项和须为正有限值的 `disposeGraceMs`,且后者不得大于仓库共享的 `MAX_TIMER_DELAY_MS`。安装、登录、`CODEX_HOME`、模型选择、基础 URL、沙箱、审批策略和产品会话设置仍由 Codex 原生机制或部署环境负责。
+`@deepseek-ai/dsh-subagent-codex` 注册固定的 `codex` 提供方,并启动 `codex app-server --stdio`,该命令从 `PATH` 解析。其公开配置包含显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `never` 的三值原生 `permissionMode`。安装、登录、`CODEX_HOME`、模型选择、基础 URL 和产品会话设置仍由 Codex 原生机制或部署环境负责;所选模式只拥有非交互权限决策中描述的线程 approval/reviewer/sandbox 字段。
 
-发布前,提供方会验证非空的纯文本任务,在父级工作区中启动受管的 app-server,完成 `initialize` → `initialized` 握手,并创建一个 `ephemeral: true` 线程。已发布的运行只拥有一次 `turn/start`;其线程 ID 与轮次 ID 保持私有,绝不会持久化到父会话。
+发布前,提供方会验证非空的纯文本任务,在父级工作区中启动受管的 app-server,完成 `initialize` → `initialized` 握手,把已解析模式映射为官方 `thread/start` 字段,并创建一个 `ephemeral: true` 线程。固定 app-server argv 不包含模式或任务文本。已发布的运行只拥有一次 `turn/start`;其线程 ID 与轮次 ID 保持私有,绝不会持久化到父会话。
 
-`turn/completed` 是权威的远端终止事实。以最后一条带有 `phase: "final_answer"` 的 `agentMessage` 为准,且选中的消息必须包含非空白文本。若产品没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退,该消息也必须包含非空白文本;过程说明绝不会取代上述任一答案。带有 `error.codexErrorInfo: "contextWindowExceeded"` 的失败轮次会成为 `max-tokens`。轮次完成却没有答案、其他任何远端失败或中断轮次、已识别的 app-server 帧中必需字段格式错误、协议关闭、进程提前退出或未知的服务器请求,都会产生 `error`;本版本没有原生的拒绝终止状态,因此不会产生 `refusal`。本地取消在竞态中胜出并保持为 `aborted`。
+`turn/completed` 是权威的远端终止事实。以最后一条带有 `phase: "final_answer"` 的 `agentMessage` 为准,且选中的消息必须包含非空白文本。若产品没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退,该消息也必须包含非空白文本;过程说明绝不会取代上述任一答案。带有 `error.codexErrorInfo: "contextWindowExceeded"` 的失败轮次会成为 `max-tokens`。轮次完成却没有答案、其他任何远端失败或中断轮次、已识别的 app-server 帧中必需字段格式错误、协议关闭、进程提前退出或未知的服务器请求,都会产生 `error`;权限相关错误可以额外携带共享安全诊断。本版本没有原生的拒绝终止状态,因此不会产生 `refusal`。本地取消在竞态中胜出并保持为 `aborted`,且不附带权限说明。
 
-对于命令与文件审批,无人值守的协议连接会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.147.0 请求形态没有决策选项列表,因此回退到 `decline`。它不授予该轮次请求的任何权限,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败,而不会等待本提供方没有提供的用户界面。
+对于命令与文件审批,无人值守的协议连接会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.147.0 请求形态没有决策选项列表,因此回退到 `decline`。它不授予该轮次请求的任何权限,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。它会记录这些请求、被拒绝的命令/文件 item 与 `sandboxError` 的安全类别。Codex 的部分早期 `never` 拒绝和 sandbox violation 只写入结构化 stderr,因此提供方会 pipe 并原样转发 stderr,同时在每次运行的有界尾部中匹配两个固定签名;原始 stderr 绝不会进入诊断。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败,而不会等待本提供方没有提供的用户界面。
 
-若启动在发布前失败,提供方会关闭协议连接、终止已获取的进程树并等待其退出,然后拒绝 `start()`。对已发布的运行执行资源释放时,提供方会尽力中断已知轮次、关闭协议连接、结束标准输入、调用共享的逐级终止机制,并等待整棵进程树退出。结果失败与清理失败仍可彼此独立地观察。
+若启动在发布前失败,提供方会关闭协议连接、终止已获取的进程树、等待其退出、移除 stderr observer,然后拒绝 `start()`。对已发布的运行执行资源释放时,提供方会尽力中断已知轮次、关闭协议连接、结束标准输入、调用共享的逐级终止机制,等待整棵进程树退出,并移除 observer。结果失败与清理失败仍可彼此独立地观察。
 
 Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端点使用 Chat Completions。因此,带密钥 Codex e2e 会采用一个仅限回环、仅供测试内部使用的桥接层来处理一次不使用工具的随机数请求:真实 Codex 将 Responses 发送到桥接层,桥接层把收到的 Bearer 凭据与提取出的任务转发到固定的 DeepSeek 官方端点,再将真实文本包装进最小化的 Responses SSE(Server-Sent Events)生命周期。该桥接层既不是生产代理,也不能作为 Codex 原生连接 DeepSeek Chat Completions 的证据。
 
@@ -50,9 +50,9 @@ Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端
 
 `@deepseek-ai/dsh-subagent-claude-code` 注册固定的 `claude-code` 提供方,并调用 `@anthropic-ai/claude-agent-sdk@0.3.220`。每次运行前,提供方经宿主 subprocess 执行世界解析固定名称 `claude`,并把准确路径作为 `pathToClaudeCodeExecutable` 交给 SDK;SDK 因此使用启动 DSH 的原生产品,而不是选择自身的 platform `optionalDependency`。Windows `.cmd` 或 `.bat` 路径会作为带引号、仅供本次 spawn 使用的环境展开值穿过 `cmd.exe /v:off`,因此路径中的百分号、与号和感叹号仍只是数据,且无需改变共享子进程约定。提供方使用官方 `query()` 入口点,并将 SDK 的 `spawnClaudeCodeProcess` 参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
 
-公开配置包含与 Codex 兄弟提供方相同、由部署方负责的两个值:显式的 `env` 覆盖项,以及须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false` 并禁用 `AskUserQuestion`。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。提供方不设置 `canUseTool`、elicitation 或对话回调,因此无人值守交互会经 SDK 失败,而不会等待本提供方不负责的用户界面。
+公开配置包含显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `dontAsk` 的五值原生 `permissionMode`。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false`、禁用 `AskUserQuestion`,并把已解析模式传给 SDK;只有 `bypassPermissions` 会取得 SDK 的显式危险确认。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。其余权限提示会被拒绝,MCP elicitation 会被拒绝,阻塞对话会快速失败,而不会等待本提供方不负责的用户界面。
 
-只有在 SDK `Query` 与受管的活动 CLI 句柄都已存在后,提供方才会发布运行。它会消费完整的 SDK 流;只有 `result` 消息具有 `subtype: "success"`、`is_error: false` 和非空白 `result`,且迭代器随后正常结束时,运行才会完成。所有 SDK 错误子类型、标记为错误的成功消息、结果缺失、迭代器失败、协议失败或进程失败都会成为 `error`。SDK 的轮次、预算和结构化输出限制不表示 token 窗口耗尽,而且 SDK 没有原生的拒绝终止状态,因此本提供方不会产生 `max-tokens` 或 `refusal`。本地取消会胜出并成为 `aborted`。
+只有在 SDK `Query` 与受管的活动 CLI 句柄都已存在后,提供方才会发布运行。它会消费完整的 SDK 流;只有 `result` 消息具有 `subtype: "success"`、`is_error: false` 和非空白 `result`,且迭代器随后正常结束时,运行才会完成。所有 SDK 错误子类型、标记为错误的成功消息、结果缺失、迭代器失败、协议失败或进程失败都会成为 `error`。当权限拒绝或无人值守回调参与了该失败时,结果还可以携带由非交互权限决策负责的有界、非 assistant 诊断。SDK 的轮次、预算和结构化输出限制不表示 token 窗口耗尽,而且 SDK 没有原生的拒绝终止状态,因此本提供方不会产生 `max-tokens` 或 `refusal`。本地取消会胜出并成为 `aborted`,且不附带权限说明。
 
 启动回滚和已发布运行的资源释放都会关闭 SDK query、中止该次运行的控制器、调用共享的进程树终止机制,并等待整棵进程树退出。`Query.close()` 表达优雅的协议关闭意图,但不能取代子进程责任方的退出证明。Query 关闭失败、进程失败和清理失败仍可彼此独立地观察。
 
@@ -62,11 +62,11 @@ Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端
 
 每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 组合 e2e 和一项带密钥 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Loader 层级会启动 README 所示的显式 Profile 配置,在同一个上下文中验证两个固定一次性工具会与通用 Job 控制工具一起公开可选后台调度,而且不会启动任何产品进程。带密钥层级会使用仅在运行时提供的密钥启动同一生产提供方与真实产品,要求从固定的 DeepSeek 官方服务取得唯一随机数,并再次证明完全停稳;仅当本地操作者未提供密钥时才会自行跳过,而受信任的 CI 会预检该 secret。
 
-Codex 证据锁定 `@openai/codex@0.147.0` 与 `codex-cli 0.147.0`。其真实产品测试会观测确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、不会产生文件副作用的无人值守命令拒绝、本地取消以及整棵进程树退出。生产环境仍提供 `codex`,并通过 `PATH` 解析。
+Codex 证据锁定 `@openai/codex@0.147.0` 与 `codex-cli 0.147.0`。其真实产品测试会观测确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、线程级 `never` 对环境中 `on-request` 的覆盖、自动评审启动、带安全诊断且不产生文件副作用的无人值守命令拒绝、测试拥有临时存储中的显式危险绕过写入、本地取消以及整棵进程树退出。生产环境仍提供 `codex`,并通过 `PATH` 解析。
 
 带密钥 Codex e2e 会注册生产提供方,启动同样的真实 app-server,并通过上述测试专用桥接层请求一个随机数。该测试固定外部端点与模型,不存储任何凭据或请求载荷,要求上游恰好完成一次响应,将去除首尾空白后的产品答案与该随机数逐字节比较,并等待所有受管句柄退出。
 
-Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Claude Code 2.1.220 CLI 作为确定性兼容性 fixture(测试前置数据),且该 fixture 经生产环境所用的同一原生可执行文件解析路径运行。其真实产品测试会观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、继承的临时宿主设置标记、进程失败、本地取消、整棵进程树退出,以及位于同时含百分号、与号和感叹号路径中的真实 Windows batch shim。这项证据证明官方 SDK/CLI 集成路径,而不证明它与每个独立安装的产品版本兼容。Loader 与随附 profile 证据会按名称解析两个产品包且不启动产品,provider 测试则证明 SDK 收到由宿主 `PATH` 解析出的可执行文件。
+Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Claude Code 2.1.220 CLI 作为确定性兼容性 fixture(测试前置数据),且该 fixture 经生产环境所用的同一原生可执行文件解析路径运行。其真实产品测试会观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、安全提供方模式对继承的交互式宿主设置的覆盖、测试所拥有临时目录中的拒绝写入与 bypass 写入、安全权限诊断、进程失败、本地取消、整棵进程树退出,以及位于同时含百分号、与号和感叹号路径中的真实 Windows batch shim。这项证据证明官方 SDK/CLI 集成路径,而不证明它与每个独立安装的产品版本兼容。Loader 与随附 profile 证据会按名称解析两个产品包且不启动产品,provider 测试则证明 SDK 收到由宿主 `PATH` 解析出的可执行文件。
 
 带密钥 Claude Code e2e 仅在提供方的内存环境中映射密钥与固定的官方端点,把模型变量设为文档所示的 `deepseek-v4-pro[1m]` 与 `deepseek-v4-flash`,并实际经过生产提供方、官方 SDK 与真实 CLI。它将去除首尾空白后的结果与一个随机数比较,并证明整棵进程树退出,且测试不会直接调用 Messages API。
 
@@ -82,7 +82,7 @@ Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Cl
 
 **以产品替身作为强制证据。** 替身可以穷尽覆盖私有协议分支,但无法证明包导出、官方发行版、身份验证或真实进程行为。强制证据会驱动每个官方产品连接回环模型 fixture。
 
-**由插件管理登录、产品主目录、模型、设置或权限。** 这些选择会在每个产品的原生配置之外建立另一套权威来源,并将一次性提供方扩张为账户管理功能。提供方只公开显式环境覆盖项和清理宽限期;无人值守交互会以默认拒绝方式失败。
+**由插件管理登录、产品主目录、模型、设置、沙箱规则或细粒度权限策略。** 这些选择会在每个产品的原生配置之外建立另一套权威来源,并将一次性提供方扩张为账户管理功能。两个产品除环境和清理配置外都只公开一个原生非交互模式选择;任一提供方都不会镜像产品规则或增加人工交互通道。
 
 **续接、进度、产品原生后台状态和共享父级上下文。** 提供方载荷仍是一项自包含任务的一个最终回答。通用 Job 层可以额外提供 id、状态、通知、收集与取消结果,但产品会话、恢复、后续交互、中间消息、父级 transcript(文本记录)传递、结构化输出和提供方专属后台状态都需要独立的用户约定,当前实现不会预先构建这些功能。
 
@@ -90,6 +90,6 @@ Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Cl
 
 用户通过官方产品集成支持的两个稳定一次性工具进行委派。显式 Profile 安装与 host plane 提供方放置由[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责;按 Preset 暴露工具以及默认前台且可选通用 Job 的调度方式由[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责。本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占作业结算与进程树完全停稳的责任。
 
-每次委派都要承担新建产品进程和独立模型上下文的开销。到达父级的产品载荷仍只有最终文本;后台调度还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。产品原生配置使行为取决于部署环境中安装的产品、账户状态和工作区设置。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
+每次委派都要承担新建产品进程和独立模型上下文的开销。成功的产品载荷仍只有最终 assistant 文本;失败的产品运行可以另行公开共享安全诊断。后台调度还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。产品原生配置使行为取决于部署环境中安装的产品、账户状态、工作区设置和所选提供方模式。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
 
 兼容性由包级单元测试覆盖率、无密钥真实产品回环测试、带密钥 DeepSeek 随机数测试、公开 Loader 组合、已构建包与 NodeNext 消费方检查、生成的文档与声明以及仓库 CI 矩阵共同锁定。更改受支持的产品基线或 DeepSeek 端点/模型基线时必须刷新这些事实;生产环境不会另行执行运行时版本探测。

+ 6 - 0
.agents/notes/implemented/feature/2026-08-05-agent-teams.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/feature/2026-08-05-agent-teams.md
+2026-08-05-agent-teams.md: 43e92d4a9807e6a178436d96722b141b1bac45bb
+2026-08-05-agent-teams.zh.md: 72989623bb49334beae3b49ce2f6973781558c69

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

@@ -0,0 +1,73 @@
+# Agent Note: Durable Agent Teams over continuable children
+
+Status: implemented
+
+English | [中文](2026-08-05-agent-teams.zh.md)
+
+## Problem
+
+The subagent seam supplies fresh/fork providers, durable child Sessions, FIFO follow-ups, and cold-resumable Activations. Its direct-parent controls do not provide peer communication, a stable named roster, or shared task ownership. A coordinator can create several workers, but workers cannot address one another, durable follow-up intent lives only in target inboxes, and no common compare-and-set board prevents stale assignment updates.
+
+All same-process Agents also share one checkout. Filesystem edit tools can reject an observed stale version, but Bash, formatters, generators, and external writers bypass that fence. Treating a teammate name or task owner as a file lock would hide rather than solve this concurrency boundary.
+
+The model-visible Team tools remain opt-in so the default tool catalog and simple-task behavior do not change. An explicitly requested Team must survive child Activation settlement and mailbox delivery races long enough for the Lead to aggregate the result before process teardown.
+
+## Decision
+
+Every ordinary runtime root is the implicit Lead of a Team identified by that root's `SessionId`. The Team has no creation event: its Lead pseudo-row exists by identity, while durable state begins with the first member, message, or task event. A roster is flat and contains at most the configured number of immutable lowercase-kebab-case names. Each teammate is a continuable direct child with a reserved Session id; only the Lead creates or interrupts teammates. Ordinary provider-owned subagents outside the roster are not Team members, and an ordinary fork is a new root whose inherited Team records are excluded by their ancestor `TeamId`.
+
+The implementation is split into `@deepseek-ai/dsh-team`, which owns `ctx.teams` and durable semantics, and `@deepseek-ai/dsh-tool-team`, which owns scoped schemas and model guidance. Every Team tool declares its complete result schema and renders that value as compact JSON, so the compiler checks each `execute` against what the model is promised and no result spends tokens on indentation. Deployments mount both plugins explicitly and may disable legacy continuable controls with the same model-visible names. The explicit delegation policy permits Team creation only when the user asks for Agent Teams or teammates. Both packages are private members of `packages/experimental/`; the [experimental package decision](../architecture/2026-08-18-experimental-agent-teams-packages.md) owns release exclusion, dependency isolation, and promotion.
+
+The Lead must wait for required work before its final answer. Process teardown remains the final lifecycle owner and drains continuation Activations; a Team task owner is durable state and is not automatically released by idle, interruption, or process exit.
+
+## Provisioning and recovery
+
+Creation first appends and flushes a `team/member` provisioning snapshot in the Lead Session, then starts the reserved continuable child through the selected fresh or fork provider. Failure before initial inbox acceptance appends a failed snapshot. Success flushes the child's accepted inbox item before appending active. Recovery recognizes that initial message while it is still pending or after it enters user-message history. Names are reserved by the first provisioning record and never reused, including after failure. Disposal closes admission, aborts and awaits admitted creation and mailbox-dispatch transactions, then stops every live child recorded by the roster; a failed child remains cleanup-owned until its Activation exits, and cleanup rejection fails disposal.
+
+A root recovery reconciles an unterminated provisioning record against the child's independently persisted Session. Matching direct-parent and continuable descriptors plus a recorded initial user message prove successful admission and produce active; absence, corruption, mismatched provider/lineage, or a missing admitted message produces failed. The creator re-reads the terminal phase under the same Lead-log serializer; if recovery marked failed while creation succeeded, it drains the child and reports a provisioning conflict instead of retaining an orphan. This avoids reconstructing an initial prompt that was never retained in the Team log and contains plugin-reload races.
+
+Fresh children have no inherited conversation. Fork children capture the Lead's completed-turn prefix once and retain it as their own durable seed. The current delegation turn remains excluded, matching the existing fork provider contract.
+
+## Mailbox and task transactions
+
+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.
+
+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.
+
+`wait_agent` blocks on one roster, mailbox, task, or live-status edge registered after the call starts instead of encouraging model polling. It does not replay an earlier edge, so callers re-read authoritative state after wakeup or timeout. Lead-only interruption cancels the current turn with inbox preservation and does not alter mailbox or task ownership.
+
+## Shared checkout boundary
+
+All members use the same cwd and observe writes immediately. The policy tells members to partition tasks, record advisory write scopes, order dependent work, and let the Lead inspect the final diff and run tests. A filesystem stale-version rejection requires rereading and rebasing the intended change. No equivalent guarantee is claimed for Bash, formatters, code generation, or direct external writes.
+
+Worktree isolation is not a harness runtime behavior. A deployment or prompt may arrange separate worktrees, but the Team domain does not infer branches, merge changes, or silently change cwd. This preserves the existing same-world subagent and sandbox contracts.
+
+## Alternatives considered
+
+**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.
+
+**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.
+
+**Create isolated worktrees automatically.** Rejected because worktree creation, branch naming, merge policy, ignored files, build artifacts, and cleanup are deployment choices. It also changes the same-world behavior existing subagents and sandboxes expose.
+
+**Enable Teams in the default catalog.** Rejected because scoped Team controls would shadow same-named legacy globals and unsolicited delegation would add latency and token cost to simple tasks. Explicit composition keeps model-visible ownership unambiguous without changing shipped requests.
+
+**Use an in-memory board and mailbox.** Rejected because child settlement, HMR, and process interruption would lose accepted coordination state and make retries ambiguous.
+
+**Return Team tool results as untyped JSON.** Rejected because an undeclared result type lets `execute` drift from the value the model is promised without a compiler error, and it invites indentation that costs tokens on every roster, task, and receipt. Each Team tool therefore declares its complete result schema and one shared helper renders it compactly.
+
+## Testing
+
+Package tests cover identity, name and authority checks, provider selection, reserved-id persistence collisions, child-before-Lead flush ordering, durable provisioning failure and pending-inbox JSONL/SQLite reconciliation, concurrent target-local ordering, pending/history de-duplication, mailbox limits, post-flush notification, bounded disposal with in-flight creation and dispatch cancellation, failed-member cleanup, task CAS and DAG validation, write-scope warnings, wait cancellation/timeout, inbox-preserving interruption, ordinary-fork isolation, legacy-control shadowing, compact declared-schema result rendering, and scoped registration HMR at per-file 100% coverage. A keyless headless Loader snapshot assembles the real Team plugins and records teammate creation, peer mail, dependent tasks, waiting, and Lead aggregation.
+
+## Consequences
+
+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.
+
+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.

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

@@ -0,0 +1,73 @@
+# Agent Note: Durable Agent Teams over continuable children
+
+Status: implemented
+
+[English](2026-08-05-agent-teams.md) | 中文
+
+## Problem
+
+subagent seam 已提供 fresh/fork provider、持久 child Session、FIFO follow-up 与可冷恢复 Activation。它的直接 parent control 不提供 peer 通讯、稳定具名 roster 或共享任务 owner。coordinator 可以创建多个 worker,但 worker 无法互相寻址;持久 follow-up 意图只存在于 target inbox;也没有公共 compare-and-set 任务板来阻止陈旧 assignment 更新。
+
+同进程 Agent 还共享一个 checkout。文件系统 edit 工具可以拒绝已观察到的陈旧版本,但 Bash、formatter、generator 与外部 writer 会绕过该屏障。把 teammate name 或 task owner 当作文件锁只会掩盖而不是解决该并发边界。
+
+面向模型的 Team 工具保持显式启用,使默认工具目录与简单任务行为不变。显式请求的 Team 必须能跨越 child Activation settlement 与 mailbox 投递竞争,使 Lead 在进程 teardown 前汇总结果。
+
+## Decision
+
+每个普通运行时 Root 都是一个隐式 Team 的 Lead,Team id 等于该 Root 的 `SessionId`。Team 没有 creation event:Lead pseudo-row 由身份直接存在,持久状态从第一条 member、message 或 task event 开始。roster 是扁平结构,最多包含配置数量、不可变且采用小写 kebab-case 的名字。每个 teammate 都是使用预留 Session id 的 continuable 直接 child;只有 Lead 可以创建或 interrupt teammate。roster 外由 provider 管理的普通 subagent 不是 Team member;普通 fork 是新的 Root,继承的 Team 记录会因 ancestor `TeamId` 被排除。
+
+实现拆分为 `@deepseek-ai/dsh-team` 与 `@deepseek-ai/dsh-tool-team`:前者负责 `ctx.teams` 和持久语义,后者负责 scoped schema 与模型指引。每个 Team 工具都声明完整的结果 schema,并把该值渲染为紧凑 JSON,因此编译器会检查每个 `execute` 是否符合对模型的承诺,也没有结果把 token 花在缩进上。部署显式挂载两个插件,并可禁用具有相同模型可见名称的旧 continuable control。显式 delegation 策略只允许在用户要求 Agent Teams 或 teammate 时创建 Team。 两个包都是 `packages/experimental/` 的私有成员;[实验性包决策](../architecture/2026-08-18-experimental-agent-teams-packages.md)负责发布排除、依赖隔离与 promotion。
+
+Lead 必须等待所需工作后才能给出最终答案。进程 teardown 仍是最终生命周期 owner,并会 drain continuation Activation;Team task owner 是持久状态,不会因 idle、interrupt 或进程退出自动释放。
+
+## Provisioning and recovery
+
+创建操作先在 Lead Session 中追加并 flush `team/member` provisioning 快照,再通过选定 fresh 或 fork provider 启动预留的 continuable child。初始 inbox 获准前的失败会追加 failed 快照;成功会先 flush child 中已接受的 inbox 条目,再追加 active。恢复会在初始消息仍处于 pending 或已进入用户消息历史时识别它。名字由第一条 provisioning 记录永久保留,包括失败后也不能复用。dispose 会关闭准入,中止并等待已获准的创建与 mailbox dispatch 事务,再停止 roster 记录的所有 live child;failed child 在 Activation 退出前仍由 cleanup 拥有,cleanup 拒绝会让 dispose 失败。
+
+Root 恢复时会把未终结 provisioning 记录与独立持久 child Session 对账。直接 parent 与 continuable descriptor 匹配,并且已经记录初始用户消息,才能证明准入成功并转为 active;缺失、损坏、provider/lineage 不匹配或缺少已准入消息都会转为 failed。creator 会在同一 Lead 日志 serializer 内重读终态;如果 recovery 在创建成功时先标记 failed,creator 会 drain child 并报告 provisioning conflict,而不是遗留孤儿。这样既无需重建从未保存在 Team 日志中的初始 prompt,也能约束插件 reload 竞争。
+
+fresh child 不继承对话。fork child 只捕获一次 Lead 已完成 turn 前缀,并保留为自己的持久 seed。当前 delegation turn 保持排除,与既有 fork provider 契约一致。
+
+## Mailbox and task transactions
+
+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。
+
+共享 task 是带 Team-local id 与单调 revision 的完整快照。每次变更都携带 `expectedRevision`。任意 member 可以创建、读取或 claim ready 且无 owner 的任务;Owner 或 Lead 可以编辑和转换;只有 Lead 可以分配给另一个 member。数字 task id 保持在安全整数分配范围内;该范围耗尽时会失败,不会复用 id。依赖必须指向未删除任务,并形成完整 DAG。删除任务保留为 tombstone。`writeScopes` 是规范化路径前缀,只产生重叠诊断,绝不会阻止 claim 或授予写权限。
+
+`wait_agent` 等待调用注册后发生的下一条 roster、mailbox、task 或实时 status 边,避免模型轮询。它不会回放更早的边,因此调用方需要在唤醒或超时后重新读取权威状态。仅限 Lead 的 interrupt 使用 inbox preservation 取消当前 turn,不改变 mailbox 或 task owner。
+
+## Shared checkout boundary
+
+所有 member 使用相同 cwd,并立即观察写入。策略要求 member 切分任务、记录提示性 write scope、为有序工作添加依赖,并由 Lead 检查最终 diff 和运行测试。文件系统 stale-version 拒绝后必须重新读取并 rebase 修改意图。Bash、formatter、codegen 与直接外部写入不具备等价保证。
+
+Worktree isolation 不是 harness runtime 行为。deployment 或 prompt 可以安排独立 worktree,但 Team 领域不会推断 branch、merge 变更或静默改变 cwd。这样保留既有 same-world subagent 与 sandbox 契约。
+
+## Alternatives considered
+
+**用 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 与去重边界。
+
+**把 task ownership 或 write scope 当作锁。** 拒绝,因为外部 writer 会绕过它们,崩溃 owner 会持久保留,而路径前缀重叠不能证明语义独立。虚假的互斥保证比明确 warning 更危险。
+
+**自动创建隔离 worktree。** 拒绝,因为 worktree 创建、branch 命名、merge 策略、ignored file、构建产物与 cleanup 都是 deployment 选择;它也会改变既有 subagent 与 sandbox 暴露的 same-world 行为。
+
+**在默认工具目录中启用 Team。** 拒绝,因为 scoped Team control 会覆盖同名旧全局工具,主动 delegation 也会给简单任务增加延迟和 token 成本。显式组合可以保持面向模型的归属明确,同时不改变默认 request。
+
+**使用内存 task board 与 mailbox。** 拒绝,因为 child settlement、HMR 与进程中断会丢失已接受协调状态,并让重试变得含糊。
+
+**让 Team 工具返回未类型化 JSON。** 拒绝,因为未声明的结果类型会让 `execute` 在没有编译错误的情况下偏离对模型的承诺,也会引入在每份 roster、task 与回执上都消耗 token 的缩进。因此每个 Team 工具都声明完整的结果 schema,并由一个共享 helper 紧凑渲染。
+
+## Testing
+
+Package test 以逐文件 100% coverage 覆盖身份、名字与权限检查、provider 选择、预留 id 持久化冲突、child-before-Lead flush 顺序、持久 provisioning 失败与 pending-inbox JSONL/SQLite 对账、target-local 并发顺序、pending/history 去重、mailbox 限额、flush 后 notification、取消在途创建与 dispatch 的有界 dispose、failed member cleanup、task CAS 与 DAG 校验、write-scope warning、wait cancel/timeout、保留 inbox 的 interrupt、普通 fork 隔离、旧 control shadowing、声明 schema 的紧凑结果渲染与 scoped registration HMR。一条 keyless headless Loader 快照会组合真实 Team 插件,并记录 teammate 创建、peer mail、依赖任务、等待与 Lead 汇总。
+
+## Consequences
+
+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 保持可见而不是静默回收身份。
+
+协调可以降低 checkout 冲突概率,但无法消除文件系统 CAS 工具之外的写入。最终 diff 与测试仍是 Lead 的集成边界。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-11-workflow-run-status-driven-disclosure.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-11-workflow-run-status-driven-disclosure.md
-2026-08-11-workflow-run-status-driven-disclosure.md: 2f452d25a8922bb6c275419af55e8af155dd2781
-2026-08-11-workflow-run-status-driven-disclosure.zh.md: 12cc106fea274a1681ee5615906ae6df266d567b
+2026-08-11-workflow-run-status-driven-disclosure.md: e97ead90460f21d4a27b49dcdabbcafc8f193cda
+2026-08-11-workflow-run-status-driven-disclosure.zh.md: ef3da3ee7cbffbb901a4b71a920bd53c2965c827

+ 16 - 14
.agents/notes/implemented/feature/2026-08-11-workflow-run-status-driven-disclosure.md

@@ -6,38 +6,40 @@ English | [中文](2026-08-11-workflow-run-status-driven-disclosure.zh.md)
 
 ## Problem
 
-A durable workflow Chat node updates in place from its running prefix to a terminal record. A disclosure choice initialized only at mount can hide a newly running phase, leave completed work occupying the conversation, or bury a failed, cancelled, or interrupted member behind two collapsed levels. Making openness a pure function of completion avoids those failures but also prevents users from reopening clean history for review.
+A durable workflow Chat node updates in place from its running prefix to a terminal record. The renderer must draw attention to new work, abnormal outcomes, and normal completion without repeatedly overriding a user's decision to reclaim conversation space.
 
-The renderer already receives every required lifecycle fact from the workflow Conversation Node. Visibility therefore needs a component-local lifecycle that gives current execution and attention states priority without adding another durable fact or taking ownership of workflow outcomes.
+The renderer already receives every durable lifecycle fact from the workflow Conversation Node. Disclosure choice therefore belongs to the mounted presentation, but its lifecycle must also preserve nested phase choices when the outer run is hidden and avoid removing content that still contains keyboard focus.
 
 ## Decision
 
-Each phase derives one visibility requirement from its current members. A running, failed, cancelled, or interrupted member forces that phase open; a phase whose members are all completed is clean. The workflow forces itself open when its own status requires attention or any phase is forced open, so an abnormal member remains visible even when the workflow outcome is recorded as completed. A completed sibling phase remains independently collapsible.
+`WorkflowRunPanel` owns one local disclosure state for the run and a map keyed by the existing phase key. A phase is clean when every member completed, abnormal when any member failed, was cancelled, or was interrupted, and running otherwise. The run is abnormal when its own status or any phase is abnormal, running when its own status or any phase is running, and clean only when the run and every phase completed normally. A mount opens running and abnormal levels and closes clean levels.
 
-A forced-open level renders as an expanded static row. It exposes no button role, focus target, keyboard toggle, or `aria-expanded` value because collapsing cannot change the result. This keeps the visual hierarchy and status summaries while making the interaction promise match the available action.
+Each level records its current mode, append-only member count, open choice, and any pending clean close. Ordinary updates within a running or abnormal interval preserve the user's choice. A phase transition from clean to activity opens that phase and the outer run once, the first transition into abnormal opens once, and a transition into clean closes once. A member-count change while a phase remains clean represents a complete activity cycle delivered in one render: it closes an open phase review and, while the run remains active, opens the outer run once without adding an activity epoch or durable field. After an automatic action, mouse, Enter, and Space control the level until another defined edge occurs.
 
-A clean level mounts an ordinary controlled disclosure in the closed state. Its local choice survives rerenders for the same continuous clean interval. New running or abnormal data replaces that manual interval with forced expansion; the next transition back to clean mounts a fresh closed disclosure, which produces one automatic fold per activity cycle. Closing the workflow naturally unmounts its phase controls, and a Session remount reconstructs every level from the current durable status rather than restoring an earlier choice.
+Phase state remains in `WorkflowRunPanel` while the outer disclosure hides its children, so closing and reopening the run restores each phase choice. Removing a phase deletes its entry; a renderer remount reconstructs every level from current durable facts rather than restoring an earlier choice.
 
-For example, a running workflow exposes its active phase and member without clicks. When that phase completes, only the phase folds while the workflow remains open; when the workflow and every phase complete, the workflow also folds. The user can then reopen both levels for review. If another member starts under the same phase key, both affected levels immediately return to forced expansion and fold again only after the new activity completes.
+Normal completion checks whether focus is inside the content before closing. Focused content remains mounted with current completed status and closes after focus leaves. When a navigable member becomes terminal while its button holds focus, `MemberRow` keeps the same button mounted as `aria-disabled` until blur; later terminal review renders the ordinary non-interactive row. This preserves the active DOM target without allowing terminal navigation or adding a focus manager.
 
-The renderer owns only this visibility lifecycle. It does not add Session events, stores, settings, acknowledgement state, timers, focus movement, automatic scrolling, or cross-remount persistence. It does not change workflow status derivation, phase grouping, member order, navigation eligibility, copy, or the shared `DisclosureRow` API. Shared `data-expandable` styling owns pointer cursors, so forced-open static rows do not advertise an unavailable action. An interrupted durable prefix remains an attention state and therefore stays visible until the underlying facts change.
+The renderer adds no Session events, store, setting, acknowledgement, timer, automatic scrolling, persistent activity identity, or `DisclosureRow` API. It does not change workflow status derivation, phase grouping, member order, navigation eligibility, copy, or visual tokens.
 
 ## Verification
 
-Component tests drive the same keyed workflow and phase through running, clean completion, manual review, renewed activity, repeated clean completion, zero-member completion, and each abnormal status. They also verify abnormal-member propagation, clean-sibling independence, mouse and keyboard review, continuous-clean choice retention, and the absence of false button and ARIA semantics while expansion is mandatory.
+Component tests drive one keyed run and its phases through initial running controls, mouse and keyboard choices, ordinary running updates, outer hide and restore, phase completion, run completion, clean review, same-key renewed activity, a fully batched clean cycle, every abnormal status, first-abnormal escalation, later abnormal updates, zero-member completion, focused-member completion, sibling independence, and renderer remount. They also verify terminal navigation remains absent after the deferred focus path settles.
 
-The shipped Web replay observes the real workflow, worker, Session log, browser plugin graph, and child navigation. It requires the live workflow and active phase to be visible without disclosure controls, the normally settled workflow and phase to fold, manual review to retain the terminal member without navigation, and a reload to reconstruct the folded history from durable facts.
+The shipped Web replay exercises the real workflow, worker, Session log, browser plugin graph, and child navigation. It collapses and reopens live run and phase controls, records the live collapsed status summary and ARIA state, verifies normal settlement folds both levels, confirms terminal review cannot navigate the member, and records the folded history reconstructed after reload.
 
 ## Alternatives considered
 
-**Keep one manual state initialized from the first render.** Rejected because later lifecycle updates cannot reopen newly active or abnormal content and cannot fold normally settled work.
+**Force every running or abnormal level open as a static row.** Rejected because it makes the attention state impossible to dismiss and removes truthful mouse, keyboard, and ARIA disclosure semantics.
 
-**Derive `open` directly from whether a level is clean.** Rejected because completed history would remain permanently closed and could not be reopened for review.
+**Keep one manual state initialized from the first render.** Rejected because later activity, abnormal escalation, and normal completion cannot perform their one-time automatic actions.
 
-**Persist expansion, acknowledgement, or read state.** Rejected because current lifecycle facts already determine mandatory visibility, while review choice belongs only to the mounted presentation. Persistence would add a second state owner and require semantics for stale choices, abnormal acknowledgement, replay, and synchronization that the user result does not need.
+**Let each phase own state inside its disclosure content.** Rejected because hiding the outer run unmounts that content and discards independent phase choices during the same mounted workflow record.
+
+**Persist expansion, acknowledgement, or an activity epoch.** Rejected because current workflow facts and the append-only member count provide every required edge. Persistence adds a second durable owner and synchronization semantics that this presentation choice does not need.
 
 ## Consequences
 
-Workflow records expose current work and abnormal outcomes without preparatory clicks, then reclaim conversation space after normal completion without sacrificing review. Interaction semantics remain truthful during automatic control, and the same durable record produces the same initial state during live rendering, refresh, and history reconstruction.
+Workflow records call attention to lifecycle changes while remaining dismissible in every status. Normal completion reclaims space, current focus remains safe, nested phase choices survive outer hiding, and the same durable record reconstructs a deterministic initial state on refresh or history replay.
 
-The trade-off is deliberate local reset behavior. A phase choice disappears when its parent workflow closes or the component unmounts, and abnormal records cannot be manually hidden because the product has no acknowledgement state. Supporting either behavior later requires a separate ownership and persistence decision rather than extending this local lifecycle implicitly.
+The local lifecycle deliberately resets on renderer remount and cannot remember a choice across refresh, devices, or users. Adding that behavior requires a separate persistence and stale-choice decision rather than extending this presentation state implicitly.

+ 16 - 14
.agents/notes/implemented/feature/2026-08-11-workflow-run-status-driven-disclosure.zh.md

@@ -6,38 +6,40 @@ Status: implemented
 
 ## 问题
 
-持久工作流 Chat 节点会在同一位置从运行前缀更新为终态记录。只在挂载时初始化的 disclosure 选择可能隐藏新开始运行的阶段,让已完成工作继续占据对话空间,或者把失败、已取消或已中断成员埋在两层折叠内容之后。若只把开合状态作为完成状态的纯派生结果,虽然能避免这些问题,却也会阻止用户重新打开干净历史进行复盘。
+持久工作流 Chat 节点会在同一位置从运行前缀更新为终态记录。renderer 必须提示新工作、异常结果和正常完成,同时不能在普通更新中反复覆盖用户回收对话空间的选择。
 
-renderer 已经从工作流 Conversation Node 收到全部所需生命周期事实。因此,可见性需要一个组件本地生命周期:让当前执行与需注意状态优先,同时不增加另一项持久事实,也不取得工作流结果的所有权。
+renderer 已经从工作流 Conversation Node 收到全部持久生命周期事实。因此,disclosure 选择属于已挂载的展示层,但它的生命周期还必须在外层运行隐藏时保留嵌套 Phase 选择,并避免移除仍含键盘焦点的内容。
 
 ## 决策
 
-每个阶段从当前成员派生一项可见性要求。存在运行中、失败、已取消或已中断成员时,该阶段强制展开;全部成员均已完成时,该阶段处于干净状态。工作流自身状态需要注意或任一阶段强制展开时,工作流也强制展开,因此即使工作流结果记录为已完成,异常成员仍保持可见。已完成的兄弟阶段继续可以独立折叠。
+`WorkflowRunPanel` 持有一项运行 disclosure 本地状态,以及一张按现有 phase key 索引的 Phase 状态表。全部成员都已完成时,Phase 为干净状态;任一成员失败、已取消或已中断时为异常状态;其余情况为运行状态。运行自身或任一 Phase 异常时,运行处于异常状态;运行自身或任一 Phase 正在运行时,运行处于运行状态;只有运行与全部 Phase 都正常完成时才处于干净状态。挂载时,运行和异常层级默认展开,干净层级默认折叠。
 
-强制展开层级渲染为静态展开行。它不提供按钮 role、焦点目标、键盘切换或 `aria-expanded` 值,因为折叠操作无法改变结果。这样既保留视觉层级与状态摘要,也让交互承诺与实际可执行动作一致。
+每个层级记录当前模式、仅追加成员数、开合选择和待执行的干净折叠。Phase 从干净状态进入新活动时,该 Phase 与外层运行自动展开一次;连续运行或异常区间内的普通更新保留用户选择,首次进入异常状态时自动展开一次,进入干净状态时自动折叠一次。若新增成员及其正常完成在同一次渲染中送达,Phase 会保持干净但成员数改变;该变化会折叠已打开的 Phase 复盘,并在运行仍处于活动状态时自动展开外层运行一次,而无需增加 activity epoch 或持久字段。自动动作完成后,鼠标、Enter 和 Space 控制该层级,直到出现下一项约定边沿。
 
-干净层级会以关闭状态挂载普通受控 disclosure。它的本地选择在同一段连续干净状态的 rerender 中保持。新的运行中或异常数据会用强制展开替代该手动区间;下一次回到干净状态时会挂载新的关闭 disclosure,从而让每个活动周期只自动折叠一次。关闭工作流会自然卸载其阶段控件;Session remount 会从当前持久状态重建每个层级,而不恢复更早的选择。
+外层 disclosure 隐藏子内容时,Phase 状态仍留在 `WorkflowRunPanel`,因此关闭并重新打开运行会恢复各 Phase 选择。Phase 被移除时,其表项同时清理;renderer remount 会从当前持久事实重建每个层级,而不恢复更早选择。
 
-例如,运行中的工作流无需点击即可展示活跃阶段与成员。该阶段完成时,只有阶段折叠,工作流继续展开;工作流自身和全部阶段均完成时,工作流也会折叠。用户随后可以重新打开两个层级复盘。若同一阶段 key 下又开始新成员,受影响的两个层级会立即恢复强制展开,并且只在新活动完成后再次折叠。
+正常完成会在折叠前检查焦点是否位于内容内。仍含焦点的内容保持挂载并立即显示完成状态,焦点离开后再折叠。可导航成员的按钮持有焦点并变为终态时,`MemberRow` 会把同一个按钮以 `aria-disabled` 形式保留到 blur;之后的终态复盘渲染普通不可交互行。这样既保留当前 DOM 目标,也不允许终态导航,并且无需增加焦点管理器。
 
-renderer 只拥有这项可见性生命周期。它不增加 Session 事件、store、设置、确认状态、计时器、焦点迁移、自动滚动或跨 remount 持久化。它不改变工作流状态派生、阶段分组、成员顺序、导航准入、文案或共享 `DisclosureRow` API。pointer 光标由共享的 `data-expandable` 样式拥有,因此强制展开的静态行不会提示无法执行的操作。持久记录中的中断前缀仍属于需注意状态,因此在底层事实改变前始终可见。
+renderer 不增加 Session 事件、store、设置、确认状态、计时器、自动滚动、持久活动身份或 `DisclosureRow` API。它不改变工作流状态派生、Phase 分组、成员顺序、导航准入、文案或视觉 token。
 
 ## 验证
 
-组件测试驱动同一个 keyed 工作流与阶段依次经过运行、干净完成、手动复盘、新活动、再次干净完成、零成员完成以及每种异常状态。测试还验证异常成员向上展开、干净兄弟阶段独立、鼠标和键盘复盘、连续干净状态中的选择保持,以及强制展开时不存在虚假按钮和 ARIA 语义。
+组件测试驱动同一个 keyed 运行及其 Phase,覆盖初始运行控件、鼠标和键盘选择、普通运行更新、外层隐藏与恢复、Phase 完成、运行完成、干净复盘、同 key 新活动、同次渲染送达的完整干净周期、每种异常状态、首次异常升级、后续异常更新、零成员完成、成员持焦点时完成、兄弟 Phase 独立以及 renderer remount。测试还确认延后焦点路径结算后,终态导航仍不存在。
 
-shipped Web 回放观察真实工作流、worker、Session 日志、浏览器插件图和子级导航。它要求实时工作流与活跃阶段无需 disclosure 控件即可见,正常结算的工作流与阶段会折叠,手动复盘仍能看到不再可导航的终态成员,并且刷新会从持久事实重建折叠历史。
+shipped Web 回放经过真实工作流、worker、Session 日志、浏览器插件图和子级导航。它折叠并重新打开实时运行与 Phase 控件,记录实时折叠标题的状态摘要和 ARIA 状态,验证正常结算会折叠两个层级,确认终态复盘不能导航成员,并记录刷新后从历史重建的折叠记录。
 
 ## 曾考虑的替代方案
 
-**保留一项从首次渲染初始化的手动状态。** 拒绝,因为后续生命周期更新无法重新打开新活动或异常内容,也无法折叠正常结算的工作。
+**把每个运行中或异常层级强制展开为静态行。** 拒绝,因为需注意状态将无法收起,也不会提供真实的鼠标、键盘和 ARIA disclosure 语义。
 
-**只根据层级是否干净来派生 `open`。** 拒绝,因为已完成历史会永久保持关闭,无法重新打开复盘。
+**保留一项从首次渲染初始化的手动状态。** 拒绝,因为后续活动、异常升级和正常完成无法执行各自的一次性自动动作。
 
-**持久化展开、确认或已读状态。** 拒绝,因为当前生命周期事实已经决定强制可见性,而复盘选择只属于已挂载的展示层。持久化会增加第二个状态归属方,并要求定义陈旧选择、异常确认、回放和同步语义,而用户结果不需要这些机制。
+**让每个 Phase 在自身 disclosure 内容中持有状态。** 拒绝,因为隐藏外层运行会卸载这些内容,并在同一条已挂载工作流记录中丢失独立的 Phase 选择。
+
+**持久化展开、确认或 activity epoch。** 拒绝,因为当前工作流事实与仅追加成员数已经提供全部所需边沿。持久化会增加第二个持久归属方以及本展示选择不需要的同步语义。
 
 ## 后果
 
-工作流记录无需预备点击即可展示当前工作与异常结果,并在正常完成后回收对话空间,同时不牺牲复盘能力。自动控制期间的交互语义保持真实,同一份持久记录在实时渲染、刷新和历史重建时得到相同初始状态。
+工作流记录会提示生命周期变化,同时在所有状态下都允许用户收起。正常完成会回收空间,当前焦点保持安全,嵌套 Phase 选择在外层隐藏期间保留;同一份持久记录在刷新或历史回放时会重建确定性的初始状态。
 
-代价是有意保留的本地重置行为。父工作流关闭或组件卸载时,阶段选择会消失;由于产品没有确认状态,异常记录不能手动隐藏。以后若要支持任一行为,需要单独决定所有权与持久化,而不能隐式扩展这项本地生命周期。
+这项本地生命周期会在 renderer remount 时重置,无法跨刷新、设备或用户记住选择。若要增加该行为,需要单独决定持久化与陈旧选择语义,而不能隐式扩展这项展示状态。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.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-12-product-subagent-one-shot-background-tasks.md
-2026-08-12-product-subagent-one-shot-background-tasks.md: b8865cf94852396c32dd6da996bc9f5c2c7d806b
-2026-08-12-product-subagent-one-shot-background-tasks.zh.md: bbc18ebb6cf0de1b04f0c2a10ddf51cea282c24e
+2026-08-12-product-subagent-one-shot-background-tasks.md: 248bb943f8ee46a7050c373b6b7c3f7dec65d566
+2026-08-12-product-subagent-one-shot-background-tasks.zh.md: d6867a97561e7efbe2b152b6c45991553393b4e7

+ 6 - 6
.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.md

@@ -14,9 +14,9 @@ Exposing background execution must not add a product session, product-specific j
 
 Production `dsh` does not install the optional product providers. A Profile that opts in installs and mounts `dsh-subagent-codex`, `dsh-subagent-claude-code`, or both once on the host plane. The `standard`, `code`, and `cordis` Agent Presets configure the corresponding dormant tool rows with `backgroundMode: one-shot`; removing a row's `disabled` field exposes the existing optional `run_in_background` argument to agents composed from that preset. Omission or `false` waits in the foreground; explicit `true` returns a parent-owned Job id after synchronous Job preflight and registration, without waiting for provider startup or completion.
 
-The [generic one-shot background adapter](2026-07-08-background-subagent-tasks.md) owns background registration and settlement. It starts the same [`SubagentRun`](2026-06-21-subagent-capability-seam.md), uses a Job-owned cancellation signal across provider startup and execution, waits for `run.result` and `run.dispose()`, maps the terminal result into the Job, and lets `job_output`, `job_list`, `job_kill`, and the existing completion notice expose that state. The [product provider decision](2026-08-04-claude-code-and-codex-subagent-backends.md) continues to own native protocols, answer selection, local cancellation, and process-tree quiescence.
+The [generic one-shot background adapter](2026-07-08-background-subagent-tasks.md) owns background registration and settlement. It starts the same [`SubagentRun`](2026-06-21-subagent-capability-seam.md), uses a Job-owned cancellation signal across provider startup and execution, waits for `run.result` and `run.dispose()`, maps the terminal result and optional safe diagnostic into the Job, and lets `job_output`, `job_list`, `job_kill`, and the existing completion notice expose that state. The [product provider decision](2026-08-04-claude-code-and-codex-subagent-backends.md) continues to own native protocols, answer selection, local cancellation, and process-tree quiescence; the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) owns each product Provider's Profile configuration and diagnostic production.
 
-No provider configuration, service interface, event, wire field, persistence format, or product identifier is added. Foreground and background differ only in which existing consumer waits for the same one-shot run.
+This scheduling decision adds no provider configuration, service interface, event, wire field, persistence format, or product identifier. A Provider may define its own Profile configuration independently; foreground and background still differ only in which existing consumer waits for the same one-shot run.
 
 ### Ownership and lifecycle
 
@@ -37,7 +37,7 @@ product tool call
 | Product selection and exposure | Agent Preset | Bind one fixed tool name to one fixed provider | Enabling one row exposes only that product tool |
 | Foreground or background choice | `dsh-tool-subagent` | Resolve `run_in_background` under `one-shot` policy | Omission is foreground; explicit `true` returns a Job id |
 | Job id, state, output, cancellation, and notice | `ctx.jobs` and `dsh-tool-jobs` | Register and present the existing one-shot run | Generic job tools collect or stop the run for the exact parent |
-| Native answer and process quiescence | Product provider and `dsh-subprocess` | Produce one final result and release one process tree | Job settlement and foreground return both wait for disposal |
+| Native result, optional diagnostic, and process quiescence | Product provider and `dsh-subprocess` | Produce one final result and release one process tree | Job settlement and foreground return consume the same result and both wait for disposal |
 
 ## Published composition
 
@@ -49,7 +49,7 @@ The ACP product compositions use the same fixed product rows and generic job con
 
 ## Verification
 
-The Web composition test explicitly mounts both optional providers from the repository examples dependency anchor, then boots four user-preset variants—neither product, Codex, Claude Code, and both—and checks that each enabled product tool exposes `run_in_background` alongside `job_output`, `job_list`, and `job_kill`. The two package-owned Loader compositions run with an empty `PATH`, inspect the same schemas and controls, and prove that explicit provider loading starts no product process. ACP keyless snapshots pin the assembled explicit product schemas, while the existing `dsh-tool-subagent` and job suites pin foreground defaulting, Job registration, final-output collection, cancellation, completion notices, owner disposal, and provider disposal.
+The Web composition test explicitly mounts both optional providers from the repository examples dependency anchor, then boots four user-preset variants—neither product, Codex, Claude Code, and both—and checks that each enabled product tool exposes `run_in_background` alongside `job_output`, `job_list`, and `job_kill`. The two package-owned Loader compositions run with an empty `PATH`, inspect the same schemas and controls, and prove that explicit provider loading starts no product process. ACP keyless snapshots pin the assembled explicit product schemas, while the existing `dsh-tool-subagent` and job suites pin foreground defaulting, Job registration, final-output collection, shared diagnostic presentation, cancellation, completion notices, owner disposal, and provider disposal. The two real product-provider suites independently prove that their native permission failures enter that same shared result before either scheduling path consumes it.
 
 ## Alternatives considered
 
@@ -65,6 +65,6 @@ The Web composition test explicitly mounts both optional providers from the repo
 
 ## Consequences
 
-Agents can continue useful work while Codex or Claude Code handles an independent one-shot task, then collect the final answer or cancel it through the same Job controls used by other background producers. Foreground callers retain their existing result and error behavior.
+Agents can continue useful work while Codex or Claude Code handles an independent one-shot task, then collect the final answer or cancel it through the same Job controls used by other background producers. Foreground and one-shot background consumers present the same safe Provider diagnostic when a failed result supplies one.
 
-Every product delegation still starts a fresh native process or query, produces final text as its only product payload, and ends with provider disposal and whole-tree exit. A background call additionally exposes the generic Job id, status, completion notice, and collection or cancellation results. Background Jobs are process-local and parent-owned: they do not survive parent disposal, do not expose intermediate product activity, and do not make a product conversation resumable. Production installs do not pay for either product integration unless a Profile explicitly installs it; any composition that exposes the background argument must also keep the generic Job provider and controls available.
+Every product delegation still starts a fresh native process or query, produces final assistant text as its only assistant payload, and ends with provider disposal and whole-tree exit. A failed result may separately carry a safe diagnostic. A background call additionally exposes the generic Job id, status, completion notice, and collection or cancellation results. Background Jobs are process-local and parent-owned: they do not survive parent disposal, do not expose intermediate product activity, and do not make a product conversation resumable. Production installs do not pay for either product integration unless a Profile explicitly installs it; any composition that exposes the background argument must also keep the generic Job provider and controls available.

+ 6 - 6
.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.zh.md

@@ -14,9 +14,9 @@ Codex 与 Claude Code 提供方已经能够运行一项自包含任务并返回
 
 生产 `dsh` 不安装可选产品提供方。选择启用产品集成的 Profile 会安装 `dsh-subagent-codex`、`dsh-subagent-claude-code` 或两者,并在 host plane(宿主平面)各挂载一次。`standard`、`code` 与 `cordis` Agent Preset 使用 `backgroundMode: one-shot` 配置相应的休眠工具行;删除某一行的 `disabled` 字段后,现有可选参数 `run_in_background` 会向由该 preset 组装的 agent 公开。省略该参数或传入 `false` 时会在前台等待;显式传入 `true` 时会在同步完成 Job 预检与登记后返回由父级拥有的 Job id,而不会等待提供方启动或完成。
 
-[通用 one-shot 后台适配器](2026-07-08-background-subagent-tasks.md)负责后台登记与结算。它会启动同一个 [`SubagentRun`](2026-06-21-subagent-capability-seam.md),让 Job 自有的取消信号覆盖提供方启动与执行,等待 `run.result` 和 `run.dispose()`,把终态结果映射进 Job,并由 `job_output`、`job_list`、`job_kill` 与现有完成通知公开该状态。[产品提供方决策](2026-08-04-claude-code-and-codex-subagent-backends.md)继续负责原生协议、答案选择、本地取消与进程树完全停稳。
+[通用 one-shot 后台适配器](2026-07-08-background-subagent-tasks.md)负责后台登记与结算。它会启动同一个 [`SubagentRun`](2026-06-21-subagent-capability-seam.md),让 Job 自有的取消信号覆盖提供方启动与执行,等待 `run.result` 和 `run.dispose()`,把终态结果与可选安全诊断映射进 Job,并由 `job_output`、`job_list`、`job_kill` 与现有完成通知公开该状态。[产品提供方决策](2026-08-04-claude-code-and-codex-subagent-backends.md)继续负责原生协议、答案选择、本地取消与进程树完全停稳;[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.md)负责各产品提供方的 Profile 配置与诊断生产。
 
-本决策不新增提供方配置、服务接口、事件、协议字段、持久化格式或产品标识符。前台与后台的区别仅在于由哪个现有消费方等待同一个 one-shot 运行。
+本调度决策不新增提供方配置、服务接口、事件、协议字段、持久化格式或产品标识符。提供方可以独立定义自己的 Profile 配置;前台与后台的区别仍然只在于由哪个现有消费方等待同一个 one-shot 运行。
 
 ### 归属与生命周期
 
@@ -37,7 +37,7 @@ product tool call
 | 产品选择与公开 | Agent Preset | 把一个固定工具名绑定到一个固定提供方 | 启用一行只会公开对应产品工具 |
 | 前台或后台选择 | `dsh-tool-subagent` | 按 `one-shot` 策略解析 `run_in_background` | 省略参数时在前台运行;显式传入 `true` 时返回 Job id |
 | Job id、状态、输出、取消与通知 | `ctx.jobs` 与 `dsh-tool-jobs` | 登记并展示现有 one-shot 运行 | 通用作业工具为准确父级收集或停止运行 |
-| 原生答案与进程完全停稳 | 产品提供方与 `dsh-subprocess` | 产生一个最终结果并释放一棵进程树 | Job 结算与前台返回都会等待资源释放 |
+| 原生结果、可选诊断与进程完全停稳 | 产品提供方与 `dsh-subprocess` | 产生一个最终结果并释放一棵进程树 | Job 结算与前台返回消费同一结果,且都会等待资源释放 |
 
 ## 发布组装
 
@@ -49,7 +49,7 @@ ACP 产品组装使用相同的固定产品行与通用作业控制工具。其
 
 ## 验证
 
-Web 组装测试会从仓库 examples 依赖锚点显式挂载两个可选提供方,再启动四种用户 preset 变体——不启用产品、只启用 Codex、只启用 Claude Code,以及同时启用两者——并检查每个已启用产品工具都会与 `job_output`、`job_list` 和 `job_kill` 一起公开 `run_in_background`。两个由包负责的 Loader 组装会在空 `PATH` 下运行,检查相同 schema 与控制工具,并证明显式加载提供方不会启动产品进程。ACP 无密钥快照会固定显式组装后的产品 schema,而现有 `dsh-tool-subagent` 与作业测试套件会固定前台默认值、Job 登记、最终输出收集、取消、完成通知、owner 资源释放与提供方资源释放。
+Web 组装测试会从仓库 examples 依赖锚点显式挂载两个可选提供方,再启动四种用户 preset 变体——不启用产品、只启用 Codex、只启用 Claude Code,以及同时启用两者——并检查每个已启用产品工具都会与 `job_output`、`job_list` 和 `job_kill` 一起公开 `run_in_background`。两个由包负责的 Loader 组装会在空 `PATH` 下运行,检查相同 schema 与控制工具,并证明显式加载提供方不会启动产品进程。ACP 无密钥快照会固定显式组装后的产品 schema,而现有 `dsh-tool-subagent` 与作业测试套件会固定前台默认值、Job 登记、最终输出收集、共享诊断呈现、取消、完成通知、owner 资源释放与提供方资源释放。两个真实产品提供方测试套件还会分别证明各自的原生权限失败先进入同一个共享结果,再由任一调度路径消费。
 
 ## 曾考虑的替代方案
 
@@ -65,6 +65,6 @@ Web 组装测试会从仓库 examples 依赖锚点显式挂载两个可选提供
 
 ## 后果
 
-agent 可以在 Codex 或 Claude Code 处理独立 one-shot 任务时继续推进其他工作,随后通过其他后台 producer 共用的 Job 控制工具收集最终回答或取消运行。前台调用方继续获得既有结果与错误行为。
+agent 可以在 Codex 或 Claude Code 处理独立 one-shot 任务时继续推进其他工作,随后通过其他后台 producer 共用的 Job 控制工具收集最终回答或取消运行。若失败结果提供了安全的提供方诊断,前台与一次性后台消费方会呈现同一内容。
 
-每次产品委托仍会启动一个全新的原生进程或 query,把最终文本作为唯一产品载荷,并以提供方资源释放和整棵进程树退出结束。后台调用还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。后台 Job 仅存在于当前进程且由父级拥有:它不会在父级资源释放后继续存活,不会公开产品中间活动,也不会让产品对话变得可恢复。只有 Profile 显式安装产品集成时,生产安装才承担对应成本;公开后台参数的任何组装还必须让通用 Job 提供方与控制工具保持可用。
+每次产品委托仍会启动一个全新的原生进程或 query,把最终 assistant 文本作为唯一 assistant 载荷,并以提供方资源释放和整棵进程树退出结束。失败结果可以另行携带安全诊断。后台调用还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。后台 Job 仅存在于当前进程且由父级拥有:它不会在父级资源释放后继续存活,不会公开产品中间活动,也不会让产品对话变得可恢复。只有 Profile 显式安装产品集成时,生产安装才承担对应成本;公开后台参数的任何组装还必须让通用 Job 提供方与控制工具保持可用。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-12-web-image-intake-and-limits-alignment.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-12-web-image-intake-and-limits-alignment.md
-2026-08-12-web-image-intake-and-limits-alignment.md: 2f8b99bb4850d9875dcba0a03ae8ad9f340d1506
-2026-08-12-web-image-intake-and-limits-alignment.zh.md: 62d5ebd54275ae8de0e0b9ba701ba34042dfcde7
+2026-08-12-web-image-intake-and-limits-alignment.md: 00cf7ea99d63e848c4b5839da1d97d94c9fb8464
+2026-08-12-web-image-intake-and-limits-alignment.zh.md: d88ba25a3f3e3be4a3655080a45123ada6cdebcd

+ 1 - 1
.agents/notes/implemented/feature/2026-08-12-web-image-intake-and-limits-alignment.md

@@ -16,7 +16,7 @@ The second alignment step for issue #2248, after the [attachment display note](2
 
 **History thumbnails (DeepSeek Chat rules).** A message's lone image renders at 240px on its long edge with the displayed ratio clamped to [0.25, 4], cropped by `cover` with the anchor at the top of very tall images and the left of very wide ones, never upscaled; several images render as fixed 64px square tiles in one wrapping row (10px gap, user messages right-aligned). Consecutive assistant `image` blocks merge into one gallery so they tile instead of each opening a one-image row.
 
-**Limits aligned and projected.** Defaults are 20 images / 5 MiB per image / 100 MiB aggregate (`attachment-local`), with the HTTP carrier cap raised to one shared `DEFAULT_MAX_REQUEST_BODY_BYTES = 160 MiB` (http-bridge, previously two independent 32 MiB literals) to satisfy the load-time capacity assertion (aggregate × 4/3 + headroom ≈ 134.3 MiB). Consumer products cluster at 10–20 attachments (ChatGPT 10, Gemini 10, Claude 20; DeepSeek Chat's 50 is the outlier), and a vision-model image costs roughly 1300–4800 tokens, so 50 images can fill a 200k context in one message. The 5 MiB per-image default admits images across Anthropic routes that impose either a 5 MiB or 10 MiB maximum; deployments using only routes with the larger limit can override it. A 512 MiB aggregate cannot pass this transport because base64-in-JSON would need a single JSON string past V8's ~512 MiB string ceiling. The limits reach clients as the `imageLimits` session projection — a constant-per-boot unit (`apply` returns the same state reference, so baselines alone carry it and no change frames exist) registered by **apiproxy**, not the attachment Service Definition: `dsh-llm` depends on `dsh-attachment` (`ImageBlock` → `ImageAttachmentRef`), so the seam package referencing `dsh-session-projection` (whose graph reaches `dsh-llm` through `dsh-session`) closes a project-reference cycle, and the per-message count/aggregate rules the value describes are the proxy's own admission checks anyway. The `SessionProjectionMap` merge rides the proxy's sessions wire-contract file, which every client program already includes through the carrier's type re-exports.
+**Limits aligned and projected.** Defaults are 20 images / 3.5 MiB per image / 100 MiB aggregate (`attachment-local`), with the HTTP carrier cap raised to one shared `DEFAULT_MAX_REQUEST_BODY_BYTES = 160 MiB` (http-bridge, previously two independent 32 MiB literals) to satisfy the load-time capacity assertion (aggregate × 4/3 + headroom ≈ 134.3 MiB). Consumer products cluster at 10–20 attachments (ChatGPT 10, Gemini 10, Claude 20; DeepSeek Chat's 50 is the outlier), and a vision-model image costs roughly 1300–4800 tokens, so 50 images can fill a 200k context in one message. Including base64 padding, a 3.5 MiB encoded file occupies at most 4.67 MiB and leaves 0.33 MiB below a 5 MiB route check. Deployments using only routes with larger limits can override it. A 512 MiB aggregate cannot pass this transport because base64-in-JSON would need a single JSON string past V8's ~512 MiB string ceiling. The limits reach clients as the `imageLimits` session projection — a constant-per-boot unit (`apply` returns the same state reference, so baselines alone carry it and no change frames exist) registered by **apiproxy**, not the attachment Service Definition: `dsh-llm` depends on `dsh-attachment` (`ImageBlock` → `ImageAttachmentRef`), so the seam package referencing `dsh-session-projection` (whose graph reaches `dsh-llm` through `dsh-session`) closes a project-reference cycle, and the per-message count/aggregate rules the value describes are the proxy's own admission checks anyway. The `SessionProjectionMap` merge rides the proxy's sessions wire-contract file, which every client program already includes through the carrier's type re-exports.
 
 **Intake pre-check and error copy.** Both intake gestures converge on one `intakeImages` wrapper in InputBar that checks count, per-image bytes, and aggregate bytes against the projection before `addImages`: a violating batch is refused whole (DeepSeek Chat semantics) with an immediate banner naming the limit — no submit-time rollback theater. The host checks stay as the backstop for callers that bypass the composer. Banner copy follows one principle the user set: reasons a user can act on (model without vision, count, size, resolution, format — now a positive list of supported formats instead of echoing the rejected MIME type) get product sentences naming the way out; reasons they cannot act on (corrupt base64, lost references, read failures) fold into one send-failed sentence that keeps the reason code, because the product currently faces developers and a reportable code beats a dead end. Non-attachment error codes keep the raw message + code presentation.
 

+ 1 - 1
.agents/notes/implemented/feature/2026-08-12-web-image-intake-and-limits-alignment.zh.md

@@ -16,7 +16,7 @@ issue #2248 的第二步对齐,接在[附件展示 note](2026-08-11-web-attach
 
 **历史缩略图(DeepSeek Chat 规则)。** 一条消息仅有的一张图长边 240px、展示比例钳制在 [0.25, 4],`cover` 裁切,特别高的图锚定顶部、特别宽的锚定左侧,从不放大;多张图渲染为固定 64px 方块,单个可换行的横排(10px 间距,用户消息右对齐)。assistant 连续的 `image` 块合并进同一个画廊,平铺而不是各占一行。
 
-**上限对齐并投影。** 默认值为每条消息 20 张、单图 5 MiB、总量 100 MiB(`attachment-local`),HTTP 载体上限提为唯一共享的 `DEFAULT_MAX_REQUEST_BODY_BYTES = 160 MiB`(http-bridge,原先是两个独立的 32 MiB 字面量),以满足加载时的容量断言(总量 × 4/3 加余量 ≈ 134.3 MiB)。消费级产品集中在 10 到 20 个附件(ChatGPT 10、Gemini 10、Claude 20;DeepSeek Chat 的 50 是例外),且视觉模型一张图约 1300 到 4800 token,因此 50 张图可在一条消息中填满 200k 上下文。默认单图上限采用 5 MiB,可适用于分别采用 5 MiB 或 10 MiB 上限的 Anthropic 路由;仅使用较大上限路由的部署可以覆盖该值。512 MiB 总量无法通过当前传输,因为 base64 进 JSON 需要一个超过 V8 约 512 MiB 字符串上限的单个 JSON 字符串。限额以 `imageLimits` 会话投影到达客户端。它是每次启动恒定的单元(`apply` 返回同一状态引用,因此只靠基线携带、不存在变更帧),由 **apiproxy** 而非 attachment Service Definition 注册:`dsh-llm` 依赖 `dsh-attachment`(`ImageBlock` → `ImageAttachmentRef`),seam 包引用 `dsh-session-projection`(其图谱经 `dsh-session` 到达 `dsh-llm`)会闭合 project-reference 环,而该值描述的每消息数量与总量规则本来就是 proxy 自己的准入检查。`SessionProjectionMap` 合并放在 proxy 的 sessions 协议文件里,每个客户端程序都经载体的类型再导出包含它。
+**上限对齐并投影。** 默认值为每条消息 20 张、单图 3.5 MiB、总量 100 MiB(`attachment-local`),HTTP 载体上限提为唯一共享的 `DEFAULT_MAX_REQUEST_BODY_BYTES = 160 MiB`(http-bridge,原先是两个独立的 32 MiB 字面量),以满足加载时的容量断言(总量 × 4/3 加余量 ≈ 134.3 MiB)。消费级产品集中在 10 到 20 个附件(ChatGPT 10、Gemini 10、Claude 20;DeepSeek Chat 的 50 是例外),且视觉模型一张图约 1300 到 4800 token,因此 50 张图可在一条消息中填满 200k 上下文。3.5 MiB 编码文件包括 base64 填充在内最多占 4.67 MiB,在 5 MiB 路由检查下保留 0.33 MiB 余量。仅使用较大上限路由的部署可以覆盖该值。512 MiB 总量无法通过当前传输,因为 base64 进 JSON 需要一个超过 V8 约 512 MiB 字符串上限的单个 JSON 字符串。限额以 `imageLimits` 会话投影到达客户端。它是每次启动恒定的单元(`apply` 返回同一状态引用,因此只靠基线携带、不存在变更帧),由 **apiproxy** 而非 attachment Service Definition 注册:`dsh-llm` 依赖 `dsh-attachment`(`ImageBlock` → `ImageAttachmentRef`),seam 包引用 `dsh-session-projection`(其图谱经 `dsh-session` 到达 `dsh-llm`)会闭合 project-reference 环,而该值描述的每消息数量与总量规则本来就是 proxy 自己的准入检查。`SessionProjectionMap` 合并放在 proxy 的 sessions 协议文件里,每个客户端程序都经载体的类型再导出包含它。
 
 **加入预检与错误文案。** 两种加入手势汇合到 InputBar 的一个 `intakeImages` 包装:在 `addImages` 之前按投影检查数量、单图字节与总字节,违规的一批整体拒收(DeepSeek Chat 语义)并立刻弹出点名上限的横幅——不再有提交时的回滚戏码。宿主检查保留,兜底绕过 composer 的调用方。横幅文案遵循用户定下的一条原则:用户能解决的原因(模型不支持视觉、数量、大小、分辨率、格式——格式改为正面列出支持列表而不是回显被拒的 MIME 类型)用点明出路的产品句子;用户无法解决的原因(base64 损坏、引用丢失、读取失败)折叠为一条保留原因码的发送失败句子,因为产品当前面向开发者,可上报的码好过死胡同。非附件错误码保留原文加错误码的展示。
 

+ 6 - 0
.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.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/feature/2026-08-15-product-subagent-noninteractive-permissions.md
+2026-08-15-product-subagent-noninteractive-permissions.md: df1f0d9939e951f16070729615a3779f1f7c2ddc
+2026-08-15-product-subagent-noninteractive-permissions.zh.md: 982b4409e08a506dec828db15c8c4aa5fcc36883

+ 88 - 0
.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md

@@ -0,0 +1,88 @@
+# Agent Note: Product subagents use Profile-selected non-interactive permissions
+
+Status: implemented
+
+English | [中文](2026-08-15-product-subagent-noninteractive-permissions.zh.md)
+
+## Problem
+
+The [Claude Code and Codex product providers](2026-08-04-claude-code-and-codex-subagent-backends.md) run without a human interface. Native permission prompts, user dialogs, or MCP elicitation therefore cannot wait for a person, but relying on either product's ambient default can still select an interactive mode. A deployment also needs to choose broader native modes without giving the parent model or one tool call a way to raise its own authority.
+
+A failed product run previously reached the [subagent seam](2026-06-21-subagent-capability-seam.md) only as a stop reason. Logs could retain the product error, but the foreground parent and a [one-shot background Job](2026-08-12-product-subagent-one-shot-background-tasks.md) could not distinguish a permission refusal from another failure. Reusing assistant output for that fact would misattribute infrastructure detail to the child model.
+
+## Decision
+
+Each product Provider owns its own Profile-level `permissionMode` value. The two Config fields deliberately use the products' native names rather than a shared restricted/automatic/full abstraction. The Provider fixes the resolved value for every run from that plugin instance. The subagent tool schema and `SubagentStartRequest` contain no permission field, so a model or individual delegation cannot change it.
+
+### Claude Code
+
+Claude Code defaults to `dontAsk` and accepts only the native non-interactive modes supported by the pinned Agent SDK:
+
+| Value | Native behavior |
+| --- | --- |
+| `dontAsk` | Deny operations that are not already authorized instead of prompting. |
+| `acceptEdits` | Accept edits; deny any remaining permission prompt through the unattended callback. |
+| `auto` | Let Claude Code's native classifier allow or deny permission requests. |
+| `plan` | Use planning mode, deny execution approval, and return the completed plan as the final answer. |
+| `bypassPermissions` | Set the SDK's explicit dangerous confirmation and bypass permission checks. |
+
+The Provider continues to omit `settingSources`: Claude Code remains the owner of user, project, and local settings, authentication, tools, and sandbox behavior outside the selected mode.
+
+Every query disables `AskUserQuestion`. Non-bypass permission callbacks deny instead of returning the SDK's indefinitely blocking `null`; plan mode also places `ExitPlanMode` in `disallowedTools`, so native allow rules cannot switch the unattended query back to execution. MCP elicitation is declined; the supported refusal dialog is cancelled; undeclared dialog kinds use the SDK's no-dialog failure behavior. A native `permission_denied` message records the same operation-local fact. These paths do not create an approval session, queue, cache, or retry loop.
+
+### Codex
+
+Codex defaults to `never` and accepts the three native non-interactive modes exposed by Codex 0.147.0. The Provider starts the fixed app-server command, then maps the selected mode into official `thread/start` fields because CLI-global permission flags do not configure threads created later by an app-server client:
+
+| Value | `thread/start` fields | Native behavior |
+| --- | --- | --- |
+| `never` | `approvalPolicy: never`; sandbox omitted | Never prompt; execution failures return to the model under the native sandbox. |
+| `approve-for-me` | `approvalPolicy: on-request`, `approvalsReviewer: auto_review`, `sandbox: workspace-write` | Route permission requests through Codex automatic review. |
+| `dangerously-bypass-approvals-and-sandbox` | `approvalPolicy: never`, `sandbox: danger-full-access` | Skip approval and sandbox enforcement. |
+
+The Provider overrides only those thread fields. `CODEX_HOME`, project configuration, model/provider selection, MCP, hooks, skills, authentication, and sandbox facts not selected by the mode remain native Codex state. The wire still denies any unexpected approval, permission, user-input, or MCP request rather than opening a dynamic allow path.
+
+### Failure diagnostic
+
+`SubagentResult` carries an optional `diagnostic` for provider-authored, non-assistant failure detail. A Provider removes tool inputs, file contents, environment values, credentials, and raw protocol payloads before producing it. The shared out-of-process result boundary limits the complete text to 4096 UTF-8 bytes and marks truncation without splitting a character.
+
+Each product records only the effective mode, request category, unattended decision, and a fixed safe reason. Claude Code derives those facts from SDK callbacks and `permission_denied` messages. Codex derives them from app-server requests, declined items, `sandboxError`, and two fixed permission signatures in a bounded stderr tail; raw stderr is still forwarded to the Host but never copied into the diagnostic. A successful result returns only the strict final answer; local cancellation remains `aborted` without permission detail; an unpublished startup failure still rejects `start()`. When a permission fact contributes to a published run that settles as `error`, the Provider attaches the diagnostic without adding it to assistant output, structured output, or `subagent/end.lastAssistantMessage`.
+
+The foreground consumer presents the stop-reason headline, then the optional diagnostic, then any partial assistant output. The one-shot background adapter stores the same diagnostic beside the stop reason in the failed Job detail. Providers that omit the field retain their previous behavior.
+
+### Ownership and lifecycle
+
+| Fact or resource | Owner | Observable behavior |
+| --- | --- | --- |
+| Profile permission choice | Each product Provider Config | Invalid, interactive, or unknown values fail during configuration. |
+| Permission and sandbox semantics | Claude Code Agent SDK or Codex app-server | Each Provider passes one native mode and does not mirror product policy. |
+| Interaction decisions and safe diagnostic | One product run | Concurrent runs keep independent mode, protocol, and diagnostic state. |
+| Diagnostic type and byte limit | `dsh-subagent` | Consumers receive a bounded optional field separate from assistant output. |
+| Foreground and Job presentation | `dsh-tool-subagent` and the generic Job runtime | Scheduling choice does not change the underlying failure fact. |
+| Process cancellation and quiescence | Product Provider and `dsh-subprocess` | Result settlement still precedes idempotent whole-tree disposal. |
+
+## Verification
+
+Package tests pin every allowed and rejected Config value, the exact SDK and app-server field mappings, dangerous confirmations, unattended terminal responses, diagnostic sanitization and UTF-8 bound, successful-result omission, concurrent-run isolation, foreground ordering, Job detail, stderr observer disposal, and process cleanup. The real Claude Agent SDK/CLI fixture proves its safe default, restricted denial, explicit bypass, and whole-tree quiescence. The real Codex app-server fixture proves that thread-level `never` overrides ambient `on-request`, automatic review starts, dangerous bypass writes only inside suite-owned temporary storage, fixed stderr signatures produce safe diagnostics, and the wrapper/native tree exits. Loader composition proves non-default modes can be published without starting either product, and keyless ACP snapshots record the shared diagnostic presentation while the model-facing product tool schemas contain no permission parameter.
+
+## Alternatives considered
+
+**Use the product's ambient permission default.** A native setting may select an interactive mode and make unattended behavior deployment-dependent. The Provider must choose a non-interactive mode explicitly for every query.
+
+**Put permission mode in the model-facing tool or each start request.** That would let task content select authority and would duplicate a Profile deployment decision on every call.
+
+**Copy product settings or map the parent Harness sandbox.** The products do not share one permission vocabulary. Mirroring their state would create a second authority and obscure the native sandbox consequences of automatic and bypass modes.
+
+**Forward prompts to a parent, Web client, or CLI.** The one-shot product run has no owned human-interaction lifecycle. Adding one would require durable request identity, routing, cancellation, and timeout semantics beyond this decision.
+
+**Return raw product errors, stderr, or tool inputs.** Those values can contain commands, paths, workspace data, environment values, or credentials. A fixed safe diagnostic keeps the failure actionable without exposing the product transcript.
+
+**Store a separate Job diagnostic.** The Job is only a scheduling adapter for the same `SubagentRun`; a second field would let foreground and background failure meanings drift.
+
+## Consequences
+
+Profiles can select each product's native restricted, automatic, planning/edit-accepting where supported, or bypass behavior before the Provider starts, while both safe defaults never ask a person. Broader modes remain explicit deployment choices and retain their native sandbox consequences.
+
+Permission failures become visible to both foreground parents and one-shot background Jobs without turning infrastructure text into an assistant answer. That diagnostic can enter model context, Job notices, API projections, and Job UI through the ordinary consumer paths, so the Provider must sanitize and bound it before result settlement.
+
+The change adds no product session persistence, human approval channel, dynamic permission operation, progress stream, retry policy, or rollback. Other Providers remain valid without producing a diagnostic or exposing a permission-mode Config.

+ 88 - 0
.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md

@@ -0,0 +1,88 @@
+# Agent Note: 产品 subagent 使用 Profile 选择的非交互权限
+
+Status: implemented
+
+[English](2026-08-15-product-subagent-noninteractive-permissions.md) | 中文
+
+## Problem
+
+[Claude Code 与 Codex 产品提供方](2026-08-04-claude-code-and-codex-subagent-backends.md)都在没有人工界面的情况下运行。因此,原生权限提示、用户对话或 MCP elicitation 不能等待人员响应,但依赖任一产品环境中的默认值仍可能选择交互模式。部署也需要选择更宽松的原生模式,同时不能让父模型或单次工具调用提升自身权限。
+
+失败的产品运行此前只能把终止原因送入 [subagent seam](2026-06-21-subagent-capability-seam.md)。日志可以保留产品错误,但前台父 agent 与[一次性后台 Job](2026-08-12-product-subagent-one-shot-background-tasks.md)无法区分权限拒绝和其他失败。若复用 assistant 输出承载该事实,则会把基础设施说明错误归因给子模型。
+
+## Decision
+
+每个产品提供方分别拥有自己的 Profile 级 `permissionMode` 值。两个 Config 字段有意使用各产品的原生名称,而不是共享的受限/自动/完全抽象。提供方会为该插件实例的每次运行固定已解析值。subagent 工具 schema 与 `SubagentStartRequest` 都不包含权限字段,因此模型或单次委派无法改变它。
+
+### Claude Code
+
+Claude Code 默认使用 `dontAsk`,而且只接受锁定版本 Agent SDK 支持的原生非交互模式:
+
+| 值 | 原生行为 |
+| --- | --- |
+| `dontAsk` | 不弹出提示,直接拒绝尚未获授权的操作。 |
+| `acceptEdits` | 接受编辑;其余权限提示由无人值守回调拒绝。 |
+| `auto` | 由 Claude Code 原生分类器允许或拒绝权限请求。 |
+| `plan` | 使用规划模式,拒绝执行审批,并把完整计划作为最终答案返回。 |
+| `bypassPermissions` | 设置 SDK 的显式危险确认并跳过权限检查。 |
+
+提供方继续省略 `settingSources`:除所选模式以外,用户、项目和本地设置、身份验证、工具与沙箱行为仍由 Claude Code 拥有。
+
+每次 query 都禁用 `AskUserQuestion`。非 bypass 模式的权限回调会拒绝请求,而不会返回 SDK 中会无限阻塞的 `null`;plan 模式还会把 `ExitPlanMode` 放入 `disallowedTools`,因此原生 allow 规则无法把无人值守 query 切回执行模式。MCP elicitation 会被拒绝;已支持的拒绝对话会被取消;未声明的对话类型使用 SDK 的无对话失败行为。原生 `permission_denied` 消息会记录同一份当前运行事实。这些路径不会创建审批会话、队列、缓存或重试循环。
+
+### Codex
+
+Codex 默认使用 `never`,并接受 Codex 0.147.0 公开的三种原生非交互模式。提供方启动固定的 app-server 命令,再把所选模式映射为官方 `thread/start` 字段,因为 CLI 全局权限 flag 不会配置之后由 app-server 客户端创建的线程:
+
+| 值 | `thread/start` 字段 | 原生行为 |
+| --- | --- | --- |
+| `never` | `approvalPolicy: never`;省略 sandbox | 永不弹出提示;执行失败会在原生 sandbox 下返回模型。 |
+| `approve-for-me` | `approvalPolicy: on-request`、`approvalsReviewer: auto_review`、`sandbox: workspace-write` | 由 Codex 自动评审权限请求。 |
+| `dangerously-bypass-approvals-and-sandbox` | `approvalPolicy: never`、`sandbox: danger-full-access` | 跳过审批与 sandbox。 |
+
+提供方只覆盖这些线程字段。`CODEX_HOME`、项目配置、模型/provider 选择、MCP、hook、skill、身份验证,以及模式未选择的 sandbox 事实仍属于 Codex 原生状态。wire 仍会拒绝任何意外到达的审批、权限、用户输入或 MCP 请求,而不会开放动态 allow 通道。
+
+### 失败诊断
+
+`SubagentResult` 携带可选的 `diagnostic`,用于提供方产生且不属于 assistant 内容的失败说明。提供方在生成它之前会排除工具输入、文件内容、环境值、凭证与原始协议载荷。共享的进程外结果边界会把完整文本限制在 4096 个 UTF-8 字节以内,并在不切断字符的前提下标记截断。
+
+每个产品都只记录有效模式、请求类别、无人值守决定与固定的安全原因。Claude Code 从 SDK 回调和 `permission_denied` 消息取得这些事实。Codex 从 app-server 请求、被拒绝的 item、`sandboxError` 与每次运行有界 stderr 尾部中的两个固定权限签名取得事实;原始 stderr 仍会转发给 Host,但绝不会复制进诊断。成功结果只返回严格的最终答案;本地取消仍以 `aborted` 结算且不附带权限说明;未发布的启动失败仍会拒绝 `start()`。当一项权限事实参与了已经发布、最终以 `error` 结算的运行时,提供方会附加诊断,但不会把它写入 assistant 输出、结构化输出或 `subagent/end.lastAssistantMessage`。
+
+前台消费方依次呈现终止原因标题、可选诊断和任何部分 assistant 输出。一次性后台适配器会在失败 Job 的 detail 中,把同一诊断与终止原因一起保存。没有填写该字段的提供方保持原有行为。
+
+### 所有权与生命周期
+
+| 事实或资源 | Owner | 可观察行为 |
+| --- | --- | --- |
+| Profile 权限选择 | 各产品提供方 Config | 配置阶段会拒绝无效、交互式或未知值。 |
+| 权限与沙箱语义 | Claude Code Agent SDK 或 Codex app-server | 各提供方传入一个原生模式,不镜像产品策略。 |
+| 交互决定与安全诊断 | 单次产品运行 | 并发运行分别拥有独立的模式、协议与诊断状态。 |
+| 诊断类型与字节上限 | `dsh-subagent` | 消费方收到与 assistant 输出分离的有界可选字段。 |
+| 前台与 Job 呈现 | `dsh-tool-subagent` 和通用 Job 运行时 | 调度选择不会改变底层失败事实。 |
+| 进程取消与完全停稳 | 产品提供方和 `dsh-subprocess` | 结果结算后仍执行幂等的完整进程树资源释放。 |
+
+## Verification
+
+包测试固定所有允许与拒绝的 Config 值、准确的 SDK 与 app-server 字段映射、危险确认、无人值守终态、诊断脱敏与 UTF-8 上限、成功结果不携带诊断、并发运行隔离、前台顺序、Job detail、stderr observer 释放和进程清理。真实 Claude Agent SDK/CLI fixture 证明其安全默认、受限拒绝、显式 bypass 与整棵进程树完全停稳。真实 Codex app-server fixture 证明线程级 `never` 覆盖环境中的 `on-request`、自动评审可以启动、危险绕过只在测试拥有的临时存储中写入、固定 stderr 签名产生安全诊断,而且 wrapper/native 进程树会退出。Loader 组装证明非默认模式可以在不启动任一产品的情况下发布;无密钥 ACP snapshot 则记录共享诊断呈现,同时面向模型的产品工具 schema 不包含权限参数。
+
+## Alternatives considered
+
+**使用产品环境中的权限默认值。** 原生设置可能选择交互模式,使无人值守行为依赖部署环境。提供方必须为每次 query 显式选择非交互模式。
+
+**把权限模式放入面向模型的工具或每次 start 请求。** 这会让任务内容选择权限,并在每次调用中重复一个 Profile 部署决定。
+
+**复制产品设置或映射父级 Harness 沙箱。** 各产品并不共享同一套权限词汇。镜像这些状态会创建第二个权威,并掩盖自动模式与 bypass 模式的原生沙箱后果。
+
+**把提示转发给父 agent、Web 客户端或 CLI。** 一次性产品运行没有由其拥有的人工交互生命周期。新增该能力需要持久请求身份、路由、取消与 timeout 语义,超出本决策范围。
+
+**返回原始产品错误、stderr 或工具输入。** 这些值可能包含命令、路径、工作区数据、环境值或凭证。固定的安全诊断既保留可操作性,也不会暴露产品 transcript。
+
+**单独保存 Job 诊断。** Job 只是同一 `SubagentRun` 的调度适配器;第二个字段会让前台和后台的失败含义发生漂移。
+
+## Consequences
+
+Profile 可以在提供方启动前选择各产品原生的受限、自动、在产品支持时仅规划/编辑放行,或 bypass 行为,而两个安全默认值都绝不会询问人员。更宽松的模式仍是显式部署选择,并保留其原生沙箱后果。
+
+权限失败会同时到达前台父 agent 和一次性后台 Job,且不会把基础设施文本伪装成 assistant 回答。该诊断可以沿普通消费路径进入模型上下文、Job 通知、API 投影与 Job UI,因此提供方必须在结果结算前完成脱敏和限长。
+
+本改动不增加产品会话持久化、人工审批通道、动态权限操作、进度流、重试策略或回滚。其他提供方无需产生诊断或公开权限模式 Config,仍然保持合法。

+ 6 - 0
.agents/notes/implemented/process/2026-08-17-readme-assets-on-cdn.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/process/2026-08-17-readme-assets-on-cdn.md
+2026-08-17-readme-assets-on-cdn.md: 8918e2118123382553e1ea295351482030e0e640
+2026-08-17-readme-assets-on-cdn.zh.md: a1314068ff0bbebfef5bb0053f04aebe650f6101

+ 35 - 0
.agents/notes/implemented/process/2026-08-17-readme-assets-on-cdn.md

@@ -0,0 +1,35 @@
+# Agent Note: README assets publish from a dedicated repository
+
+Status: implemented
+
+English | [中文](2026-08-17-readme-assets-on-cdn.zh.md)
+
+## Problem
+
+The public Chinese README embeds three community QR codes. Repository-relative images make each replacement depend on a source change and the separate public-repository publication flow, even though the image bytes do not change product code or documentation text.
+
+The images need stable public URLs while their source bytes, publication credentials, cache behavior, and update history remain explicit and reviewable.
+
+## Decision
+
+The README references fixed URLs under `https://cdn.deepseek.com/harness/readme/`. The private [`deepseek-harness/readme-cdn-assets`](https://github.com/deepseek-harness/readme-cdn-assets) repository owns the three allowlisted PNG files, their tests, and their publication code. A push to its `master` branch runs `publish.yml`, which installs the pinned Huawei OBS SDK, tests `scripts/upload.mjs`, and publishes the images.
+
+The uploader accepts only the three README filenames, verifies each source is a PNG file, and uploads it to `dp-cdn-deepseek/harness/readme/` with `Content-Type: image/png` and `Cache-Control: no-store`. It checks the OBS response status, reports the resulting public URL, and closes the client on both success and failure. Repository Actions Secrets supply `OBS_DSH_README_ACCESS_KEY_ID` and `OBS_DSH_README_SECRET_ACCESS_KEY`; the OBS identity needs write access only to that object prefix.
+
+The assets repository provides the update history and rollback source. The public README keeps the same URLs across image replacements, so ordinary image updates do not require a product-repository change or a public-repository synchronization.
+
+## Alternatives considered
+
+**Keep repository-relative images on `master`.** This preserves GitHub as the only image host, but every operational QR-code replacement remains coupled to the code review and public-repository publication path.
+
+**Keep a long-lived assets branch in the product repository.** A branch avoids product `master` changes, but it leaves image ownership, OBS credentials, and publication workflow attached to the product repository and its repository-wide automation. A dedicated repository gives that operational source one default branch and one narrow responsibility.
+
+**Use content-addressed CDN object names.** Immutable objects avoid stale caches, but each image replacement must also change the README URL, which removes the independent update path this workflow exists to provide.
+
+**Allow the uploader to publish arbitrary paths.** A generic uploader could serve future assets without code changes, but the same credentials could then overwrite unrelated CDN objects. The fixed allowlist keeps this publication job limited to the README images it owns.
+
+## Consequences
+
+Community QR codes can change through one assets-repository push while the public README remains unchanged. The product repository carries no OBS dependency or credential, uploads retain an auditable git source, and CDN responses carry `Cache-Control: no-store`.
+
+The README depends on the public CDN and GitHub's image proxy, while publication depends on a second private repository and its two Actions Secrets. `no-store` gives up edge and browser caching for these small files.

+ 35 - 0
.agents/notes/implemented/process/2026-08-17-readme-assets-on-cdn.zh.md

@@ -0,0 +1,35 @@
+# Agent Note: README 资产通过专用仓库发布
+
+Status: implemented
+
+[English](2026-08-17-readme-assets-on-cdn.md) | 中文
+
+## 问题
+
+公开中文 README 嵌入了 3 张社区二维码。使用仓库相对路径时,每次替换都依赖源码变更以及独立的公开仓库发布流程,即使图片字节并未改变产品代码或文档文字。
+
+这些图片需要稳定的公开 URL,同时必须明确并可评审地保存源文件字节、发布凭证、缓存行为和更新历史。
+
+## 决策
+
+README 引用 `https://cdn.deepseek.com/harness/readme/` 下的固定 URL。私有仓库 [`deepseek-harness/readme-cdn-assets`](https://github.com/deepseek-harness/readme-cdn-assets) 负责管理 3 张允许发布的 PNG 文件、相应测试和发布代码。向该仓库的 `master` 分支 push 会运行 `publish.yml`,安装固定版本的华为云 OBS SDK、测试 `scripts/upload.mjs` 并发布图片。
+
+上传脚本只接受 3 个 README 图片文件名,验证每个源文件均为 PNG,并以 `Content-Type: image/png` 和 `Cache-Control: no-store` 上传到 `dp-cdn-deepseek/harness/readme/`。脚本检查 OBS 响应状态、报告对应公开 URL,并在成功或失败后关闭客户端。仓库级 GitHub Actions Secret 提供 `OBS_DSH_README_ACCESS_KEY_ID` 和 `OBS_DSH_README_SECRET_ACCESS_KEY`;OBS 身份只需拥有该对象前缀的写权限。
+
+资产仓库提供更新记录和回滚真源。图片替换后,公开 README 继续使用相同 URL,因此常规图片更新无需修改产品仓库或同步公开仓库。
+
+## 曾考虑的替代方案
+
+**继续在 `master` 上使用仓库相对图片。**这种做法只使用 GitHub 托管图片,但每次运营二维码替换仍与代码评审和公开仓库发布流程耦合。
+
+**在产品仓库中保留长期资产分支。**资产分支可以避免修改产品 `master`,但图片所有权、OBS 凭证和发布工作流仍依附于产品仓库及其全仓自动化。专用仓库为这项运营资源提供单一默认分支和单一职责。
+
+**使用内容寻址的 CDN 对象名。**不可变对象不会产生陈旧缓存,但每次替换图片还必须修改 README URL,无法提供此工作流所需的独立更新路径。
+
+**允许上传脚本发布任意路径。**通用上传脚本可以在不改代码的情况下支持未来资产,但同一组凭证也能覆盖无关 CDN 对象。固定允许列表将发布任务限制在它负责的 README 图片内。
+
+## 后果
+
+社区二维码可以通过一次资产仓库 push 更新,公开 README 无需改变。产品仓库不携带 OBS 依赖或凭证;上传内容保留可审计的 git 真源;CDN 响应携带 `Cache-Control: no-store`。
+
+README 依赖公开 CDN 和 GitHub 图片代理,发布流程则依赖另一个私有仓库及其 2 个 GitHub Actions Secret。`no-store` 为这些小文件放弃边缘节点和浏览器缓存。

+ 0 - 37
.agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.md

@@ -1,37 +0,0 @@
-# Agent Note: Remove the empty experimental package group
-
-Status: implemented
-
-English | [中文](2026-08-11-remove-empty-experimental-package-group.zh.md)
-
-## Problem
-
-The package hierarchy reserves `packages/experimental/` for prototypes and internal-only plugins, but no package has used the group. The empty group adds placement, dependency, promotion, and release rules without a current package or release mechanism that needs them.
-
-The original group aimed to let the team share prototypes against the real plugin graph without implying product support. That need remains possible, but it does not justify a permanent repository category before a concrete package exists.
-
-## Decision
-
-The package hierarchy has no reserved experimental or internal-only group. Packages continue to live in groups selected for their current product role.
-
-A concrete package that needs different release, stability, or dependency treatment requires a decision based on its actual consumers and release mechanism. That decision may reintroduce a dedicated group when it can also define and enforce the exclusion rules.
-
-This note consolidates and supersedes the experimental-package-group decision, whose active triplet is removed with the empty directory.
-
-## Alternatives considered
-
-**Keep the empty group.** It provides an obvious future incubation location, but it also keeps repository rules with no current owner, package, or enforcement mechanism.
-
-**Move the experimental rules into the general package instructions.** This preserves the policy without an empty directory, but makes every package change carry rules for a hypothetical package class.
-
-**Put concrete experimental packages in product-role groups with README labels.** This preserves product-role colocation, but labels alone cannot enforce release and runtime-dependency rules. A future package can evaluate this option against its actual release mechanism.
-
-**Treat every package as experimental until the first tagged release.** This applies a broad temporary status without providing durable treatment for packages that remain experimental after releases begin.
-
-**Require prototypes to stay outside the repository.** This would lose access to the real plugin graph, examples, snapshots, and lifecycle checks. Removing the reserved group does not impose that restriction; a concrete prototype can establish the placement it needs.
-
-## Consequences
-
-The hierarchy loses an unused group and its special release and dependency policy. It also gives up a predeclared location for team discovery and a ready-made promotion path.
-
-The first package that needs experimental or internal-only treatment must define where it lives, how releases exclude it, which runtime dependencies are allowed, and what condition promotes or removes it. A dedicated group can return when those rules have a current consumer and enforceable mechanism.

+ 0 - 37
.agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.zh.md

@@ -1,37 +0,0 @@
-# Agent Note: 移除空的实验性包分组
-
-Status: implemented
-
-[English](2026-08-11-remove-empty-experimental-package-group.md) | 中文
-
-## 问题
-
-包层级结构预留 `packages/experimental/` 用于原型和内部专用插件,但从未有包使用该分组。这个空分组添加了放置、依赖、提升和发布规则,却没有需要这些规则的现有包或发布机制。
-
-原分组旨在让团队基于真实插件图共享原型,同时不暗示产品会提供支持。这项需求将来可能出现,但在具体包出现前,不足以支持一个永久的仓库类别。
-
-## 决策
-
-包层级结构不再预留实验性或内部专用分组。包继续按照当前产品职责放入对应分组。
-
-如果具体包需要不同的发布、稳定性或依赖处理,必须根据其实际消费方和发布机制做出决策。只要该决策同时定义并强制执行排除规则,就可以重新引入专用分组。
-
-本 Agent Note 整合并取代实验性包分组决策;该旧决策的活跃三文件组随空目录一并移除。
-
-## 考虑过的替代方案
-
-**保留空分组。** 它为未来孵化工作提供明确位置,但也会保留没有当前负责人、包或强制执行机制的仓库规则。
-
-**将实验性规则移入通用包指令。** 这可以在不保留空目录的情况下延续政策,但会让每次包变更都携带针对假设包类别的规则。
-
-**将具体实验性包放入产品职责分组,并用 README 标注。** 这会保持产品职责共置,但仅靠标注无法强制执行发布和运行时依赖规则。未来的包可以根据实际发布机制评估此选项。
-
-**在首个带标签的版本发布前,将每个包都视为实验性。** 这会施加宽泛的临时状态,却无法为发布开始后仍处于实验状态的包提供持久处理方式。
-
-**要求原型留在仓库外。** 这会失去真实插件图、示例、快照和生命周期检查。移除预留分组并不施加这项限制;具体原型可以建立自身所需的放置规则。
-
-## 后果
-
-包层级结构移除了未使用的分组及其特殊发布和依赖政策,同时也放弃了预先声明的团队发现位置和现成的提升路径。
-
-第一个需要实验性或内部专用处理的包必须定义其存放位置、发布版本如何排除它、允许哪些运行时依赖,以及包在何种条件下获得提升或被移除。当这些规则具有当前消费方和可强制执行的机制时,可以恢复专用分组。

+ 6 - 0
.agents/notes/implemented/simplification/2026-08-12-trim-agent-teams-read-and-lifecycle-surface.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-12-trim-agent-teams-read-and-lifecycle-surface.md
+2026-08-12-trim-agent-teams-read-and-lifecycle-surface.md: 736ed2587c8b2ffd9d824efa14dd0d249c43f669
+2026-08-12-trim-agent-teams-read-and-lifecycle-surface.zh.md: be30d9239d4dc0c44adda63d9488e7a25d217a9e

+ 57 - 0
.agents/notes/implemented/simplification/2026-08-12-trim-agent-teams-read-and-lifecycle-surface.md

@@ -0,0 +1,57 @@
+# Agent Note: Trim the Agent Teams read and lifecycle surface
+
+Status: implemented
+
+English | [中文](2026-08-12-trim-agent-teams-read-and-lifecycle-surface.zh.md)
+
+## Problem
+
+Agent Teams correctly owns durable roster, peer-mailbox, and shared-task policy, while the subagent continuation manager owns continuable child Activations. The first implementation nevertheless duplicated data and lifecycle mechanics across those roles.
+
+The read surface published `TeamSnapshot`, including pending mail that no production caller read. Web `team.get` called that snapshot for Team identity and a global revision, discarded its collections, then called `listMembers()` and `listTasks()`; the browser used neither the Team id it had already addressed nor the global revision. Public member and task views also repeated internal fields: member `error` duplicated `diagnostics`, task `ownerId` duplicated the model/UI owner name, and task timestamps had no reader. `SpawnTeammateResult.initialMessageId`, `TeamDeliverySource`, and the public resolved config type likewise had no consumer.
+
+Durable member, task, message, and acknowledgement payloads copied timestamps already owned by the Session event envelope. Message `targetName` repeated the immutable roster lookup. The fold validated these values only against their previous copies, so the extra fields added format and validation code without deciding behavior.
+
+`waitForChange()` returned a zero-or-one `changes` array with a domain kind and Lead-log revision even though every caller immediately re-listed authoritative state. The accompanying `team/changed` event had no production listener. Team interruption bypassed subagent authorization and cancellation semantics by calling `Agent.cancel()` directly. Team teardown separately combined cancellation, descendant drain, `whenIdle()`, and Agent-registry polling even though the continuation manager alone owns Activation release.
+
+## Decision
+
+The Team service keeps the distinct product responsibilities: durable named roster, Lead-log mailbox, and task DAG. It does not merge with the generic subagent catalog or task service.
+
+Inside `@deepseek-ai/dsh-team`, `TeamService` is the Cordis-facing façade and disposal coordinator. `TeamJournal` owns per-Lead transaction order and append-plus-flush publication; `TeamRoster` owns membership and provisioning; `TeamMailbox` owns target-local dispatch, acknowledgement, and retry state; `TeamTaskBoard` owns task authorization, DAG transitions, and derived views; `TeamActivity` owns current waiters; and `TeamRuntimeLifecycle` owns the single admission cutoff and bounded settlement. These package-private collaborators share the existing service capability without publishing additional Cordis services.
+
+The unused snapshot API and global Team revision are removed. Host reads return only roster and task views; they do not repeat the already-addressed Team id. Member failures appear once in `diagnostics`. Task views expose `ownerName` but keep `ownerId` inside the durable service implementation. Spawn returns the member view only, and validated config is private.
+
+Durable Team values retain only fields needed to replay Team behavior. Session event `seq` and `time` own ordering and timing; roster membership owns immutable names. Member/task/message timestamps, message `targetName`, and acknowledgement `deliveredAt` are removed. Task CAS retains its task-local `revision`, which is behavioral rather than observational metadata.
+
+`waitForChange()` now returns `{ timedOut }`. A committed Team append or live member-status edge wakes current waiters after the owning flush, and callers re-list. The unused `team/changed` event, change kind, change revision, and disposal sentinel are removed.
+
+Team `interrupt()` resolves the durable roster name, then delegates to `SubagentService.interrupt()` with exact ancestor authority. Team teardown selects the roster's exact live direct-child ids and calls the new `drainContinuableChildren(parent, childIds)` continuation operation. That operation authorizes exact direct ownership, opens selected Activation disposal synchronously, recursively releases descendants child-first, leaves siblings and parent-wide admission alone, and treats absent targets as no-ops. Full teardown clears pending inbox work; only interrupt promises `keepInbox`.
+
+Creation and dispatch remain separate in-flight sets because disposal must await creation before dispatches that creation recovery can register. The mailbox's durable enqueue/acknowledgement, target-side de-duplication, FIFO dispatch repair, provisioning reconciliation, and Host fold fallback remain unchanged.
+
+## Alternatives considered
+
+**Merge Team messaging into subagent follow-up.** Rejected. Subagent follow-up addresses a child by Session id and owns Activation delivery; Team messaging adds immutable names, peer authorization, durable enqueue-before-delivery, quiet inactive behavior, acknowledgement, retry, and sender framing.
+
+**Replace Team tasks with the generic task service.** Rejected. The Team board is a Lead-log DAG with CAS revisions, member ownership, dependencies, tombstones, and advisory write scopes. Those are product semantics, not duplicate storage plumbing.
+
+**Keep the public fields for future consumers.** Rejected before the first tagged release. Every removed field lacked a production reader, and pending mail or timing can be projected from the authoritative log if a concrete product later needs them.
+
+**Subscribe to `agent/disposed` from Team teardown.** Rejected. The Team fiber is already unwinding when teardown runs, so new event registration is invalid. More importantly, an observer would still duplicate the continuation manager's ownership instead of asking that owner to release exact children.
+
+**Use `drainContinuableDescendants()` on the Lead.** Rejected because it would stop non-Team continuable children and close admission for the whole Lead lineage. Draining descendants of each teammate stops only grandchildren and leaves the teammate Activation itself to Team polling. The exact-child operation expresses the required set directly.
+
+**Preserve teammate inboxes during full teardown.** Rejected after testing the real handle lifecycle. `AgentHandle.dispose()` is a full release and clears unclaimed inbox work. Describing it as resumable parking would be false; interruption remains the non-disposing operation that preserves pending input.
+
+**Keep all runtime responsibilities in one `TeamService` class.** Rejected because the class would own unrelated task policy, roster provisioning, mailbox delivery queues, waiters, and shutdown settlement. Package-private state owners retain one public service while making each asynchronous set and lifecycle controller belong to the operation family that settles it.
+
+## Testing
+
+Subagent tests cover exact-child selection, duplicate ids, sibling isolation, recursive descendant release, wrong-parent authorization, and a manager-less no-op. Team tests cover delegated interrupt, bounded exact-child teardown, provisioning cleanup, mailbox recovery, wait wake/timeout/disposal, and the reduced views and durable records; white-box failure injection addresses the package-private roster, mailbox, and journal owners instead of widening `TeamService`. Host, tool, and client tests cover the reduced wire and model-visible results. Type checking covers the public deletion across host and browser faces.
+
+## Consequences
+
+Team and subagent remain separate capability seams with one lifecycle owner. Team chooses which roster children belong to its runtime; subagent performs interruption and Activation teardown. The Team surface is smaller, persisted records no longer mirror their Session envelope, and wait consumers cannot mistake an advisory change kind or revision for a coherent snapshot. Package-private state ownership keeps `TeamService` focused on the public operations, Cordis event wiring, recovery order, and disposal order; the split adds internal modules but no public API or durable-format change.
+
+Web `team.get` still folds once for members and once for tasks. A coherent combined snapshot was not a consumer requirement, and adding an incremental cache would introduce a separate consistency mechanism rather than simplify this seam.

+ 57 - 0
.agents/notes/implemented/simplification/2026-08-12-trim-agent-teams-read-and-lifecycle-surface.zh.md

@@ -0,0 +1,57 @@
+# Agent Note: Trim the Agent Teams read and lifecycle surface
+
+Status: implemented
+
+[English](2026-08-12-trim-agent-teams-read-and-lifecycle-surface.md) | 中文
+
+## Problem
+
+Agent Teams 合理地拥有持久 roster、peer mailbox 与共享任务策略,subagent continuation manager 则拥有 continuable child Activation。但第一版仍在这两类角色之间重复了数据和生命周期机制。
+
+读取表面发布了 `TeamSnapshot`,其中包含没有生产调用方读取的 pending mail。Web `team.get` 为 Team identity 与全局 revision 调用该 snapshot,丢弃其中的集合,再调用 `listMembers()` 和 `listTasks()`;浏览器既不用已经寻址过的 Team id,也不用全局 revision。公开 member 与 task view 还重复内部字段:member `error` 与 `diagnostics` 重复,task `ownerId` 与模型/UI 使用的 owner name 重复,task 时间戳没有读取方。`SpawnTeammateResult.initialMessageId`、`TeamDeliverySource` 和公开的 resolved config type 同样没有消费者。
+
+持久 member、task、message 与 acknowledgement payload 复制了 Session event envelope 已拥有的时间戳。message `targetName` 重复不可变 roster lookup。fold 只拿这些值与其旧副本互相校验,因此额外字段增加格式与校验代码,却不决定行为。
+
+`waitForChange()` 返回长度为零或一的 `changes` 数组,携带领域 kind 与 Lead-log revision,但所有调用方都会立即重新列出权威状态。配套 `team/changed` event 没有生产 listener。Team interrupt 绕过 subagent 授权与取消语义,直接调用 `Agent.cancel()`。Team teardown 又自行组合 cancel、descendant drain、`whenIdle()` 与 Agent registry 轮询,尽管只有 continuation manager 拥有 Activation release。
+
+## Decision
+
+Team 服务保留独立的产品职责:持久具名 roster、Lead-log mailbox 与 task DAG。它不会与通用 subagent catalog 或 task service 合并。
+
+在 `@deepseek-ai/dsh-team` 内,`TeamService` 是面向 Cordis 的 façade 与 disposal 协调者。`TeamJournal` 负责每个 Lead 的 transaction 顺序以及 append-plus-flush 发布;`TeamRoster` 负责 membership 与 provisioning;`TeamMailbox` 负责 target-local dispatch、acknowledgement 与 retry 状态;`TeamTaskBoard` 负责 task 授权、DAG transition 与派生 view;`TeamActivity` 负责当前 waiter;`TeamRuntimeLifecycle` 负责唯一的准入截止与有界 settlement。这些包内 collaborator 共享现有 service capability,不发布额外 Cordis service。
+
+删除未使用的 snapshot API 与全局 Team revision。Host 读取只返回 roster 与 task view,不重复已经寻址的 Team id。member failure 只在 `diagnostics` 出现一次。task view 暴露 `ownerName`,把 `ownerId` 留在持久服务实现内部。spawn 只返回 member view,已校验 config 改为私有。
+
+持久 Team value 只保留回放 Team 行为所需字段。Session event 的 `seq` 与 `time` 负责顺序和时间;roster membership 负责不可变名字。member/task/message 时间戳、message `targetName` 与 acknowledgement `deliveredAt` 均删除。task CAS 保留 task-local `revision`,因为它是行为字段,而非观测元数据。
+
+`waitForChange()` 现在返回 `{ timedOut }`。已提交 Team append 或 live member-status edge 会在所属 flush 后唤醒当前 waiter,调用方随后重新列出状态。未使用的 `team/changed` event、change kind、change revision 与 disposal sentinel 一并删除。
+
+Team `interrupt()` 先解析持久 roster name,再以确切 ancestor authority 委托 `SubagentService.interrupt()`。Team teardown 选择 roster 中确切的 live direct-child id,并调用新的 continuation 操作 `drainContinuableChildren(parent, childIds)`。该操作验证确切直接所有权,同步开启所选 Activation 的 disposal,递归以 child-first 释放后代,不影响 sibling 与 parent-wide admission,缺席目标视为 no-op。完整 teardown 会清空 pending inbox;只有 interrupt 承诺 `keepInbox`。
+
+创建与 dispatch 继续使用分离的 in-flight set,因为 dispose 必须先等待创建,再等待创建 recovery 可能注册的 dispatch。mailbox 的持久 enqueue/acknowledgement、target-side 去重、FIFO dispatch 修复、provisioning 对账与 Host fold fallback 保持不变。
+
+## Alternatives considered
+
+**把 Team messaging 合并进 subagent follow-up。** 否决。subagent follow-up 按 Session id 寻址 child 并负责 Activation 投递;Team messaging 额外提供不可变名字、peer 授权、先持久 enqueue 再投递、quiet inactive 行为、acknowledgement、retry 与 sender framing。
+
+**用通用 task service 替换 Team task。** 否决。Team board 是带 CAS revision、member owner、dependency、tombstone 与 advisory write scope 的 Lead-log DAG。这些是产品语义,不是重复存储 plumbing。
+
+**为未来消费者保留公开字段。** 在首次 tag 发布前否决。每个删除字段都没有生产读取方;若未来有具体产品需要 pending mail 或时间,可从权威日志投影。
+
+**在 Team teardown 中订阅 `agent/disposed`。** 否决。teardown 运行时 Team fiber 已在解绑,新 event registration 无效。更重要的是,observer 仍会重复 continuation manager 的所有权,而不是要求 owner 释放确切 child。
+
+**对 Lead 使用 `drainContinuableDescendants()`。** 否决,因为它会停止非 Team continuable child,并关闭整个 Lead 谱系的准入。对每个 teammate drain descendants 又只会停止孙级,把 teammate Activation 本身留给 Team 轮询。exact-child 操作直接表达所需集合。
+
+**在完整 teardown 时保留 teammate inbox。** 在真实 handle lifecycle 测试后否决。`AgentHandle.dispose()` 是完整 release,会清空未 claim 的 inbox 工作。把它描述成可恢复 parking 是错误的;interrupt 仍是保留 pending input 的非 disposing 操作。
+
+**把全部 runtime 职责保留在一个 `TeamService` class 中。** 否决,因为该 class 会同时拥有互不相关的 task policy、roster provisioning、mailbox delivery queue、waiter 与 shutdown settlement。包内 state owner 在保留单一公开 service 的同时,让每组异步状态与 lifecycle controller 归属于负责结算它们的 operation family。
+
+## Testing
+
+Subagent 测试覆盖 exact-child selection、重复 id、sibling 隔离、递归 descendant release、错误 parent 授权与 manager 缺席时的 no-op。Team 测试覆盖委托 interrupt、有界 exact-child teardown、provisioning cleanup、mailbox recovery、wait 的 wake/timeout/dispose,以及缩减后的 view 与持久记录;白盒 failure injection 直接访问包内 roster、mailbox 与 journal owner,不扩宽 `TeamService`。Host、tool 和 client 测试覆盖缩减后的 wire 与模型可见结果。typecheck 覆盖 Host 与浏览器表面的公开删除。
+
+## Consequences
+
+Team 与 subagent 仍是独立 capability seam,但只有一个生命周期 owner。Team 选择哪些 roster child 属于其运行时;subagent 执行 interrupt 与 Activation teardown。Team 表面更小,持久记录不再镜像 Session envelope,wait 消费方也不会把提示性的 change kind 或 revision 误当作一致 snapshot。包内 state ownership 让 `TeamService` 专注于公开 operation、Cordis event wiring、recovery 顺序与 disposal 顺序;该拆分增加内部 module,但不改变公开 API 或持久格式。
+
+Web `team.get` 仍为 member 与 task 各折叠一次。消费方不要求一致的组合 snapshot,而增加增量 cache 会引入独立一致性机制,不是对该 seam 的简化。

+ 1 - 0
AGENTS.md

@@ -43,6 +43,7 @@ packages/    @deepseek-ai/dsh-<pkg> workspaces at packages/<group>/<pkg>/
   boot/        shared app-bin glue
   sdk/         JSON-RPC protocol, server, and TypeScript client
   examples/    demo bundles (agent-spine + CLI/ACP/JSON-RPC bins)
+  experimental/ private prototypes excluded from official releases
   support/     dev/test infrastructure
   util/        zero-dependency utilities
 python/      Python SDK and bundled runtime (see python/README.md)

+ 1 - 1
README.i18n.yaml

@@ -3,4 +3,4 @@
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write README.md
 README.md: 8a4bd01332a23ce4144c661784bc549e0ba72d21
-README.zh.md: b7bc214bfb1fd8a76a47de3f0aa242122aeb7603
+README.zh.md: c507bf884bd426feead6a96adbdb5c136456e3b5

+ 3 - 3
README.zh.md

@@ -50,9 +50,9 @@ pnpm dsh web
   </thead>
   <tbody>
     <tr>
-      <td align="center"><img src="assets/community-wecom-assistant.png" alt="DeepSeek Harness 企微小助手二维码" width="180" height="180"></td>
-      <td align="center"><a href="https://trtgsjkv6r.feishu.cn/share/base/form/shrcnIt5twSVdLGD52KJBckGCgg"><img src="assets/community-wecom-survey.png" alt="DeepSeek Harness 入群问卷二维码" width="180" height="180"></a></td>
-      <td align="center"><img src="assets/community-wechat-official-account.png" alt="DeepSeek Harness 团队微信公众号二维码" width="180" height="180"></td>
+      <td align="center"><img src="https://cdn.deepseek.com/harness/readme/community-wecom-assistant.png" alt="DeepSeek Harness 企微小助手二维码" width="180" height="180"></td>
+      <td align="center"><a href="https://trtgsjkv6r.feishu.cn/share/base/form/shrcnIt5twSVdLGD52KJBckGCgg"><img src="https://cdn.deepseek.com/harness/readme/community-wecom-survey.png" alt="DeepSeek Harness 入群问卷二维码" width="180" height="180"></a></td>
+      <td align="center"><img src="https://cdn.deepseek.com/harness/readme/community-wechat-official-account.png" alt="DeepSeek Harness 团队微信公众号二维码" width="180" height="180"></td>
     </tr>
   </tbody>
 </table>

+ 6 - 2
apps/web/tests/cordis-tool-round.e2e.ts

@@ -108,12 +108,16 @@ describe('web e2e: Cordis tools use their owned cards', () => {
     // NOT the plugin running. Until a person answers, the browser half has not
     // been fetched, evaluated, or mounted anywhere on this page.
     expect(await page.locator('[data-snapshot-probe]').count()).toBe(0)
+    const sessionId = await runTurnSettled
+    // Approving from idle makes the run-outcome steer a distinct continuation
+    // turn, matching the recorded replay and keeping turn grouping deterministic.
     const approvalTurnSettled = scaffold.whenTurnSettled()
     await approve.click()
     await expect.poll(() => page.locator('[data-snapshot-probe]').count(), { timeout: 30_000 }).toBe(1)
     await approvalTurnSettled
-
-    const sessionId = await runTurnSettled
+    await expect.poll(() => page.getByText('The Cordis Plugin is running.', { exact: true }).count(), { timeout: 15_000 })
+      .toBeGreaterThanOrEqual(1)
+    await expect.poll(() => input.isEnabled(), { timeout: 15_000 }).toBe(true)
     const stopTurnSettled = scaffold.whenTurnSettled()
     await input.fill(STOP_PROMPT)
     await input.press('Enter')

+ 39 - 1
apps/web/tests/models-settings.e2e.ts

@@ -30,6 +30,7 @@ const EMPTY_EXPECTED = join(SNAPSHOT_DIR, 'empty.expected.md')
 const CONFIGURED_EXPECTED = join(SNAPSHOT_DIR, 'configured.expected.md')
 const DECLARED_EXPECTED = join(SNAPSHOT_DIR, 'declared.expected.md')
 const DECLARED_EDIT_EXPECTED = join(SNAPSHOT_DIR, 'declared-edit.expected.md')
+const MODEL_PICKER_EXPECTED = join(SNAPSHOT_DIR, 'model-picker.expected.md')
 const NATIVE_DELETE_EXPECTED = join(SNAPSHOT_DIR, 'native-delete.expected.md')
 const DELETE_EXPECTED = join(SNAPSHOT_DIR, 'delete.expected.md')
 const MODE = webSnapshotMode()
@@ -178,6 +179,42 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
     expect(tripwire.pageErrors).toEqual([])
   }, 60_000)
 
+  it('selects and clears the discovered model catalog in one action', async () => {
+    onTestFailed(() => saveFailureShot(page, 'web-e2e-models-picker'))
+    const settingsDialog = page.getByRole('dialog', { name: '设置' })
+    await settingsDialog.getByRole('button', { name: '编辑 minimax-cn' }).click()
+    await settingsDialog.getByText('自定义设置').click()
+    await settingsDialog.getByRole('button', { name: '获取可用模型' }).click()
+
+    const picker = page.getByRole('dialog', { name: '选择要添加的模型' })
+    await picker.waitFor({ timeout: 10_000 })
+    const boxes = picker.getByRole('checkbox')
+    const count = await boxes.count()
+    expect(count).toBeGreaterThan(0)
+    expect(await boxes.evaluateAll(nodes => nodes.map(node => (node as HTMLInputElement).checked))).toEqual(
+      Array.from({ length: count }, () => true),
+    )
+
+    await picker.getByRole('button', { name: '取消全选' }).click()
+    expect(await boxes.evaluateAll(nodes => nodes.map(node => (node as HTMLInputElement).checked))).toEqual(
+      Array.from({ length: count }, () => false),
+    )
+    await picker.getByRole('button', { name: '全选' }).waitFor()
+    const snapshot = await captureStableAria(
+      page,
+      '[role="dialog"][aria-label="选择要添加的模型"]',
+      scaffold.workspaceCwd,
+    )
+    await compareOrRefreshGolden(MODEL_PICKER_EXPECTED, snapshot, MODE)
+
+    await picker.getByRole('button', { name: '全选' }).click()
+    expect(await boxes.evaluateAll(nodes => nodes.map(node => (node as HTMLInputElement).checked))).toEqual(
+      Array.from({ length: count }, () => true),
+    )
+    await picker.getByRole('button', { name: '取消', exact: true }).click()
+    await settingsDialog.getByRole('button', { name: '取消', exact: true }).click()
+  }, 60_000)
+
   it('declares a route the adapter does not ship', async () => {
     onTestFailed(() => saveFailureShot(page, 'web-e2e-models-declare'))
     const dialog = page.getByRole('dialog', { name: '设置' })
@@ -280,7 +317,8 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
   it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
     await assertFixtureInventory(SNAPSHOT_DIR, [
       'configured.expected.md', 'declared-edit.expected.md', 'declared.expected.md',
-      'delete.expected.md', 'empty.expected.md', 'native-delete.expected.md',
+      'delete.expected.md', 'empty.expected.md', 'model-picker.expected.md',
+      'native-delete.expected.md',
     ])
   })
 })

+ 18 - 0
apps/web/tests/snapshots/models-settings/model-picker.expected.md

@@ -0,0 +1,18 @@
+- dialog "选择要添加的模型":
+  - heading "选择要添加的模型" [level=2]
+  - button "关闭":
+    - img
+  - paragraph: 以下是模型提供方的可用模型,勾选要添加的模型。
+  - button "全选"
+  - list:
+    - listitem:
+      - checkbox "MiniMax-M2.7"
+      - text: MiniMax-M2.7
+    - listitem:
+      - checkbox "MiniMax-M2.7-highspeed"
+      - text: MiniMax-M2.7-highspeed
+    - listitem:
+      - checkbox "MiniMax-M3"
+      - text: MiniMax-M3
+  - button "取消"
+  - button "添加所选"

+ 6 - 0
apps/web/tests/snapshots/workflow-run/ui-live.expected.md

@@ -0,0 +1,6 @@
+- button "snapshot-flow 1 member Running" [expanded]:
+  - img
+  - text: snapshot-flow 1 member Running
+- button "Run 1 member Running 1":
+  - img
+  - text: Run 1 member Running 1

+ 1 - 5
apps/web/tests/snapshots/workflow-run/ui.expected.md

@@ -13,13 +13,9 @@
   - img
   - img
   - text: Tool call workflow ·
-- button "snapshot-flow 1 member Completed" [expanded]:
+- button "snapshot-flow 1 member Completed":
   - img
   - text: snapshot-flow 1 member Completed
-- button "Run 1 member Completed 1" [expanded]:
-  - img
-  - text: Run 1 member Completed 1
-- text: Reply with exactly the word WF_CHILD_OK and not… Completed
 - button "Think The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop.":
   - img
   - img

+ 27 - 10
apps/web/tests/workflow-run.e2e.ts

@@ -20,6 +20,7 @@ import {
 
 const MODE = webSnapshotMode()
 const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/workflow-run', import.meta.url))
+const UI_LIVE_EXPECTED = join(SNAPSHOT_DIR, 'ui-live.expected.md')
 const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
 const PARENT_FIXTURE = join(REPO_ROOT, 'examples/acp-agent/tests/snapshots/workflow-run/session.jsonl')
 const CHILD_FIXTURE = join(REPO_ROOT, 'examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl')
@@ -52,7 +53,7 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () =
     scaffold = await launchWebScaffold({
       replayFixture: PARENT_FIXTURE,
       replayChildFixtures: [CHILD_FIXTURE],
-      paceMs: 25,
+      paceMs: 50,
     })
     browser = await chromium.launch()
     page = await newEnglishPage(browser)
@@ -78,14 +79,30 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () =
     await workflow.waitFor({ timeout: 30_000 })
     const disclosures = workflow.locator('[data-disclosure-row]')
     await disclosures.nth(1).waitFor({ timeout: 15_000 })
-    expect(await disclosures.nth(0).getAttribute('role')).toBeNull()
-    expect(await disclosures.nth(0).getAttribute('aria-expanded')).toBeNull()
-    expect(await disclosures.nth(1).getAttribute('role')).toBeNull()
-    expect(await disclosures.nth(1).getAttribute('aria-expanded')).toBeNull()
-    expect(await disclosures.nth(0).evaluate(element => getComputedStyle(element).cursor)).not.toBe('pointer')
-    expect(await disclosures.nth(1).evaluate(element => getComputedStyle(element).cursor)).not.toBe('pointer')
+    const runDisclosure = disclosures.nth(0)
+    const phaseDisclosure = disclosures.nth(1)
+    expect(await runDisclosure.getAttribute('role')).toBe('button')
+    expect(await runDisclosure.getAttribute('aria-expanded')).toBe('true')
+    expect(await phaseDisclosure.getAttribute('role')).toBe('button')
+    expect(await phaseDisclosure.getAttribute('aria-expanded')).toBe('true')
+    expect(await runDisclosure.evaluate(element => getComputedStyle(element).cursor)).toBe('pointer')
+    expect(await phaseDisclosure.evaluate(element => getComputedStyle(element).cursor)).toBe('pointer')
     const member = page.getByRole('button', { name: /^Open Reply with exactly the word/ })
     await member.waitFor({ timeout: 15_000 })
+
+    await phaseDisclosure.click()
+    expect(await phaseDisclosure.getAttribute('aria-expanded')).toBe('false')
+    expect(await member.count()).toBe(0)
+    const liveSnapshot = await captureStableAria(page, '[data-workflow-run]', scaffold.workspaceCwd)
+    await compareOrRefreshGolden(UI_LIVE_EXPECTED, liveSnapshot, MODE)
+    await phaseDisclosure.press('Enter')
+    await member.waitFor()
+    await runDisclosure.click()
+    expect(await runDisclosure.getAttribute('aria-expanded')).toBe('false')
+    expect(await disclosures.count()).toBe(1)
+    await runDisclosure.press('Space')
+    expect(await disclosures.count()).toBe(2)
+    expect(await phaseDisclosure.getAttribute('aria-expanded')).toBe('true')
     await member.focus()
 
     const lightColor = await member.locator('[data-member-label]').evaluate(element => getComputedStyle(element).color)
@@ -171,6 +188,8 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () =
     const workflow = page.getByRole('button', { name: /^snapshot-flow/ })
     await workflow.waitFor({ timeout: 15_000 })
     expect(await workflow.getAttribute('aria-expanded')).toBe('false')
+    const snapshot = await captureStableAria(page, '[data-chat-flow]', scaffold.workspaceCwd)
+    await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
     await workflow.click()
     const phase = page.getByRole('button', { name: /^Run/ })
     await phase.waitFor()
@@ -179,13 +198,11 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () =
     await page.getByText(CHILD_PROMPT, { exact: false }).waitFor()
     expect(await page.getByRole('button', { name: /^Open Reply with exactly the word/ }).count()).toBe(0)
 
-    const snapshot = await captureStableAria(page, '[data-chat-flow]', scaffold.workspaceCwd)
-    await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
   }, 60_000)
 
   it('stays clean and owns only its one golden', async () => {
     expect(tripwire.pageErrors).toEqual([])
     expect(tripwire.warnings).toEqual([])
-    await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
+    await assertFixtureInventory(SNAPSHOT_DIR, ['ui-live.expected.md', 'ui.expected.md'])
   })
 })

BIN
assets/community-wechat-official-account.png


BIN
assets/community-wecom-assistant.png


+ 2 - 2
docs/architecture.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/architecture.md
-architecture.md: a1507fa5e54f6703e89f09a5d387e6c9afc81ade
-architecture.zh.md: 4642a1e7691bccf4d52d9a84c92c8237c3c6658b
+architecture.md: 2a51992a4f99d1827259c0a2ca2daa309f0ddd95
+architecture.zh.md: d1629a088ed8d2fe61a7e20ab2ab1fd3709abd50

+ 2 - 0
docs/architecture.md

@@ -101,6 +101,8 @@ A **seam** is a swappable capability with three roles: a **Service Definition**
 
 Seams are why one provider swap changes the whole product. Filesystem and subprocess providers share one execution world, so pointing them at a remote sandbox moves Bash, PTY, and LSP with them, with no provider forks. [Subagent providers](subsystems/subagent.md) vary just as widely behind one interface, from a fresh child agent to a delegated turn in another product.
 
+[Experimental Agent Teams](subsystems/team.md) is a private opt-in coordination seam on `ctx.teams`, with a durable roster, task board, and mailbox layered over continuable subagents.
+
 ## Where new behavior goes
 
 New behavior attaches to a documented extension point. Changing the loop itself updates this map.

+ 2 - 0
docs/architecture.zh.md

@@ -105,6 +105,8 @@ turn/end
 
 seam 正是替换一个提供方就能改变整个产品的原因。文件系统与进程提供方共享同一个执行世界,因此把它们指向远程沙箱,也就把 Bash、PTY 和 LSP 一并搬了过去,无需提供方专用 fork。[subagent 提供方](subsystems/subagent.md)在同一个接口之后同样千差万别,从新建一个子 agent,到把一个轮次委派给另一个产品。
 
+[实验性 Agent Teams](subsystems/team.md) 是 `ctx.teams` 上的私有显式启用协作 seam,在可继续 subagent 之上提供持久 roster、任务板和 mailbox。
+
 ## 新行为的归属位置
 
 新行为附加到已有文档记录的扩展点。改动循环本身时,本映射随之更新。

+ 2 - 2
docs/capability-seams.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/capability-seams.md
-capability-seams.md: a990a9dd4d92d10e37b82e6a63caa4a5a469c441
-capability-seams.zh.md: 441d9222835e67c4a9f657d2335860005934094b
+capability-seams.md: 710c399510b6b123123e0a9586d86bfc3a96dff9
+capability-seams.zh.md: e0559d464ba1ecd2160eaba40c24bfd829e4a0d6

+ 6 - 0
docs/capability-seams.md

@@ -157,6 +157,9 @@ flowchart LR
   pkg_subagent_dsh_sdk["subagent-dsh-sdk"]
   pkg_tool_subagent_control["tool-subagent-control"]
   pkg_tool_ralph["tool-ralph"]
+  pkg_team["team"]
+  svc_teams["ctx.teams<br/>Agent Teams coordination domain"]
+  pkg_tool_team["tool-team"]
   pkg_jobs["jobs"]
   svc_jobs["ctx.jobs<br/>Background job registry"]
   pkg_jobs_local["jobs-local"]
@@ -279,6 +282,7 @@ flowchart LR
   pkg_subprocess_e2b --> svc_subprocess
   pkg_subprocess_local --> svc_subprocess
   pkg_system_prompt --> svc_systemPrompt
+  pkg_team --> svc_teams
   pkg_terminal --> svc_terminals
   pkg_terminal_bash --> svc_terminals
   pkg_token_meter --> svc_tokenMeter
@@ -383,6 +387,7 @@ flowchart LR
   svc_systemPrompt --> pkg_tool_terminal
   svc_systemPrompt --> pkg_tool_web
   svc_systemPrompt --> pkg_tools
+  svc_teams --> pkg_tool_team
   svc_terminals --> pkg_tool_terminal
   svc_tokenMeter --> pkg_compaction_basic
   svc_toolResultPruner --> pkg_compaction_basic
@@ -456,6 +461,7 @@ flowchart LR
 | `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-e2b`](../packages/e2b/fs-e2b) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-observation-policy contributes observed-state checks through the fs/* event gate. |
 | `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; there is no model-facing compact tool. |
 | `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. |
+| `ctx.teams` | `core` | [`team`](../packages/experimental/team) | - | [`tool-team`](../packages/experimental/tool-team) | - | Owns the implicit-root roster, durable peer mailbox, shared task DAG, and continuable-child lifecycle; tool-team contributes the scoped model policy and controls. |
 | `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | - | Producers (background bash, PTY sends, and subagent delegations) register running work; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry. |
 | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
 | `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. |

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

@@ -159,6 +159,9 @@ flowchart LR
   pkg_subagent_dsh_sdk["subagent-dsh-sdk"]
   pkg_tool_subagent_control["tool-subagent-control"]
   pkg_tool_ralph["tool-ralph"]
+  pkg_team["team"]
+  svc_teams["ctx.teams<br/>Agent Teams coordination domain"]
+  pkg_tool_team["tool-team"]
   pkg_jobs["jobs"]
   svc_jobs["ctx.jobs<br/>Background job registry"]
   pkg_jobs_local["jobs-local"]
@@ -281,6 +284,7 @@ flowchart LR
   pkg_subprocess_e2b --> svc_subprocess
   pkg_subprocess_local --> svc_subprocess
   pkg_system_prompt --> svc_systemPrompt
+  pkg_team --> svc_teams
   pkg_terminal --> svc_terminals
   pkg_terminal_bash --> svc_terminals
   pkg_token_meter --> svc_tokenMeter
@@ -385,6 +389,7 @@ flowchart LR
   svc_systemPrompt --> pkg_tool_terminal
   svc_systemPrompt --> pkg_tool_web
   svc_systemPrompt --> pkg_tools
+  svc_teams --> pkg_tool_team
   svc_terminals --> pkg_tool_terminal
   svc_tokenMeter --> pkg_compaction_basic
   svc_toolResultPruner --> pkg_compaction_basic
@@ -458,6 +463,7 @@ flowchart LR
 | `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-e2b`](../packages/e2b/fs-e2b) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs 通过 ctx.fs 执行读取/写入/编辑;fs-sandbox 按共享沙箱模式限制变更;fs-observation-policy 通过 fs/* 事件门禁贡献基于观测状态的检查。 |
 | `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 基础后端消费步骤后的压力事件和请求错误恢复事件;不存在面向模型的压缩工具。 |
 | `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | 提供方实现传输;该服务还负责可选的、基于 Activation 的延续编排,tool-subagent 选择一次性或可延续委派,tool-subagent-control 传递后续消息,而 tool-ralph 要求一条全新的结构化输出路由。 |
+| `ctx.teams` | `core` | [`team`](../packages/experimental/team) | - | [`tool-team`](../packages/experimental/tool-team) | - | 负责隐式 Root roster、持久 peer mailbox、共享任务 DAG 与 continuable child 生命周期;tool-team 提供作用域化模型策略和控制工具。 |
 | `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | - | 生产方(后台 bash、PTY 发送和 subagent 委派)登记正在运行的工作;tool-jobs 是面向模型的控制器,用于读取、列出和终止这些工作;jobs-local 是进程本地注册表。 |
 | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | 搜索和抓取提供方注册到同一个 ctx.web seam;tool-web 负责稳定的面向模型名称。 |
 | `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | 后端保存过大的工具文本,并返回面向模型的定位信息和取回提示;spill-policy 是 tools/post-execute 消费方,负责决定何时 spill。 |

+ 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: 6759d4b8e953c2a147a0441be21a25e0920cc1df
-config-catalog.zh.md: e59ad1ac07f46b7cb044a83012aa97d1a78f769b
+config-catalog.md: 3a471ea06e911d3d29ebef4ce80353b15a21cb43
+config-catalog.zh.md: f1b53d4a2b3c15abb3176c7af5de9c577f74825f

+ 72 - 5
docs/config-catalog.md

@@ -985,6 +985,13 @@ export interface PiAiProviderProfile {
   websocketConnectTimeoutMs?: number
   /** Maximum provider idle time while one stream read is outstanding. */
   streamIdleTimeoutMs?: number
+  /**
+   * Maximum base64-encoded image payload per request. When a request's
+   * accumulated images exceed it, the oldest images are replaced by text
+   * placeholders until the request fits, so a long session keeps completing
+   * requests instead of being rejected by a request-size cap.
+   */
+  maxRequestImageBytes?: number
   /** Provider-owned model-request retry policy; omission uses normal defaults. */
   retryPolicy?: RetryPolicyConfig
 }
@@ -1081,7 +1088,7 @@ type WithheldThinkingFormat = 'chat-template' | 'qwen-chat-template'
 
 Depends on: `Api` (`@earendil-works/pi-ai`) · `CacheRetention` (`@earendil-works/pi-ai`) · `Model` (`@earendil-works/pi-ai`) · `ModelThinkingLevel` (`@earendil-works/pi-ai`) · `OpenAICompletionsCompat` (`@earendil-works/pi-ai`) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · `ThinkingBudgets` (`@earendil-works/pi-ai`) · `Transport` (`@earendil-works/pi-ai`)
 
-Source: [`packages/llm/llm-pi-ai/src/config.ts:172`](../packages/llm/llm-pi-ai/src/config.ts)
+Source: [`packages/llm/llm-pi-ai/src/config.ts:192`](../packages/llm/llm-pi-ai/src/config.ts)
 
 <a id="deepseek-aidsh-llm-replay"></a>
 
@@ -2083,19 +2090,29 @@ Source: [`packages/subagent/subagent-acp/src/index.ts:27`](../packages/subagent/
 Requires: `subagents` · `subprocess`
 
 ```ts config-catalog
-/** Deployment-owned environment and process-release bound. */
+/** Deployment-owned permission, environment, and process-release settings. */
 export interface Config {
   /**
    * Explicit environment entries layered over the subprocess seam's
    * credential-scrubbed parent environment.
    */
   env?: Record<string, string>
+  /**
+   * Native non-interactive mode fixed for this Provider instance. Defaults to
+   * `dontAsk`; `acceptEdits` accepts edits, `auto` uses the native classifier,
+   * `plan` returns a plan without approving execution, and
+   * `bypassPermissions` explicitly skips permission checks.
+   */
+  permissionMode?: ClaudeCodePermissionMode
   /** Grace in milliseconds for Claude Code process-tree termination. */
   disposeGraceMs?: number
 }
+
+/** Profile-selectable non-interactive Claude Code permission mode. */
+export type ClaudeCodePermissionMode = typeof CLAUDE_CODE_PERMISSION_MODES[number]
 ```
 
-Source: [`packages/subagent/subagent-claude-code/src/index.ts:32`](../packages/subagent/subagent-claude-code/src/index.ts)
+Source: [`packages/subagent/subagent-claude-code/src/index.ts:35`](../packages/subagent/subagent-claude-code/src/index.ts)
 
 <a id="deepseek-aidsh-subagent-codex"></a>
 
@@ -2104,19 +2121,27 @@ Source: [`packages/subagent/subagent-claude-code/src/index.ts:32`](../packages/s
 Requires: `subagents` · `subprocess`
 
 ```ts config-catalog
-/** Deployment-owned environment and process-release bound. */
+/** Deployment-owned permission, environment, and process-release settings. */
 export interface Config {
   /**
    * Explicit environment entries layered over the subprocess seam's
    * credential-scrubbed parent environment.
    */
   env?: Record<string, string>
+  /** Native non-interactive permission mode fixed for this Provider instance. */
+  permissionMode?: CodexPermissionMode
   /** Grace in milliseconds for app-server process-tree termination. */
   disposeGraceMs?: number
 }
+
+/** Profile-selectable non-interactive Codex permission mode. */
+export type CodexPermissionMode =
+  | 'never'
+  | 'approve-for-me'
+  | 'dangerously-bypass-approvals-and-sandbox'
 ```
 
-Source: [`packages/subagent/subagent-codex/src/index.ts:30`](../packages/subagent/subagent-codex/src/index.ts)
+Source: [`packages/subagent/subagent-codex/src/index.ts:33`](../packages/subagent/subagent-codex/src/index.ts)
 
 <a id="deepseek-aidsh-subagent-dsh-sdk"></a>
 
@@ -2246,6 +2271,30 @@ export interface Config {
 
 Source: [`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
 
+<a id="deepseek-aidsh-team"></a>
+
+## `@deepseek-ai/dsh-team`
+
+Requires: `agents` · `sessions` · `sessionPersistence` · `subagents`
+
+```ts config-catalog
+/** Team-service deployment limits. */
+export interface Config {
+  /** Maximum immutable teammate names retained by one Team. */
+  readonly maxMembers?: number
+  /** Maximum non-deleted tasks retained by one Team. */
+  readonly maxTasks?: number
+  /** Maximum queued-minus-delivered messages for one target member. */
+  readonly maxPendingMessagesPerMember?: number
+  /** Maximum UTF-8 bytes in one complete sender-framed delivery. */
+  readonly maxMessageBytes?: number
+  /** Maximum milliseconds allowed for Team-owned runtime disposal. */
+  readonly disposalTimeoutMs?: number
+}
+```
+
+Source: [`packages/experimental/team/src/types.ts:125`](../packages/experimental/team/src/types.ts)
+
 <a id="deepseek-aidsh-terminal-bash"></a>
 
 ## `@deepseek-ai/dsh-terminal-bash`
@@ -2678,6 +2727,24 @@ Depends on: [`SubagentReportDelivery`](subsystems/subagent.md)
 
 Source: [`packages/subagent/tool-subagent-report/src/index.ts:27`](../packages/subagent/tool-subagent-report/src/index.ts)
 
+<a id="deepseek-aidsh-tool-team"></a>
+
+## `@deepseek-ai/dsh-tool-team`
+
+Requires: `agents` · `teams` · `tools` · `systemPrompt`
+
+```ts config-catalog
+/** Tool routing configuration. */
+export interface Config {
+  /** Continuable-subagent provider used for fresh teammates. */
+  readonly freshProvider?: string
+  /** Continuable-subagent provider used for completed-prefix fork teammates. */
+  readonly forkProvider?: string
+}
+```
+
+Source: [`packages/experimental/tool-team/src/index.ts:17`](../packages/experimental/tool-team/src/index.ts)
+
 <a id="deepseek-aidsh-tool-terminal"></a>
 
 ## `@deepseek-ai/dsh-tool-terminal`

+ 74 - 5
docs/config-catalog.zh.md

@@ -987,6 +987,13 @@ export interface PiAiProviderProfile {
   websocketConnectTimeoutMs?: number
   /** Maximum provider idle time while one stream read is outstanding. */
   streamIdleTimeoutMs?: number
+  /**
+   * Maximum base64-encoded image payload per request. When a request's
+   * accumulated images exceed it, the oldest images are replaced by text
+   * placeholders until the request fits, so a long session keeps completing
+   * requests instead of being rejected by a request-size cap.
+   */
+  maxRequestImageBytes?: number
   /** Provider-owned model-request retry policy; omission uses normal defaults. */
   retryPolicy?: RetryPolicyConfig
 }
@@ -1083,7 +1090,7 @@ type WithheldThinkingFormat = 'chat-template' | 'qwen-chat-template'
 
 依赖:`Api`(`@earendil-works/pi-ai`)· `CacheRetention`(`@earendil-works/pi-ai`)· `Model`(`@earendil-works/pi-ai`)· `ModelThinkingLevel`(`@earendil-works/pi-ai`)· `OpenAICompletionsCompat`(`@earendil-works/pi-ai`)· [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · `ThinkingBudgets`(`@earendil-works/pi-ai`)· `Transport`(`@earendil-works/pi-ai`)
 
-来源:[`packages/llm/llm-pi-ai/src/config.ts:172`](../packages/llm/llm-pi-ai/src/config.ts)
+来源:[`packages/llm/llm-pi-ai/src/config.ts:192`](../packages/llm/llm-pi-ai/src/config.ts)
 
 <a id="deepseek-aidsh-llm-replay"></a>
 
@@ -2085,19 +2092,29 @@ export type PermissionPolicy = 'allow' | 'reject'
 需要:`subagents` · `subprocess`
 
 ```ts config-catalog
-/** Deployment-owned environment and process-release bound. */
+/** Deployment-owned permission, environment, and process-release settings. */
 export interface Config {
   /**
    * Explicit environment entries layered over the subprocess seam's
    * credential-scrubbed parent environment.
    */
   env?: Record<string, string>
+  /**
+   * Native non-interactive mode fixed for this Provider instance. Defaults to
+   * `dontAsk`; `acceptEdits` accepts edits, `auto` uses the native classifier,
+   * `plan` returns a plan without approving execution, and
+   * `bypassPermissions` explicitly skips permission checks.
+   */
+  permissionMode?: ClaudeCodePermissionMode
   /** Grace in milliseconds for Claude Code process-tree termination. */
   disposeGraceMs?: number
 }
+
+/** Profile-selectable non-interactive Claude Code permission mode. */
+export type ClaudeCodePermissionMode = typeof CLAUDE_CODE_PERMISSION_MODES[number]
 ```
 
-来源:[`packages/subagent/subagent-claude-code/src/index.ts:32`](../packages/subagent/subagent-claude-code/src/index.ts)
+来源:[`packages/subagent/subagent-claude-code/src/index.ts:35`](../packages/subagent/subagent-claude-code/src/index.ts)
 
 <a id="deepseek-aidsh-subagent-codex"></a>
 
@@ -2106,19 +2123,27 @@ export interface Config {
 需要:`subagents` · `subprocess`
 
 ```ts config-catalog
-/** Deployment-owned environment and process-release bound. */
+/** Deployment-owned permission, environment, and process-release settings. */
 export interface Config {
   /**
    * Explicit environment entries layered over the subprocess seam's
    * credential-scrubbed parent environment.
    */
   env?: Record<string, string>
+  /** Native non-interactive permission mode fixed for this Provider instance. */
+  permissionMode?: CodexPermissionMode
   /** Grace in milliseconds for app-server process-tree termination. */
   disposeGraceMs?: number
 }
+
+/** Profile-selectable non-interactive Codex permission mode. */
+export type CodexPermissionMode =
+  | 'never'
+  | 'approve-for-me'
+  | 'dangerously-bypass-approvals-and-sandbox'
 ```
 
-来源:[`packages/subagent/subagent-codex/src/index.ts:30`](../packages/subagent/subagent-codex/src/index.ts)
+来源:[`packages/subagent/subagent-codex/src/index.ts:33`](../packages/subagent/subagent-codex/src/index.ts)
 
 <a id="deepseek-aidsh-subagent-dsh-sdk"></a>
 
@@ -2248,6 +2273,31 @@ export interface Config {
 
 来源:[`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
 
+<a id="deepseek-aidsh-team"></a>
+
+## `@deepseek-ai/dsh-team`
+
+需要:`agents` · `sessions` · `sessionPersistence` · `subagents`
+
+```ts config-catalog
+/** Team-service deployment limits. */
+export interface Config {
+  /** Maximum immutable teammate names retained by one Team. */
+  readonly maxMembers?: number
+  /** Maximum non-deleted tasks retained by one Team. */
+  readonly maxTasks?: number
+  /** Maximum queued-minus-delivered messages for one target member. */
+  readonly maxPendingMessagesPerMember?: number
+  /** Maximum UTF-8 bytes in one complete sender-framed delivery. */
+  readonly maxMessageBytes?: number
+  /** Maximum milliseconds allowed for Team-owned runtime disposal. */
+  readonly disposalTimeoutMs?: number
+}
+```
+
+来源:[`packages/experimental/team/src/types.ts:125`](../packages/experimental/team/src/types.ts)
+
+
 <a id="deepseek-aidsh-terminal-bash"></a>
 
 ## `@deepseek-ai/dsh-terminal-bash`
@@ -2680,6 +2730,25 @@ export interface Config {
 
 来源:[`packages/subagent/tool-subagent-report/src/index.ts:27`](../packages/subagent/tool-subagent-report/src/index.ts)
 
+<a id="deepseek-aidsh-tool-team"></a>
+
+## `@deepseek-ai/dsh-tool-team`
+
+需要:`agents` · `teams` · `tools` · `systemPrompt`
+
+```ts config-catalog
+/** Tool routing configuration. */
+export interface Config {
+  /** Continuable-subagent provider used for fresh teammates. */
+  readonly freshProvider?: string
+  /** Continuable-subagent provider used for completed-prefix fork teammates. */
+  readonly forkProvider?: string
+}
+```
+
+来源:[`packages/experimental/tool-team/src/index.ts:17`](../packages/experimental/tool-team/src/index.ts)
+
+
 <a id="deepseek-aidsh-tool-terminal"></a>
 
 ## `@deepseek-ai/dsh-tool-terminal`

+ 2 - 2
docs/event-producer-consumer.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
-event-producer-consumer.md: 6a79e6f7ce5addc64b10efa8da7a886dcfb36dc2
-event-producer-consumer.zh.md: f7576a8e28e4f1db2c65c324595c05c98b8fe488
+event-producer-consumer.md: c7b474a15c701781a70019f0703c0d60da87bcae
+event-producer-consumer.zh.md: 2e0667288ea44f83b7030e686db194f04d550a9b

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

@@ -9,8 +9,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | --- | --- | --- | --- | --- |
 | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
 | `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `apiproxy` |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule) |
-| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
+| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
+| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
 | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
 | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
 | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
@@ -18,8 +18,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
 | `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
 | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
-| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `apiproxy`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server` |
+| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`team`](../packages/experimental/team) |
+| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `apiproxy`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, [`team`](../packages/experimental/team) |
 | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:278`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
 | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/index.ts:30`](../packages/interaction/user-approval/src/index.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `apiproxy` |
 | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:72`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `apiproxy` |
@@ -39,8 +39,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
-| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
-| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team) |
+| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
 | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
 | `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
@@ -68,7 +68,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 
 | Event string | Dispatchers | Listeners |
 | --- | --- | --- |
-| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
+| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`team`](../packages/experimental/team), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
 | `internal/plugin` | - | `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), `webserver` |
 | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` |
 | `internal/status` | - | [`agent`](../packages/core/agent) |

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

@@ -11,8 +11,8 @@
 | --- | --- | --- | --- | --- |
 | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
 | `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `apiproxy` |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule) |
-| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
+| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), [`tool-team`](../packages/experimental/tool-team) |
+| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-team`](../packages/experimental/tool-team) |
 | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
 | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
 | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
@@ -20,8 +20,8 @@
 | `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
 | `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
 | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
-| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `apiproxy`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server` |
+| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`team`](../packages/experimental/team) |
+| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `apiproxy`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, [`team`](../packages/experimental/team) |
 | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:278`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
 | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/index.ts:30`](../packages/interaction/user-approval/src/index.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `apiproxy` |
 | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:72`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `apiproxy` |
@@ -41,8 +41,8 @@
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
-| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
-| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team) |
+| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`team`](../packages/experimental/team), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
 | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
 | `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
@@ -70,7 +70,7 @@
 
 | 事件字符串 | 派发方 | 监听方 |
 | --- | --- | --- |
-| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
+| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`team`](../packages/experimental/team), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
 | `internal/plugin` | - | `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), `webserver` |
 | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` |
 | `internal/status` | - | [`agent`](../packages/core/agent) |

+ 2 - 2
docs/module-graph.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/module-graph.md
-module-graph.md: 285caa985be5f2cf3cb363651d0e0a250c232873
-module-graph.zh.md: a571f53da1726637b88b95cdf07ab9e3fe952168
+module-graph.md: acc9af96bcb004acc3ce780294797df31587a6d6
+module-graph.zh.md: 0347a6f48c52e74b371d3ab7d94045dee23c4db7

+ 19 - 0
docs/module-graph.md

@@ -188,6 +188,10 @@ flowchart TD
     pkg_agent_spine_demo["agent-spine-demo"]
     pkg_sdk_jsonrpc_demo["sdk-jsonrpc-demo"]
   end
+  subgraph group_experimental["packages/experimental"]
+    pkg_team["team"]
+    pkg_tool_team["tool-team"]
+  end
   subgraph group_extensions["packages/extensions"]
     pkg_client_ui_cordis["client-ui-cordis"]
     pkg_cordis_client_runner["cordis-client-runner"]
@@ -998,6 +1002,13 @@ flowchart TD
   pkg_compaction_tool_result_pruner --> pkg_llm
   pkg_compaction_tool_result_pruner --> pkg_session
   pkg_compaction_tool_result_pruner --> pkg_token_meter
+  pkg_team --> pkg_agent
+  pkg_team --> pkg_brand
+  pkg_team --> pkg_invariants
+  pkg_team --> pkg_llm
+  pkg_team --> pkg_session
+  pkg_team --> pkg_session_persistence
+  pkg_team --> pkg_subagent
   pkg_tool_cordis --> pkg_agent
   pkg_tool_cordis --> pkg_cordis_host_runner
   pkg_tool_cordis --> pkg_invariants
@@ -1103,6 +1114,12 @@ flowchart TD
   pkg_agent_spine_demo --> pkg_tool_jobs
   pkg_agent_spine_demo --> pkg_tool_skill
   pkg_agent_spine_demo --> pkg_tools
+  pkg_tool_team --> pkg_agent
+  pkg_tool_team --> pkg_invariants
+  pkg_tool_team --> pkg_session
+  pkg_tool_team --> pkg_system_prompt
+  pkg_tool_team --> pkg_team
+  pkg_tool_team --> pkg_tools
   pkg_sdk_client --> pkg_invariants
   pkg_sdk_client --> pkg_llm
   pkg_sdk_client --> pkg_sdk_protocol
@@ -1560,6 +1577,7 @@ flowchart TD
 | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
 | [`web-app`](../packages/bundle/web-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
 | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
+| [`team`](../packages/experimental/team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent) |
 | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
@@ -1573,6 +1591,7 @@ flowchart TD
 | [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
 | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
+| [`tool-team`](../packages/experimental/tool-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`team`](../packages/experimental/team), [`tools`](../packages/core/tools) |
 | [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
 | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
 | [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |

+ 19 - 0
docs/module-graph.zh.md

@@ -190,6 +190,10 @@ flowchart TD
     pkg_agent_spine_demo["agent-spine-demo"]
     pkg_sdk_jsonrpc_demo["sdk-jsonrpc-demo"]
   end
+  subgraph group_experimental["packages/experimental"]
+    pkg_team["team"]
+    pkg_tool_team["tool-team"]
+  end
   subgraph group_extensions["packages/extensions"]
     pkg_client_ui_cordis["client-ui-cordis"]
     pkg_cordis_client_runner["cordis-client-runner"]
@@ -1000,6 +1004,13 @@ flowchart TD
   pkg_compaction_tool_result_pruner --> pkg_llm
   pkg_compaction_tool_result_pruner --> pkg_session
   pkg_compaction_tool_result_pruner --> pkg_token_meter
+  pkg_team --> pkg_agent
+  pkg_team --> pkg_brand
+  pkg_team --> pkg_invariants
+  pkg_team --> pkg_llm
+  pkg_team --> pkg_session
+  pkg_team --> pkg_session_persistence
+  pkg_team --> pkg_subagent
   pkg_tool_cordis --> pkg_agent
   pkg_tool_cordis --> pkg_cordis_host_runner
   pkg_tool_cordis --> pkg_invariants
@@ -1105,6 +1116,12 @@ flowchart TD
   pkg_agent_spine_demo --> pkg_tool_jobs
   pkg_agent_spine_demo --> pkg_tool_skill
   pkg_agent_spine_demo --> pkg_tools
+  pkg_tool_team --> pkg_agent
+  pkg_tool_team --> pkg_invariants
+  pkg_tool_team --> pkg_session
+  pkg_tool_team --> pkg_system_prompt
+  pkg_tool_team --> pkg_team
+  pkg_tool_team --> pkg_tools
   pkg_sdk_client --> pkg_invariants
   pkg_sdk_client --> pkg_llm
   pkg_sdk_client --> pkg_sdk_protocol
@@ -1562,6 +1579,7 @@ flowchart TD
 | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
 | [`web-app`](../packages/bundle/web-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
 | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
+| [`team`](../packages/experimental/team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent) |
 | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
@@ -1575,6 +1593,7 @@ flowchart TD
 | [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
 | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
+| [`tool-team`](../packages/experimental/tool-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`team`](../packages/experimental/team), [`tools`](../packages/core/tools) |
 | [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
 | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
 | [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |

+ 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: c400298f7d37c590918820bcbda10e6550f197e8
-persistence-catalog.zh.md: 65ec0e3fbdd226c51a371dc9a90f10db5c929c7a
+persistence-catalog.md: cf796ca322027886b1a0b78d69ac1d3a98d9459f
+persistence-catalog.zh.md: 5f82254813ef6fb0b3c9244a2bd03fb2177e5559

+ 59 - 0
docs/persistence-catalog.md

@@ -706,6 +706,65 @@ Source: [`packages/core/session/src/types.ts:254`](../packages/core/session/src/
 
 Source: [`packages/subagent/subagent/src/descriptor.ts:37`](../packages/subagent/subagent/src/descriptor.ts)
 
+### `team/*`
+
+<a id="teammember--log-only"></a>
+
+#### `team/member` — log-only
+
+```ts persistence-catalog
+/** Whole teammate lifecycle value, stored only in the Team Lead Session. */
+'team/member': { version: 1; teamId: TeamId; member: TeamMemberSnapshot }
+```
+
+Types: [TeamId](subsystems/team.md) · [TeamMemberSnapshot](subsystems/team.md)
+
+Source: [`packages/experimental/team/src/types.ts:206`](../packages/experimental/team/src/types.ts)
+
+<a id="teammessagedelivered--log-only"></a>
+
+#### `team/message/delivered` — log-only
+
+```ts persistence-catalog
+/** Durable acknowledgement that the target Session recorded the message. */
+'team/message/delivered': {
+  version: 1
+  teamId: TeamId
+  messageId: TeamMessageId
+  targetId: SessionId
+}
+```
+
+Types: [TeamId](subsystems/team.md) · [TeamMessageId](subsystems/team.md)
+
+Source: [`packages/experimental/team/src/types.ts:212`](../packages/experimental/team/src/types.ts)
+
+<a id="teammessagequeued--log-only"></a>
+
+#### `team/message/queued` — log-only
+
+```ts persistence-catalog
+/** Durable mailbox enqueue, stored before delivery is attempted. */
+'team/message/queued': { version: 1; teamId: TeamId; message: TeamMessageSnapshot }
+```
+
+Types: [TeamId](subsystems/team.md) · [TeamMessageSnapshot](subsystems/team.md)
+
+Source: [`packages/experimental/team/src/types.ts:210`](../packages/experimental/team/src/types.ts)
+
+<a id="teamtask--log-only"></a>
+
+#### `team/task` — log-only
+
+```ts persistence-catalog
+/** Whole shared-task value, stored only in the Team Lead Session. */
+'team/task': { version: 1; teamId: TeamId; task: TeamTaskSnapshot }
+```
+
+Types: [TeamId](subsystems/team.md) · [TeamTaskSnapshot](subsystems/team.md)
+
+Source: [`packages/experimental/team/src/types.ts:208`](../packages/experimental/team/src/types.ts)
+
 ### `todo/*`
 
 <a id="todowrite--log-only"></a>

+ 59 - 0
docs/persistence-catalog.zh.md

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

+ 2 - 2
docs/subsystems/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 docs/subsystems/README.md
-README.md: a1c2262f40aebdf4b0fdcdca4b0c05bae443fe2f
-README.zh.md: 0b5c00e6321a67a11dc6f98caedc7e26fb98eec2
+README.md: b3832e44d4fc2150efc4e81aab00d1006cb816b6
+README.zh.md: 5f5a44e9f21956921472c23a141eabac236704e9

+ 1 - 0
docs/subsystems/README.md

@@ -38,6 +38,7 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
 | [skills.md](skills.md) | the skill service: discovery priority, `SkillSummary`/`SkillDefinition`, session-prefix catalog, model-facing `skill` loading |
 | [compaction.md](compaction.md) | the compaction seam: the `compaction/*` session events, `CompactionResult`, the `CompactionEngine` interface |
 | [subagent.md](subagent.md) | the subagent seam: the named-provider registry, `SubagentStartRequest`/`Result`/`Run`, the start-time-vs-runtime capability split |
+| [team.md](team.md) | Agent Teams: implicit Lead identity, named continuable teammates, durable peer mailbox, and shared task DAG |
 | [web.md](web.md) | the web access seam: `WebSearchRequest`/`Result`, `WebFetchRequest`/`Result`, `WebFetchBody`, provider availability, `WebError` |
 | [spill.md](spill.md) | the spill storage seam: `SaveTextSpill`, `SpillOwner`/`SpillSource`, `SpillRef`, the branded `SpillLocator` |
 | [workflow.md](workflow.md) | the workflow seam: `WorkflowStartRequest`, `WorkflowMeta`, `WorkflowRun`/`Result`, the `workflow/*` event payloads, `WorkflowError` fatality |

+ 1 - 0
docs/subsystems/README.zh.md

@@ -38,6 +38,7 @@
 | [skills.md](skills.md) | skill(技能)服务:发现优先级、`SkillSummary`/`SkillDefinition`、会话前缀目录、面向模型的 `skill` 加载 |
 | [compaction.md](compaction.md) | 压缩(compaction)seam:`compaction/*` 会话事件、`CompactionResult`、`CompactionEngine` 接口 |
 | [subagent.md](subagent.md) | subagent seam:命名提供方注册表、`SubagentStartRequest`/`Result`/`Run`、启动时与运行时能力拆分 |
+| [team.md](team.md) | Agent Teams:隐式 Lead 身份、具名 continuable teammate、持久 peer mailbox 与共享任务 DAG |
 | [web.md](web.md) | Web 访问 seam:`WebSearchRequest`/`Result`、`WebFetchRequest`/`Result`、`WebFetchBody`、提供方可用性、`WebError` |
 | [spill.md](spill.md) | spill 存储 seam:`SaveTextSpill`、`SpillOwner`/`SpillSource`、`SpillRef`、品牌类型 `SpillLocator` |
 | [workflow.md](workflow.md) | 工作流 seam:`WorkflowStartRequest`、`WorkflowMeta`、`WorkflowRun`/`Result`、`workflow/*` 事件载荷、`WorkflowError` 致命性 |

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/subagent.md
-subagent.md: 00fa8faf2f6609d469f87f10aa7cbaa6e30b7eee
-subagent.zh.md: 8dc578d1a3b4456edac4888bdc6dd670b59f5cb8
+subagent.md: fee5f95d4a8810959e452653ecf9d7bcf36b7387
+subagent.zh.md: 38136f27172c071af95bf3b3cce7986af499368f

+ 20 - 1
docs/subsystems/subagent.md

@@ -307,7 +307,7 @@ type SubagentDescendantListEntry = SubagentListEntry & {
 
 ## The terminal result: `SubagentResult`
 
-The outcome of a one-shot run, resolved by `SubagentRun.result`. `structured` is present only after a requested `outputSchema` was successfully satisfied; requesting a schema does not guarantee it, and a provider may return `stopReason: 'error'` when the child fails or finishes without a valid capture. A non-`completed` `stopReason` means `output` may be partial — the consumer maps it to an `isError` tool result rather than reporting partial output as success.
+The outcome of a one-shot run, resolved by `SubagentRun.result`. `structured` is present only after a requested `outputSchema` was successfully satisfied; requesting a schema does not guarantee it, and a provider may return `stopReason: 'error'` when the child fails or finishes without a valid capture. A provider may attach a safe, non-assistant `diagnostic` to a non-`completed` result; the provider removes tool inputs, file contents, environment values, credentials, and raw protocol payloads and limits the complete value to 4096 UTF-8 bytes before consumers present it separately from `output`. A non-`completed` `stopReason` means `output` may be partial — the consumer maps it to an `isError` tool result rather than reporting partial output as success.
 
 ```ts type-equiv
 /**
@@ -330,6 +330,13 @@ interface SubagentResult {
    * schema-agnostic.
    */
   readonly structured?: unknown
+  /**
+   * Provider-authored, non-assistant failure detail for a non-`completed`
+   * result. Providers keep this text free of tool inputs, file contents,
+   * environment values, credentials, and raw protocol payloads, and limit it
+   * to 4096 UTF-8 bytes. Consumers present it separately from {@link output}.
+   */
+  readonly diagnostic?: string
   /** Why the run ended. A non-`completed` reason means `output` may be partial. */
   readonly stopReason: SubagentStopReason
 }
@@ -562,6 +569,18 @@ registerContinuableSetup(contribution: ContinuableSetupContribution): () => void
  */
 async drainContinuableDescendants(parents: readonly Agent[]): Promise<void>
 
+/**
+ * Release selected resident continuable direct children of one exact live
+ * parent. Other children of the same parent remain admitted and resident.
+ * Absent targets and a manager-less composition are accepted no-ops.
+ * @param parent - exact live direct parent authorizing the selected release.
+ * @param childIds - durable direct-child ids to release when resident.
+ * @returns once every selected Activation released its `AgentHandle`.
+ * @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a
+ *   different parent or the supplied parent identity is stale.
+ */
+async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void>
+
 /**
  * Enumerate the parent's direct session-backed subagents without loading or
  * resuming an Agent and without any query service: the listing merges the live

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

@@ -307,7 +307,7 @@ type SubagentDescendantListEntry = SubagentListEntry & {
 
 ## 终态结果:`SubagentResult`
 
-单次 run 的最终产出,由 `SubagentRun.result` resolve。`structured` 仅在请求了 `outputSchema` 且成功满足时才存在;请求 schema 不保证一定能得到它,当子 agent 失败或结束时未产出有效 capture 时,提供方可能返回 `stopReason: 'error'`。非 `completed` 的 `stopReason` 意味着 `output` 可能不完整——消费方将其映射为 `isError` 的工具结果,而非将部分输出报告为成功。
+单次 run 的最终产出,由 `SubagentRun.result` resolve。`structured` 仅在请求了 `outputSchema` 且成功满足时才存在;请求 schema 不保证一定能得到它,当子 agent 失败或结束时未产出有效 capture 时,提供方可能返回 `stopReason: 'error'`。提供方可以为非 `completed` 结果附带安全且不属于 assistant 内容的 `diagnostic`;在消费方将它与 `output` 分开呈现前,提供方会排除工具输入、文件内容、环境值、凭证与原始协议载荷,并把完整值限制在 4096 个 UTF-8 字节以内。非 `completed` 的 `stopReason` 意味着 `output` 可能不完整——消费方将其映射为 `isError` 的工具结果,而非将部分输出报告为成功。
 
 ```ts type-equiv
 /**
@@ -330,6 +330,13 @@ interface SubagentResult {
    * schema-agnostic.
    */
   readonly structured?: unknown
+  /**
+   * Provider-authored, non-assistant failure detail for a non-`completed`
+   * result. Providers keep this text free of tool inputs, file contents,
+   * environment values, credentials, and raw protocol payloads, and limit it
+   * to 4096 UTF-8 bytes. Consumers present it separately from {@link output}.
+   */
+  readonly diagnostic?: string
   /** Why the run ended. A non-`completed` reason means `output` may be partial. */
   readonly stopReason: SubagentStopReason
 }
@@ -564,6 +571,18 @@ registerContinuableSetup(contribution: ContinuableSetupContribution): () => void
  */
 async drainContinuableDescendants(parents: readonly Agent[]): Promise<void>
 
+/**
+ * Release selected resident continuable direct children of one exact live
+ * parent. Other children of the same parent remain admitted and resident.
+ * Absent targets and a manager-less composition are accepted no-ops.
+ * @param parent - exact live direct parent authorizing the selected release.
+ * @param childIds - durable direct-child ids to release when resident.
+ * @returns once every selected Activation released its `AgentHandle`.
+ * @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a
+ *   different parent or the supplied parent identity is stale.
+ */
+async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void>
+
 /**
  * Enumerate the parent's direct session-backed subagents without loading or
  * resuming an Agent and without any query service: the listing merges the live

+ 6 - 0
docs/subsystems/team.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 docs/subsystems/team.md
+team.md: 926434bb85d8cb3739e66fdf559c1c51d807613d
+team.zh.md: 7eeb4ef5aeeeee119ea931912390d4ff3d42bc85

+ 183 - 0
docs/subsystems/team.md

@@ -0,0 +1,183 @@
+# Agent Teams
+
+English | [中文](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/team/src/types.ts`](../../packages/experimental/team/src/types.ts).
+
+## Identity and roster
+
+`TeamId` is the root `SessionId` under a distinct [brand](core.md#branded-ids). `TeamTaskId` is Team-local and monotonically allocated as `task-<n>`; `TeamMessageId` is globally random. A teammate's Session id remains its persistent identity, while `name` is an immutable model/UI label.
+
+```ts type-equiv
+/** Whole durable value written on every teammate lifecycle change. */
+interface TeamMemberSnapshot {
+  readonly id: SessionId
+  readonly name: string
+  readonly description: string
+  readonly provider: string
+  readonly context: 'fresh' | 'fork'
+  readonly phase: TeamMemberPhase
+  readonly error?: string
+}
+```
+
+Every member starts in `provisioning` and reaches exactly one terminal roster phase, `active` or `failed`. Runtime `running`/`idle`/`inactive` status is derived separately and never rewrites this record.
+
+## Durable mailbox
+
+The Lead Session first stores the complete queued message. A target receipt is acknowledged only after its pending inbox item or recorded user message is durable, leaving queued-minus-delivered as the recovery mailbox.
+
+```ts type-equiv
+/** One peer message retained until its target Session records it. */
+interface TeamMessageSnapshot {
+  readonly id: TeamMessageId
+  readonly senderId: SessionId
+  readonly senderName: string
+  readonly targetId: SessionId
+  readonly delivery: 'quiet' | 'wakeup'
+  readonly content: ContentBlock[]
+}
+```
+
+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
+/** Source retained by the target Session for durable mailbox de-duplication. */
+interface TeamMessageSource {
+  readonly kind: 'team-message'
+  readonly teamId: TeamId
+  readonly messageId: TeamMessageId
+  readonly senderId: SessionId
+  readonly senderName: string
+}
+```
+
+## Shared task DAG
+
+Every task event stores a complete snapshot. `revision` is the compare-and-set value and increments by one per mutation. `blockedBy` edges must name non-deleted tasks and keep the graph acyclic. `writeScopes` are normalized advisory path prefixes rather than locks.
+
+```ts type-equiv
+/** Whole durable task snapshot; every mutation increments {@link revision}. */
+interface TeamTaskSnapshot {
+  readonly id: TeamTaskId
+  readonly revision: number
+  readonly subject: string
+  readonly description: string
+  readonly status: TeamTaskStatus
+  readonly ownerId?: SessionId
+  readonly blockedBy: TeamTaskId[]
+  readonly writeScopes: string[]
+}
+```
+
+`pending` is unstarted or released, `in_progress` carries an owner, `completed` satisfies blockers, and `deleted` is a retained tombstone. Views add owner name, readiness, and write-scope overlap warnings without changing the durable snapshot.
+
+## Replay
+
+`foldTeam()` replays one root Session into the roster, task board, and queued-minus-delivered mailbox that every Team operation reads. It selects records by `TeamId`, so events inherited by an ordinary fork retain the ancestor id and never enter the new root's state. Session event `seq` and `time` remain the ordering and timing record; Team snapshots do not duplicate them. Roster and task reads reach callers as views that add owner name, readiness, and write-scope warnings, while pending mail stays internal to delivery and recovery. The package [README](../../packages/experimental/team/README.md) owns operation, authorization, recovery, and limit behavior.
+
+<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
+
+<a id="cordis-surface"></a>
+
+## Cordis API
+
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+
+<a id="ctxteams--teamservice"></a>
+
+### `ctx.teams` — `TeamService`
+
+Agent Teams service backed by the exact live Lead Session log.
+
+```ts cordis-catalog
+/**
+ * Resolve one exact live Agent's Team role.
+ * @param agent - exact live Agent used as the authority credential.
+ * @returns its root, Team identity, role, and model-facing name.
+ */
+membership(agent: Agent): TeamMembership
+
+/**
+ * List the runtime-enriched roster visible to one Team member.
+ * @param agent - exact live Team member.
+ * @returns Lead and teammate rows in creation order.
+ */
+listMembers(agent: Agent): TeamMemberView[]
+
+/**
+ * Create one named, continuable direct child of the Team Lead.
+ * @param caller - exact live Lead Agent.
+ * @param request - immutable name, description, prompt, context mode, provider, and cancellation.
+ * @returns the active roster row.
+ */
+async spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise<SpawnTeammateResult>
+
+/**
+ * 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.
+ * @returns durable message identity and immediate-delivery observation.
+ */
+async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise<SendTeamMessageResult>
+
+/**
+ * Create one unowned pending task in the Team Lead log.
+ * @param caller - exact live Team member creating the task.
+ * @param request - task text, blockers, and advisory write scopes.
+ * @returns the revision-one task view.
+ */
+async createTask(caller: Agent, request: CreateTeamTaskRequest): Promise<TeamTaskView>
+
+/**
+ * Return one task, including a deleted tombstone.
+ * @param caller - exact live Team member reading the task.
+ * @param id - Team-local task identity.
+ * @returns the latest task value and derived readiness diagnostics.
+ */
+getTask(caller: Agent, id: TeamTaskId): TeamTaskView
+
+/**
+ * List current non-deleted tasks in numeric creation order.
+ * @param caller - exact live Team member reading the board.
+ * @returns detached current task views.
+ */
+listTasks(caller: Agent): TeamTaskView[]
+
+/**
+ * Compare-and-set one authorized task transition.
+ * @param caller - exact live Team member authorizing the mutation.
+ * @param request - task identity, expected revision, action, and action fields.
+ * @returns the committed next task revision.
+ */
+async updateTask(caller: Agent, request: UpdateTeamTaskRequest): Promise<TeamTaskView>
+
+/**
+ * Wait for the next Team-domain or member-status change.
+ * @param caller - exact live Team member waiting for activity.
+ * @param timeoutMs - bounded wait duration from ten seconds through one hour.
+ * @param signal - caller cancellation for the wait only.
+ * @returns one observed change or a timeout result.
+ */
+async waitForChange(caller: Agent, timeoutMs: number, signal: AbortSignal): Promise<TeamWaitResult>
+
+/**
+ * Interrupt one live teammate turn without clearing its pending inbox.
+ * @param caller - exact live Lead Agent.
+ * @param targetName - durable teammate name.
+ * @returns the target status sampled before cancellation.
+ */
+interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'idle' | 'inactive' }
+
+/**
+ * Resolve a caller without throwing, used by scoped-tool installation and observers.
+ * @param agent - candidate exact live Agent.
+ * @returns Team membership, or undefined for non-Team subagents and stale identities.
+ */
+tryMembership(agent: Agent): TeamMembership | undefined
+```
+
+Types: [Agent](core.md)
+
+Source: [`packages/experimental/team/src/index.ts:56`](../../packages/experimental/team/src/index.ts)
+<!-- END GENERATED cordis-surface -->

+ 183 - 0
docs/subsystems/team.zh.md

@@ -0,0 +1,183 @@
+# Agent Teams
+
+[English](team.md) | 中文
+
+实验性隐式 Root Team 领域、模型工具与宿主适配器共享的类型。[Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md)负责身份、mailbox、task 与共享 checkout 决策;本页记录 [`packages/experimental/team/src/types.ts`](../../packages/experimental/team/src/types.ts) 中的字面持久形式。
+
+## 身份与 roster
+
+`TeamId` 是具有独立[品牌](core.md#branded-ids)的 Root `SessionId`。`TeamTaskId` 在 Team 内按 `task-<n>` 单调分配;`TeamMessageId` 是全局随机值。teammate 的 Session id 始终是持久身份,而 `name` 是不可变的模型/UI 标签。
+
+```ts type-equiv
+/** Whole durable value written on every teammate lifecycle change. */
+interface TeamMemberSnapshot {
+  readonly id: SessionId
+  readonly name: string
+  readonly description: string
+  readonly provider: string
+  readonly context: 'fresh' | 'fork'
+  readonly phase: TeamMemberPhase
+  readonly error?: string
+}
+```
+
+每个 member 都从 `provisioning` 开始,并且只到达一个终态 roster phase:`active` 或 `failed`。运行时 `running`/`idle`/`inactive` 状态单独派生,绝不会重写该记录。
+
+## 持久 mailbox
+
+Lead Session 首先存储完整 queued message。只有 target 的 pending inbox 条目或已记录用户消息完成持久化,才会写入独立 acknowledgement event,queued-minus-delivered 因而构成恢复 mailbox。
+
+```ts type-equiv
+/** One peer message retained until its target Session records it. */
+interface TeamMessageSnapshot {
+  readonly id: TeamMessageId
+  readonly senderId: SessionId
+  readonly senderName: string
+  readonly targetId: SessionId
+  readonly delivery: 'quiet' | 'wakeup'
+  readonly content: ContentBlock[]
+}
+```
+
+target Session 会在 pending inbox 条目和最终用户消息上保留消息身份与发送者归因。跨 inbox 与历史折叠该 source 构成 target 侧去重键;模型可见的 framing 会重复 id 和发送者。
+
+```ts type-equiv
+/** Source retained by the target Session for durable mailbox de-duplication. */
+interface TeamMessageSource {
+  readonly kind: 'team-message'
+  readonly teamId: TeamId
+  readonly messageId: TeamMessageId
+  readonly senderId: SessionId
+  readonly senderName: string
+}
+```
+
+## 共享任务 DAG
+
+每条 task event 都存储完整快照。`revision` 是 compare-and-set 值,每次变更递增 1。`blockedBy` edge 必须指向未删除任务,并维持无环图。`writeScopes` 是规范化的提示性路径前缀,不是锁。
+
+```ts type-equiv
+/** Whole durable task snapshot; every mutation increments {@link revision}. */
+interface TeamTaskSnapshot {
+  readonly id: TeamTaskId
+  readonly revision: number
+  readonly subject: string
+  readonly description: string
+  readonly status: TeamTaskStatus
+  readonly ownerId?: SessionId
+  readonly blockedBy: TeamTaskId[]
+  readonly writeScopes: string[]
+}
+```
+
+`pending` 表示尚未开始或已经释放,`in_progress` 携带 owner,`completed` 满足 blocker,`deleted` 是保留的 tombstone。view 会添加 owner name、readiness 和 write-scope 重叠警告,但不会改变持久快照。
+
+## 回放
+
+`foldTeam()` 把一个 Root Session 回放成每个 Team 操作所读取的 roster、任务板与 queued-minus-delivered mailbox。它按 `TeamId` 选取记录,因此普通 fork 继承的 event 保留 ancestor id,绝不会进入新 Root 的状态。Session event 的 `seq` 与 `time` 继续负责顺序和时间记录,Team snapshot 不再重复保存它们。roster 与 task 读取以 view 形式到达调用方,附带 owner name、readiness 与 write-scope 警告,而 pending 邮件仅供投递与恢复内部使用。包 [README](../../packages/experimental/team/README.md)负责 operation、authorization、recovery 和限制行为。
+
+<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
+
+<a id="cordis-surface"></a>
+
+## Cordis API
+
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+
+<a id="ctxteams--teamservice"></a>
+
+### `ctx.teams` — `TeamService`
+
+Agent Teams service backed by the exact live Lead Session log.
+
+```ts cordis-catalog
+/**
+ * Resolve one exact live Agent's Team role.
+ * @param agent - exact live Agent used as the authority credential.
+ * @returns its root, Team identity, role, and model-facing name.
+ */
+membership(agent: Agent): TeamMembership
+
+/**
+ * List the runtime-enriched roster visible to one Team member.
+ * @param agent - exact live Team member.
+ * @returns Lead and teammate rows in creation order.
+ */
+listMembers(agent: Agent): TeamMemberView[]
+
+/**
+ * Create one named, continuable direct child of the Team Lead.
+ * @param caller - exact live Lead Agent.
+ * @param request - immutable name, description, prompt, context mode, provider, and cancellation.
+ * @returns the active roster row.
+ */
+async spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise<SpawnTeammateResult>
+
+/**
+ * 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.
+ * @returns durable message identity and immediate-delivery observation.
+ */
+async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise<SendTeamMessageResult>
+
+/**
+ * Create one unowned pending task in the Team Lead log.
+ * @param caller - exact live Team member creating the task.
+ * @param request - task text, blockers, and advisory write scopes.
+ * @returns the revision-one task view.
+ */
+async createTask(caller: Agent, request: CreateTeamTaskRequest): Promise<TeamTaskView>
+
+/**
+ * Return one task, including a deleted tombstone.
+ * @param caller - exact live Team member reading the task.
+ * @param id - Team-local task identity.
+ * @returns the latest task value and derived readiness diagnostics.
+ */
+getTask(caller: Agent, id: TeamTaskId): TeamTaskView
+
+/**
+ * List current non-deleted tasks in numeric creation order.
+ * @param caller - exact live Team member reading the board.
+ * @returns detached current task views.
+ */
+listTasks(caller: Agent): TeamTaskView[]
+
+/**
+ * Compare-and-set one authorized task transition.
+ * @param caller - exact live Team member authorizing the mutation.
+ * @param request - task identity, expected revision, action, and action fields.
+ * @returns the committed next task revision.
+ */
+async updateTask(caller: Agent, request: UpdateTeamTaskRequest): Promise<TeamTaskView>
+
+/**
+ * Wait for the next Team-domain or member-status change.
+ * @param caller - exact live Team member waiting for activity.
+ * @param timeoutMs - bounded wait duration from ten seconds through one hour.
+ * @param signal - caller cancellation for the wait only.
+ * @returns one observed change or a timeout result.
+ */
+async waitForChange(caller: Agent, timeoutMs: number, signal: AbortSignal): Promise<TeamWaitResult>
+
+/**
+ * Interrupt one live teammate turn without clearing its pending inbox.
+ * @param caller - exact live Lead Agent.
+ * @param targetName - durable teammate name.
+ * @returns the target status sampled before cancellation.
+ */
+interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'idle' | 'inactive' }
+
+/**
+ * Resolve a caller without throwing, used by scoped-tool installation and observers.
+ * @param agent - candidate exact live Agent.
+ * @returns Team membership, or undefined for non-Team subagents and stale identities.
+ */
+tryMembership(agent: Agent): TeamMembership | undefined
+```
+
+Types: [Agent](core.md)
+
+Source: [`packages/experimental/team/src/index.ts:56`](../../packages/experimental/team/src/index.ts)
+<!-- END GENERATED cordis-surface -->

+ 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: 02501d0721768d38c1e51bdfb87dcc79696bfd04
-tool-catalog.zh.md: 3583662dbaf72ea78bddb6a2b7d6b18360f3f3e1
+tool-catalog.md: b3f59ed76ad1a26a4da207c52bf0e64c40148a94
+tool-catalog.zh.md: 27ae60554393fc390386b7b0aef64f6d0758cf28

+ 317 - 0
docs/tool-catalog.md

@@ -36,6 +36,7 @@ This table connects model-visible tool names to the plugin package and service s
 | `@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-subagent-report` | `report` | `ctx.subagents`, `ctx.systemPrompt`, `a live continuable in-process child Agent` | `tool/call`, `tool/result`, `a user-role message in the direct parent session` | - | Registered per continuable in-process child rather than globally, so this schema is visible only inside such a child and survives its global `toolFilter`. The same contribution installs the child-scoped `tool:report` prompt section, which this catalog does not render. The parent-facing `send_message` tool is installed independently. |
 | `@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-tool-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.teams`, `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-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. |
@@ -1679,6 +1680,322 @@ Source: [`packages/jobs/tool-jobs/src/index.ts`](../packages/jobs/tool-jobs/src/
 
 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()`.
 
+<a id="deepseek-aidsh-tool-team"></a>
+
+## `@deepseek-ai/dsh-tool-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-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `interrupt_agent`
+
+Interrupt one teammate's current turn while preserving its pending inbox. Team Lead only.
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "target": {
+      "type": "string",
+      "description": "Teammate name."
+    }
+  },
+  "required": [
+    "target"
+  ]
+}
+```
+
+Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `list_agents`
+
+List the Lead and every durable teammate with current runtime status.
+
+```json
+{
+  "type": "object",
+  "properties": {}
+}
+```
+
+Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `send_message`
+
+Send durable information to another Team member without starting an idle member.
+
+```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-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `spawn_teammate`
+
+Create one named, durable teammate. Only the Team Lead may call this tool.
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "name": {
+      "type": "string",
+      "description": "Unique lower-kebab-case teammate name."
+    },
+    "description": {
+      "type": "string",
+      "description": "Short description of the delegated responsibility."
+    },
+    "prompt": {
+      "type": "string",
+      "description": "Complete initial task for the teammate."
+    },
+    "context": {
+      "type": "string",
+      "description": "fresh starts without Lead history; fork inherits completed Lead turns. Defaults to fresh.",
+      "enum": [
+        "fresh",
+        "fork"
+      ]
+    }
+  },
+  "required": [
+    "name",
+    "description",
+    "prompt"
+  ]
+}
+```
+
+Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `team_task_create`
+
+Create one unowned pending task on the shared Team task board.
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "subject": {
+      "type": "string",
+      "description": "Concise task title."
+    },
+    "description": {
+      "type": "string",
+      "description": "Complete task details and acceptance criteria."
+    },
+    "blocked_by": {
+      "type": "array",
+      "description": "Task ids that must complete first.",
+      "items": {
+        "type": "string"
+      }
+    },
+    "write_scopes": {
+      "type": "array",
+      "description": "Advisory workspace-relative file or directory prefixes this task expects to modify.",
+      "items": {
+        "type": "string"
+      }
+    }
+  },
+  "required": [
+    "subject",
+    "description"
+  ]
+}
+```
+
+Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `team_task_get`
+
+Read the complete latest value of one shared task before changing or executing it.
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "task_id": {
+      "type": "string",
+      "description": "Shared task id."
+    }
+  },
+  "required": [
+    "task_id"
+  ]
+}
+```
+
+Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `team_task_list`
+
+List shared tasks, including readiness, owner, revision, blockers, and write-scope warnings.
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "status": {
+      "type": "string",
+      "description": "Optional exact status filter.",
+      "enum": [
+        "pending",
+        "in_progress",
+        "completed"
+      ]
+    },
+    "owner": {
+      "type": "string",
+      "description": "Optional member-name filter; use unowned for tasks without an owner."
+    },
+    "ready": {
+      "type": "boolean",
+      "description": "Optional readiness filter."
+    },
+    "cursor": {
+      "type": "integer",
+      "description": "Zero-based result offset. Defaults to 0."
+    },
+    "limit": {
+      "type": "integer",
+      "description": "Number of rows, 1 through 100. Defaults to 50."
+    }
+  }
+}
+```
+
+Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `team_task_update`
+
+Compare-and-set a shared task action using the latest revision from team_task_get or team_task_list.
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "task_id": {
+      "type": "string",
+      "description": "Shared task id."
+    },
+    "expected_revision": {
+      "type": "integer",
+      "description": "Current task revision used as the CAS precondition."
+    },
+    "action": {
+      "type": "string",
+      "description": "Task transition to apply.",
+      "enum": [
+        "claim",
+        "release",
+        "edit",
+        "set_dependencies",
+        "complete",
+        "reopen",
+        "reassign",
+        "delete"
+      ]
+    },
+    "subject": {
+      "type": "string",
+      "description": "Replacement title for edit."
+    },
+    "description": {
+      "type": "string",
+      "description": "Replacement details for edit."
+    },
+    "blocked_by": {
+      "type": "array",
+      "description": "Complete blocker list for set_dependencies.",
+      "items": {
+        "type": "string"
+      }
+    },
+    "write_scopes": {
+      "type": "array",
+      "description": "Replacement advisory write scopes for edit.",
+      "items": {
+        "type": "string"
+      }
+    },
+    "owner": {
+      "type": "string",
+      "description": "Member name for Lead-only reassign; omit to unassign."
+    }
+  },
+  "required": [
+    "task_id",
+    "expected_revision",
+    "action"
+  ]
+}
+```
+
+Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `wait_agent`
+
+Wait for the next teammate status, mailbox, or shared-task change after this call starts. This never wakes inactive members and returns noProgress immediately when no other member is running or provisioning. Re-list after wakeup or timeout instead of polling.
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "timeout_ms": {
+      "type": "integer",
+      "description": "Wait duration in milliseconds, from 10000 through 3600000. Defaults to 30000."
+    }
+  }
+}
+```
+
+Source: [`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-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.
+
 <a id="deepseek-aidsh-tool-todo"></a>
 
 ## `@deepseek-ai/dsh-tool-todo`

+ 316 - 0
docs/tool-catalog.zh.md

@@ -38,6 +38,7 @@
 | `@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-subagent-report` | `report` | `ctx.subagents`、`ctx.systemPrompt`、`a live continuable in-process child Agent` | `tool/call`、`tool/result`、`a user-role message in the direct parent session` | - | 按可继续的进程内子级注册,而非全局注册,因此该 schema 仅在这种子级内部可见,并且不受其全局 `toolFilter` 影响。同一份贡献还会安装子级作用域的 `tool:report` 系统提示词 section,本目录不渲染该 section。面向父级的 `send_message` 工具单独安装。 |
 | `@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-tool-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.teams`、`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-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 在更换后端时保持稳定。 |
@@ -1685,6 +1686,321 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
 
 <a id="deepseek-aidsh-tool-todo"></a>
 
+## `@deepseek-ai/dsh-tool-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-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `interrupt_agent`
+
+中断一名 teammate 的当前 turn,同时保留其待处理 inbox。仅 Team Lead 可用。
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "target": {
+      "type": "string",
+      "description": "Teammate name."
+    }
+  },
+  "required": [
+    "target"
+  ]
+}
+```
+
+来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `list_agents`
+
+列出 Lead 与所有持久 teammate,以及各自当前的运行时状态。
+
+```json
+{
+  "type": "object",
+  "properties": {}
+}
+```
+
+来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `send_message`
+
+向另一名 Team member 发送持久信息,但不启动 idle member。
+
+```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-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `spawn_teammate`
+
+创建一名具名、持久的 teammate。只有 Team Lead 可以调用此工具。
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "name": {
+      "type": "string",
+      "description": "Unique lower-kebab-case teammate name."
+    },
+    "description": {
+      "type": "string",
+      "description": "Short description of the delegated responsibility."
+    },
+    "prompt": {
+      "type": "string",
+      "description": "Complete initial task for the teammate."
+    },
+    "context": {
+      "type": "string",
+      "description": "fresh starts without Lead history; fork inherits completed Lead turns. Defaults to fresh.",
+      "enum": [
+        "fresh",
+        "fork"
+      ]
+    }
+  },
+  "required": [
+    "name",
+    "description",
+    "prompt"
+  ]
+}
+```
+
+来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `team_task_create`
+
+在共享 Team 任务板上创建一个无 owner 的 pending task。
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "subject": {
+      "type": "string",
+      "description": "Concise task title."
+    },
+    "description": {
+      "type": "string",
+      "description": "Complete task details and acceptance criteria."
+    },
+    "blocked_by": {
+      "type": "array",
+      "description": "Task ids that must complete first.",
+      "items": {
+        "type": "string"
+      }
+    },
+    "write_scopes": {
+      "type": "array",
+      "description": "Advisory workspace-relative file or directory prefixes this task expects to modify.",
+      "items": {
+        "type": "string"
+      }
+    }
+  },
+  "required": [
+    "subject",
+    "description"
+  ]
+}
+```
+
+来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `team_task_get`
+
+在修改或执行共享任务前,读取其完整的最新值。
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "task_id": {
+      "type": "string",
+      "description": "Shared task id."
+    }
+  },
+  "required": [
+    "task_id"
+  ]
+}
+```
+
+来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `team_task_list`
+
+列出共享任务,包括 readiness、owner、revision、blocker 与 write-scope warning。
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "status": {
+      "type": "string",
+      "description": "Optional exact status filter.",
+      "enum": [
+        "pending",
+        "in_progress",
+        "completed"
+      ]
+    },
+    "owner": {
+      "type": "string",
+      "description": "Optional member-name filter; use unowned for tasks without an owner."
+    },
+    "ready": {
+      "type": "boolean",
+      "description": "Optional readiness filter."
+    },
+    "cursor": {
+      "type": "integer",
+      "description": "Zero-based result offset. Defaults to 0."
+    },
+    "limit": {
+      "type": "integer",
+      "description": "Number of rows, 1 through 100. Defaults to 50."
+    }
+  }
+}
+```
+
+来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `team_task_update`
+
+使用 team_task_get 或 team_task_list 返回的最新 revision,对共享任务操作执行 compare-and-set。
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "task_id": {
+      "type": "string",
+      "description": "Shared task id."
+    },
+    "expected_revision": {
+      "type": "integer",
+      "description": "Current task revision used as the CAS precondition."
+    },
+    "action": {
+      "type": "string",
+      "description": "Task transition to apply.",
+      "enum": [
+        "claim",
+        "release",
+        "edit",
+        "set_dependencies",
+        "complete",
+        "reopen",
+        "reassign",
+        "delete"
+      ]
+    },
+    "subject": {
+      "type": "string",
+      "description": "Replacement title for edit."
+    },
+    "description": {
+      "type": "string",
+      "description": "Replacement details for edit."
+    },
+    "blocked_by": {
+      "type": "array",
+      "description": "Complete blocker list for set_dependencies.",
+      "items": {
+        "type": "string"
+      }
+    },
+    "write_scopes": {
+      "type": "array",
+      "description": "Replacement advisory write scopes for edit.",
+      "items": {
+        "type": "string"
+      }
+    },
+    "owner": {
+      "type": "string",
+      "description": "Member name for Lead-only reassign; omit to unassign."
+    }
+  },
+  "required": [
+    "task_id",
+    "expected_revision",
+    "action"
+  ]
+}
+```
+
+来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+### `wait_agent`
+
+等待本次调用开始后下一次 teammate 状态、mailbox 或共享任务变更。它绝不会唤醒 inactive member;若没有其他 member 正在 running 或 provisioning,则立即返回 noProgress。唤醒或超时后应重新列出状态,而不是轮询。
+
+```json
+{
+  "type": "object",
+  "properties": {
+    "timeout_ms": {
+      "type": "integer",
+      "description": "Wait duration in milliseconds, from 10000 through 3600000. Defaults to 30000."
+    }
+  }
+}
+```
+
+来源:[`packages/experimental/tool-team/src/index.ts`](../packages/experimental/tool-team/src/index.ts)
+
+这 10 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。
+
+
 ## `@deepseek-ai/dsh-tool-todo`
 
 ### `todo_write`

+ 4 - 0
examples/acp-agent/product-subagent-both.cordis.snapshot.yml

@@ -20,8 +20,12 @@
                     - id: deepseek-v4-pro
           - id: subagent-codex
             name: '@deepseek-ai/dsh-subagent-codex'
+            config:
+              permissionMode: approve-for-me
           - id: subagent-claude-code
             name: '@deepseek-ai/dsh-subagent-claude-code'
+            config:
+              permissionMode: acceptEdits
           - id: tool-subagent-codex
             name: '@deepseek-ai/dsh-tool-subagent'
             config:

+ 4 - 0
examples/acp-agent/product-subagent-both.cordis.yml

@@ -9,8 +9,12 @@
       - insert:
           - id: subagent-codex
             name: '@deepseek-ai/dsh-subagent-codex'
+            config:
+              permissionMode: approve-for-me
           - id: subagent-claude-code
             name: '@deepseek-ai/dsh-subagent-claude-code'
+            config:
+              permissionMode: acceptEdits
           - id: tool-subagent-codex
             name: '@deepseek-ai/dsh-tool-subagent'
             config:

+ 2 - 0
examples/acp-agent/product-subagent-codex.cordis.snapshot.yml

@@ -20,6 +20,8 @@
                     - id: deepseek-v4-pro
           - id: subagent-codex
             name: '@deepseek-ai/dsh-subagent-codex'
+            config:
+              permissionMode: approve-for-me
           - id: tool-subagent-codex
             name: '@deepseek-ai/dsh-tool-subagent'
             config:

+ 2 - 0
examples/acp-agent/product-subagent-codex.cordis.yml

@@ -9,6 +9,8 @@
       - insert:
           - id: subagent-codex
             name: '@deepseek-ai/dsh-subagent-codex'
+            config:
+              permissionMode: approve-for-me
           - id: tool-subagent-codex
             name: '@deepseek-ai/dsh-tool-subagent'
             config:

+ 29 - 0
examples/acp-agent/subagent-result-diagnostic.cordis.snapshot.yml

@@ -0,0 +1,29 @@
+# Keyless twin of subagent-result-diagnostic.cordis.yml: keep the same test
+# provider/tool and replace only the external model adapter.
+- id: base
+  name: '@deepseek-ai/cordis-plugin-include'
+  config:
+    path: ./cordis.yml
+    patches:
+      - insert:
+          - id: llm-replay
+            name: '@deepseek-ai/dsh-llm-replay'
+            config:
+              providers:
+                - id: deepseek-official
+                  name: DeepSeek
+                  models:
+                    - id: deepseek-v4-flash
+                    - id: deepseek-v4-pro
+          - id: subagent-result-diagnostic
+            name: './tests/fixtures/subagent-result-diagnostic.ts'
+          - id: tool-subagent-codex
+            name: '@deepseek-ai/dsh-tool-subagent'
+            config:
+              provider: snapshot-diagnostic
+              toolName: subagent_codex
+              backgroundMode: one-shot
+              maxDepth: provider-managed
+      - id: llm-deepseek
+        name: '@deepseek-ai/dsh-llm-deepseek'
+        disabled: true

+ 17 - 0
examples/acp-agent/subagent-result-diagnostic.cordis.yml

@@ -0,0 +1,17 @@
+# Test-only product-shaped composition: mount a deterministic provider behind
+# the same one-shot tool schema as the public Codex example.
+- id: base
+  name: '@deepseek-ai/cordis-plugin-include'
+  config:
+    path: ./cordis.yml
+    patches:
+      - insert:
+          - id: subagent-result-diagnostic
+            name: './tests/fixtures/subagent-result-diagnostic.ts'
+          - id: tool-subagent-codex
+            name: '@deepseek-ai/dsh-tool-subagent'
+            config:
+              provider: snapshot-diagnostic
+              toolName: subagent_codex
+              backgroundMode: one-shot
+              maxDepth: provider-managed

+ 107 - 1
examples/acp-agent/tests/acp.snapshot.ts

@@ -1,11 +1,19 @@
 import { fileURLToPath } from 'node:url'
 import { readFileSync } from 'node:fs'
 import { spawnSync } from 'node:child_process'
+import { createServer } from 'node:http'
+import type { IncomingMessage, ServerResponse } from 'node:http'
 import { mkdir, utimes, writeFile } from 'node:fs/promises'
 import { dirname, join } from 'node:path'
 import { homedir } from 'node:os'
 import { expect, it } from 'vitest'
-import { defineAcpSnapshotSuite, type Scenario, type SnapshotSuiteOptions } from '@deepseek-ai/dsh-acp-snapshot'
+import {
+  defineAcpSnapshotSuite,
+  runScenario,
+  type InputScript,
+  type Scenario,
+  type SnapshotSuiteOptions,
+} from '@deepseek-ai/dsh-acp-snapshot'
 import { resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local'
 import { decodeStorageRecord } from '@deepseek-ai/dsh-session'
 
@@ -40,6 +48,7 @@ const ADVANCED_CONFIG = fileURLToPath(new URL('../advanced.cordis.yml', import.m
 const FS_CONFIG = fileURLToPath(new URL('../fs.cordis.yml', import.meta.url))
 const SESSION_QUERY_CONFIG = fileURLToPath(new URL('../session-query.cordis.yml', import.meta.url))
 const IMAGE_CONFIG = fileURLToPath(new URL('../image.cordis.yml', import.meta.url))
+const IMAGE_OFFLOAD_CONFIG = fileURLToPath(new URL('./fixtures/image-offload.cordis.yml', import.meta.url))
 const IMAGE_TEXT_ROUTE_CONFIG = fileURLToPath(new URL('../image-text-route.cordis.yml', import.meta.url))
 const PTY_CONFIG = fileURLToPath(new URL('../pty.cordis.yml', import.meta.url))
 const DEPTH_TWO_CONFIG = fileURLToPath(new URL('../depth-two.cordis.yml', import.meta.url))
@@ -66,6 +75,9 @@ const BACKGROUND_TASK_ADMISSION_CONFIG = fileURLToPath(
 )
 const PRODUCT_SUBAGENT_CODEX_CONFIG = fileURLToPath(new URL('../product-subagent-codex.cordis.yml', import.meta.url))
 const PRODUCT_SUBAGENT_BOTH_CONFIG = fileURLToPath(new URL('../product-subagent-both.cordis.yml', import.meta.url))
+const PRODUCT_SUBAGENT_RESULT_DIAGNOSTIC_CONFIG = fileURLToPath(
+  new URL('../subagent-result-diagnostic.cordis.yml', import.meta.url),
+)
 const FS_DIFF_BOUND_CONFIG = fileURLToPath(new URL('./fs-diff-bound.cordis.yml', import.meta.url))
 const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
 const PACKED_CHUNKS_SOURCE = 'hook-cc-pretool-deny'
@@ -158,6 +170,14 @@ const SCENARIOS: Scenario[] = [
     systemPromptSource: 'product-subagent-codex',
     configPath: PRODUCT_SUBAGENT_BOTH_CONFIG,
   },
+  {
+    name: 'product-subagent-result-diagnostic',
+    hasModelTurn: true,
+    recorded: false,
+    overridden: true,
+    headerClass: 'product-subagent-codex',
+    configPath: PRODUCT_SUBAGENT_RESULT_DIAGNOSTIC_CONFIG,
+  },
   {
     name: 'session-title-after-turn',
     hasModelTurn: true,
@@ -655,6 +675,92 @@ defineAcpSnapshotSuite({
   hasPwsh,
 })
 
+it('pins pi-ai image offload in the request sent by the assembled app', async () => {
+  const requests: Record<string, unknown>[] = []
+  const server = createServer((request: IncomingMessage, response: ServerResponse) => {
+    let body = ''
+    request.setEncoding('utf8')
+    request.on('data', (chunk: string) => { body += chunk })
+    request.on('end', () => {
+      requests.push(JSON.parse(body) as Record<string, unknown>)
+      response.writeHead(200, { 'content-type': 'text/event-stream' })
+      response.end([
+        'data: {"choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":null}]}',
+        'data: {"choices":[{"delta":{"content":"DONE"},"index":0,"finish_reason":null}]}',
+        'data: {"choices":[{"delta":{},"index":0,"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}',
+        'data: [DONE]',
+        '',
+      ].join('\n\n'))
+    })
+  })
+  await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
+  const address = server.address()
+  if (address === null || typeof address === 'string') throw new Error('image-offload snapshot server has no port')
+
+  const image = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC'
+  const input: InputScript = {
+    steps: [
+      { op: 'initialize' },
+      { op: 'newSession' },
+      {
+        op: 'promptContent',
+        content: [
+          { type: 'text', text: 'Compare the older image ' },
+          { type: 'image', data: image, mimeType: 'image/png' },
+          { type: 'text', text: ' with the newer image ' },
+          { type: 'image', data: image, mimeType: 'image/png' },
+          { type: 'text', text: ', then reply with DONE.' },
+        ],
+      },
+    ],
+  }
+
+  try {
+    const result = await runScenario(input, {
+      agent: AGENT,
+      mode: 'record',
+      configPath: IMAGE_OFFLOAD_CONFIG,
+      fixtureFile: join(SNAPSHOTS_DIR, 'image-offload-request', 'session.jsonl'),
+      env: {
+        DSH_SNAPSHOT_API_KEY: 'snapshot-key',
+        DSH_SNAPSHOT_BASE_URL: `http://127.0.0.1:${address.port}/v1`,
+      },
+    })
+    expect(result.stderr).toBe('')
+    expect(requests).toHaveLength(1)
+    const messages = requests[0]?.messages as { content?: unknown }[] | undefined
+    const offloaded = messages?.find(message => JSON.stringify(message.content).includes('[image omitted'))
+    expect(offloaded?.content).toMatchInlineSnapshot(`
+      [
+        {
+          "text": "Compare the older image ",
+          "type": "text",
+        },
+        {
+          "text": "[image omitted to keep the request within its image limit; older images are omitted first. If this image is still needed, read its file again when a path is available; otherwise ask the user to attach it again.]",
+          "type": "text",
+        },
+        {
+          "text": " with the newer image ",
+          "type": "text",
+        },
+        {
+          "image_url": {
+            "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC",
+          },
+          "type": "image_url",
+        },
+        {
+          "text": ", then reply with DONE.",
+          "type": "text",
+        },
+      ]
+    `)
+  } finally {
+    await new Promise<void>(resolve => server.close(() => { resolve() }))
+  }
+}, 45_000)
+
 it('packed ACP fixture retains every chunk row kind without changing the logical session', () => {
   const source = fixtureRecords(PACKED_CHUNKS_SOURCE)
   const packed = fixtureRecords('packed-chunks')

+ 41 - 0
examples/acp-agent/tests/fixtures/image-offload.cordis.yml

@@ -0,0 +1,41 @@
+# Keyless assembled-request snapshot for pi-ai image offload. The local
+# provider endpoint is supplied by the snapshot test; the real attachment
+# store and ACP bridge carry two uploaded images into one model request.
+- id: base
+  name: '@deepseek-ai/cordis-plugin-include'
+  config:
+    path: ../../cordis.yml
+    patches:
+      - id: llm-deepseek
+        name: '@deepseek-ai/dsh-llm-deepseek'
+        disabled: true
+      - id: acp-agent
+        name: '@deepseek-ai/dsh-acp-demo'
+        config:
+          provider: snapshot
+          model: vision
+          persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
+          persistenceCompression: none
+          workspaceContext:
+            maxBytes: 65536
+          persona: |
+            You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}.
+
+            Keep answers brief and factual.
+      - insert:
+          - id: attachment-local
+            name: '@deepseek-ai/dsh-attachment-local'
+          - id: llm-pi-ai
+            name: '@deepseek-ai/dsh-llm-pi-ai'
+            config:
+              providers:
+                snapshot:
+                  apiKeyEnv: DSH_SNAPSHOT_API_KEY
+                  api: openai-completions
+                  baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL
+                  maxRequestImageBytes: 92
+                  models:
+                    - id: vision
+                      contextWindow: 32768
+                      maxTokens: 1024
+                      input: [text, image]

+ 50 - 0
examples/acp-agent/tests/fixtures/subagent-result-diagnostic.ts

@@ -0,0 +1,50 @@
+/** Deterministic provider for model-visible foreground and Job diagnostic snapshots. */
+
+import type { Context } from '@deepseek-ai/cordis'
+import {
+  NO_START_CAPABILITIES,
+  type ResolvedSubagentStartRequest,
+  type SubagentProvider,
+} from '@deepseek-ai/dsh-subagent'
+import { SessionId } from '@deepseek-ai/dsh-session'
+
+export const name = 'subagent-result-diagnostic'
+export const inject = ['subagents']
+
+const DIAGNOSTIC = 'Claude Code unattended decision (mode: dontAsk; request: tool permission; decision: denied): Claude Code denied the request before an interactive prompt'
+
+class DiagnosticProvider implements SubagentProvider {
+  readonly name = 'snapshot-diagnostic'
+  readonly capabilities = NO_START_CAPABILITIES
+  readonly inheritsParentContext = false
+  private starts = 0
+
+  async start(request: ResolvedSubagentStartRequest) {
+    if (request.signal.aborted) {
+      throw new Error('snapshot diagnostic provider start aborted')
+    }
+    const index = this.starts++
+    if (index > 1) {
+      throw new Error('snapshot diagnostic provider expected exactly two starts')
+    }
+    return {
+      id: SessionId(index === 0
+        ? '00000000-0000-4000-8000-0000000000d1'
+        : '00000000-0000-4000-8000-0000000000d2'),
+      localAgent: undefined,
+      result: Promise.resolve({
+        output: index === 0
+          ? [{ type: 'text' as const, text: 'partial assistant text' }]
+          : [],
+        diagnostic: DIAGNOSTIC,
+        stopReason: 'error' as const,
+      }),
+      dispose: async () => {},
+    }
+  }
+}
+
+/** Register the fixed provider behind the public Codex-shaped snapshot tool. */
+export function apply(ctx: Context): void {
+  ctx.subagents.registerProvider(new DiagnosticProvider())
+}

Some files were not shown because too many files changed in this diff