Ver código fonte

Merge remote-tracking branch 'origin/master' into worktree/code-diff-card

# Conflicts:
#	docs/event-producer-consumer.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/event-producer-consumer.zh.md
#	docs/module-graph.i18n.yaml
#	docs/module-graph.md
#	docs/module-graph.zh.md
#	pnpm-lock.yaml
creatixchu 2 semanas atrás
pai
commit
e5ac976e4b
100 arquivos alterados com 1879 adições e 261 exclusões
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
  2. 9 11
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
  3. 9 11
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.zh.md
  7. 6 0
      .agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.i18n.yaml
  8. 31 0
      .agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.md
  9. 31 0
      .agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.zh.md
  10. 6 0
      .agents/notes/implemented/architecture/2026-09-12-browser-use-provider-registration.i18n.yaml
  11. 53 0
      .agents/notes/implemented/architecture/2026-09-12-browser-use-provider-registration.md
  12. 53 0
      .agents/notes/implemented/architecture/2026-09-12-browser-use-provider-registration.zh.md
  13. 6 0
      .agents/notes/implemented/bug-fix/2026-09-11-incremental-terminal-retention.i18n.yaml
  14. 64 0
      .agents/notes/implemented/bug-fix/2026-09-11-incremental-terminal-retention.md
  15. 64 0
      .agents/notes/implemented/bug-fix/2026-09-11-incremental-terminal-retention.zh.md
  16. 2 2
      .agents/notes/implemented/feature/2026-06-30-interception-extension-points.i18n.yaml
  17. 1 1
      .agents/notes/implemented/feature/2026-06-30-interception-extension-points.md
  18. 1 1
      .agents/notes/implemented/feature/2026-06-30-interception-extension-points.zh.md
  19. 2 2
      .agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.i18n.yaml
  20. 1 1
      .agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md
  21. 1 1
      .agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md
  22. 2 2
      .agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.i18n.yaml
  23. 10 2
      .agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md
  24. 10 2
      .agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md
  25. 2 2
      .agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.i18n.yaml
  26. 2 0
      .agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md
  27. 2 0
      .agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.zh.md
  28. 2 2
      .agents/notes/rejected/simplification/2026-07-26-builtin-timer-promises-for-hand-rolled-sleeps.i18n.yaml
  29. 1 1
      .agents/notes/rejected/simplification/2026-07-26-builtin-timer-promises-for-hand-rolled-sleeps.md
  30. 1 1
      .agents/notes/rejected/simplification/2026-07-26-builtin-timer-promises-for-hand-rolled-sleeps.zh.md
  31. 1 0
      AGENTS.md
  32. 4 0
      THIRD_PARTY_NOTICES.md
  33. 14 0
      apps/web/tests/expected/sidebar-terminal/theme.expected.md
  34. 57 0
      apps/web/tests/sidebar-terminal.e2e.ts
  35. 3 0
      benchmarks/package.json
  36. 2 0
      benchmarks/terminal-io/session-adapter.ts
  37. 69 0
      benchmarks/terminal-io/terminal-io.bench.ts
  38. 106 0
      benchmarks/terminal-io/terminal-io.worker.ts
  39. 7 0
      benchmarks/tsdown.config.ts
  40. 2 2
      docs/architecture.i18n.yaml
  41. 2 0
      docs/architecture.md
  42. 2 0
      docs/architecture.zh.md
  43. 2 2
      docs/capability-seams.i18n.yaml
  44. 13 0
      docs/capability-seams.md
  45. 13 0
      docs/capability-seams.zh.md
  46. 2 2
      docs/config-catalog.i18n.yaml
  47. 75 1
      docs/config-catalog.md
  48. 75 1
      docs/config-catalog.zh.md
  49. 2 2
      docs/cookbook/extension-cookbook.i18n.yaml
  50. 1 1
      docs/cookbook/extension-cookbook.md
  51. 1 1
      docs/cookbook/extension-cookbook.zh.md
  52. 2 2
      docs/event-producer-consumer.i18n.yaml
  53. 17 18
      docs/event-producer-consumer.md
  54. 18 19
      docs/event-producer-consumer.zh.md
  55. 2 2
      docs/glossary.i18n.yaml
  56. 1 1
      docs/glossary.md
  57. 1 1
      docs/glossary.zh.md
  58. 2 2
      docs/module-graph.i18n.yaml
  59. 29 0
      docs/module-graph.md
  60. 29 0
      docs/module-graph.zh.md
  61. 2 2
      docs/subsystems/README.i18n.yaml
  62. 1 0
      docs/subsystems/README.md
  63. 1 0
      docs/subsystems/README.zh.md
  64. 6 0
      docs/subsystems/browser-use.i18n.yaml
  65. 67 0
      docs/subsystems/browser-use.md
  66. 67 0
      docs/subsystems/browser-use.zh.md
  67. 2 2
      docs/subsystems/core.i18n.yaml
  68. 25 43
      docs/subsystems/core.md
  69. 25 43
      docs/subsystems/core.zh.md
  70. 2 2
      docs/subsystems/subagent.i18n.yaml
  71. 1 1
      docs/subsystems/subagent.md
  72. 1 1
      docs/subsystems/subagent.zh.md
  73. 2 2
      docs/tool-catalog.i18n.yaml
  74. 248 0
      docs/tool-catalog.md
  75. 248 0
      docs/tool-catalog.zh.md
  76. 5 0
      package.json
  77. 2 2
      packages/README.i18n.yaml
  78. 1 0
      packages/README.md
  79. 1 0
      packages/README.zh.md
  80. 3 3
      packages/api/session-controller/tests/agent.host.spec.ts
  81. 1 1
      packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts
  82. 7 7
      packages/api/session-controller/tests/commands-upload-file.host.spec.ts
  83. 1 1
      packages/api/session-controller/tests/control-jobs.host.spec.ts
  84. 1 1
      packages/api/session-controller/tests/controller.host.spec.ts
  85. 10 10
      packages/api/session-controller/tests/session-cold.host.spec.ts
  86. 12 12
      packages/api/session-controller/tests/session-fork.host.spec.ts
  87. 5 5
      packages/api/session-controller/tests/session-list-blank.host.spec.ts
  88. 1 1
      packages/api/session-controller/tests/session-models.host.spec.ts
  89. 2 2
      packages/api/session-controller/tests/session-presets.host.spec.ts
  90. 9 9
      packages/api/session-controller/tests/session-rename.host.spec.ts
  91. 1 1
      packages/api/session-controller/tests/session-skills.host.spec.ts
  92. 2 2
      packages/api/terminal-controller/README.i18n.yaml
  93. 1 1
      packages/api/terminal-controller/README.md
  94. 1 1
      packages/api/terminal-controller/README.zh.md
  95. 1 1
      packages/api/terminal-controller/src/index.ts
  96. 6 0
      packages/browser-use/README.i18n.yaml
  97. 40 0
      packages/browser-use/README.md
  98. 40 0
      packages/browser-use/README.zh.md
  99. 6 0
      packages/browser-use/browser-use/README.i18n.yaml
  100. 90 0
      packages/browser-use/browser-use/README.md

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.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-12-agent-scope-runtime-design.md
-2026-07-12-agent-scope-runtime-design.md: cd916ab1bf9f4bb7a02cb5c6a8458630b757e09d
-2026-07-12-agent-scope-runtime-design.zh.md: a0db51d7129d744b96946782a920e99b0cd7b518
+2026-07-12-agent-scope-runtime-design.md: b9b2c837f560c2b8b98da37e3656caf298b705e2
+2026-07-12-agent-scope-runtime-design.zh.md: 4b94d1b8449bd619c47f6bee0712ec8ed0b39593

+ 9 - 11
.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md

@@ -119,30 +119,28 @@ Publication admits and announces resources in the order required by observers:
 1. Enter the session.
 2. Enter the agent.
 3. Announce `session/created`.
-4. Announce `agent/created`.
-5. Enable public driving.
-6. Emit `agent/session-start`.
-7. Start the driver.
+4. Await serial `agent/created` listeners.
+5. Release queued input to the driver.
 
-The agent never drives before both registries and creation notifications agree. A synchronous listener may veto or dispose an owner; the transaction records publication in progress and waits for that callback stack to unwind before teardown continues. Every creation announcement that begins has a matching disposal announcement during rollback.
+The agent never drives before both registries and creation listeners finish. A listener may reject or dispose an owner; the transaction retains the scope and session until dispatch settles before teardown continues. Every creation announcement that begins has a matching disposal announcement during rollback. The [awaited creation decision](2026-09-09-awaited-agent-creation.md) owns asynchronous initializer timing.
 
-The sequence diagram isolates the non-obvious race: a synchronous creation listener can request disposal while the publication call stack still owns both registry entries. Teardown must deactivate immediately but wait for that stack to unwind before stopping and detaching anything.
+A creation listener can request disposal while publication still owns both registry entries. Teardown deactivates immediately and waits for the awaited dispatch before stopping and detaching anything.
 
 ```mermaid
 sequenceDiagram
   participant Tx as AgentCreationTransaction
   participant Registries
-  participant Listener as Synchronous listener
+  participant Listener as Creation listener
   participant Driver
 
   Tx->>Tx: mark publication in progress
   Tx->>Registries: announce agent/created
-  Registries->>Listener: invoke inside the same call stack
+  Registries->>Listener: await listener
   Listener->>Tx: dispose reentrantly
   Tx->>Tx: deactivate, teardown waits for publication
   Tx-->>Listener: disposal request accepted
-  Listener-->>Registries: return
-  Registries-->>Tx: announcement unwound
+  Listener-->>Registries: settle
+  Registries-->>Tx: dispatch settled
   Tx->>Tx: resolve publication settlement
   Tx->>Driver: stop and drain
   Tx->>Registries: detach agent, then session
@@ -153,7 +151,7 @@ sequenceDiagram
 
 Every teardown request joins one memoized path. The order is:
 
-1. Deactivate creation or driving and let synchronous publication finish.
+1. Deactivate creation or driving and await creation dispatch.
 2. Stop and drain the driver, discarding any injection that remains pending.
 3. Detach the agent.
 4. Detach the session.

+ 9 - 11
.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md

@@ -119,30 +119,28 @@ Setup 接收完整的子上下文和确切的未发布 Agent,可以等待插
 1. 将会话写入注册表。
 2. 将 agent 写入注册表。
 3. 宣告 `session/created`。
-4. 宣告 `agent/created`。
-5. 启用公开驱动。
-6. 发射 `agent/session-start`。
-7. 启动 driver。
+4. 等待串行 `agent/created` 监听器。
+5. 向驱动器释放已排队输入。
 
-Agent 在两个注册表和创建通知都达成一致之前绝不驱动。同步监听器可以否决或 dispose 一个所有者;事务记录发布进行中,并等待该回调栈展开后再继续拆除。每个已开始的创建宣告在回滚期间都有匹配的销毁宣告。
+Agent 在两个注册表与创建监听器都完成前绝不驱动。监听器可以拒绝或 dispose 一个所有者;事务保留作用域与会话,等待分发结算后再继续拆除。每个已开始的创建宣告在回滚期间都有匹配的销毁宣告。[可等待创建决策](2026-09-09-awaited-agent-creation.zh.md)拥有异步初始化器时序。
 
-以下序列图隔离了非显而易见的竞态:同步创建监听器可以在发布调用栈仍拥有两个注册表条目时请求 dispose。拆除必须立即停用,但要等待该栈展开后才停止和分离任何东西。
+创建监听器可以在发布仍拥有两个注册表条目时请求 dispose。Teardown 会立即停用,并等待所调用的异步分发完成后才停止和分离资源。
 
 ```mermaid
 sequenceDiagram
   participant Tx as AgentCreationTransaction
   participant Registries
-  participant Listener as Synchronous listener
+  participant Listener as Creation listener
   participant Driver
 
   Tx->>Tx: mark publication in progress
   Tx->>Registries: announce agent/created
-  Registries->>Listener: invoke inside the same call stack
+  Registries->>Listener: await listener
   Listener->>Tx: dispose reentrantly
   Tx->>Tx: deactivate, teardown waits for publication
   Tx-->>Listener: disposal request accepted
-  Listener-->>Registries: return
-  Registries-->>Tx: announcement unwound
+  Listener-->>Registries: settle
+  Registries-->>Tx: dispatch settled
   Tx->>Tx: resolve publication settlement
   Tx->>Driver: stop and drain
   Tx->>Registries: detach agent, then session
@@ -153,7 +151,7 @@ sequenceDiagram
 
 每个拆除请求加入一条记忆化路径。顺序为:
 
-1. 停用创建或驱动,让同步发布完成。
+1. 停用创建或驱动,并等待创建分发。
 2. 停止并排空 driver,丢弃仍处于待处理状态的注入。
 3. 分离 agent。
 4. 分离会话。

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.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-session-end-seed-log-boundary.md
-2026-07-30-session-end-seed-log-boundary.md: 76f8904f75f6c4b5a1a6acab8d69ef2290be718e
-2026-07-30-session-end-seed-log-boundary.zh.md: 4310654c841305a7f0de2f46434d0f839085c482
+2026-07-30-session-end-seed-log-boundary.md: 9cf7c7f20701688208e8b1c343904f2a9f2cab9e
+2026-07-30-session-end-seed-log-boundary.zh.md: fdf4ec9de610f17e792de98334dd5d37f5e4c355

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md

@@ -8,7 +8,7 @@ English | [中文](2026-07-30-session-end-seed-log-boundary.zh.md)
 
 A plugin that owns a standalone open/close bracket in the session log cannot tell a dead marker from a live one. `compaction/start` … `compaction/end` is the shipped case: on picking up a log whose last compaction event is an unmatched `compaction/start`, "the previous writer died mid-compaction" and "a compaction is running right now" are byte-identical stored history. The owner must either refuse to compact a log that is actually free (wedging the session) or proceed over one that is genuinely busy.
 
-Nothing in the log marked where inherited history ended. `session/created`, `session/disposed`, and `session/flush` are cordis runtime signals, not log events; `agent/session-start` is emit-only. `Session.firstLiveSeq` already held the answer exactly — the seq of this lifecycle's first own write — but only in memory, so a consumer reading stored bytes could not see it.
+Nothing in the log marked where inherited history ended. `session/created`, `session/disposed`, and `session/flush` are cordis runtime signals, not log events; `agent/created` is also a non-durable runtime event. `Session.firstLiveSeq` already held the answer exactly — the seq of this lifecycle's first own write — but only in memory, so a consumer reading stored bytes could not see it.
 
 Crash repair does not close the gap and must not: `interruptedTurnClosers` synthesizes turn, step, and tool boundaries because core owns that vocabulary, and `compaction/*` belongs to the compaction seam. A core repair pass that closed plugin brackets would put every plugin's bracket semantics in core.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.zh.md

@@ -8,7 +8,7 @@ Status: implemented
 
 在会话日志中拥有独立开/闭括号的插件无法区分一个已死的标记和一个存活的标记。`compaction/start` … `compaction/end` 就是已发布的实例:当接手一份日志、而它最后的压缩(compaction)事件是一个未配对的 `compaction/start` 时,「上一个写入方在压缩中途死掉了」与「此刻正有一次压缩在运行」在存储历史中是逐字节相同的。该括号所有方只能二选一:拒绝压缩一份其实空闲的日志(把会话卡死),或者在一份确实繁忙的日志上继续压缩。
 
-日志中没有任何东西标出继承历史在哪里结束。`session/created`、`session/disposed` 与 `session/flush` 是 Cordis 运行时信号,不是日志事件;`agent/session-start` 只发射不落盘。`Session.firstLiveSeq` 本来就精确地持有这个答案——本生命周期第一次自有写入的 seq——但只存在于内存中,因此读取存储字节的消费方看不到它。
+日志中没有任何东西标出继承历史在哪里结束。`session/created`、`session/disposed` 与 `session/flush` 是 Cordis 运行时信号,不是日志事件;`agent/created` 同样是不落盘的运行时事件。`Session.firstLiveSeq` 本来就精确地持有这个答案——本生命周期第一次自有写入的 seq——但只存在于内存中,因此读取存储字节的消费方看不到它。
 
 崩溃修复既没有填上这个缺口,也不应该去填:`interruptedTurnClosers` 合成轮次、步骤与工具边界,是因为核心拥有那套词汇表,而 `compaction/*` 属于压缩 seam。一个会关闭插件括号的核心修复流程,等于把每个插件的括号语义都搬进核心。
 

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.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/architecture/2026-09-09-awaited-agent-creation.md
+2026-09-09-awaited-agent-creation.md: 27353225a8d99d038d5c61b1eea0fa67d9120a55
+2026-09-09-awaited-agent-creation.zh.md: f664e89da0794acd1d2a6c0c8a4820a7056dcac6

+ 31 - 0
.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.md

@@ -0,0 +1,31 @@
+# Agent Note: Awaited per-agent initialization
+
+Status: implemented
+
+English | [中文](2026-09-09-awaited-agent-creation.zh.md)
+
+## Problem
+
+Shared presets install tools and prompt sections separately for each Agent. That installation can await plugin activation, and external SessionStart hooks can produce context asynchronously. A creator must know that these contributions have finished before the Agent's first model request.
+
+## Decision
+
+`agent/created` is the serial initialization event after factory setup and registry entry. Each listener finishes before the next starts; a throw or rejection fails creation and skips later listeners. The payload retains `SessionStartSource` and accepts the factory's cancellation signal. `register()` and `announce()` are awaited by their callers. Lifecycle source selection belongs to factory publication through `announce()`; `register()` announces fresh startup.
+
+AgentLoop holds its existing maintenance activity through setup and creation dispatch. Input may enter the inbox during initialization, but the driver starts only after successful completion. Failure cancels that activity without waking queued input; ordered teardown owns inbox cleanup. Keeping these operations separate preserves the initialization error when another teardown has already removed the inbox projection.
+
+Creation dispatch retains the scope and Session while listeners await. Disposal cancels initialization and joins the dispatch before releasing those resources. A listener must not await its own Agent's idle state or its owner's disposal, because each waits for that listener to finish. Background work on another Agent follows that Agent's own initialization lifecycle.
+
+This decision owns asynchronous creation timing. The [scope runtime decision](2026-07-12-agent-scope-runtime-design.md) retains registry identity and teardown ownership, while the [interception decision](../feature/2026-06-30-interception-extension-points.md) retains policy and tool-event semantics.
+
+## Alternatives considered
+
+**A separate setup event.** Existing creation listeners already install per-agent contributions. A second initialization event splits that responsibility without a distinct consumer need.
+
+**Detached initialization.** Returning before plugin activation or hook context settles lets the first request omit required tools or context and disconnects initialization failure from the creator.
+
+## Consequences
+
+Creation latency includes asynchronous plugin and SessionStart work. Initializer failures become caller-visible creation failures, and cancellation relies on listeners settling cooperatively. Notifications already delivered cannot be undone; rollback pairs them with disposal notifications.
+
+Scoped installer tests verify rollback, lifecycle tests verify ordered completion and cancellation, and the SDK serial-created scenario verifies that asynchronous prompt context reaches the first request. Hook tests cover awaited context injection and subprocess disposal.

+ 31 - 0
.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.zh.md

@@ -0,0 +1,31 @@
+# Agent Note: 可等待的单个 agent 初始化
+
+Status: implemented
+
+[English](2026-09-09-awaited-agent-creation.md) | 中文
+
+## 问题
+
+共享 preset 为每个 Agent(智能体)分别安装工具与提示词段。安装可能要等待插件激活,外部 SessionStart 钩子也可能异步产生上下文。创建方必须知道这些贡献已完成,才能允许 Agent 发起首次模型请求。
+
+## 决策
+
+`agent/created` 是工厂 setup 完成且进入注册表后的串行初始化事件。每个监听器完成后才启动下一个;抛出或拒绝会使创建失败,并跳过后续监听器。载荷保留 `SessionStartSource`,并接受工厂的取消信号。调用方等待 `register()` 与 `announce()` 完成。生命周期来源由工厂发布时通过 `announce()` 指定;`register()` 宣告全新启动。
+
+AgentLoop 在 setup 与创建分发期间保留现有的维护活动。初始化期间输入可以进入收件箱,但只有成功完成后驱动器才会启动。失败会取消该活动而不唤醒已排队输入;有序 teardown 拥有收件箱清理。区分这两项操作,能够在另一条 teardown 已移除收件箱投影时保留初始化错误。
+
+创建分发在监听器等待期间保留作用域与 Session。Dispose(资源释放)会取消初始化,并在释放这些资源前等待分发结束。监听器不得等待自身 Agent 的空闲状态或自身所有者的 dispose,因为两者都要等待该监听器完成。另一个 Agent 上的后台工作遵循该 Agent 自己的初始化生命周期。
+
+本决策拥有异步创建时序。[作用域运行时决策](2026-07-12-agent-scope-runtime-design.zh.md)继续拥有注册表身份与 teardown 归属,[拦截决策](../feature/2026-06-30-interception-extension-points.zh.md)继续拥有策略与工具事件语义。
+
+## 考虑过的替代方案
+
+**单独的 setup 事件。** 现有创建监听器已经安装针对单个 agent 的贡献。第二个初始化事件会拆分这一职责,却没有独立的消费方需求。
+
+**分离运行的初始化。** 在插件激活或钩子上下文结算前返回,会让首次请求缺少所需工具或上下文,也会使初始化失败脱离创建方。
+
+## 后果
+
+创建延迟包括异步插件与 SessionStart 工作。初始化器失败成为调用方可见的创建失败,取消依赖监听器协作结算。已经送达的通知无法撤回;回滚会为其配对销毁通知。
+
+作用域安装器测试验证回滚,生命周期测试验证有序完成与取消,SDK serial-created 场景验证异步提示词上下文进入首次请求。钩子测试覆盖需等待的上下文注入与子进程 dispose。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-12-browser-use-provider-registration.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/architecture/2026-09-12-browser-use-provider-registration.md
+2026-09-12-browser-use-provider-registration.md: a0d3cfdbd798dadee6a9514dffdc619d95811c4d
+2026-09-12-browser-use-provider-registration.zh.md: b0016b6655292ccb5259adcb5a07decedda2e21e

+ 53 - 0
.agents/notes/implemented/architecture/2026-09-12-browser-use-provider-registration.md

@@ -0,0 +1,53 @@
+# Agent Note: Browser-use provider registration and Session ownership
+
+Status: implemented
+
+English | [中文](2026-09-12-browser-use-provider-registration.zh.md)
+
+## Problem
+
+Browser-control backends expose different operations and observation formats. A common browser action API would constrain those experiments before a portable consumer exists. Browser sessions can be isolated, while attaching an existing logged-in browser must preserve its state and prevent concurrent ownership inside a provider.
+
+## Decision
+
+[`dsh-browser-use`](../../../../packages/browser-use/browser-use/README.md) owns `ctx.browserUse`, which registers one provider-owned name and returns its effect disposer. A second registration fails regardless of its name. The service contains no browser object, shared operation type, dispatch method, resource lifecycle, or runtime selector. The [computer-use registration decision](2026-09-12-computer-use-provider-registration.md) remains the independent owner of desktop-provider registration and shared-desktop coordination.
+
+[Playwright MCP](../../../../packages/experimental/browser-use-playwright-mcp/README.md), [Chrome DevTools MCP](../../../../packages/experimental/browser-use-chrome-devtools-mcp/README.md), and [native Stagehand](../../../../packages/experimental/browser-use-stagehand-native/README.md) own their browser tools and integrate through the normal DSH tool pipeline. They are public experimental opt-ins. DSH owns task planning and the task loop; Stagehand contributes individual AI-assisted operations. Profile or preset configuration selects launch or attachment for each provider activation.
+
+Browser resources belong to the exact live Agent and Session, not merely a reusable Session id. Calls retain state across turns. Runtime disposal closes launched resources, and reload or fork does not inherit a launched profile. Attachment preserves existing browser state and reserves the external browser exclusively for one Session within that provider instance. Cleanup disconnects without closing the external browser.
+
+The [experimental runtime helper](../../../../packages/experimental/browser-use-runtime/README.md) owns shared resource lifetime and attachment reservation without introducing those methods into the browser-use service. Canceling a caller's acquisition wait leaves initialization and its reservation owned by the Session. An active operation's Agent-disposal cancellation starts resource shutdown before the Agent waits for idle, because browser calls may settle only after their connections close. Provider teardown stops tool admission and retains its registration until resource cleanup and active calls settle. The service remains independent of all experimental packages.
+
+Stagehand's launcher inherits its process environment, and SDK initialization can time out before its cleanup settles. The provider host uses `@puppeteer/browsers` to own launched Chromium and its temporary profile before CDP or SDK readiness, with a scrubbed child environment. For both launch and attachment, an isolated Worker runs the SDK and only connects over CDP. Native inference has no abort signal. SDK close waits for active work; successful cleanup permits later reconnection while retaining browser state. Failed SDK drain blocks reuse while Chromium remains alive. Final cleanup can release a launched-browser reservation after owned Chromium and its Worker terminate. Failed SDK drain during attachment, failed Worker termination, or failed owned-process cleanup retains the reservation. The host kills only its own Chromium process and waits for child closure before removing the profile; an externally owned browser remains running.
+
+Stagehand uses an explicitly configured native model from its pinned SDK catalog. The configured API key and optional headers are forwarded into the browser extension, where Stagehand performs inference. Browser tool inputs and returned data, including SDK result metadata, use the existing Session log. DSH model routing, credential reuse, underlying inference request/response capture, and integration into Session usage accounting remain deferred; this integration adds no persistence events or Session schema changes.
+
+MCP client activation waits for connection and tool discovery, but provider activation can finish before any Session exists. That activation promise cannot represent the clients owned by future Sessions. Each MCP browser provider awaits one client startup attempt within the existing serial `agent/created` event. The [awaited Agent creation decision](2026-09-09-awaited-agent-creation.md) owns queued-input ordering and creation rollback. Successful creation or resume exposes the completed catalog to prompt assembly and direct callers.
+
+Startup failure or cancellation rejects creation or resume and triggers rollback of the Agent and its client resources. A busy attachment skips startup permanently for the activation while its other work continues; a newly created or resumed Agent can acquire the attachment after release. Late installation and reload apply only to future activations, following the [Schedule mounting policy](../../../../packages/schedule/schedule/README.md#use-this-package). Browser tools and resource requests for a successful client share the Session queue; other Sessions cannot execute those requests or receive that server's instructions.
+
+## Alternatives considered
+
+**Unified browser action API.** Playwright, Chrome DevTools, and Stagehand have different native semantics. No current consumer requires interchangeable action methods, so provider-owned tools retain those semantics.
+
+**A provider-owned startup phase.** Existing serial `agent/created` awaits Session-owned setup and makes failures visible to the creator. A separate maintenance task would duplicate that lifecycle ownership.
+
+**Discovery during prompt assembly.** Prompt and tool collection need ready registrations. Starting discovery there either exposes an incomplete catalog or requires another collection pass; awaited creation completes discovery before a turn begins.
+
+**One shared browser across Sessions.** Browser tabs, navigation, and login state can be isolated per Session. Sharing them would introduce cross-Session interference that the desktop integrations cannot generally avoid.
+
+**Fresh context when attaching.** A new browser context does not inherit the existing logged-in state. Exclusive use of the attached browser preserves the workflow that attachment enables.
+
+**Browser profiles restored with Sessions.** Durable browser state introduces profile storage and migration ownership beyond the Session log. Launched state lasts only for the live runtime; externally owned browsers retain their own persistence policy.
+
+**Only isolated launch.** Users need both clean browser sessions and access to existing authenticated state. Configuration selects the ownership policy explicitly.
+
+**Delegated browser agents.** The experiments compare browser-control backends. Delegating the whole task to another planner would alter DSH's control of the task loop.
+
+**DSH model bridge.** Native Stagehand configuration keeps its browser integration independent of DSH model-request adaptation and persistence. Session model selection, credential reuse, underlying inference capture, and Session usage accounting are deferred as one coordinated integration.
+
+## Consequences
+
+Providers evolve their tools independently while the shared service remains a name-only registry. The three providers and their runtime helper publish as experimental packages without enabling them in shipped defaults. The browser package group has no dependency on experimental runtime code.
+
+Attachment reservations apply within one provider instance; they do not coordinate separate DSH processes or external browser clients. Browser state is absent from Session replay, and cancellation does not undo delivered browser actions. Provider READMEs own engine support, model requirements, and upstream restrictions.

+ 53 - 0
.agents/notes/implemented/architecture/2026-09-12-browser-use-provider-registration.zh.md

@@ -0,0 +1,53 @@
+# Agent Note: 浏览器操作提供方注册与 Session 所有权
+
+Status: implemented
+
+[English](2026-09-12-browser-use-provider-registration.md) | 中文
+
+## 问题
+
+浏览器控制后端暴露不同的操作与观测格式。在可移植消费方存在之前,通用浏览器动作 API 会约束这些实验。浏览器会话可以隔离,而附加现有已登录浏览器必须保留其状态,并防止一个提供方内出现并发所有权。
+
+## 决策
+
+[`dsh-browser-use`](../../../../packages/browser-use/browser-use/README.zh.md) 拥有 `ctx.browserUse`,注册一个提供方拥有的名称并返回其 effect 清理器。第二次注册无论名称为何都会失败。服务不包含浏览器对象、共享操作类型、分派方法、资源生命周期或运行时选择器。[计算机操作注册决策](2026-09-12-computer-use-provider-registration.zh.md)仍独立拥有桌面提供方注册与共享桌面协调规则。
+
+[Playwright MCP](../../../../packages/experimental/browser-use-playwright-mcp/README.zh.md)、[Chrome DevTools MCP](../../../../packages/experimental/browser-use-chrome-devtools-mcp/README.zh.md) 与[原生 Stagehand](../../../../packages/experimental/browser-use-stagehand-native/README.zh.md) 拥有自己的浏览器工具,并通过常规 DSH 工具管线集成。它们是公共实验性可选功能。DSH 拥有任务规划与任务循环;Stagehand 提供单项 AI(人工智能)辅助操作。Profile 或 preset 配置为每次提供方激活选择启动或附加模式。
+
+浏览器资源属于确切的实时 Agent 与 Session,而非仅凭可复用的 Session id。调用跨轮次保留状态。运行时释放会关闭启动的资源,重新加载或 fork 不会继承启动的 profile。附加保留现有浏览器状态,并在该提供方实例内将外部浏览器独占保留给一个 Session。清理断开连接而不关闭外部浏览器。
+
+[实验性运行时辅助库](../../../../packages/experimental/browser-use-runtime/README.zh.md)拥有共享资源生命周期与附加保留机制,而不向浏览器操作服务引入这些方法。取消调用方对资源获取的等待后,初始化及其保留仍归 Session 所有。活动操作收到 Agent 释放的取消信号时,在 Agent 等待空闲之前启动资源关闭,因为浏览器调用可能只有在连接关闭后才能结束。提供方清理停止接收工具调用,并保留注册,直到资源清理与活动调用完成。服务保持独立于所有实验包。
+
+Stagehand 的启动器继承其进程环境,SDK 初始化可能在清理完成前超时。提供方 Host 使用 `@puppeteer/browsers` 在 CDP 或 SDK 就绪前取得所启动 Chromium 及其临时 profile 的所有权,并清理子进程环境。启动和附加模式都由隔离的 Worker 运行 SDK,且只通过 CDP 连接。原生推理不接受 abort signal。SDK 关闭会等待活动工作;清理成功后可重新连接并保留浏览器状态。SDK 工作未能结束时,只要 Chromium 仍在运行,就阻止复用。最终清理可在自有 Chromium 和 Worker 终止后释放启动浏览器的占用。附加模式下 SDK 工作未能结束、Worker 终止失败或自有进程清理失败时,保留占用。Host 仅终止自己拥有的 Chromium 进程,等待子进程关闭后才删除 profile;外部拥有的浏览器保持运行。
+
+Stagehand 使用显式配置的固定版本 SDK 目录内原生模型。配置的 API 密钥与可选请求头会转发到浏览器扩展,由 Stagehand 在扩展内执行推理。浏览器工具输入和返回数据使用现有 Session 日志,包括 SDK 结果元数据。DSH 模型路由、凭据复用、底层推理请求/响应捕获,以及与 Session 用量计量的集成仍属暂缓工作;本集成不增加持久化事件或 Session schema 变更。
+
+MCP 客户端激活会等待连接和工具发现,但提供方可能在任何 Session 存在之前就完成激活。该激活 promise 无法代表未来各 Session 拥有的客户端。每个 MCP 浏览器提供方在现有的串行 `agent/created` 事件中等待一次客户端启动尝试。[等待 Agent 创建的决策](2026-09-09-awaited-agent-creation.zh.md)负责排队输入顺序与创建回滚。创建或恢复成功后,提示词组装与直接调用方即可读取完整目录。
+
+启动失败或取消会拒绝创建或恢复,并触发 Agent 及其客户端资源的回滚。附加连接被占用时,本次激活永久跳过启动,但其他工作继续运行;连接释放后,新创建或恢复的 Agent 可以获取连接。较晚安装和重新加载只作用于后续激活,与 [Schedule 的挂载策略](../../../../packages/schedule/schedule/README.zh.md#use-this-package)一致。成功客户端的浏览器工具与资源请求共享 Session 队列;其他 Session 不能执行这些请求,也不能收到该服务器的指导。
+
+## 考虑过的替代方案
+
+**统一浏览器动作 API。** Playwright、Chrome DevTools 与 Stagehand 的原生语义不同。当前没有消费方要求可互换的动作方法,因此由提供方拥有工具以保留这些语义。
+
+**提供方自有的启动阶段。** 现有的串行 `agent/created` 等待 Session 自有设置,并向创建方报告失败。独立维护任务会重复这份生命周期所有权。
+
+**在提示词组装期间发现。** 提示词和工具收集需要已就绪的注册。此时启动发现要么暴露不完整目录,要么要求再次收集;等待创建会在轮次开始前完成发现。
+
+**多个 Session 共享一个浏览器。** 浏览器标签页、导航与登录状态可以按 Session 隔离。共享它们会引入桌面集成通常无法避免的跨 Session 干扰。
+
+**附加时创建全新 context。** 新浏览器 context 不继承现有登录状态。独占使用附加的浏览器可保留附加所支持的工作流。
+
+**随 Session 恢复浏览器 profile。** 持久浏览器状态引入 Session 日志之外的 profile 存储与迁移所有权。启动的状态仅在实时运行时存续;外部拥有的浏览器保留自己的持久化策略。
+
+**仅支持隔离启动。** 用户既需要干净的浏览器会话,也需要访问现有身份验证状态。配置显式选择所有权策略。
+
+**委托浏览器 agent。** 这些实验比较浏览器控制后端。将整个任务委托给另一规划器会改变 DSH 对任务循环的控制。
+
+**DSH 模型桥接。** 原生 Stagehand 配置使浏览器集成独立于 DSH 模型请求适配和持久化。Session 模型选择、凭据复用、底层推理捕获与 Session 用量计量作为一个协调集成暂缓。
+
+## 影响
+
+提供方独立演进自己的工具,而共享服务保持仅注册名称。三个提供方及其运行时辅助库作为实验性包发布,但不在内置默认配置中启用。浏览器包组不依赖实验性运行时代码。
+
+附加保留机制作用于单个提供方实例;它不协调独立 DSH 进程或外部浏览器客户端。Session 重放不包含浏览器状态,取消不会撤销已交付的浏览器操作。提供方 README 拥有引擎支持、模型要求与上游限制。

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-09-11-incremental-terminal-retention.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-09-11-incremental-terminal-retention.md
+2026-09-11-incremental-terminal-retention.md: 7e2507775bc39ed2599c2af1173f949bbf352145
+2026-09-11-incremental-terminal-retention.zh.md: 6c6fb95fa0c593ae2b7abb0007d5e7c6fc9f4e7d

+ 64 - 0
.agents/notes/implemented/bug-fix/2026-09-11-incremental-terminal-retention.md

@@ -0,0 +1,64 @@
+# Agent Note: incremental terminal retention
+
+Status: implemented
+
+English | [中文](2026-09-11-incremental-terminal-retention.zh.md)
+
+## Problem
+
+Persistent terminal output passes through scrollback and unread-send byte limits on every PTY callback. Rebuilding the entire retained string to enforce those limits makes callback cost grow with retained output. A 4 MiB scrollback window makes this repeated work substantial even when each incoming chunk is small.
+
+## Decision
+
+The private buffer in [terminal-bash](../../../../packages/terminal/terminal-bash/src/session.ts) retains a linked sequence of strings, a head offset, and aggregate UTF-8 byte and newline counts. Appends inspect incoming text and evict only the oldest code points until both limits hold. Every evicted code point is charged to an earlier append, so total retention work is linear in input size. Reads assemble the retained strings; they remain proportional to retained output.
+
+The retained suffix matches line trimming followed by UTF-8 trimming. A trailing newline contributes an empty logical line. Truncation stays sticky until consumption clears the buffer. Adjacent surrogate halves across chunks count as one four-byte code point and are evicted together; unpaired halves retain JavaScript string identity and count as three UTF-8 bytes. The read-time `utf8Tail()` remains independent and unchanged.
+
+Every nonempty input is copied through UTF-16 before retention, preserving unpaired surrogates while detaching slices returned by the sanitizer from discarded control text. This applies to small pending fragments as well as large chunks. Private `truncated` and `isEmpty` getters let send settlement and startup polling inspect status without assembling scrollback.
+
+After at least half of the leading string is discarded, its suffix is copied to release the original backing storage. The copy costs no more than the discarded prefix, preserving amortized linear work and bounding retained string storage by the retained window. Linked nodes avoid array shifts or periodic scans of all retained chunks. Small inputs coalesce in the non-head tail up to 4096 UTF-16 units; allocating its successor copies those fragments into one owned string. Large tails already own their storage and are not copied again at this point. The head never grows during appends, and a cached last code unit avoids flattening pending fragments to inspect a cross-chunk surrogate pair. This bounds fragment metadata even for one-byte callbacks without rescanning retained text.
+
+The [persistent PTY decision](../feature/2026-07-16-persistent-pty-sessions.md) continues to own session lifecycle, model-visible output, and retention semantics. This decision specializes storage and performance; it supersedes no active decision record.
+
+## Measurement design
+
+The terminal I/O benchmark drives `LocalPtySession` with a synthetic subprocess handle. Fixed 16 KiB ASCII chunks without newlines exercise the byte limit with a long logical line. Steady-state cases fill either a 128 KiB or 4 MiB window, then append the same additional 1 MiB. A separate empty-window case sends 5 MiB. The line limit is 10,000; unread output is limited to the smaller of 256 KiB and the scrollback capacity.
+
+Synchronous ingestion measures the send start and provider callbacks. Completion additionally waits for emulator processing and readiness, then reads the bounded terminal result. Retained heap is sampled after explicit GC while the session and returned output remain reachable. Built JavaScript runs under plain Node. These measurements exclude shell startup, operating-system PTY transport, model latency, and browser rendering.
+
+### Local reference measurements
+
+On Apple M5 Pro, macOS arm64, Node v26.5.0, five fresh workers per case compare the eager-retention baseline with incremental retention. The same worker and inputs measure both versions; only the private session implementation differs. Times below are milliseconds in sample order.
+
+| Case / metric | Eager retention samples | Incremental retention samples |
+|---|---|---|
+| 128 KiB steady / ingestion | 232.164, 234.564, 219.511, 219.615, 221.434 | 10.493, 10.474, 9.917, 10.524, 10.327 |
+| 128 KiB steady / completion | 245.365, 247.826, 233.322, 232.854, 234.926 | 19.600, 19.991, 19.290, 20.049, 19.773 |
+| 4 MiB steady / ingestion | 4159.780, 4271.184, 4179.700, 4223.583, 4128.541 | 8.752, 8.584, 8.808, 8.582, 10.073 |
+| 4 MiB steady / completion | 4183.374, 4296.630, 4205.122, 4247.787, 4151.634 | 29.760, 30.679, 31.510, 31.781, 37.748 |
+| 5 MiB send / ingestion | 5186.998, 5122.875, 5127.790, 5177.788, 5157.274 | 26.440, 27.453, 26.710, 26.328, 27.334 |
+| 5 MiB send / completion | 5297.536, 5181.298, 5182.416, 5234.920, 5211.802 | 89.475, 83.310, 88.491, 89.440, 89.339 |
+
+The large/small steady-ingestion median ratio is 18.88 for eager retention and 0.84 for incremental retention. The 5 MiB completion median falls from 5211.802 ms to 89.339 ms (58.3×). Maximum retained heap for the large steady case rises from 4,512,656 to 6,219,296 bytes; this measures live session and result allocations together, not just buffer strings.
+
+A separate memory case sends 5 MiB in 16-byte callbacks and samples retained heap once after completion. It retains 5,802,840 bytes with tail aggregation. The same assertion with uncoalesced linked nodes fails at 22,969,720 bytes against the 16 MiB bound. This case has no performance timing verdict.
+
+The filtered-output memory case emits 513 callbacks of 64 KiB each, containing a complete 56 KiB OSC sequence followed by 8 KiB of visible text. This passes through the production sanitizer before filling a 4 MiB visible window and taking a bounded read. With incoming slices retained directly, the assertion fails at 36,706,592 bytes. Copying inputs into independent storage reduces retained heap to 7,635,440 bytes, below the unchanged 16 MiB limit. These measurements use Node v26.5.0 and fresh workers.
+
+A real PTY diagnostic runs `node -e 'process.stdout.write("x".repeat(5*1024*1024))'` through the built local subprocess provider. One baseline sample takes 106962.523 ms; one final candidate sample takes 249.007 ms. Timing begins before PTY/process spawn and ends after `session_exit` and the bounded read. Both samples exit with code 0, no signal, and truncated 256 KiB viewport/read payloads. This includes native PTY transport and Node startup, but excludes an interactive shell and prompt-readiness round trip.
+
+The [required benchmark](../../../../benchmarks/terminal-io/terminal-io.bench.ts) applies the shared CI scale and headroom to reference expectations of 20 ms steady ingestion, 50 ms steady completion, and 120 ms full completion, yielding limits of 50/125/300 ms. Median capacity scaling must stay below 4×; maximum retained heap is 16 MiB. Ratios and memory limits are unscaled. Substituting the original compiled session worker makes both timing cases fail: capacity ratio 18.977 exceeds 4, and full completion 5066.719 ms exceeds 300 ms. The final worker passes all four cases. The local benchmark command is `pnpm exec vitest run --config vitest.bench.config.ts benchmarks/terminal-io/terminal-io.bench.ts` after the benchmark build.
+
+## Alternatives considered
+
+**Cache only the byte count.** This leaves the per-append line split and full-string prefix deletion dependent on retained output. Both limits need incremental accounting.
+
+**Keep the complete output until a read.** This makes producer cost small but permits unbounded retention between reads. The configured limits apply during production.
+
+**Retain one node per callback.** Tiny callbacks make node metadata much larger than the bounded text. Bounded tail aggregation keeps node count tied to stored text blocks.
+
+**Store encoded UTF-8 chunks.** Encoding replaces unpaired UTF-16 surrogates. String chunks preserve the existing buffer semantics without adding a second text representation.
+
+## Consequences
+
+Append-time work no longer depends on repeatedly scanning the retained window. Snapshot and consume still allocate a combined string. Retention adds linked nodes and a bounded collection of pending small fragments. Functional tests cover byte/line interactions, consumption, split surrogate pairs, and 4 MiB retention. Performance evidence complements these output assertions; a synthetic provider does not establish real-shell command latency.

+ 64 - 0
.agents/notes/implemented/bug-fix/2026-09-11-incremental-terminal-retention.zh.md

@@ -0,0 +1,64 @@
+# Agent Note: 增量终端保留策略
+
+Status: implemented
+
+[English](2026-09-11-incremental-terminal-retention.md) | 中文
+
+## 问题
+
+持久终端输出在每次 PTY 回调中都受 scrollback 和未读发送输出的字节上限约束。如果每次执行这些限制都重建完整的保留字符串,回调成本就会随保留输出量增长。即使每个输入分片很小,4 MiB scrollback 窗口也会使这项重复工作产生显著开销。
+
+## 决策
+
+[terminal-bash](../../../../packages/terminal/terminal-bash/src/session.ts) 的私有缓冲区保留字符串链表、头部偏移,以及 UTF-8 字节数与换行符数的汇总值。追加操作检查输入文本,并仅淘汰最旧的码点,直到两个限制都满足。每个被淘汰码点的成本可归于之前的追加操作,因此保留策略的总工作量与输入量呈线性关系。读取时拼接保留字符串,成本仍与保留输出量成正比。
+
+保留后缀与先按行数裁剪、再按 UTF-8 字节数裁剪的结果一致。末尾换行符贡献一个空逻辑行。截断标志保持为真,直到消费操作清空缓冲区。跨分片相邻的代理项两半按一个四字节码点计数,并共同淘汰;未配对代理项保留 JavaScript 字符串原值,按三个 UTF-8 字节计数。读取时的 `utf8Tail()` 保持独立且不变。
+
+每个非空输入都在保留前通过 UTF-16 复制,在保留未配对代理项的同时,使清理器返回的切片脱离已丢弃的控制文本。待处理的小片段与大分片都遵循这一规则。私有 `truncated` 和 `isEmpty` getter 让发送结算和启动轮询无需拼接 scrollback 即可检查状态。
+
+头部字符串至少一半被丢弃后,其后缀会被复制,以释放原始底层存储。复制成本不超过已丢弃前缀,因此维持摊还线性工作量,并使保留字符串存储受保留窗口约束。链表节点避免数组头部移除或定期扫描所有保留分片。小输入在非头部的尾节点合并,最多积累 4096 个 UTF-16 单元;分配后继节点时,将这些片段复制为一个独立字符串。大尾块已经拥有独立存储,此处不会再次复制。追加期间头节点不会增长,缓存的末尾码元也避免了为检查跨分片代理对而将待处理片段展平。这样即使每次回调只有一个字节,片段元数据也有界,且无需重新扫描保留文本。
+
+[持久 PTY 决策](../feature/2026-07-16-persistent-pty-sessions.zh.md)继续负责会话生命周期、模型可见输出与保留语义。本决策细化存储与性能,不取代任何活跃决策记录。
+
+## 测量设计
+
+终端 I/O 基准通过合成子进程句柄驱动 `LocalPtySession`。固定的 16 KiB ASCII 分片不含换行符,以长逻辑行触发字节上限。稳态场景先填满 128 KiB 或 4 MiB 窗口,再追加同样的 1 MiB。独立的空窗口场景发送 5 MiB。行数上限为 10,000;未读输出上限为 256 KiB 与 scrollback 容量中的较小值。
+
+同步接收计时覆盖发送启动与提供方回调。完成计时还等待终端模拟器处理与就绪,再读取有界终端结果。保留堆在显式 GC 后采样,此时会话与返回输出仍可达。构建后的 JavaScript 在普通 Node 下运行。这些测量不含 shell 启动、操作系统 PTY 传输、模型延迟或浏览器渲染。
+
+### 本地参考测量
+
+在 Apple M5 Pro、macOS arm64、Node v26.5.0 上,每个场景用五个全新 worker 比较全量保留计算基线 与增量保留策略。两个版本使用相同 worker 和输入,仅私有会话实现不同。下表时间单位为毫秒,按采样顺序排列。
+
+| 场景 / 指标 | 全量保留计算样本 | 增量保留计算样本 |
+|---|---|---|
+| 128 KiB 稳态 / 接收 | 232.164, 234.564, 219.511, 219.615, 221.434 | 10.493, 10.474, 9.917, 10.524, 10.327 |
+| 128 KiB 稳态 / 完成 | 245.365, 247.826, 233.322, 232.854, 234.926 | 19.600, 19.991, 19.290, 20.049, 19.773 |
+| 4 MiB 稳态 / 接收 | 4159.780, 4271.184, 4179.700, 4223.583, 4128.541 | 8.752, 8.584, 8.808, 8.582, 10.073 |
+| 4 MiB 稳态 / 完成 | 4183.374, 4296.630, 4205.122, 4247.787, 4151.634 | 29.760, 30.679, 31.510, 31.781, 37.748 |
+| 5 MiB 发送 / 接收 | 5186.998, 5122.875, 5127.790, 5177.788, 5157.274 | 26.440, 27.453, 26.710, 26.328, 27.334 |
+| 5 MiB 发送 / 完成 | 5297.536, 5181.298, 5182.416, 5234.920, 5211.802 | 89.475, 83.310, 88.491, 89.440, 89.339 |
+
+大/小窗口稳态接收时间的中位数比值在全量保留计算下为 18.88,在增量保留计算下为 0.84。5 MiB 完成时间的中位数从 5211.802 ms 降至 89.339 ms(58.3×)。大窗口稳态场景的最大保留堆从 4,512,656 字节升至 6,219,296 字节;该指标同时测量活跃会话与结果分配,并非仅缓冲区字符串。
+
+独立的内存场景以 16 字节回调发送 5 MiB,在完成后对保留堆采样一次。尾部合并时保留 5,802,840 字节。相同断言在未合并链表节点下失败:22,969,720 字节超过 16 MiB 上限。此场景不判定性能耗时。
+
+过滤输出的内存场景发送 513 个 64 KiB 回调,每个含完整的 56 KiB OSC 序列及其后的 8 KiB 可见文本。数据经过生产清理器,填满 4 MiB 可见窗口后进行有界读取。直接保留输入切片时,断言以 36,706,592 字节失败。将输入复制到独立存储后,保留堆降至 7,635,440 字节,低于不变的 16 MiB 上限。这些测量使用 Node v26.5.0 和全新 worker。
+
+真实 PTY 诊断通过构建后的本地子进程提供方运行 `node -e 'process.stdout.write("x".repeat(5*1024*1024))'`。一次基线样本耗时 106962.523 ms;一次最终候选样本耗时 249.007 ms。计时从 PTY/进程 spawn 前开始,到 `session_exit` 和有界读取完成后结束。两个样本均以代码 0 退出,无信号,viewport/read 载荷均为已截断的 256 KiB。这包括原生 PTY 传输与 Node 启动,不包括交互式 shell 及提示符就绪往返。
+
+[必需基准](../../../../benchmarks/terminal-io/terminal-io.bench.ts)对参考预期值应用共享 CI 系数与余量:稳态接收 20 ms、稳态完成 50 ms、完整发送完成 120 ms,对应限制为 50/125/300 ms。容量扩展的中位数比值必须低于 4×;最大保留堆为 16 MiB。比值与内存限制不缩放。替换为原始编译后会话 worker 时,两个计时场景都失败:容量比值 18.977 超过 4,完整发送完成时间 5066.719 ms 超过 300 ms。最终 worker 的四个场景均通过。本地命令为 `pnpm exec vitest run --config vitest.bench.config.ts benchmarks/terminal-io/terminal-io.bench.ts`,在基准构建完成后执行。
+
+## 考虑过的替代方案
+
+**仅缓存字节数。** 这仍会使每次追加的按行拆分和完整字符串前缀删除依赖保留输出量。两个限制都需要增量计数。
+
+**在读取前保留全部输出。** 这使生产成本较小,但允许读取之间的保留量无限增长。配置的限制必须在产生输出时生效。
+
+**每次回调保留一个节点。** 微小回调会使节点元数据远大于有界文本。受限的尾部合并使节点数与存储的文本块相关。
+
+**保留编码后的 UTF-8 分片。** 编码会替换未配对 UTF-16 代理项。字符串分片无需引入第二种文本表示,即可保留现有缓冲区语义。
+
+## 影响
+
+追加工作不再依赖重复扫描保留窗口。Snapshot 和 consume 仍分配拼接后的字符串。保留策略额外维护链表节点与有界的待处理小片段集合。功能测试覆盖字节数与行数的交互、消费操作、跨分片代理对,以及 4 MiB 保留窗口。性能证据补充这些输出断言;合成提供方不能证明真实 shell 命令延迟。

+ 2 - 2
.agents/notes/implemented/feature/2026-06-30-interception-extension-points.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-30-interception-extension-points.md
-2026-06-30-interception-extension-points.md: d628cdce0bc6c13a90a25eeebcfd977602dd76e3
-2026-06-30-interception-extension-points.zh.md: 689d7f6ee6d5a552c2eaf950ce5930be559eb5bb
+2026-06-30-interception-extension-points.md: 5adffd1679c97cc69cf18991913113b84d88ba3c
+2026-06-30-interception-extension-points.zh.md: c3aa374a22d58c5185a3bd8e79b4040a04bd43e3

+ 1 - 1
.agents/notes/implemented/feature/2026-06-30-interception-extension-points.md

@@ -15,7 +15,7 @@ The surface needs distinct contracts for per-prompt policy (CC's `UserPromptSubm
 The canonical surface separates transformable policy, around-dispatch control, and observe-only notification. Policy waterfalls return small extension-point-specific **typed Decision unions**; wrappers return normalized results; notifications receive immutable snapshots and cannot affect the outcome. The set covers the hook points in scope (`session-start`, `prompt-submit`, `pre-tool`, `post-tool`, `stop`-via-continuation) while leaving non-hook execution policy independently composable.
 
 **Agent events** (`dsh-agent`):
-- `agent/session-start({ agent, source })` — emit, once before turn 1, carrying a `SessionStartSource` (`startup` for a fresh/forked create, `resume` for a reloaded persisted session; `clear`/`compact` reserved). A pure notification — it CANNOT block startup (a deliberate gap: a bridge logs/injects, it does not gate startup). A listener seeds context via `agent.inject()`.
+- `agent/created({ agent, source, signal? })` — serial per-agent initialization before the first turn, carrying `SessionStartSource`. Listeners may install tools and seed context through `agent.inject()`; creation awaits them and rejects on failure. The [awaited creation decision](../architecture/2026-09-09-awaited-agent-creation.md) owns this timing and supersedes the observe-only startup policy.
 - `agent/pre-step({ agent, messages, turn, step, signal }, next) → PreStepDecision` — waterfall, fired before every proposed step after the loop has atomically removed its exclusive inbox batch. The payload carries the request's `turn`, `step`, and cancellation `signal` (the retired `PreStepContext` fields live in the payload; see the [payload-object events decision](../../archived/architecture/2026-08-06-agent-event-payload-objects.md)); `messages` is empty for a tool continuation with no intervening input. `enter` returns the complete message batch, including any current-request context a listener contributes; `reject` opens no step and leaves the claimed messages removed.
 
 **`agent/turn-stopping`** is an awaited notification at the natural stop boundary. A listener that needs another step calls `agent.steer()` with explicitly sourced model-facing content; the loop then re-reads the outbox and either continues or closes the turn.

+ 1 - 1
.agents/notes/implemented/feature/2026-06-30-interception-extension-points.zh.md

@@ -15,7 +15,7 @@ harness 需要一套钩子子系统:用户像 Claude Code(CC)和 Codex 那
 规范接口将可变换策略、环绕调度控制与仅观测通知分离。策略 waterfall(瀑布式事件)返回小型的、扩展点专属的**类型化 Decision 联合类型**;包装层返回规范化结果;通知接收不可变快照,无法影响结果。覆盖的钩子点包括 `session-start`、`prompt-submit`、`pre-tool`、`post-tool`、通过 continuation 实现的 `stop`,同时将非钩子的执行策略留作独立可组合。
 
 **Agent 事件**(`dsh-agent`):
-- `agent/session-start({ agent, source })` ——emit,在第 1 轮次之前触发一次,携带 `SessionStartSource`(`startup` 表示全新/fork 创建,`resume` 表示重新加载的持久化会话;`clear`/`compact` 保留)。纯通知,不能阻塞启动(这是有意的空白:桥接可以记录/注入,但不管控启动)。监听器通过 `agent.inject()` 注入上下文。
+- `agent/created({ agent, source, signal? })` ——首个轮次前针对单个 agent 的串行初始化,携带 `SessionStartSource`。监听器可以安装工具,并通过 `agent.inject()` 注入上下文;创建等待监听器完成,失败时拒绝。[可等待创建决策](../architecture/2026-09-09-awaited-agent-creation.zh.md)拥有这一时序,并取代只观察启动的策略。
 - `agent/pre-step({ agent, messages, turn, step, signal }, next) → PreStepDecision` ——waterfall,在每个拟议步骤之前、循环原子移除其独占 inbox 批次后触发。payload 携带该请求的 `turn`、`step` 与取消 `signal`(已退役的 `PreStepContext` 字段位于 payload 中;参见 [payload-object 事件决策](../../archived/architecture/2026-08-06-agent-event-payload-objects.md));没有中途输入的工具续步会收到空批次。`enter` 返回完整消息批次,其中包括监听器为当前请求贡献的上下文;`reject` 不打开步骤,并让已领取消息保持已删除。
 
 **`agent/turn-stopping`** 是自然停止边界上的一次 awaited 通知。需要再执行一步的监听器调用 `agent.steer()`,传入来源显式的 steering(中途引导)内容供模型使用;循环随后重新读取 outbox,继续执行或关闭轮次。

+ 2 - 2
.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.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-19-persisted-same-session-goal-domain.md
-2026-07-19-persisted-same-session-goal-domain.md: bc6c9ec14c16b20bc8434bac0f1eb6070ea8920d
-2026-07-19-persisted-same-session-goal-domain.zh.md: 30b9d22633f37dda971104f8d2d5be0d3c8c3163
+2026-07-19-persisted-same-session-goal-domain.md: 4ddc02c3bd3b7bbf04b3c6867a76673431309460
+2026-07-19-persisted-same-session-goal-domain.zh.md: 2bb937b0f637e54ab6d9a6e10ebe237849e08171

+ 1 - 1
.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md

@@ -28,7 +28,7 @@ Incremental replay advances its cursor after each valid event and remains positi
 
 At most one goal is current. Create requires no current non-complete goal and always generates a revision-one id not used earlier in the session; a completed goal may be replaced. Every other mutation carries the expected `GoalRef`, and stale ids or revisions reject. Resume accepts a paused or blocked phase, or a disarmed active goal, only when the round cap has remaining capacity. The domain validates blocker reason shape but deliberately leaves reason codes and the decision to block to policy consumers.
 
-A cache built from any seed starts disarmed, and every `agent/session-start` edge disarms it again. `GoalService.disarm(agent)` also lets a lifecycle owner remove process-local authority without a session event, revision change, or `goal/changed` notification. Resume, fork, and continuation-driver replacement therefore preserve the durable objective and history but never initiate work on their own. A later human prompt can be interpreted by the model, whose policy API may explicitly call resume and arm the goal.
+A cache built from any seed starts disarmed, and every `agent/created` edge disarms it again. `GoalService.disarm(agent)` also lets a lifecycle owner remove process-local authority without a session event, revision change, or `goal/changed` notification. Resume, fork, and continuation-driver replacement therefore preserve the durable objective and history but never initiate work on their own. A later human prompt can be interpreted by the model, whose policy API may explicitly call resume and arm the goal.
 
 ### Service boundary
 

+ 1 - 1
.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md

@@ -28,7 +28,7 @@ Status: implemented
 
 最多只有一个当前目标。创建要求不存在未完成的当前目标,并始终生成该会话此前未使用过、修订号为一的 id;已完成目标可以被替换。其他每次变更都携带预期的 `GoalRef`,陈旧的 id 或修订号会被拒绝。仅当 Goal Round 上限仍有余量时,暂停或阻塞阶段以及已解除激活的活跃目标才能恢复。领域层校验阻塞原因的形状,但会把原因代码和是否阻塞的决策留给策略消费方。
 
-从任何种子构建的缓存都以未激活状态开始,每次 `agent/session-start` 边沿也会再次解除激活。`GoalService.disarm(agent)` 还允许生命周期所有者移除进程内权限,而不写入会话事件、不改变修订号,也不发出 `goal/changed` 通知。因此,会话恢复、fork 和继续执行驱动器替换都会保留持久化目标与历史,但绝不会自行启动工作。后续人类提示词可由模型解释,其策略 API 可以显式调用恢复操作并激活目标。
+从任何种子构建的缓存都以未激活状态开始,每次 `agent/created` 边沿也会再次解除激活。`GoalService.disarm(agent)` 还允许生命周期所有者移除进程内权限,而不写入会话事件、不改变修订号,也不发出 `goal/changed` 通知。因此,会话恢复、fork 和继续执行驱动器替换都会保留持久化目标与历史,但绝不会自行启动工作。后续人类提示词可由模型解释,其策略 API 可以显式调用恢复操作并激活目标。
 
 ### 服务边界
 

+ 2 - 2
.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.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-06-manager-owned-subagent-settlement-delivery.md
-2026-08-06-manager-owned-subagent-settlement-delivery.md: 9d14d4af775809a974349c753a7b4b593a145489
-2026-08-06-manager-owned-subagent-settlement-delivery.zh.md: 4bbee373aa2b50902af5319f9398a89da9cc3143
+2026-08-06-manager-owned-subagent-settlement-delivery.md: 5553d4c06a6776f0ef025e19ae7f4f67fe277a56
+2026-08-06-manager-owned-subagent-settlement-delivery.zh.md: 6d7ee756ad531e123508e8667adc4e5f99c2b9ea

+ 10 - 2
.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md

@@ -16,7 +16,11 @@ The signal already existed. `subagent/end` has carried `stopReason` and `lastAss
 
 The continuation manager delivers the account itself, from inside the disposal transaction that ends the Activation.
 
-When a resident Activation settles, `notifySettlement()` resolves the child's durable direct parent and sends it one user-role message: the epoch's outcome as a sentence the parent can act on, then the child's final assistant content, or a statement that it produced none. Delivery is unconditional for every child whose id a caller actually received. It does not consult whether the child reported, and it keeps no bookkeeping that could make the promise conditional — that unconditionality is what lets `tool-subagent` promise a runtime notice containing the outcome and any final assistant message. A materialization rolled back before its first accepted message stays silent, because the caller was told that child was not established.
+When a resident Activation settles, `notifySettlement()` resolves the child's durable direct parent and sends it one user-role message: the epoch's outcome as a sentence the parent can act on, then the text from the child's final assistant output, or a statement that it produced no closing text. Delivery is unconditional for every child whose id a caller actually received. It does not consult whether the child reported, and it keeps no bookkeeping that could make the promise conditional — that unconditionality is what lets `tool-subagent` promise a runtime notice containing `its outcome and any final assistant message`. A materialization rolled back before its first accepted message stays silent, because the caller was told that child was not established.
+
+### Closing text
+
+[`createSettlementMessage()`](../../../../packages/subagent/subagent/src/continuation-messages.ts) projects the selected assistant output to nonempty text blocks before creating the user-role notice. It preserves text bytes and block order, excludes every nontext block, and uses `It left no closing message.` when no nonempty text remains. Text-only notices work across parent providers without depending on their support for particular content types. Reasoning and tool calls, for example, cannot appear in a DeepSeek Messages user message; images are accepted by Messages but not by every parent model. The conversion belongs to notice construction; `AssistantOutputFold`, `SubagentResult.output`, and `subagent/end.lastAssistantMessage` retain complete child output for SDK and UI consumers.
 
 ### Runtime source
 
@@ -62,7 +66,7 @@ Both matter past the notice: `subagent/end` carries `stopReason` to the jsonrpc
 
 Three assembled ACP scenarios cover the notice: a child that sends no message, a child that sends a message first, and a child driven through several Agent-message turns. All three need an explicit fence. The notice arrives once the child's teardown finishes, which races whatever the parent is already doing, so each scenario holds the child behind the parent's spawn turn and then waits for the parent turn the notice opens (`waitForTurnStart` at that turn, then `waitForTurnEnd`) before the script continues. Waiting for a turn the run is not fenced to produce is not coverage: it is a timeout when the notice lands in the turn already running instead.
 
-`subagent-continuable` is the one that pins a failure. Its child's last turn dies on the forced durability checkpoint without entering a step, so that transcript is where the stop-reason rule above is visible end to end: the notice says the child *failed*, carries the earlier `SECOND_OK` as its last content rather than as a result, and the parent's own acknowledgement turn reaches the ACP client.
+`subagent-continuable` pins a completed SDK settlement with `SECOND_OK` and no child reasoning in the parent's notice; its child finishes in turn 1, so the configured turn-3 checkpoint failure is not exercised.
 
 A keyless headless Loader snapshot covers the user-visible path end to end. Its replay parent omits `run_in_background` to exercise the continuable background default, never calls `list_agents`, `send_message`, or Task tools, consumes the manager-authored `subagent-settled` notice, and produces its final answer. The child sends no Agent message, so the transcript depends only on the runtime notice. A test-only Loader fence holds the parent's post-spawn request until the real manager notice enters its inbox, removing platform scheduling from the transcript without synthesizing the notice.
 
@@ -84,6 +88,10 @@ The refusal and interruption wordings are pinned verbatim in unit tests rather t
 
 **Always use `followup`.** Simpler and uniform, but a fan-out of children settling together would cost one parent turn each. The step-boundary batch already exists; using it is free.
 
+**Filter the canonical child output.** Removing nontext blocks in `AssistantOutputFold` would discard assistant content needed by SDK and UI consumers. Only the parent notice requires a text projection.
+
+**Relax the Messages serializer.** Accepting or silently discarding invalid user-role blocks would conceal the producer's role conversion error. Notice construction supplies content that every parent provider can represent while protocol validation remains strict.
+
 ## Consequences
 
 - A continuable child's parent receives one message per settled Activation. Fan-out deployments therefore add parent turns; steering keeps a simultaneous batch to one step.

+ 10 - 2
.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md

@@ -16,7 +16,11 @@ Status: implemented
 
 继续执行管理器自己投递这份记账,就在结束 Activation 的那笔 dispose 事务内部完成。
 
-当驻留 Activation 结算时,`notifySettlement()` 解析该 child 持久化的直接父级,并向它发送一条用户角色消息:先是父级可据以行动的一句结果说明,然后是 child 的最终 assistant 内容,或一句说明它没有产出内容。对每个调用方真正拿到过 id 的 child,投递都是无条件的。它不查询 child 是否上报过,也不保留任何可能让这项承诺变成有条件的记账——正是这种无条件性,才让 `tool-subagent` 能够承诺一条包含结局与可能存在的最终 assistant 消息的运行时通知。在第一条消息被接受之前就回滚的物化保持静默,因为调用方已被告知该 child 未建立。
+当驻留 Activation 结算时,`notifySettlement()` 解析该 child 持久化的直接父级,并向它发送一条用户角色消息:先是父级可据以行动的一句结果说明,然后是 child 最终 assistant 输出中的文本,或一句说明它没有产出收尾文本。对每个调用方真正拿到过 id 的 child,投递都是无条件的。它不查询 child 是否上报过,也不保留任何可能让这项承诺变成有条件的记账——正是这种无条件性,才让 `tool-subagent` 能够承诺一条包含 `its outcome and any final assistant message` 的运行时通知。在第一条消息被接受之前就回滚的物化保持静默,因为调用方已被告知该 child 未建立。
+
+### 收尾文本
+
+[`createSettlementMessage()`](../../../../packages/subagent/subagent/src/continuation-messages.ts) 在创建用户角色通知前,将选中的 assistant 输出投影为非空文本块。它保留文本字节与块顺序,排除所有非文本块,并在没有剩余非空文本时使用 `It left no closing message.`。纯文本通知适用于不同的父级提供方,不依赖它们对特定内容类型的支持。例如,推理与工具调用不能出现在 DeepSeek Messages 的用户消息中;Messages 接受图片,但并非每个父级模型都支持图片。这项转换归通知构建所有;`AssistantOutputFold`、`SubagentResult.output` 与 `subagent/end.lastAssistantMessage` 为 SDK 和 UI 消费方保留完整的子级输出。
 
 ### 来源信息
 
@@ -62,7 +66,7 @@ Status: implemented
 
 三个整体组装的 ACP 场景覆盖该通知:一个不发送消息的 child、一个先发送消息的 child,以及一个被多轮 Agent 消息驱动的 child。三者都需要显式栅栏。通知在 child 拆卸完成后才到达,会与父级当时正在做的事竞争,因此每个场景都会把 child 保持到父级启动轮次结束,再等待该通知开启的那个父级轮次(先 `waitForTurnStart` 到该轮次,再 `waitForTurnEnd`),然后脚本才继续。等待一个运行并未被栅栏保证会产生的轮次不算覆盖:一旦通知落进已经在跑的那个轮次,它就是一次超时。
 
-`subagent-continuable` 是其中固定失败结局的那个。它的 child 最后一个轮次在被强制的持久化检查点上死亡,且未进入任何 step,因此该 transcript 正是上面那条终止原因规则的端到端可见之处:通知说该 child **失败**,把此前的 `SECOND_OK` 作为它最后产出的内容而非结果携带,而父级自己的确认轮次会到达 ACP 客户端。
+`subagent-continuable` 固定了 SDK 中已完成的结算:父级通知包含 `SECOND_OK`,但不含子级推理;子级在第 1 轮结束,因此未覆盖已配置的第 3 轮检查点失败。
 
 另有一个无密钥的 headless Loader 快照端到端覆盖用户可见路径。其重放父级省略 `run_in_background` 以覆盖可继续后台默认路径,从不调用 `list_agents`、`send_message` 或 Task 工具,消费管理器写入的 `subagent-settled` 通知,并给出最终答案。child 不发送 Agent 消息,因此该 transcript 只依赖运行时通知。一个仅用于测试的 Loader 栅栏会把父级启动后的请求保持到真实管理器通知进入其 inbox 为止,从 transcript 中排除平台调度差异,但不会伪造该通知。
 
@@ -84,6 +88,10 @@ Status: implemented
 
 **始终使用 `followup`。** 更简单也更统一,但一批同时结算的 child 会各自消耗一个父级轮次。step 边界的批量语义本来就存在,用它是免费的。
 
+**过滤规范的子级输出。** 在 `AssistantOutputFold` 中删除非文本块,会丢弃 SDK 和 UI 消费方需要的 assistant 内容。只有父级通知需要文本投影。
+
+**放宽 Messages 序列化器。** 接受或静默丢弃用户角色中的无效块,会掩盖生产方的角色转换错误。通知构建提供每种父级提供方都能表示的内容,协议校验仍保持严格。
+
 ## 后果
 
 - 可继续 child 的父级会为每个已结算 Activation 收到一条消息。因此,做扇出的部署会增加父级轮次;steer 会把同时结算的一批压缩到一个 step。

+ 2 - 2
.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.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-09-09-web-sidebar-terminal.md
-2026-09-09-web-sidebar-terminal.md: ce1162a84d3f96336cbb217dbdfe62d12ac23280
-2026-09-09-web-sidebar-terminal.zh.md: 51e0f374b5fde389323868e56fbd86d3bc5720b3
+2026-09-09-web-sidebar-terminal.md: db7f42e9f53940262d806ff1133c1059e3f74829
+2026-09-09-web-sidebar-terminal.zh.md: cbb6bc6bab5e3ed4927399b90363332f1624825b

+ 2 - 0
.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md

@@ -10,6 +10,8 @@ Web users need an interactive shell beside a Session to inspect the workspace an
 
 ## Decision
 
+The application theme owns terminal background, default text, cursor and selection colors. The terminal body reads the rendered DSH palette after framework-delivered theme changes, so CSS token overrides remain effective. Updating the existing xterm instance preserves its output and PTY attachment; shell-provided ANSI colors remain independent.
+
 `api-terminal-controller` owns user terminals by Session and exposes the `terminal` Remote namespace. `ui-sidebar-terminal` registers native right-sidebar tabs, xterm.js rendering and FitAddon sizing. A new terminal offers installed shells and waits for Start. Host discovery verifies the configured candidates, with the execution default first; creation accepts only a currently discovered path. The browser remembers the last selected shell path in origin-scoped localStorage and falls back to the current default if that path is unavailable. The terminal type declares independent instances, so ordinary page deduplication cannot collapse separate processes when opening or docking tabs. The existing sidebar controls open additional tabs; double-clicking a tab title renames its terminal. Terminal processes use the composed subprocess provider and Session sandbox policy. Shell resolution occurs during discovery and creation; reading limits and reconnecting an existing process do not depend on the default executable remaining available. Interactive shell configuration supplies Tab completion and optional inline suggestions.
 
 Close and replacement remove the tab synchronously and run process cleanup in the background. The Client first records the unfinished close request under a terminal-specific localStorage key; success removes it, and startup retries requests that remain. A cleanup failure produces a lightweight notification with a retry action without reopening the tab. Independent keys prevent another window from overwriting unrelated cleanup requests. Collapse, tab/Session switching, floating, fullscreen and browser disconnection preserve the process. Component cleanup and `TabDomain.signal` only detach browser work because the same lifetime can end during plugin reload. Failed process cleanup retains ownership, including failures after allocation but before create publication. Session owner disposal and Host plugin disposal also clean up terminals. A definitive missing-Session response retires its saved close request because the Session owns process cleanup; transport failures remain retryable. Client plugin disposal awaits every detached stream so a replacement plugin does not inherit unfinished Client cleanup.

+ 2 - 0
.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.zh.md

@@ -10,6 +10,8 @@ Web 用户需要在 Session 旁使用交互式 shell 检查工作区和运行命
 
 ## 决定
 
+应用主题负责终端背景、默认文字、光标和选区颜色。终端正文在收到框架传递的主题变化后读取 DSH 渲染配色,使 CSS token 覆盖继续生效。更新现有 xterm 实例会保留输出和 PTY 连接;shell 输出的 ANSI 颜色独立生效。
+
 `api-terminal-controller` 按 Session 管理用户终端并提供 `terminal` Remote namespace。`ui-sidebar-terminal` 注册原生右侧栏标签页,使用 xterm.js 渲染和 FitAddon 测量尺寸。新终端提供已安装 shell 的选择并等待用户启动。Host 探测会验证配置的候选,并把执行环境默认项放在首位;创建只接受当前探测返回的路径。浏览器在当前站点 localStorage 中记住上次选择的 shell 路径,该路径不可用时回到当前默认项。终端类型声明独立实例,因此打开或停靠标签页时,普通页面的去重规则不会合并不同进程。已有侧栏控件负责打开更多标签页,双击标签页标题可重命名终端。终端进程使用组合的 subprocess provider 和 Session sandbox policy。shell 在探测和创建时解析;读取限制和重新连接已有进程不依赖默认可执行文件仍然可用。交互式 shell 配置提供 Tab 补全和可选的内联建议。
 
 关闭和替换会同步移除标签页,并在后台清理进程。Client 先以终端独立的 localStorage key 保存未完成的关闭请求;成功后删除,启动时重试剩余请求。清理失败时显示带重试操作的轻量通知,不重新打开标签页。独立 key 避免其他窗口覆盖无关的清理请求。折叠、切换标签页或 Session、浮动、全屏和浏览器断线均保留进程。组件清理和 `TabDomain.signal` 只停止浏览器工作,因为插件重新加载也会结束这些生命周期。进程清理失败时保留所有权,包括分配完成但 create 尚未发布时的失败。Session owner 和 Host 插件卸载也会清理终端。 明确的 Session 不存在响应会清除已保存的关闭请求,因为进程清理由 Session 负责;传输失败仍可重试。Client 插件卸载等待所有断开的流结束,避免替换插件继承未完成的 Client 清理。

+ 2 - 2
.agents/notes/rejected/simplification/2026-07-26-builtin-timer-promises-for-hand-rolled-sleeps.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/rejected/simplification/2026-07-26-builtin-timer-promises-for-hand-rolled-sleeps.md
-2026-07-26-builtin-timer-promises-for-hand-rolled-sleeps.md: 8c46de37cbb936f31dc498b2100a031610b6d1e7
-2026-07-26-builtin-timer-promises-for-hand-rolled-sleeps.zh.md: 4795abff037ee19a7407cae02aee71ec3abcca44
+2026-07-26-builtin-timer-promises-for-hand-rolled-sleeps.md: d925222c6c1f07707815353caa15b2ff51212e7f
+2026-07-26-builtin-timer-promises-for-hand-rolled-sleeps.zh.md: 34128c671e898d7a6729ca3fa132585c7b53bdbe

+ 1 - 1
.agents/notes/rejected/simplification/2026-07-26-builtin-timer-promises-for-hand-rolled-sleeps.md

@@ -9,7 +9,7 @@ English | [中文](2026-07-26-builtin-timer-promises-for-hand-rolled-sleeps.zh.m
 Three packages hand-roll promise-wrapped timers that the `node:timers/promises` builtin already provides, while other packages (`dsh-llm-mock-server` `pause()`, `dsh-lsp-stdio`, `dsh-acp-snapshot`) already use the builtin — so the hand-rolled copies are also a consistency gap:
 
 - `packages/llm/llm-retry/src/index.ts` `cancellableDelay()` (~14 lines): `new Promise` + `setTimeout` + manual abort-listener add/remove, resolving `true` on elapse and `false` on abort, consumed once for the backoff wait.
-- `packages/workflow/workflow-worker-thread/src/host.ts` `sleep()` (~7 lines): promise-wrapped unref'd `setTimeout` used as the dispose-grace bound.
+- The former `workflow-worker-thread` host's `sleep()` (~7 lines, evaluated in PR #679): promise-wrapped unref'd `setTimeout` used as the dispose-grace bound.
 - `packages/terminal/terminal-bash/src/session.ts` `delay()` (~4 lines): bare promise-wrapped `setTimeout` used in polling/teardown waits.
 
 ## Proposal

+ 1 - 1
.agents/notes/rejected/simplification/2026-07-26-builtin-timer-promises-for-hand-rolled-sleeps.zh.md

@@ -9,7 +9,7 @@ Status: rejected — 实现(PR #679)证伪了行为等价前提:vitest 的
 三个包手写了用 promise 包装的定时器,而 `node:timers/promises` 内置模块早已提供同等能力;其他包(`dsh-llm-mock-server` 的 `pause()`、`dsh-lsp-stdio`、`dsh-acp-snapshot`)已经在使用该内置模块,因此这些手写副本同时也是一处一致性缺口:
 
 - `packages/llm/llm-retry/src/index.ts` 的 `cancellableDelay()`(约 14 行):`new Promise` + `setTimeout` + 手动添加和移除中止监听器,定时器触发时 resolve 为 `true`、被中止时 resolve 为 `false`,仅在退避等待处消费一次。
-- `packages/workflow/workflow-worker-thread/src/host.ts` 的 `sleep()`(约 7 行):promise 包装、已 unref 的 `setTimeout`,用作 dispose(资源释放)宽限的时间上界。
+- 原 `workflow-worker-thread` host 的 `sleep()`(约 7 行,在 PR #679 中评估):promise 包装、已 unref 的 `setTimeout`,用作 dispose(资源释放)宽限的时间上界。
 - `packages/terminal/terminal-bash/src/session.ts` 的 `delay()`(约 4 行):朴素的 promise 包装 `setTimeout`,用于轮询与拆卸等待。
 
 ## 提案

+ 1 - 0
AGENTS.md

@@ -31,6 +31,7 @@ packages/    @deepseek-ai/dsh-<pkg> workspaces at packages/<group>/<pkg>/
   skill/                skill loading
   web/                  search/fetch tools
   computer-use/         computer interaction
+  browser-use/          browser interaction
   compaction/           context compaction
   context/              request context
   subagent/             delegated agents

+ 4 - 0
THIRD_PARTY_NOTICES.md

@@ -35,6 +35,7 @@ External packages installed for runtime use or distributed inside the prebuilt b
 | [`@anthropic-ai/claude-agent-sdk`](https://github.com/anthropics/claude-agent-sdk-typescript) | SEE LICENSE IN README.md |
 | [`@anthropic-ai/sdk`](https://github.com/anthropics/anthropic-sdk-typescript) | MIT |
 | [`@babel/code-frame`](https://github.com/babel/babel) | MIT |
+| [`@browserbasehq/stagehand`](https://github.com/browserbase/stagehand) | MIT |
 | [`@earendil-works/pi-ai`](https://github.com/earendil-works/pi) | MIT |
 | [`@joplin/turndown-plugin-gfm`](https://github.com/laurent22/joplin-turndown-plugin-gfm) | MIT |
 | [`@jridgewell/gen-mapping`](https://github.com/jridgewell/sourcemaps) | MIT |
@@ -53,6 +54,8 @@ External packages installed for runtime use or distributed inside the prebuilt b
 | [`@opentelemetry/otlp-exporter-base`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
 | [`@opentelemetry/resources`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
 | [`@opentelemetry/sdk-logs`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
+| [`@playwright/mcp`](https://github.com/microsoft/playwright-mcp) | Apache-2.0 |
+| [`@puppeteer/browsers`](https://github.com/puppeteer/puppeteer/tree/main/packages/browsers) | Apache-2.0 |
 | [`@shikijs/langs`](https://github.com/shikijs/shiki) | MIT |
 | [`@standard-schema/spec`](https://github.com/standard-schema/standard-schema) | MIT |
 | [`@tanstack/react-virtual`](https://github.com/TanStack/virtual) | MIT |
@@ -67,6 +70,7 @@ External packages installed for runtime use or distributed inside the prebuilt b
 | [`anser`](https://github.com/IonicaBizau/anser) | MIT |
 | [`buffer`](https://github.com/feross/buffer) | MIT |
 | [`chokidar`](https://github.com/paulmillr/chokidar) | MIT |
+| [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp) | Apache-2.0 |
 | [`clsx`](https://github.com/lukeed/clsx) | MIT |
 | [`commander`](https://github.com/tj/commander.js) | MIT |
 | [`compression`](https://github.com/expressjs/compression) | MIT |

+ 14 - 0
apps/web/tests/expected/sidebar-terminal/theme.expected.md

@@ -0,0 +1,14 @@
+{
+  "light": {
+    "surface": "rgb(255, 255, 255)",
+    "viewport": "rgb(255, 255, 255)",
+    "underlay": "rgb(255, 255, 255)",
+    "foreground": "rgb(15, 17, 21)"
+  },
+  "dark": {
+    "surface": "rgb(21, 21, 23)",
+    "viewport": "rgb(21, 21, 23)",
+    "underlay": "rgb(21, 21, 23)",
+    "foreground": "rgb(249, 250, 251)"
+  }
+}

+ 57 - 0
apps/web/tests/sidebar-terminal.e2e.ts

@@ -81,6 +81,63 @@ describe.skipIf(process.platform === 'win32')('Web sidebar terminal', () => {
     }
   })
 
+  it('follows light, dark and system themes while preserving the running shell and its output', async () => {
+    await page.emulateMedia({ colorScheme: 'light' })
+    await openTerminal(page)
+    const process = processIdentity(0)
+    const terminal = page.locator('[data-sidebar-terminal]')
+    const screen = page.locator('.xterm-rows:visible')
+    await command(page, "DSH_THEME_PROBE=retained; PS1=''; printf '\\033cTHEME_CONTENT_RETAINED\\n'")
+    await expect.poll(() => screen.innerText()).toContain('THEME_CONTENT_RETAINED')
+    const readColors = () => terminal.evaluate((root) => {
+      const xterm = root.querySelector('.xterm')!
+      const rows = root.querySelector('.xterm-rows')!
+      return {
+        surface: getComputedStyle(xterm.parentElement!).backgroundColor,
+        viewport: getComputedStyle(root.querySelector('.xterm-scrollable-element')!).backgroundColor,
+        underlay: getComputedStyle(root.querySelector('.xterm-viewport')!).backgroundColor,
+        foreground: getComputedStyle(rows).color,
+      }
+    })
+    const selectTheme = async (name: string) => {
+      await page.getByRole('button', { name: 'Settings', exact: true }).click()
+      const dialog = page.getByRole('dialog', { name: 'Settings' })
+      const [response] = await Promise.all([
+        page.waitForResponse(candidate => new URL(candidate.url()).pathname === '/api/settings/mutate' && candidate.request().method() === 'POST'),
+        dialog.getByRole('button', { name, exact: true }).click(),
+      ])
+      expect(response.ok()).toBe(true)
+      await page.keyboard.press('Escape')
+      await dialog.waitFor({ state: 'hidden' })
+    }
+    const light = await readColors()
+    expect(light.viewport).toBe(light.surface)
+    expect(light.underlay).toBe(light.surface)
+    await terminal.screenshot({ path: `${shots}/theme-light.png`, animations: 'disabled' })
+    await selectTheme('Dark')
+    await expect.poll(async () => (await readColors()).viewport).not.toBe(light.viewport)
+    const dark = await readColors()
+    expect(dark.viewport).toBe(dark.surface)
+    expect(dark.underlay).toBe(dark.surface)
+    expect(dark.foreground).not.toBe(light.foreground)
+    await terminal.screenshot({ path: `${shots}/theme-dark.png`, animations: 'disabled' })
+    await selectTheme('Light')
+    await expect.poll(readColors).toEqual(light)
+    await selectTheme('System')
+    await page.emulateMedia({ colorScheme: 'dark' })
+    await expect.poll(readColors).toEqual(dark)
+    await page.emulateMedia({ colorScheme: 'light' })
+    await expect.poll(readColors).toEqual(light)
+    await expect.poll(() => screen.innerText()).toContain('THEME_CONTENT_RETAINED')
+    await command(page, 'printf "THEME_STATE:%s\\n" "$DSH_THEME_PROBE"')
+    await expect.poll(() => screen.innerText()).toContain('THEME_STATE:retained')
+    expect(handles).toHaveLength(1)
+    expect(alive(process)).toBe(true)
+    await compareOrRefreshGolden(fileURLToPath(new URL('./expected/sidebar-terminal/theme.expected.md', import.meta.url)),
+      JSON.stringify({ light, dark }, null, 2), webSnapshotMode())
+    expect(tripwire.pageErrors).toEqual([])
+  })
+
   it('completes commands, preserves the process through collapse and reload, resizes, and kills on tab close', async () => {
     onTestFailed(() => saveFailureShot(page, 'sidebar-terminal'))
     await openTerminal(page)

+ 3 - 0
benchmarks/package.json

@@ -6,6 +6,9 @@
   "type": "module",
   "devDependencies": {
     "playwright": "^1.49.0",
+    "@xterm/headless": "^6.0.0",
+    "@deepseek-ai/dsh-terminal": "workspace:^",
+    "@deepseek-ai/dsh-subprocess": "workspace:^",
     "@deepseek-ai/dsh-llm-replay": "workspace:^",
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",

+ 2 - 0
benchmarks/terminal-io/session-adapter.ts

@@ -0,0 +1,2 @@
+/** Private production entry bundled into the plain-Node terminal I/O worker. */
+export { LocalPtySession } from '../../packages/terminal/terminal-bash/src/session.ts'

+ 69 - 0
benchmarks/terminal-io/terminal-io.bench.ts

@@ -0,0 +1,69 @@
+/** Bounded terminal output must not rescan a full retained window on every chunk. */
+import { join } from 'node:path'
+import { expect, it } from 'vitest'
+import { runBuiltBenchmarkWorker } from '../support/built-worker.ts'
+import { ciTimeBudget } from '../support/calibration.ts'
+import type { TerminalIoReport } from './terminal-io.worker.ts'
+
+const MIB = 1024 * 1024
+const ATTEMPTS = 5
+const WORKER = join(import.meta.dirname, '..', '.dsh-build', 'terminal-io', 'terminal-io.worker.js')
+/** M5 Pro / Node 26.5 reference expectations, before shared CI scaling and headroom. */
+const EXPECTED_MS = { steadyIngest: 20, steadyComplete: 50, fullComplete: 120 }
+const MAX_CAPACITY_RATIO = 4
+const MAX_RETAINED_HEAP_BYTES = 16 * MIB
+
+function median(values: readonly number[]): number {
+  return [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)] as number
+}
+
+async function sample(capacity: number, mode: 'steady' | 'full' | 'tiny' | 'filtered'): Promise<TerminalIoReport> {
+  const outcome = await runBuiltBenchmarkWorker<TerminalIoReport>({
+    worker: WORKER, args: [String(capacity), mode], timeoutMs: 120_000, exposeGc: true,
+  })
+  if (outcome.timedOut || outcome.signal !== null || outcome.exitCode !== 0 || outcome.report === undefined) {
+    throw new Error('terminal I/O worker failed: ' + JSON.stringify(outcome))
+  }
+  return outcome.report
+}
+
+it('bounds steady overflow cost as retained terminal capacity grows 32 times', async () => {
+  const small: TerminalIoReport[] = []
+  const large: TerminalIoReport[] = []
+  for (let attempt = 0; attempt < ATTEMPTS; attempt += 1) {
+    small.push(await sample(128 * 1024, 'steady'))
+    large.push(await sample(4 * MIB, 'steady'))
+  }
+  const capacityRatio = median(large.map(row => row.ingestMs)) / median(small.map(row => row.ingestMs))
+  const ingestBudgetMs = ciTimeBudget(EXPECTED_MS.steadyIngest)
+  const completeBudgetMs = ciTimeBudget(EXPECTED_MS.steadyComplete)
+  console.log(JSON.stringify({ scenario: 'terminal-steady', small, large, capacityRatio, ingestBudgetMs, completeBudgetMs }))
+  expect(capacityRatio).toBeLessThanOrEqual(MAX_CAPACITY_RATIO)
+  for (const rows of [small, large]) {
+    expect(median(rows.map(row => row.ingestMs))).toBeLessThanOrEqual(ingestBudgetMs)
+    expect(median(rows.map(row => row.completeMs))).toBeLessThanOrEqual(completeBudgetMs)
+    expect(Math.max(...rows.map(row => row.retainedHeapBytes))).toBeLessThanOrEqual(MAX_RETAINED_HEAP_BYTES)
+  }
+})
+
+it('bounds retained memory when five MiB arrives in sixteen-byte chunks', async () => {
+  const report = await sample(4 * MIB, 'tiny')
+  console.log(JSON.stringify({ scenario: 'terminal-tiny-chunks', report, heapBudgetBytes: MAX_RETAINED_HEAP_BYTES }))
+  expect(report.retainedHeapBytes).toBeLessThanOrEqual(MAX_RETAINED_HEAP_BYTES)
+})
+
+it('releases filtered OSC storage behind retained visible string slices', async () => {
+  const report = await sample(4 * MIB, 'filtered')
+  console.log(JSON.stringify({ scenario: 'terminal-filtered-chunks', report, heapBudgetBytes: MAX_RETAINED_HEAP_BYTES }))
+  expect(report.retainedHeapBytes).toBeLessThanOrEqual(MAX_RETAINED_HEAP_BYTES)
+})
+
+it('completes a five MiB terminal send with bounded retained output', async () => {
+  const samples: TerminalIoReport[] = []
+  for (let attempt = 0; attempt < ATTEMPTS; attempt += 1) samples.push(await sample(4 * MIB, 'full'))
+  const completeMedianMs = median(samples.map(row => row.completeMs))
+  const completeBudgetMs = ciTimeBudget(EXPECTED_MS.fullComplete)
+  console.log(JSON.stringify({ scenario: 'terminal-five-mib', samples, completeMedianMs, completeBudgetMs }))
+  expect(completeMedianMs).toBeLessThanOrEqual(completeBudgetMs)
+  expect(Math.max(...samples.map(row => row.retainedHeapBytes))).toBeLessThanOrEqual(MAX_RETAINED_HEAP_BYTES)
+})

+ 106 - 0
benchmarks/terminal-io/terminal-io.worker.ts

@@ -0,0 +1,106 @@
+/** Terminal output ingestion and readiness with a deterministic provider boundary. */
+import { Buffer } from 'node:buffer'
+import { performance } from 'node:perf_hooks'
+import { Readable } from 'node:stream'
+import type { SubprocessOutcome, SubprocessTerminalHandle } from '@deepseek-ai/dsh-subprocess'
+import { assertBuiltBenchmarkRuntime } from '../support/built-worker.ts'
+import { LocalPtySession } from './session-adapter.ts'
+
+const MIB = 1024 * 1024
+const CHUNK_BYTES = 16 * 1024
+
+/** One fresh-session sample; retained heap includes the live session and returned output. */
+export interface TerminalIoReport {
+  mode: string
+  capacityBytes: number
+  chunkBytes: number
+  prefillBytes: number
+  timedBytes: number
+  ingestMs: number
+  completeMs: number
+  retainedHeapBytes: number
+  viewportBytes: number
+  readBytes: number
+  truncated: boolean
+}
+
+async function measure(capacityBytes: number, mode: string): Promise<TerminalIoReport> {
+  const chunkBytes = mode === 'filtered' ? 64 * 1024 : mode === 'tiny' ? 16 : CHUNK_BYTES
+  const chunk = Buffer.alloc(chunkBytes, 'x')
+  if (mode === 'filtered') {
+    // Each decoded callback has 56 KiB of discarded OSC followed by an 8 KiB string slice.
+    chunk.write('\x1b]0;', 0)
+    chunk[56 * 1024 - 1] = 7
+  }
+  const prefillBytes = mode === 'steady' ? capacityBytes : 0
+  const timedBytes = mode === 'filtered' ? 513 * chunkBytes : mode === 'steady' ? MIB : 5 * MIB
+  const output = new Readable({ read() {} })
+  const ended = Promise.withResolvers<SubprocessOutcome>()
+  const writeReady = Promise.withResolvers<void>()
+  const terminal: SubprocessTerminalHandle = {
+    pid: 1,
+    output,
+    done: ended.promise,
+    async write() { writeReady.resolve() },
+    async resize() {},
+    async inspectForeground() { return { processGroupId: 1, inputWaiting: false } },
+    async signalForeground() { return 1 },
+    async terminate() {
+      output.emit('end')
+      ended.resolve({ exitCode: 0, signal: null })
+      await ended.promise
+    },
+  }
+  global.gc?.()
+  const heapBefore = process.memoryUsage().heapUsed
+  const session = new LocalPtySession(terminal, {
+    backendType: 'shell', shellDialect: 'bash', shellPath: '/bin/bash', shellArgs: [],
+    rows: 40, cols: 160, scrollbackLines: 10_000, scrollbackMaxBytes: capacityBytes,
+    maxReadBytes: Math.min(256 * 1024, capacityBytes),
+    pollIntervalMs: 1, exactProbeAfterMs: 150, idleSilenceMs: 1,
+    handoffGraceMs: 1, timeoutMs: 120_000, disposeGraceMs: 1,
+  })
+  try {
+    if (prefillBytes > 0) {
+      const prefill = session.startSend({ text: 'prefill', submit: false })
+      await writeReady.promise
+      for (let bytes = 0; bytes < prefillBytes; bytes += chunkBytes) output.emit('data', chunk)
+      const ready = await prefill.done
+      if (ready.waitReason !== 'inferred_idle') throw new Error('prefill did not reach readiness')
+    }
+    const start = performance.now()
+    const operation = session.startSend({ text: '', submit: false })
+    for (let bytes = 0; bytes < timedBytes; bytes += chunkBytes) output.emit('data', chunk)
+    const ingestMs = performance.now() - start
+    const result = await operation.done
+    const read = session.read({ count: 10_000 })
+    const completeMs = performance.now() - start
+    if (result.waitReason !== 'inferred_idle' || !result.truncated || !read.truncated) {
+      throw new Error('output did not reach bounded ready endpoint')
+    }
+    const expectedBytes = Math.min(256 * 1024, capacityBytes)
+    if (Buffer.byteLength(result.viewport) !== expectedBytes || Buffer.byteLength(read.text) !== expectedBytes) {
+      throw new Error('bounded output endpoint has unexpected byte count')
+    }
+    global.gc?.()
+    return {
+      mode, capacityBytes, chunkBytes, prefillBytes, timedBytes, ingestMs, completeMs,
+      retainedHeapBytes: process.memoryUsage().heapUsed - heapBefore,
+      viewportBytes: Buffer.byteLength(result.viewport), readBytes: Buffer.byteLength(read.text),
+      truncated: result.truncated && read.truncated,
+    }
+  } finally {
+    await session.close('benchmark complete')
+    output.destroy()
+  }
+}
+
+assertBuiltBenchmarkRuntime(import.meta.url, {
+  '@deepseek-ai/dsh-terminal': import.meta.resolve('@deepseek-ai/dsh-terminal'),
+})
+const capacityBytes = Number(process.argv[2])
+const mode = process.argv[3]
+if (![128 * 1024, 4 * MIB].includes(capacityBytes) || (mode !== 'steady' && mode !== 'full' && mode !== 'tiny' && mode !== 'filtered')) {
+  throw new Error('usage: terminal-io.worker.js <131072|4194304> <steady|full|tiny|filtered>')
+}
+console.log(JSON.stringify(await measure(capacityBytes, mode)))

+ 7 - 0
benchmarks/tsdown.config.ts

@@ -14,6 +14,13 @@ const shared = {
 
 /** Compile measured benchmark workers while keeping workspace packages on their built `lib` entries. */
 export default defineConfig([
+  {
+    ...shared,
+    entry: { 'terminal-io.worker': 'terminal-io/terminal-io.worker.ts' },
+    outDir: '.dsh-build/terminal-io',
+    clean: true,
+    tsconfig: 'tsconfig.host.json',
+  },
   {
     ...shared,
     entry: { 'reconnect.worker': 'active-stream-reconnect/reconnect.worker.client.ts' },

+ 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: 3addf70ab997590d97a2581ea0a459e6d4e86911
-architecture.zh.md: 078d99dc06cf49233d0ca4c036bab89a3f664d57
+architecture.md: efd726f9fcccc53c5bf927f4eadf20b6f5b36be8
+architecture.zh.md: 043eeecd1299e06ad7d728a09036f07dddca6825

+ 2 - 0
docs/architecture.md

@@ -75,6 +75,8 @@ Events are the extension points, and picking the right domain is the first decis
 - **Agent events** (`agent/*`) carry a live `Agent`: inbox, step, status, request, validation, continuation. Use one to observe or intercept work in flight.
 - **Capability events** attach policy and adapters to a seam (`fs/*`, `tools/*`, `telemetry/*`) without importing the loop.
 
+AgentLoop awaits serial `agent/created` initialization before starting queued work. Initialization failure rolls back creation; [agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) defines teardown ordering.
+
 The [event map](event-producer-consumer.md) lists every event's producers and consumers.
 
 ## Turn flow

+ 2 - 0
docs/architecture.zh.md

@@ -77,6 +77,8 @@ Electron 通过内置的上游 Node.js 进程启动私有 Desktop Host 包;该
 - **Agent 事件**(`agent/*`)携带活跃 `Agent`:inbox、步骤、状态、请求、验证、续跑。要观察或拦截进行中的工作时,使用它。
 - **能力事件**无需导入循环即可向某个 seam(`fs/*`、`tools/*`、`telemetry/*`)附加策略和适配器。
 
+AgentLoop 在启动已排队工作前等待串行 `agent/created` 初始化。初始化失败会回滚创建;[agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation)定义 teardown 顺序。
+
 [事件映射](event-producer-consumer.zh.md)列出每个事件的生产方与消费方。
 
 <a id="turn-flow"></a>

+ 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: 2b5a06b77fed90e0d9ef518b99e745379b41d8c2
-capability-seams.zh.md: 7b7630592f94807a56239fc9c8c198fe22c3db39
+capability-seams.md: 839d241869f8f88125cd7db6ad75646f25b4460d
+capability-seams.zh.md: eb6088bf63e51e6a4bb1c41453dd8a2af102b610

+ 13 - 0
docs/capability-seams.md

@@ -10,6 +10,11 @@ flowchart LR
   pkg_mcp_resources["mcp-resources"]
   svc_mcpResources["ctx.mcpResources<br/>Scoped MCP resource access"]
   pkg_mcp_client["mcp-client"]
+  pkg_browser_use["browser-use"]
+  svc_browserUse["ctx.browserUse<br/>Browser-use provider registration"]
+  pkg_experimental_browser_use_playwright_mcp["experimental-browser-use-playwright-mcp"]
+  pkg_experimental_browser_use_chrome_devtools_mcp["experimental-browser-use-chrome-devtools-mcp"]
+  pkg_experimental_browser_use_stagehand_native["experimental-browser-use-stagehand-native"]
   pkg_computer_use["computer-use"]
   svc_computerUse["ctx.computerUse<br/>Computer-use provider registration"]
   pkg_experimental_computer_use_cua_driver_mcp["experimental-computer-use-cua-driver-mcp"]
@@ -253,6 +258,7 @@ flowchart LR
   pkg_authorization --> svc_authorization
   pkg_bash_local --> svc_shell
   pkg_bash_sandbox --> svc_shell
+  pkg_browser_use --> svc_browserUse
   pkg_client_file_upload --> svc_fileUploads
   pkg_client_modules --> svc_clientModules
   pkg_command_feedback --> svc_sessionFeedback
@@ -267,6 +273,9 @@ flowchart LR
   pkg_credentials_local --> svc_credentials
   pkg_deepseek_llm_api_extensions --> svc_deepseekLlmApiExtensions
   pkg_experimental_agent_team --> svc_agentTeams
+  pkg_experimental_browser_use_chrome_devtools_mcp --> svc_browserUse
+  pkg_experimental_browser_use_playwright_mcp --> svc_browserUse
+  pkg_experimental_browser_use_stagehand_native --> svc_browserUse
   pkg_experimental_computer_use_cua_driver_mcp --> svc_computerUse
   pkg_experimental_computer_use_cua_driver_native --> svc_computerUse
   pkg_experimental_ptc_runtime_python --> svc_ptcRuntime
@@ -377,6 +386,9 @@ flowchart LR
   svc_attachments --> pkg_llm_pi_ai
   svc_attachments --> pkg_tool_fs
   svc_authorization --> pkg_llm_pi_ai
+  svc_browserUse --> pkg_experimental_browser_use_chrome_devtools_mcp
+  svc_browserUse --> pkg_experimental_browser_use_playwright_mcp
+  svc_browserUse --> pkg_experimental_browser_use_stagehand_native
   svc_clientModules --> pkg_client_hmr
   svc_compaction --> pkg_compaction_basic
   svc_computerUse --> pkg_experimental_computer_use_cua_driver_mcp
@@ -497,6 +509,7 @@ flowchart LR
 | ctx key | Role | Owner | Implementations | Direct consumers | Companion plugins | Note |
 | --- | --- | --- | --- | --- | --- | --- |
 | `ctx.mcpResources` | `seam` | [`mcp-resources`](../packages/mcp/mcp-resources) | [`mcp-client`](../packages/mcp/mcp-client) | [`mcp-resources`](../packages/mcp/mcp-resources) | - | Connection-owned providers serve shared resource tools in the calling agent scope. |
+| `ctx.browserUse` | `seam` | [`browser-use`](../packages/browser-use/browser-use) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | - | One provider-owned name per service instance. Providers own their tools and browser resources per live Session; the shared service has no browser operation API. |
 | `ctx.computerUse` | `seam` | [`computer-use`](../packages/computer-use/computer-use) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | - | One provider-owned name per service instance. Each provider also owns its model tools; the service has no common action API, runtime selection, or Session workflow lock. |
 | `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | [`api-session-controller`](../packages/api/session-controller), [`tool-fs`](../packages/fs/tool-fs), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-deepseek`](../packages/llm/llm-deepseek) | - | The host commits accepted images before session events; provider adapters resolve authorized durable references into provider-native content. |
 | `ctx.fileUploads` | `core` | [`client-file-upload`](../packages/client/file-upload) | - | [`api-session-controller`](../packages/api/session-controller) | - | Owns streaming intake, durable storage, and staged receipt lifetime; the Session controller binds receipts to accepted submissions. |

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

@@ -12,6 +12,11 @@ flowchart LR
   pkg_mcp_resources["mcp-resources"]
   svc_mcpResources["ctx.mcpResources<br/>Scoped MCP resource access"]
   pkg_mcp_client["mcp-client"]
+  pkg_browser_use["browser-use"]
+  svc_browserUse["ctx.browserUse<br/>Browser-use provider registration"]
+  pkg_experimental_browser_use_playwright_mcp["experimental-browser-use-playwright-mcp"]
+  pkg_experimental_browser_use_chrome_devtools_mcp["experimental-browser-use-chrome-devtools-mcp"]
+  pkg_experimental_browser_use_stagehand_native["experimental-browser-use-stagehand-native"]
   pkg_computer_use["computer-use"]
   svc_computerUse["ctx.computerUse<br/>Computer-use provider registration"]
   pkg_experimental_computer_use_cua_driver_mcp["experimental-computer-use-cua-driver-mcp"]
@@ -255,6 +260,7 @@ flowchart LR
   pkg_authorization --> svc_authorization
   pkg_bash_local --> svc_shell
   pkg_bash_sandbox --> svc_shell
+  pkg_browser_use --> svc_browserUse
   pkg_client_file_upload --> svc_fileUploads
   pkg_client_modules --> svc_clientModules
   pkg_command_feedback --> svc_sessionFeedback
@@ -269,6 +275,9 @@ flowchart LR
   pkg_credentials_local --> svc_credentials
   pkg_deepseek_llm_api_extensions --> svc_deepseekLlmApiExtensions
   pkg_experimental_agent_team --> svc_agentTeams
+  pkg_experimental_browser_use_chrome_devtools_mcp --> svc_browserUse
+  pkg_experimental_browser_use_playwright_mcp --> svc_browserUse
+  pkg_experimental_browser_use_stagehand_native --> svc_browserUse
   pkg_experimental_computer_use_cua_driver_mcp --> svc_computerUse
   pkg_experimental_computer_use_cua_driver_native --> svc_computerUse
   pkg_experimental_ptc_runtime_python --> svc_ptcRuntime
@@ -379,6 +388,9 @@ flowchart LR
   svc_attachments --> pkg_llm_pi_ai
   svc_attachments --> pkg_tool_fs
   svc_authorization --> pkg_llm_pi_ai
+  svc_browserUse --> pkg_experimental_browser_use_chrome_devtools_mcp
+  svc_browserUse --> pkg_experimental_browser_use_playwright_mcp
+  svc_browserUse --> pkg_experimental_browser_use_stagehand_native
   svc_clientModules --> pkg_client_hmr
   svc_compaction --> pkg_compaction_basic
   svc_computerUse --> pkg_experimental_computer_use_cua_driver_mcp
@@ -499,6 +511,7 @@ flowchart LR
 | ctx 键 | 角色 | 所属包 | 实现 | 直接消费方 | 配套插件 | 说明 |
 | --- | --- | --- | --- | --- | --- | --- |
 | `ctx.mcpResources` | `seam` | [`mcp-resources`](../packages/mcp/mcp-resources) | [`mcp-client`](../packages/mcp/mcp-client) | [`mcp-resources`](../packages/mcp/mcp-resources) | - | 连接所有者提供的操作在调用 agent 的作用域内服务于共享资源工具。 |
+| `ctx.browserUse` | `seam` | [`browser-use`](../packages/browser-use/browser-use) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | - | 每个服务实例注册一个提供方拥有的名称。提供方按实时 Session 拥有自己的工具与浏览器资源;共享服务不提供浏览器操作 API。 |
 | `ctx.computerUse` | `seam` | [`computer-use`](../packages/computer-use/computer-use) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | - | 每个服务实例只注册一个提供方自定的名称。各提供方也拥有自己的模型工具;服务不提供通用操作 API、运行时选择或 Session 流程锁。 |
 | `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | [`api-session-controller`](../packages/api/session-controller), [`tool-fs`](../packages/fs/tool-fs), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-deepseek`](../packages/llm/llm-deepseek) | - | 宿主会在会话事件之前提交已接受的图片;提供方适配器将已授权的持久引用解析为提供方原生内容。 |
 | `ctx.fileUploads` | `core` | [`client-file-upload`](../packages/client/file-upload) | - | [`api-session-controller`](../packages/api/session-controller) | - | 负责流式接收、持久存储和暂存回执生命周期;Session Controller 将回执绑定到已接受的提交。 |

+ 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: fba7074ec4e97b765a9ca627ced216d27ab1db85
-config-catalog.zh.md: 329d38717ec1b4c650a5a03c521eaead7b2f42d6
+config-catalog.md: 6b5a03e9f5f59ff987093a94334b00b0d81b2c9d
+config-catalog.zh.md: 6eb6c566b87ad54a886b272b18f13473b8b2ec63

+ 75 - 1
docs/config-catalog.md

@@ -111,7 +111,7 @@ export interface Config {
 
 Depends on: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/core.md)
 
-Source: [`packages/core/agent-loop/src/index.ts:318`](../packages/core/agent-loop/src/index.ts)
+Source: [`packages/core/agent-loop/src/index.ts:317`](../packages/core/agent-loop/src/index.ts)
 
 <a id="deepseek-aidsh-agent-presets"></a>
 
@@ -566,6 +566,78 @@ export interface Config {
 
 Source: [`packages/experimental/agent-team/src/types.ts:130`](../packages/experimental/agent-team/src/types.ts)
 
+<a id="deepseek-aidsh-experimental-browser-use-chrome-devtools-mcp"></a>
+
+## `@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp`
+
+Requires: `browserUse` · `agents` · `tools` · `systemPrompt`
+
+```ts config-catalog
+/** Fixed Chromium launch or existing-browser attachment settings. */
+export type Config = BrowserMcpConfig
+```
+
+Depends on: `BrowserMcpConfig` (`@deepseek-ai/dsh-experimental-browser-use-runtime/mcp`)
+
+Source: [`packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts:14`](../packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts)
+
+<a id="deepseek-aidsh-experimental-browser-use-playwright-mcp"></a>
+
+## `@deepseek-ai/dsh-experimental-browser-use-playwright-mcp`
+
+Requires: `browserUse` · `agents` · `tools` · `systemPrompt`
+
+```ts config-catalog
+/** Fixed Chromium launch or existing-browser attachment settings. */
+export type Config = BrowserMcpConfig
+```
+
+Depends on: `BrowserMcpConfig` (`@deepseek-ai/dsh-experimental-browser-use-runtime/mcp`)
+
+Source: [`packages/experimental/browser-use-playwright-mcp/src/index.ts:15`](../packages/experimental/browser-use-playwright-mcp/src/index.ts)
+
+<a id="deepseek-aidsh-experimental-browser-use-stagehand-native"></a>
+
+## `@deepseek-ai/dsh-experimental-browser-use-stagehand-native`
+
+Requires: `browserUse` · `agents` · `tools` · `systemPrompt`
+
+```ts config-catalog
+/** Profile-owned browser connection and independent Stagehand model credentials. */
+export interface Config {
+  /** Native Stagehand model and credentials; independent of the Session model. */
+  model: StagehandModelConfig
+  /** Launch a fresh browser or attach to the configured existing endpoint. */
+  mode: 'launch' | 'attach'
+  /** CDP HTTP or WebSocket endpoint, required only for attach mode. */
+  cdpEndpoint?: string
+  /** Optional Stagehand extension id for an existing browser. */
+  extensionId?: string
+  /** Installed Chrome/Chromium executable used in launch mode. */
+  executablePath?: string
+  /** Hide an owned browser's window. */
+  headless?: boolean
+  /** Deadline for Chromium startup and Stagehand navigation/action operations. */
+  operationTimeoutMs?: number
+  /** Grace for native SDK cleanup before its connection Worker is terminated. */
+  shutdownGraceMs?: number
+}
+
+/** Profile-owned model settings accepted by the pinned Stagehand SDK. */
+export interface StagehandModelConfig {
+  /** Provider-prefixed model name from Stagehand's supported model catalog. */
+  modelName: ModelConfig['modelName']
+  /** Explicit API key sent to Stagehand's browser extension. */
+  apiKey: string
+  /** Additional headers sent with the extension's model requests. */
+  headers?: Record<string, string>
+}
+```
+
+Depends on: `ModelConfig` (`@browserbasehq/stagehand`)
+
+Source: [`packages/experimental/browser-use-stagehand-native/src/index.ts:28`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
 <a id="deepseek-aidsh-experimental-computer-use-cua-driver-mcp"></a>
 
 ## `@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp`
@@ -3544,6 +3616,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
 - `@deepseek-ai/dsh-api-remotes` — requires `typertGateway` ([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts))
 - `@deepseek-ai/dsh-api-workspace-controller` — requires `typert` · `workspaceRegistry` ([`packages/api/workspace-controller/src/index.ts`](../packages/api/workspace-controller/src/index.ts))
 - `@deepseek-ai/dsh-authorization` — requires `credentials` ([`packages/credentials/authorization/src/index.ts`](../packages/credentials/authorization/src/index.ts))
+- `@deepseek-ai/dsh-browser-use` ([`packages/browser-use/browser-use/src/index.ts`](../packages/browser-use/browser-use/src/index.ts))
 - `@deepseek-ai/dsh-client-file-upload` — requires `agents` · `attachments` · `commands` · `connection` ([`packages/client/file-upload/src/index.ts`](../packages/client/file-upload/src/index.ts))
 - `@deepseek-ai/dsh-client-locale` ([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts))
 - `@deepseek-ai/dsh-client-modules` — requires `loader` ([`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts))
@@ -3674,6 +3747,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
 - `@deepseek-ai/dsh-deque` ([`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts))
 - `@deepseek-ai/dsh-experimental-agent-team-profile` ([`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts))
 - `@deepseek-ai/dsh-experimental-agent-team-web-profile` ([`packages/experimental/agent-team-web-profile/src/index.ts`](../packages/experimental/agent-team-web-profile/src/index.ts))
+- `@deepseek-ai/dsh-experimental-browser-use-runtime` ([`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts))
 - `@deepseek-ai/dsh-experimental-webworker-packer` ([`packages/experimental/webworker-packer/src/index.ts`](../packages/experimental/webworker-packer/src/index.ts))
 - `@deepseek-ai/dsh-experimental-webworker-runtime` ([`packages/experimental/webworker-runtime/src/index.ts`](../packages/experimental/webworker-runtime/src/index.ts))
 - `@deepseek-ai/dsh-home-paths` ([`packages/util/home-paths/src/index.ts`](../packages/util/home-paths/src/index.ts))

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

@@ -113,7 +113,7 @@ export interface Config {
 
 依赖:[`AgentOptions`](subsystems/core.zh.md) · [`SessionId`](subsystems/core.zh.md)
 
-来源:[`packages/core/agent-loop/src/index.ts:318`](../packages/core/agent-loop/src/index.ts)
+来源:[`packages/core/agent-loop/src/index.ts:317`](../packages/core/agent-loop/src/index.ts)
 
 <a id="deepseek-aidsh-agent-presets"></a>
 
@@ -568,6 +568,78 @@ export interface Config {
 
 来源:[`packages/experimental/agent-team/src/types.ts:130`](../packages/experimental/agent-team/src/types.ts)
 
+<a id="deepseek-aidsh-experimental-browser-use-chrome-devtools-mcp"></a>
+
+## `@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp`
+
+需要:`browserUse` · `agents` · `tools` · `systemPrompt`
+
+```ts config-catalog
+/** Fixed Chromium launch or existing-browser attachment settings. */
+export type Config = BrowserMcpConfig
+```
+
+依赖:`BrowserMcpConfig` (`@deepseek-ai/dsh-experimental-browser-use-runtime/mcp`)
+
+来源:[`packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts:14`](../packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts)
+
+<a id="deepseek-aidsh-experimental-browser-use-playwright-mcp"></a>
+
+## `@deepseek-ai/dsh-experimental-browser-use-playwright-mcp`
+
+需要:`browserUse` · `agents` · `tools` · `systemPrompt`
+
+```ts config-catalog
+/** Fixed Chromium launch or existing-browser attachment settings. */
+export type Config = BrowserMcpConfig
+```
+
+依赖:`BrowserMcpConfig` (`@deepseek-ai/dsh-experimental-browser-use-runtime/mcp`)
+
+来源:[`packages/experimental/browser-use-playwright-mcp/src/index.ts:15`](../packages/experimental/browser-use-playwright-mcp/src/index.ts)
+
+<a id="deepseek-aidsh-experimental-browser-use-stagehand-native"></a>
+
+## `@deepseek-ai/dsh-experimental-browser-use-stagehand-native`
+
+需要:`browserUse` · `agents` · `tools` · `systemPrompt`
+
+```ts config-catalog
+/** Profile-owned browser connection and independent Stagehand model credentials. */
+export interface Config {
+  /** Native Stagehand model and credentials; independent of the Session model. */
+  model: StagehandModelConfig
+  /** Launch a fresh browser or attach to the configured existing endpoint. */
+  mode: 'launch' | 'attach'
+  /** CDP HTTP or WebSocket endpoint, required only for attach mode. */
+  cdpEndpoint?: string
+  /** Optional Stagehand extension id for an existing browser. */
+  extensionId?: string
+  /** Installed Chrome/Chromium executable used in launch mode. */
+  executablePath?: string
+  /** Hide an owned browser's window. */
+  headless?: boolean
+  /** Deadline for Chromium startup and Stagehand navigation/action operations. */
+  operationTimeoutMs?: number
+  /** Grace for native SDK cleanup before its connection Worker is terminated. */
+  shutdownGraceMs?: number
+}
+
+/** Profile-owned model settings accepted by the pinned Stagehand SDK. */
+export interface StagehandModelConfig {
+  /** Provider-prefixed model name from Stagehand's supported model catalog. */
+  modelName: ModelConfig['modelName']
+  /** Explicit API key sent to Stagehand's browser extension. */
+  apiKey: string
+  /** Additional headers sent with the extension's model requests. */
+  headers?: Record<string, string>
+}
+```
+
+依赖:`ModelConfig` (`@browserbasehq/stagehand`)
+
+来源:[`packages/experimental/browser-use-stagehand-native/src/index.ts:28`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
 <a id="deepseek-aidsh-experimental-computer-use-cua-driver-mcp"></a>
 
 ## `@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp`
@@ -3546,6 +3618,7 @@ export interface Config {
 - `@deepseek-ai/dsh-api-remotes` — 需要 `typertGateway`([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts))
 - `@deepseek-ai/dsh-api-workspace-controller` — 需要 `typert` · `workspaceRegistry`([`packages/api/workspace-controller/src/index.ts`](../packages/api/workspace-controller/src/index.ts))
 - `@deepseek-ai/dsh-authorization` — 需要 `credentials`([`packages/credentials/authorization/src/index.ts`](../packages/credentials/authorization/src/index.ts))
+- `@deepseek-ai/dsh-browser-use` ([`packages/browser-use/browser-use/src/index.ts`](../packages/browser-use/browser-use/src/index.ts))
 - `@deepseek-ai/dsh-client-file-upload` — 需要 `agents` · `attachments` · `commands` · `connection`([`packages/client/file-upload/src/index.ts`](../packages/client/file-upload/src/index.ts))
 - `@deepseek-ai/dsh-client-locale`([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts))
 - `@deepseek-ai/dsh-client-modules` — 需要 `loader`([`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts))
@@ -3675,6 +3748,7 @@ export interface Config {
 - `@deepseek-ai/dsh-deque`([`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts))
 - `@deepseek-ai/dsh-experimental-agent-team-profile`([`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts))
 - `@deepseek-ai/dsh-experimental-agent-team-web-profile`([`packages/experimental/agent-team-web-profile/src/index.ts`](../packages/experimental/agent-team-web-profile/src/index.ts))
+- `@deepseek-ai/dsh-experimental-browser-use-runtime` ([`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts))
 - `@deepseek-ai/dsh-experimental-webworker-packer`([`packages/experimental/webworker-packer/src/index.ts`](../packages/experimental/webworker-packer/src/index.ts))
 - `@deepseek-ai/dsh-experimental-webworker-runtime`([`packages/experimental/webworker-runtime/src/index.ts`](../packages/experimental/webworker-runtime/src/index.ts))
 - `@deepseek-ai/dsh-home-paths`([`packages/util/home-paths/src/index.ts`](../packages/util/home-paths/src/index.ts))

+ 2 - 2
docs/cookbook/extension-cookbook.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/cookbook/extension-cookbook.md
-extension-cookbook.md: 40d91dbe698a2f2801503a5278c885d6b7a39ed5
-extension-cookbook.zh.md: 149817dec36c5a625e417e9cc1e2879a012682d6
+extension-cookbook.md: b54f0191b48c109b21e111cac27020efcb37174a
+extension-cookbook.zh.md: c03e92b21e156a82d51d2afd742f1dd880e32a47

+ 1 - 1
docs/cookbook/extension-cookbook.md

@@ -103,7 +103,7 @@ Every product feature maps to a listener on a documented extension point — the
 
 | Product feature | Plugin mechanism |
 |---|---|
-| Hook system (user + project level) | listeners on `agent/session-start`, `agent/pre-step`, `agent/request`, `tools/pre-execute`, `tools/post-execute`, and `agent/turn-stopping`; the waterfalls return typed decisions, while `agent/turn-stopping` may steer another step; the `dsh-hooks-claude-code` / `dsh-hooks-codex` bridges map hook config files onto these extension points |
+| Hook system (user + project level) | listeners on `agent/created`, `agent/pre-step`, `agent/request`, `tools/pre-execute`, `tools/post-execute`, and `agent/turn-stopping`; the waterfalls return typed decisions, while `agent/turn-stopping` may steer another step; the `dsh-hooks-claude-code` / `dsh-hooks-codex` bridges map hook config files onto these extension points |
 | `/goal` | `ctx.goals` owns durable state, `dsh-goal-round-driver` schedules same-session rounds through the public `Agent`, and separate command/tool producers expose human/model control |
 | `/loop` | on the `turn/end` session event, `followup()` the next iteration; or force-continue |
 | Dynamic workflow | `ctx.workflowEngine` + the PTC workflow engine + the `workflow` tool; structured in-process children enforce output with scoped prompt/tool registrations, a monotonic tool guard, final `tools/result` commit (including enclosing `run_code`), and the structured-output execution's monotonic `concludeTurn()` marker |

+ 1 - 1
docs/cookbook/extension-cookbook.zh.md

@@ -107,7 +107,7 @@ export function apply(ctx: Context) {
 
 | 产品功能 | 插件机制 |
 |---|---|
-| 钩子系统(用户级 + 项目级) | `agent/session-start`、`agent/pre-step`、`agent/request`、`tools/pre-execute`、`tools/post-execute` 和 `agent/turn-stopping` 上的监听器;waterfall 返回类型化决策,`agent/turn-stopping` 则可通过 steering(中途引导)触发下一步;`dsh-hooks-claude-code` / `dsh-hooks-codex` 桥接器将钩子配置文件映射到这些扩展点上 |
+| 钩子系统(用户级 + 项目级) | `agent/created`、`agent/pre-step`、`agent/request`、`tools/pre-execute`、`tools/post-execute` 和 `agent/turn-stopping` 上的监听器;waterfall 返回类型化决策,`agent/turn-stopping` 则可通过 steering(中途引导)触发下一步;`dsh-hooks-claude-code` / `dsh-hooks-codex` 桥接器将钩子配置文件映射到这些扩展点上 |
 | `/goal` | `ctx.goals` 管理持久状态,`dsh-goal-round-driver` 通过公共 `Agent` 调度同会话 Round,独立的命令/工具生产方分别提供人类/模型控制 |
 | `/loop` | 在 `turn/end` 会话事件上 `followup()` 下一次迭代;或强制继续 |
 | 动态工作流 | `ctx.workflowEngine` + PTC 工作流引擎 + `workflow` 工具;结构化的进程内子任务通过作用域化的提示词/工具注册、单调工具守卫、最终 `tools/result` 提交(包括外层 `run_code`)和结构化输出执行的单调 `concludeTurn()` 标记来强制输出 |

+ 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: 3a7c8798771b00ebea90a1e8bfbc0265a9a13592
-event-producer-consumer.zh.md: 1041898b52bed1b20bd9a3cedb3d93d1515d614f
+event-producer-consumer.md: 1d829922a9bc889c598e2dc6271c21b2e8cbd36e
+event-producer-consumer.zh.md: 3880102c79b29f02c1e0fff8389c606a71654605

+ 17 - 18
docs/event-producer-consumer.md

@@ -7,21 +7,20 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 
 | Event | Mode | Declared in | Dispatchers | Listeners |
 | --- | --- | --- | --- | --- |
-| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:246`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
+| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:245`](../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:82`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` |
-| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:373`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:258`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:267`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:403`](../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), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
-| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:296`](../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:304`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
-| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
-| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:330`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`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), [`session-reference`](../packages/context/session-reference), [`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), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:347`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
-| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
-| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:316`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`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:277`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
-| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:391`](../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), [`workspace-changes`](../packages/deliverables/workspace-changes) |
+| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
+| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | [`agent-presets`](../packages/preset/agent-presets), `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`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), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../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), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
+| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../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:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
+| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
+| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`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), [`session-reference`](../packages/context/session-reference), [`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), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
+| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
+| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
+| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../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), [`workspace-changes`](../packages/deliverables/workspace-changes) |
 | `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:601`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
 | `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:581`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
 | `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:608`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
@@ -61,10 +60,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:144`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
 | `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:150`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
 | `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:161`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
-| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt) |
+| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt) |
 | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
-| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:201`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:157`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
+| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:201`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:157`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
 | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:169`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
 | `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:146`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
 | `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:183`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
@@ -83,7 +82,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | Event string | Dispatchers | Listeners |
 | --- | --- | --- |
 | `internal/dispatch` | - | `agent-team`, [`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-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), `terminal-controller`, [`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), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) |
-| `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), `modules` |
+| `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), [`mcp-client`](../packages/mcp/mcp-client), `modules` |
 | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` |
 | `internal/status` | - | [`agent`](../packages/core/agent), `inspector` |
 | `internal/update` | - | [`app-boot`](../packages/boot/app-boot) |

+ 18 - 19
docs/event-producer-consumer.zh.md

@@ -7,23 +7,22 @@
 
 本矩阵展示哪些包会派发各个 harness 自有事件,以及哪些包会监听这些事件。事件之间存在多对多关系,因此密集的关系数据以表格而非一张大型关系图呈现。接收方和事件名称类型还涵盖有意绕过 `ctx.emit` 的内含派发位置,例如 subagent 生命周期封装。
 
-| 事件 | 模式 | 声明位置 | 派发方 | 监听方 |
+| Event | Mode | Declared in | Dispatchers | Listeners |
 | --- | --- | --- | --- | --- |
-| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:246`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
+| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:245`](../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:82`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` |
-| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:373`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:258`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:267`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:403`](../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), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
-| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:296`](../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:304`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
-| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
-| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:330`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`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), [`session-reference`](../packages/context/session-reference), [`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), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:347`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
-| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
-| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:316`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`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:277`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
-| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:391`](../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), [`workspace-changes`](../packages/deliverables/workspace-changes) |
+| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
+| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | [`agent-presets`](../packages/preset/agent-presets), `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`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), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../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), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
+| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../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:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
+| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
+| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`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), [`session-reference`](../packages/context/session-reference), [`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), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
+| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
+| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
+| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../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), [`workspace-changes`](../packages/deliverables/workspace-changes) |
 | `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:601`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
 | `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:581`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
 | `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:608`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
@@ -63,10 +62,10 @@
 | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:144`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
 | `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:150`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
 | `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:161`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
-| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt) |
+| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt) |
 | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
-| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:201`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:157`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
+| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:201`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:157`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
 | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:169`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
 | `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:146`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
 | `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:183`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
@@ -85,7 +84,7 @@
 | Event string | Dispatchers | Listeners |
 | --- | --- | --- |
 | `internal/dispatch` | - | `agent-team`, [`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-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), `terminal-controller`, [`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), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) |
-| `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), `modules` |
+| `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), [`mcp-client`](../packages/mcp/mcp-client), `modules` |
 | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` |
 | `internal/status` | - | [`agent`](../packages/core/agent), `inspector` |
 | `internal/update` | - | [`app-boot`](../packages/boot/app-boot) |

+ 2 - 2
docs/glossary.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/glossary.md
-glossary.md: 891234c487eea37e6f6beb4779c47054dd011168
-glossary.zh.md: d8fef23873d4c0098b774cf846c8fec156c2d269
+glossary.md: 1a9ac39a73526833be3cdc191d67f519bdf8fc90
+glossary.zh.md: cd41876d41148082ddc18a08c49cf2abba882c79

+ 1 - 1
docs/glossary.md

@@ -17,7 +17,7 @@ Domain vocabulary for DeepSeek Harness uses one canonical term per concept. Term
 - **scoped dispatch** — the rule: an event about one agent's activity dispatches with that agent's carrier. Events about a registry itself (a tool was added) are *registry-subject* and stay unfiltered.
 - **shadowing** — most-specific-wins name resolution: a scoped tool/section/variable replaces its same-named global twin for that scope alone. The per-agent persona and per-agent tool-variant mechanism.
 - **restriction / scope-local registration** — a restriction (`tools.restrict`) filters the GLOBAL tool set for one scope (compose by intersection); scope-local registrations are merged after that filter. A filtered-away global tool is absent from the prompt AND refuses execution, indistinguishably from a nonexistent one.
-- **setup window** — the creation slot where a creator composes an agent's scoped world (`CreateAgentOptions.setup`): after the scope and agent object exist but before the agent or session is published, `agent/session-start` fires, or the first prompt is assembled. Setup registers; it never drives the agent.
+- **setup window** — the creation slot where a creator composes an agent's scoped world (`CreateAgentOptions.setup`): after the scope and agent object exist but before the agent or session is published, `agent/created` fires, or the first prompt is assembled. Setup registers; it never drives the agent.
 - **lineage** — parent/child facts carried as data (`parentSession`, durable `delegationDepth`, runtime `subagentDepth`); never affects visibility. <a id="lineage"></a>
 
 ## goal

+ 1 - 1
docs/glossary.zh.md

@@ -17,7 +17,7 @@ DeepSeek Harness 的领域词汇为每个概念规定一个规范术语。各术
 - **scoped dispatch**:规则是:关于某个 agent 的活动的事件以该 agent 的 carrier 进行分发。关于注册表本身的事件(如「一个工具被添加了」)属于*注册表主体*事件,保持不过滤。
 - **shadowing**:最具体者胜出的名称解析:一个带作用域的工具/片段/变量仅在该 scope 内替换同名的全局对应项。这是按 agent 定制 persona 和按 agent 定制工具变体的机制。
 - **restriction / scope-local 注册**:restriction(`tools.restrict`)为单个 scope 过滤全局工具集合(多个 restriction 取交集组合);scope-local 注册在过滤之后合并。被过滤掉的全局工具既不出现在提示词中,也拒绝执行,与不存在的工具无法区分。
-- **setup window**:创建者组装 agent 作用域环境的创建时隙(`CreateAgentOptions.setup`):此时 scope 和 agent 对象已存在,但 agent 或会话尚未发布,`agent/session-start` 尚未触发,首次提示词尚未组装。setup 只做注册,从不驱动 agent。
+- **setup window**:创建者组装 agent 作用域环境的创建时隙(`CreateAgentOptions.setup`):此时 scope 和 agent 对象已存在,但 agent 或会话尚未发布,`agent/created` 尚未触发,首次提示词尚未组装。setup 只做注册,从不驱动 agent。
 - **lineage**:以数据形式携带的父子关系事实(`parentSession`、持久的 `delegationDepth`、运行时 `subagentDepth`);从不影响可见性。<a id="lineage"></a>
 
 ## 目标

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/module-graph.md
-module-graph.md: 37fc86c39054b9e9f60098ac9b191a435afe8e2e
-module-graph.zh.md: cf01c57e30caa3d1a608e48a4a4dfad6f9ab2798
+module-graph.md: e5c37f2ac69c89f64b796d1754cc00643075860b
+module-graph.zh.md: 84236800283a184c12fe9cb30feef2934d07f030

+ 29 - 0
docs/module-graph.md

@@ -126,6 +126,9 @@ flowchart TD
     pkg_app_boot["app-boot"]
     pkg_cmdline["cmdline"]
   end
+  subgraph group_browser_use["packages/browser-use"]
+    pkg_browser_use["browser-use"]
+  end
   subgraph group_bundle["packages/bundle"]
     pkg_acp_app["acp-app"]
     pkg_base["base"]
@@ -221,6 +224,10 @@ flowchart TD
     pkg_experimental_agent_team_profile["experimental-agent-team-profile"]
     pkg_experimental_agent_team_web_profile["experimental-agent-team-web-profile"]
     pkg_experimental_auto_review["experimental-auto-review"]
+    pkg_experimental_browser_use_chrome_devtools_mcp["experimental-browser-use-chrome-devtools-mcp"]
+    pkg_experimental_browser_use_playwright_mcp["experimental-browser-use-playwright-mcp"]
+    pkg_experimental_browser_use_runtime["experimental-browser-use-runtime"]
+    pkg_experimental_browser_use_stagehand_native["experimental-browser-use-stagehand-native"]
     pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"]
     pkg_experimental_computer_use_cua_driver_mcp["experimental-computer-use-cua-driver-mcp"]
     pkg_experimental_computer_use_cua_driver_native["experimental-computer-use-cua-driver-native"]
@@ -393,6 +400,7 @@ flowchart TD
   pkg_scope --> pkg_invariants
   pkg_web --> pkg_llm
   pkg_attachment --> pkg_brand
+  pkg_browser_use --> pkg_brand
   pkg_computer_use --> pkg_brand
   pkg_credentials --> pkg_invariants
   pkg_experimental_inspector --> pkg_client_modules
@@ -814,6 +822,22 @@ flowchart TD
   pkg_workspace_changes --> pkg_session
   pkg_workspace_changes --> pkg_subprocess
   pkg_workspace_changes --> pkg_tools
+  pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_agent
+  pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_browser_use
+  pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_system_prompt
+  pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_tools
+  pkg_experimental_browser_use_playwright_mcp --> pkg_agent
+  pkg_experimental_browser_use_playwright_mcp --> pkg_browser_use
+  pkg_experimental_browser_use_playwright_mcp --> pkg_system_prompt
+  pkg_experimental_browser_use_playwright_mcp --> pkg_tools
+  pkg_experimental_browser_use_runtime --> pkg_agent
+  pkg_experimental_browser_use_runtime --> pkg_browser_use
+  pkg_experimental_browser_use_runtime --> pkg_system_prompt
+  pkg_experimental_browser_use_runtime --> pkg_tools
+  pkg_experimental_browser_use_stagehand_native --> pkg_agent
+  pkg_experimental_browser_use_stagehand_native --> pkg_browser_use
+  pkg_experimental_browser_use_stagehand_native --> pkg_system_prompt
+  pkg_experimental_browser_use_stagehand_native --> pkg_tools
   pkg_experimental_computer_use_cua_driver_native --> pkg_computer_use
   pkg_experimental_computer_use_cua_driver_native --> pkg_system_prompt
   pkg_experimental_computer_use_cua_driver_native --> pkg_tools
@@ -1358,6 +1382,7 @@ flowchart TD
 | [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) |
 | [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand) |
+| [`browser-use`](../packages/browser-use/browser-use) | `browser-use` | [`brand`](../packages/util/brand) |
 | [`computer-use`](../packages/computer-use/computer-use) | `computer-use` | [`brand`](../packages/util/brand) |
 | [`credentials`](../packages/credentials/credentials) | `credentials` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
@@ -1469,6 +1494,10 @@ flowchart TD
 | [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
 | [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`home-paths`](../packages/util/home-paths), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) |
+| [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`experimental-browser-use-runtime`](../packages/experimental/browser-use-runtime) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | `experimental` | [`computer-use`](../packages/computer-use/computer-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
 | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-protocol`](../packages/typert/protocol) |

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

@@ -128,6 +128,9 @@ flowchart TD
     pkg_app_boot["app-boot"]
     pkg_cmdline["cmdline"]
   end
+  subgraph group_browser_use["packages/browser-use"]
+    pkg_browser_use["browser-use"]
+  end
   subgraph group_bundle["packages/bundle"]
     pkg_acp_app["acp-app"]
     pkg_base["base"]
@@ -223,6 +226,10 @@ flowchart TD
     pkg_experimental_agent_team_profile["experimental-agent-team-profile"]
     pkg_experimental_agent_team_web_profile["experimental-agent-team-web-profile"]
     pkg_experimental_auto_review["experimental-auto-review"]
+    pkg_experimental_browser_use_chrome_devtools_mcp["experimental-browser-use-chrome-devtools-mcp"]
+    pkg_experimental_browser_use_playwright_mcp["experimental-browser-use-playwright-mcp"]
+    pkg_experimental_browser_use_runtime["experimental-browser-use-runtime"]
+    pkg_experimental_browser_use_stagehand_native["experimental-browser-use-stagehand-native"]
     pkg_experimental_client_ui_agent_team["experimental-client-ui-agent-team"]
     pkg_experimental_computer_use_cua_driver_mcp["experimental-computer-use-cua-driver-mcp"]
     pkg_experimental_computer_use_cua_driver_native["experimental-computer-use-cua-driver-native"]
@@ -395,6 +402,7 @@ flowchart TD
   pkg_scope --> pkg_invariants
   pkg_web --> pkg_llm
   pkg_attachment --> pkg_brand
+  pkg_browser_use --> pkg_brand
   pkg_computer_use --> pkg_brand
   pkg_credentials --> pkg_invariants
   pkg_experimental_inspector --> pkg_client_modules
@@ -816,6 +824,22 @@ flowchart TD
   pkg_workspace_changes --> pkg_session
   pkg_workspace_changes --> pkg_subprocess
   pkg_workspace_changes --> pkg_tools
+  pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_agent
+  pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_browser_use
+  pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_system_prompt
+  pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_tools
+  pkg_experimental_browser_use_playwright_mcp --> pkg_agent
+  pkg_experimental_browser_use_playwright_mcp --> pkg_browser_use
+  pkg_experimental_browser_use_playwright_mcp --> pkg_system_prompt
+  pkg_experimental_browser_use_playwright_mcp --> pkg_tools
+  pkg_experimental_browser_use_runtime --> pkg_agent
+  pkg_experimental_browser_use_runtime --> pkg_browser_use
+  pkg_experimental_browser_use_runtime --> pkg_system_prompt
+  pkg_experimental_browser_use_runtime --> pkg_tools
+  pkg_experimental_browser_use_stagehand_native --> pkg_agent
+  pkg_experimental_browser_use_stagehand_native --> pkg_browser_use
+  pkg_experimental_browser_use_stagehand_native --> pkg_system_prompt
+  pkg_experimental_browser_use_stagehand_native --> pkg_tools
   pkg_experimental_computer_use_cua_driver_native --> pkg_computer_use
   pkg_experimental_computer_use_cua_driver_native --> pkg_system_prompt
   pkg_experimental_computer_use_cua_driver_native --> pkg_tools
@@ -1360,6 +1384,7 @@ flowchart TD
 | [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) |
 | [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand) |
+| [`browser-use`](../packages/browser-use/browser-use) | `browser-use` | [`brand`](../packages/util/brand) |
 | [`computer-use`](../packages/computer-use/computer-use) | `computer-use` | [`brand`](../packages/util/brand) |
 | [`credentials`](../packages/credentials/credentials) | `credentials` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
@@ -1471,6 +1496,10 @@ flowchart TD
 | [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
 | [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`home-paths`](../packages/util/home-paths), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) |
+| [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`experimental-browser-use-runtime`](../packages/experimental/browser-use-runtime) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | `experimental` | [`computer-use`](../packages/computer-use/computer-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
 | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-protocol`](../packages/typert/protocol) |

+ 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: de98afec0af29a068fe4dc939802f479373240d5
-README.zh.md: 362398a368737c8ac34001a6133976ab5a376034
+README.md: 9ee98bfa5f0d468cde8e11dad22567b05de9e41f
+README.zh.md: 22e3cd76801354228d00742a2698929e319948cb

+ 1 - 0
docs/subsystems/README.md

@@ -37,6 +37,7 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
 | [sandbox.md](sandbox.md) | per-session policy resolution and the process-confinement seam: file-effect modes, execution/provider policies, `ConfinedArgv`, enforcement and fail-closed errors |
 | [ptc-runtime.md](ptc-runtime.md) | the PTC execution seam: `PtcRunRequest`/`Result`, binding namespaces, captured logs, the `PtcRunFailure` taxonomy |
 | [computer-use.md](computer-use.md) | exclusive named computer-use provider registration and Cua Driver integration choices |
+| [browser-use.md](browser-use.md) | exclusive named browser-use registration, provider choices, and per-Session browser ownership |
 | [extensions.md](extensions.md) | versioned dynamic Cordis Plugins and Packages, Host/Client activation, approval, runtime inspection, and lifecycle teardown |
 | [filesystem.md](filesystem.md) | the filesystem seam: `FsTarget`, read/write/edit outcomes, observed-file state, `FsErrorCode` |
 | [lsp.md](lsp.md) | the LSP navigation seam: `LspQueryRequest`/`Result`, `LspProvider`/`Service`, four operations, `LspError` |

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

@@ -37,6 +37,7 @@
 | [sandbox.md](sandbox.zh.md) | 每会话策略解析与进程约束 seam:文件效果模式、执行/提供方策略、`ConfinedArgv`、强制执行与故障关闭错误 |
 | [ptc-runtime.md](ptc-runtime.zh.md) | PTC 执行 seam:`PtcRunRequest`/`Result`、绑定命名空间、捕获日志、`PtcRunFailure` 分类体系 |
 | [computer-use.md](computer-use.zh.md) | 按名称独占注册计算机操作提供方,以及 Cua Driver 集成选项 |
+| [browser-use.md](browser-use.zh.md) | 按名称独占注册浏览器操作提供方、提供方选项与按 Session 管理的浏览器所有权 |
 | [extensions.md](extensions.zh.md) | 带版本的动态 Cordis 插件与包、Host/Client 激活、审批、运行时检查和生命周期清理 |
 | [filesystem.md](filesystem.zh.md) | 文件系统 seam:`FsTarget`、读/写/编辑结果、观测到的文件状态、`FsErrorCode` |
 | [lsp.md](lsp.zh.md) | LSP 导航 seam:`LspQueryRequest`/`Result`、`LspProvider`/`Service`、四种操作、`LspError` |

+ 6 - 0
docs/subsystems/browser-use.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/browser-use.md
+browser-use.md: 73cedae85752b296c3f85e53d40448e25a711685
+browser-use.zh.md: 573b15962eddab154947a33e9bebe2303cb24fea

+ 67 - 0
docs/subsystems/browser-use.md

@@ -0,0 +1,67 @@
+# Browser use
+
+English | [中文](browser-use.zh.md)
+
+Browser use lets a model inspect and operate web pages through a configured backend. DSH owns the task loop; the provider supplies browser operations and keeps browser state across turns of one live Session.
+
+## Choose a provider
+
+Mount [`dsh-browser-use`](../../packages/browser-use/browser-use/README.md) and one provider in the same composition. The providers are experimental public npm packages and require explicit activation. Their initial browser engine is Chromium.
+
+| Provider | Integration |
+|---|---|
+| [Playwright MCP](../../packages/experimental/browser-use-playwright-mcp/README.md) | Playwright's browser-control MCP tools |
+| [Chrome DevTools MCP](../../packages/experimental/browser-use-chrome-devtools-mcp/README.md) | Chrome DevTools inspection and control through MCP |
+| [Stagehand](../../packages/experimental/browser-use-stagehand-native/README.md) | Native browser operations with AI-assisted actions, observation, and extraction |
+
+The shared service registers only a name and rejects any second provider, including another instance with the same name. It has no common browser-operation methods, browser resources, or model-controlled selector. Provider configuration in a profile or preset selects launch or attachment for that activation.
+
+## Session ownership
+
+A launched browser belongs to the exact live Agent and Session that uses it. Calls across turns reuse that browser. Disposing the Session runtime closes its launched resources; reloading or forking a Session starts fresh browser state. Browser profiles and login state are not restored from the Session log.
+
+An attached browser remains externally owned. The provider reserves it for one Session within that provider instance, preserves its existing browser state, and rejects simultaneous attachment by another Session. Teardown disconnects and leaves the external browser running. Separate DSH processes and other clients remain outside this reservation.
+
+Provider shutdown stops tool admission and waits for owned work and resource cleanup before releasing the shared provider registration. Cancellation cannot undo a browser action already delivered.
+
+## MCP initialization
+
+An MCP provider initializes one client for each live Agent created after the provider loads. The existing serial `agent/created` event awaits connection and discovery before creation or resume completes and queued input runs. The client remains with the Session across turns. Startup failure or cancellation rejects creation or resume and triggers client cleanup.
+
+If an attachment is busy, that activation continues without the browser and does not retry on later turns. After release, a newly created or resumed activation can acquire it. Loading or reloading the provider does not adopt already active Sessions; the [shared runtime](../../packages/experimental/browser-use-runtime/README.md) owns these initialization rules.
+
+## Tools and recorded results
+
+Provider tools use the normal DSH execution pipeline and Session log. The providers own their tool schemas, result rendering, image support, configuration, and upstream limitations; the shared service adds no model-visible content. Stagehand's AI-assisted operations use its explicitly configured native model while DSH retains the task loop. DSH model routing, credential reuse, underlying inference request/response capture, and integration into Session usage accounting are deferred; returned SDK data and metadata remain ordinary logged tool results.
+
+Browser MCP connections also expose [resources and server instructions](mcp.md). Resource calls addressed to a browser server use its Session queue and reject other Sessions; server instructions are assembled only for its owning Session.
+
+The [decision record](../../.agents/notes/implemented/architecture/2026-09-12-browser-use-provider-registration.md) explains the registration-only service and per-Session ownership.
+
+<!-- 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`) — the language sides differ only in locale-specific paired document paths. 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="ctxbrowseruse--browseruseregistry"></a>
+
+### `ctx.browserUse` — `BrowserUseRegistry`
+
+Owns one optional provider registration in the shared browser-use service.
+
+```ts cordis-catalog
+/**
+ * Reserve the sole provider slot until the contribution is disposed.
+ * A second registration fails even when it repeats the current name. Providers
+ * must stop their tools and await owned work before releasing this registration.
+ * @param name - provider-owned name used in registration diagnostics.
+ * @returns the effect disposer for this exact registration.
+ */
+register(name: BrowserUseProviderName): () => Promise<void>
+```
+
+Source: [`packages/browser-use/browser-use/src/index.ts`](../../packages/browser-use/browser-use/src/index.ts)
+<!-- END GENERATED cordis-surface -->

+ 67 - 0
docs/subsystems/browser-use.zh.md

@@ -0,0 +1,67 @@
+# 浏览器操作
+
+[English](browser-use.md) | 中文
+
+浏览器操作让模型通过配置的后端检查与操作网页。DSH 拥有任务循环;提供方提供浏览器操作,并在一个实时 Session 的多个轮次之间保留浏览器状态。
+
+## 选择提供方
+
+在同一组合中挂载 [`dsh-browser-use`](../../packages/browser-use/browser-use/README.zh.md) 和一个提供方。这些提供方是实验性公共 npm 包,需要显式激活。它们最初支持的浏览器引擎是 Chromium。
+
+| 提供方 | 集成方式 |
+|---|---|
+| [Playwright MCP](../../packages/experimental/browser-use-playwright-mcp/README.zh.md) | Playwright 的浏览器控制 MCP 工具 |
+| [Chrome DevTools MCP](../../packages/experimental/browser-use-chrome-devtools-mcp/README.zh.md) | 通过 MCP 进行 Chrome DevTools 检查与控制 |
+| [Stagehand](../../packages/experimental/browser-use-stagehand-native/README.zh.md) | 原生浏览器操作,支持 AI(人工智能)辅助的动作、观测与提取 |
+
+共享服务只注册名称,并拒绝任何第二次提供方注册,包括同名实例。它不包含通用浏览器操作方法、浏览器资源或模型控制的选择器。Profile 或 preset 中的提供方配置为此次激活选择启动或附加模式。
+
+## Session 所有权
+
+启动的浏览器属于使用它的确切实时 Agent 与 Session。跨轮次的调用复用该浏览器。Session 运行时释放时关闭其启动的资源;重新加载或 fork Session 时创建全新浏览器状态。浏览器 profile 和登录状态不会从 Session 日志恢复。
+
+附加的浏览器仍归外部所有。提供方在该提供方实例内将浏览器保留给一个 Session,保留现有浏览器状态,并拒绝另一个 Session 同时附加。清理会断开连接并保持外部浏览器运行。独立 DSH 进程与其他客户端不受此保留约束。
+
+提供方关闭时先停止接收工具调用,并等待自有工作与资源清理完成,再释放共享提供方注册。取消无法撤销已交付的浏览器操作。
+
+## MCP 初始化
+
+MCP 提供方为其加载后创建的每个活动 Agent 初始化一个客户端。现有的串行 `agent/created` 事件等待连接和发现结束后,创建或恢复才完成,排队输入才开始运行。客户端跨轮次归 Session 所有。启动失败或取消会拒绝创建或恢复,并触发客户端清理。
+
+如果附加连接已被占用,本次激活不使用浏览器,但继续运行,后续轮次不会重试。连接释放后,新创建或恢复的激活可以获取它。加载或重新加载提供方不会接管已经活动的 Session;[共享运行时](../../packages/experimental/browser-use-runtime/README.zh.md)拥有这些初始化规则。
+
+## 工具与记录结果
+
+提供方工具使用常规 DSH 执行管线与 Session 日志。提供方拥有自己的工具 schema、结果渲染、图像支持、配置和上游限制;共享服务不添加模型可见内容。Stagehand 的 AI 辅助操作使用其显式配置的原生模型,DSH 保留任务循环。DSH 模型路由、凭据复用、底层推理请求/响应捕获,以及与 Session 用量计量的集成均属暂缓工作;返回的 SDK 数据和元数据仍作为普通工具结果记录。
+
+浏览器 MCP 连接还提供[资源和服务器指令](mcp.zh.md)。发往浏览器服务器的资源调用使用其 Session 队列,并拒绝其他 Session 的请求;服务器指令只会组装到所属 Session 的提示词中。
+
+[决策记录](../../.agents/notes/implemented/architecture/2026-09-12-browser-use-provider-registration.zh.md)解释只注册名称的服务与按 Session 管理的所有权。
+
+<!-- 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`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+
+<a id="ctxbrowseruse--browseruseregistry"></a>
+
+### `ctx.browserUse` — `BrowserUseRegistry`
+
+Owns one optional provider registration in the shared browser-use service.
+
+```ts cordis-catalog
+/**
+ * Reserve the sole provider slot until the contribution is disposed.
+ * A second registration fails even when it repeats the current name. Providers
+ * must stop their tools and await owned work before releasing this registration.
+ * @param name - provider-owned name used in registration diagnostics.
+ * @returns the effect disposer for this exact registration.
+ */
+register(name: BrowserUseProviderName): () => Promise<void>
+```
+
+Source: [`packages/browser-use/browser-use/src/index.ts`](../../packages/browser-use/browser-use/src/index.ts)
+<!-- END GENERATED cordis-surface -->

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/core.md
-core.md: deb4743df146983857555016b5bdbe8d2ce29b42
-core.zh.md: f18a00eb02aac23ec07e80b4a9e3ea2234d7e576
+core.md: e5a155854a0a5ecf7b663087bf45f78aeb79297d
+core.zh.md: 24aebd77f54e2b2c0c94e2a044d51b2b14e83f6f

+ 25 - 43
docs/subsystems/core.md

@@ -336,7 +336,7 @@ type RequestErrorAction = { kind: 'retry' } | undefined
 
 `agent/pre-step` is the only waterfall listener chain before request derivation. `agent/turn-stopping` runs when a turn has no tool or steering continuation, before one final steering drain.
 
-`agent/session-start` carries a `SessionStartSource` (why the session lifecycle began; a bridge keys its SessionStart matcher on it):
+`agent/created` carries a `SessionStartSource` (why the session lifecycle began; a bridge keys its SessionStart matcher on it):
 
 ```ts type-equiv
 /** Why a session lifecycle began; seeded creates are `startup`, while persisted loads are `resume`. */
@@ -818,16 +818,16 @@ async create(options: CreateAgentOptions): Promise<AgentHandle>
 async resume(options: ResumeAgentOptions): Promise<AgentHandle>
 
 /**
- * Register a live agent. Throws if an agent with the same id is already
- * registered. Emits `agent/created` on registration and `agent/disposed`
+ * Register a live agent with source `startup`. Rejects if the id is already registered or a
+ * serial `agent/created` listener fails. Emits `agent/disposed`
  * when the calling fiber is disposed — both with the agent's scope carrier
  * (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the
  * emits are scope-filtered regardless of which context invoked `register`
  * (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
  * requires passing the carrier). The entry is a runtime root; factory-backed
- * creation uses `options.parentAgent` for child ownership. Returns the disposer.
+ * creation uses `options.parentAgent` for child ownership. Await the registration before using the agent.
  * @param agent - the already-constructed agent to record in the store.
- * @returns the EXACT Cordis effect disposer (single-shot; a repeat call
+ * @returns the awaitable Cordis effect disposer (single-shot; a repeat call
  *   returns undefined without awaiting an in-flight teardown). Exact
  *   identity is load-bearing: a composite (generator) effect that owns a
  *   teardown ORDER — the agent factory's lifecycle chain — must yield THIS
@@ -836,7 +836,7 @@ async resume(options: ResumeAgentOptions): Promise<AgentHandle>
  *   owner unload, unregistering the agent (and emitting `agent/disposed`)
  *   while its final turn is still draining.
  */
-register(agent: Agent): () => void
+register(agent: Agent): ReturnType<Context['effect']>
 
 /**
  * Insert an already-constructed agent without announcing it. This is the
@@ -850,19 +850,22 @@ register(agent: Agent): () => void
  *   the resumed session's durable parent lineage.
  * @returns an idempotent closure that removes this exact entry and emits
  *   `agent/disposed` with listener failures contained. When called from a
- *   synchronous `agent/created` listener, removal and disposal wait until
- *   that creation dispatch unwinds.
+ *   `agent/created` listener, removal and disposal wait until the serial
+ *   creation dispatch settles.
  */
 enter(agent: Agent, owner: Agent | undefined): () => void
 
 /**
  * Announce an agent previously inserted with {@link enter}.
  * @param agent - the live inserted agent to announce.
+ * @param source - fresh creation, resume, clear, or compaction source.
+ * @param signal - optional factory initialization cancellation signal passed to listeners.
+ * @returns completion of the serial creation listeners; a listener failure rejects.
  * @throws if `agent` is not the exact live registry entry for its id, or its
  *   creation announcement already began (including a reentrant call from a
  *   creation listener).
  */
-announce(agent: Agent): void
+async announce(agent: Agent, source: SessionStartSource, signal?: AbortSignal): Promise<void>
 
 /**
  * Look up a live agent.
@@ -925,24 +928,27 @@ Types: [Scoped](scope.md)
 
 Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
 
-<a id="agentcreated--emit"></a>
+<a id="agentcreated--serial"></a>
 
-#### `agent/created` — emit
+#### `agent/created` — serial
 
-A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving extension point. Synchronous listener failure vetoes publication, while returned-promise rejection is reported. Detach requested during dispatch waits until every creation listener has observed the stable entry.
+An entered agent is ready for per-agent initialization after factory setup. Listeners run in order and are awaited before creation resolves. AgentLoop holds queued input until all listeners finish. A throw or rejection fails creation and skips later listeners. Disposal retains the scope and session until dispatch settles; listeners must not await agent.whenIdle() or their own owner's disposal.
 
 ```ts cordis-catalog
 /**
- * A fully configured agent and live session were published. Setup is
- * composition-only; `agent/session-start` is the first startup-driving extension point.
- * Synchronous listener failure vetoes publication, while returned-promise
- * rejection is reported. Detach requested during dispatch waits until every
- * creation listener has observed the stable entry.
+ * An entered agent is ready for per-agent initialization after factory setup.
+ * Listeners run in order and are awaited before creation resolves. AgentLoop
+ * holds queued input until all listeners finish. A throw or rejection fails
+ * creation and skips later listeners. Disposal retains the scope and session
+ * until dispatch settles; listeners must not await agent.whenIdle() or their
+ * own owner's disposal.
  * @param payload.agent - the newly registered agent with its live session and completed setup.
+ * @param payload.source - fresh creation, resume, clear, or compaction source.
+ * @param payload.signal - factory initialization cancellation signal, when provided.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
+ * @mode serial
  */
-'agent/created'(this: Scoped<Agent>, payload: { agent: Agent }): void
+'agent/created'(this: Scoped<Agent>, payload: { agent: Agent; source: SessionStartSource; signal?: AbortSignal }): undefined | Promise<undefined>
 ```
 
 Types: [Scoped](scope.md)
@@ -1145,30 +1151,6 @@ Types: [LlmFailure](llm-streaming.md) · [ResolvedRetryPolicy](llm-streaming.md)
 
 Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
 
-<a id="agentsession-start--emit"></a>
-
-#### `agent/session-start` — emit
-
-The session lifecycle began, once before the first turn. Use `agent.inject()` to seed model-facing context. This is a notification, not a veto; disposal requested by a lifecycle owner is rechecked before the driver starts.
-
-```ts cordis-catalog
-/**
- * The session lifecycle began, once before the first turn. Use
- * `agent.inject()` to seed model-facing context. This is a notification, not
- * a veto; disposal requested by a lifecycle owner is rechecked before the
- * driver starts.
- * @param payload.agent - the agent whose session lifecycle began.
- * @param payload.source - why the session started (fresh startup, resume, …).
- * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
- */
-'agent/session-start'(this: Scoped<Agent>, payload: { agent: Agent; source: SessionStartSource }): void
-```
-
-Types: [Scoped](scope.md)
-
-Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
-
 <a id="agentstatus--emit"></a>
 
 #### `agent/status` — emit

+ 25 - 43
docs/subsystems/core.zh.md

@@ -344,7 +344,7 @@ type RequestErrorAction = { kind: 'retry' } | undefined
 
 `agent/pre-step` 是请求推导前唯一的 waterfall(瀑布式)监听器链。`agent/turn-stopping` 在轮次没有工具或 steering(中途引导)后续时运行,先于最后一次 steering 排空。
 
-`agent/session-start` 携带 `SessionStartSource`(会话生命周期为何开始;桥接层据此匹配其 SessionStart):
+`agent/created` 携带 `SessionStartSource`(会话生命周期为何开始;桥接层据此匹配其 SessionStart):
 
 ```ts type-equiv
 /** Why a session lifecycle began; seeded creates are `startup`, while persisted loads are `resume`. */
@@ -828,16 +828,16 @@ async create(options: CreateAgentOptions): Promise<AgentHandle>
 async resume(options: ResumeAgentOptions): Promise<AgentHandle>
 
 /**
- * Register a live agent. Throws if an agent with the same id is already
- * registered. Emits `agent/created` on registration and `agent/disposed`
+ * Register a live agent with source `startup`. Rejects if the id is already registered or a
+ * serial `agent/created` listener fails. Emits `agent/disposed`
  * when the calling fiber is disposed — both with the agent's scope carrier
  * (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the
  * emits are scope-filtered regardless of which context invoked `register`
  * (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
  * requires passing the carrier). The entry is a runtime root; factory-backed
- * creation uses `options.parentAgent` for child ownership. Returns the disposer.
+ * creation uses `options.parentAgent` for child ownership. Await the registration before using the agent.
  * @param agent - the already-constructed agent to record in the store.
- * @returns the EXACT Cordis effect disposer (single-shot; a repeat call
+ * @returns the awaitable Cordis effect disposer (single-shot; a repeat call
  *   returns undefined without awaiting an in-flight teardown). Exact
  *   identity is load-bearing: a composite (generator) effect that owns a
  *   teardown ORDER — the agent factory's lifecycle chain — must yield THIS
@@ -846,7 +846,7 @@ async resume(options: ResumeAgentOptions): Promise<AgentHandle>
  *   owner unload, unregistering the agent (and emitting `agent/disposed`)
  *   while its final turn is still draining.
  */
-register(agent: Agent): () => void
+register(agent: Agent): ReturnType<Context['effect']>
 
 /**
  * Insert an already-constructed agent without announcing it. This is the
@@ -860,19 +860,22 @@ register(agent: Agent): () => void
  *   the resumed session's durable parent lineage.
  * @returns an idempotent closure that removes this exact entry and emits
  *   `agent/disposed` with listener failures contained. When called from a
- *   synchronous `agent/created` listener, removal and disposal wait until
- *   that creation dispatch unwinds.
+ *   `agent/created` listener, removal and disposal wait until the serial
+ *   creation dispatch settles.
  */
 enter(agent: Agent, owner: Agent | undefined): () => void
 
 /**
  * Announce an agent previously inserted with {@link enter}.
  * @param agent - the live inserted agent to announce.
+ * @param source - fresh creation, resume, clear, or compaction source.
+ * @param signal - optional factory initialization cancellation signal passed to listeners.
+ * @returns completion of the serial creation listeners; a listener failure rejects.
  * @throws if `agent` is not the exact live registry entry for its id, or its
  *   creation announcement already began (including a reentrant call from a
  *   creation listener).
  */
-announce(agent: Agent): void
+async announce(agent: Agent, source: SessionStartSource, signal?: AbortSignal): Promise<void>
 
 /**
  * Look up a live agent.
@@ -935,24 +938,27 @@ Types: [Scoped](scope.zh.md)
 
 Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
 
-<a id="agentcreated--emit"></a>
+<a id="agentcreated--serial"></a>
 
-#### `agent/created` — emit
+#### `agent/created` — serial
 
-A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving extension point. Synchronous listener failure vetoes publication, while returned-promise rejection is reported. Detach requested during dispatch waits until every creation listener has observed the stable entry.
+An entered agent is ready for per-agent initialization after factory setup. Listeners run in order and are awaited before creation resolves. AgentLoop holds queued input until all listeners finish. A throw or rejection fails creation and skips later listeners. Disposal retains the scope and session until dispatch settles; listeners must not await agent.whenIdle() or their own owner's disposal.
 
 ```ts cordis-catalog
 /**
- * A fully configured agent and live session were published. Setup is
- * composition-only; `agent/session-start` is the first startup-driving extension point.
- * Synchronous listener failure vetoes publication, while returned-promise
- * rejection is reported. Detach requested during dispatch waits until every
- * creation listener has observed the stable entry.
+ * An entered agent is ready for per-agent initialization after factory setup.
+ * Listeners run in order and are awaited before creation resolves. AgentLoop
+ * holds queued input until all listeners finish. A throw or rejection fails
+ * creation and skips later listeners. Disposal retains the scope and session
+ * until dispatch settles; listeners must not await agent.whenIdle() or their
+ * own owner's disposal.
  * @param payload.agent - the newly registered agent with its live session and completed setup.
+ * @param payload.source - fresh creation, resume, clear, or compaction source.
+ * @param payload.signal - factory initialization cancellation signal, when provided.
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
+ * @mode serial
  */
-'agent/created'(this: Scoped<Agent>, payload: { agent: Agent }): void
+'agent/created'(this: Scoped<Agent>, payload: { agent: Agent; source: SessionStartSource; signal?: AbortSignal }): undefined | Promise<undefined>
 ```
 
 Types: [Scoped](scope.zh.md)
@@ -1155,30 +1161,6 @@ Types: [LlmFailure](llm-streaming.zh.md) · [ResolvedRetryPolicy](llm-streaming.
 
 Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
 
-<a id="agentsession-start--emit"></a>
-
-#### `agent/session-start` — emit
-
-The session lifecycle began, once before the first turn. Use `agent.inject()` to seed model-facing context. This is a notification, not a veto; disposal requested by a lifecycle owner is rechecked before the driver starts.
-
-```ts cordis-catalog
-/**
- * The session lifecycle began, once before the first turn. Use
- * `agent.inject()` to seed model-facing context. This is a notification, not
- * a veto; disposal requested by a lifecycle owner is rechecked before the
- * driver starts.
- * @param payload.agent - the agent whose session lifecycle began.
- * @param payload.source - why the session started (fresh startup, resume, …).
- * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
- */
-'agent/session-start'(this: Scoped<Agent>, payload: { agent: Agent; source: SessionStartSource }): void
-```
-
-Types: [Scoped](scope.zh.md)
-
-Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
-
 <a id="agentstatus--emit"></a>
 
 #### `agent/status` — emit

+ 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: ae04dfd06cfda39a5bc8d7a079c9c3097c11193f
-subagent.zh.md: 730039af6a75e070b8b7f08021353a289927449e
+subagent.md: 21c75626158db666fd72bed2742b30e12f4cb719
+subagent.zh.md: ca4bb724b82f4f9e81f1e5b7653950ad8c2d3745

+ 1 - 1
docs/subsystems/subagent.md

@@ -199,7 +199,7 @@ interface ContinuableStart {
 }
 ```
 
-When a resident Activation settles, the manager delivers one notice to the child's durable direct parent describing how that epoch ended and carrying its final assistant content. That delivery is unconditional for every child whose id a caller received, happens before the ownership release that would let the parent be judged settled, and reaches a resident parent through the same waking Agent delivery as an Agent message. A parent whose own lineage is already tearing down receives it without a wake, because waking an idle Agent starts a turn rather than queueing work. Its source has a distinct kind so a transcript never presents a runtime account as something the child wrote.
+When a resident Activation settles, the manager delivers one notice to the child's durable direct parent describing how that epoch ended and carrying the nonempty text blocks from its final assistant output, or `It left no closing message.` when none remain. That delivery is unconditional for every child whose id a caller received, happens before the ownership release that would let the parent be judged settled, and reaches a resident parent through the same waking Agent delivery as an Agent message. A parent whose own lineage is already tearing down receives it without a wake, because waking an idle Agent starts a turn rather than queueing work. Its source has a distinct kind so a transcript never presents a runtime account as something the child wrote.
 
 ```ts type-equiv
 /**

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

@@ -199,7 +199,7 @@ interface ContinuableStart {
 }
 ```
 
-当驻留 Activation 结算时,管理器会向该 child 持久化的直接 parent 投递一条通知,说明该 epoch 如何结束,并携带其最终 assistant 内容。对每个调用方拿到过 id 的 child,这条投递都是无条件的;它发生在会让 parent 被判定为已结算的所有权释放之前,并通过与 Agent 消息相同的唤醒 Agent 投递到达驻留 parent。若 parent 自身所在的谱系已在拆卸中,这条通知会以不唤醒的方式送达,因为唤醒一个 idle Agent 是开启一个轮次,而不是排队等待工作。其来源信息使用一个独立的 kind,因此 transcript(文本记录)绝不会把运行时的记账呈现为 child 自己写下的内容。
+当驻留 Activation 结算时,管理器会向该 child 持久化的直接 parent 投递一条通知,说明该 epoch 如何结束,并携带其最终 assistant 输出中的非空文本块;若没有剩余的非空文本,则携带 `It left no closing message.`。对每个调用方拿到过 id 的 child,这条投递都是无条件的;它发生在会让 parent 被判定为已结算的所有权释放之前,并通过与 Agent 消息相同的唤醒 Agent 投递到达驻留 parent。若 parent 自身所在的谱系已在拆卸中,这条通知会以不唤醒的方式送达,因为唤醒一个 idle Agent 是开启一个轮次,而不是排队等待工作。其来源信息使用一个独立的 kind,因此 transcript(文本记录)绝不会把运行时的记账呈现为 child 自己写下的内容。
 
 ```ts type-equiv
 /**

+ 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: acd0e6cc35be25e7d75f02cd4b895f905ff3beb2
-tool-catalog.zh.md: 8f8198c5306954cd17c3677dc76e771f92f2887f
+tool-catalog.md: 939fc3ef34366b6983b39bbdd072859a966445b9
+tool-catalog.zh.md: ff8a230a1c7dfacc36d294244a6ce22eead3d166

+ 248 - 0
docs/tool-catalog.md

@@ -16,6 +16,7 @@ This table connects model-visible tool names to the plugin package and service s
 | Tool package | Model-visible names | Requires | Writes / affects | Shipped aliases | Deployment note |
 | --- | --- | --- | --- | --- | --- |
 | `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - |
+| `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`, `stagehand_extract`, `stagehand_navigate`, `stagehand_observe`, `stagehand_screenshot`, `stagehand_tabs` | `ctx.browserUse`, `ctx.agents`, `ctx.tools`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | - |
 | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userQuestions` | `tool/call`, `tool/result after a UI/provider answers the question` | - | ask_user_question pauses the tool call until the active UI provider returns a human answer. |
 | `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.ptcRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` / `mode: both` (see the PTC mode Agent Note). Under `ptc` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. |
 | `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`, `ctx.systemPrompt`, `ctx.userQuestions (execution time, opportunistic)` | `tool/call`, `plan/mode inactive on an approved review`, `tool/result` | - | exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. |
@@ -123,6 +124,253 @@ Read an MCP resource by URI from the named server. Use a listed URI or an expand
 
 Source: [`packages/mcp/mcp-resources/src/tools.ts`](../packages/mcp/mcp-resources/src/tools.ts)
 
+<a id="deepseek-aidsh-experimental-browser-use-stagehand-native"></a>
+
+## `@deepseek-ai/dsh-experimental-browser-use-stagehand-native`
+
+### `stagehand_act`
+
+Perform one natural-language browser action using the configured Stagehand model.
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "type": "object",
+  "properties": {
+    "pageId": {
+      "type": "string",
+      "minLength": 1
+    },
+    "instruction": {
+      "type": "string",
+      "minLength": 1
+    }
+  },
+  "required": [
+    "instruction"
+  ],
+  "additionalProperties": false
+}
+```
+
+Source: [`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
+### `stagehand_extract`
+
+Extract page data using the configured Stagehand model and an optional JSON Schema.
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "type": "object",
+  "properties": {
+    "pageId": {
+      "type": "string",
+      "minLength": 1
+    },
+    "instruction": {
+      "type": "string",
+      "minLength": 1
+    },
+    "schema": {
+      "type": "object",
+      "propertyNames": {
+        "type": "string"
+      },
+      "additionalProperties": {
+        "$ref": "#/$defs/__schema0"
+      }
+    }
+  },
+  "required": [
+    "instruction"
+  ],
+  "additionalProperties": false,
+  "$defs": {
+    "__schema0": {
+      "anyOf": [
+        {
+          "type": "string"
+        },
+        {
+          "type": "number"
+        },
+        {
+          "type": "boolean"
+        },
+        {
+          "type": "null"
+        },
+        {
+          "type": "array",
+          "items": {
+            "$ref": "#/$defs/__schema0"
+          }
+        },
+        {
+          "type": "object",
+          "propertyNames": {
+            "type": "string"
+          },
+          "additionalProperties": {
+            "$ref": "#/$defs/__schema0"
+          }
+        }
+      ]
+    }
+  }
+}
+```
+
+Source: [`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
+### `stagehand_navigate`
+
+Navigate a Stagehand browser tab to a URL.
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "type": "object",
+  "properties": {
+    "pageId": {
+      "type": "string",
+      "minLength": 1
+    },
+    "url": {
+      "type": "string",
+      "format": "uri"
+    }
+  },
+  "required": [
+    "url"
+  ],
+  "additionalProperties": false
+}
+```
+
+Source: [`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
+### `stagehand_observe`
+
+Find browser actions matching an instruction using the configured Stagehand model.
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "type": "object",
+  "properties": {
+    "pageId": {
+      "type": "string",
+      "minLength": 1
+    },
+    "instruction": {
+      "type": "string",
+      "minLength": 1
+    }
+  },
+  "required": [
+    "instruction"
+  ],
+  "additionalProperties": false
+}
+```
+
+Source: [`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
+### `stagehand_screenshot`
+
+Capture a Stagehand tab screenshot for visual inspection.
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "type": "object",
+  "properties": {
+    "pageId": {
+      "type": "string",
+      "minLength": 1
+    },
+    "fullPage": {
+      "default": false,
+      "type": "boolean"
+    }
+  },
+  "required": [
+    "fullPage"
+  ],
+  "additionalProperties": false
+}
+```
+
+Source: [`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
+### `stagehand_tabs`
+
+List, create, select, or close a Stagehand browser tab.
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "oneOf": [
+    {
+      "type": "object",
+      "properties": {
+        "action": {
+          "type": "string",
+          "const": "list"
+        }
+      },
+      "required": [
+        "action"
+      ],
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "properties": {
+        "action": {
+          "type": "string",
+          "const": "new"
+        },
+        "url": {
+          "type": "string",
+          "format": "uri"
+        }
+      },
+      "required": [
+        "action"
+      ],
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "properties": {
+        "action": {
+          "type": "string",
+          "enum": [
+            "select",
+            "close"
+          ]
+        },
+        "pageId": {
+          "type": "string",
+          "minLength": 1
+        }
+      },
+      "required": [
+        "action",
+        "pageId"
+      ],
+      "additionalProperties": false
+    }
+  ],
+  "type": "object"
+}
+```
+
+Source: [`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
 <a id="deepseek-aidsh-tool-ask-user"></a>
 
 ## `@deepseek-ai/dsh-tool-ask-user`

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

@@ -20,6 +20,7 @@
 | 工具包 | 模型可见名称 | 依赖 | 写入/影响 | 随产品发布的别名 | 部署说明 |
 | --- | --- | --- | --- | --- | --- |
 | `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - |
+| `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`、`stagehand_extract`、`stagehand_navigate`、`stagehand_observe`、`stagehand_screenshot`、`stagehand_tabs` | `ctx.browserUse`、`ctx.agents`、`ctx.tools`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | - |
 | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`、`ctx.userQuestions` | `tool/call`、`tool/result after a UI/provider answers the question` | - | ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类答案。 |
 | `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`、`ctx.ptcRuntime (execution time)`、`ctx.systemPrompt` | `tool/call`、`one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`、`tool/result` | - | 在 `mode: ptc`/`mode: both` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 PTC mode Agent Note)。在 `ptc` 下,它是注册表对协议格式(wire format)的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。 |
 | `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`、`ctx.systemPrompt`、`ctx.userQuestions (execution time, opportunistic)` | `tool/call`、`plan/mode inactive on an approved review`、`tool/result` | - | 规划未激活时,exit_plan_mode 仍保留在面向模型的 schema 中,这样状态转换不会在规划策略变更之外额外造成工具目录变动。其执行路径会拒绝规划模式之外的调用;在规划模式下,它通过用户交互 seam 提交计划(批准/根据反馈继续规划),批准后会在步骤边界记录规划模式已停用。 |
@@ -127,6 +128,253 @@
 
 来源: [`packages/mcp/mcp-resources/src/tools.ts`](../packages/mcp/mcp-resources/src/tools.ts)
 
+<a id="deepseek-aidsh-experimental-browser-use-stagehand-native"></a>
+
+## `@deepseek-ai/dsh-experimental-browser-use-stagehand-native`
+
+### `stagehand_act`
+
+使用配置的 Stagehand 模型执行一次自然语言浏览器操作。
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "type": "object",
+  "properties": {
+    "pageId": {
+      "type": "string",
+      "minLength": 1
+    },
+    "instruction": {
+      "type": "string",
+      "minLength": 1
+    }
+  },
+  "required": [
+    "instruction"
+  ],
+  "additionalProperties": false
+}
+```
+
+来源:[`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
+### `stagehand_extract`
+
+使用配置的 Stagehand 模型与可选的 JSON Schema 提取页面数据。
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "type": "object",
+  "properties": {
+    "pageId": {
+      "type": "string",
+      "minLength": 1
+    },
+    "instruction": {
+      "type": "string",
+      "minLength": 1
+    },
+    "schema": {
+      "type": "object",
+      "propertyNames": {
+        "type": "string"
+      },
+      "additionalProperties": {
+        "$ref": "#/$defs/__schema0"
+      }
+    }
+  },
+  "required": [
+    "instruction"
+  ],
+  "additionalProperties": false,
+  "$defs": {
+    "__schema0": {
+      "anyOf": [
+        {
+          "type": "string"
+        },
+        {
+          "type": "number"
+        },
+        {
+          "type": "boolean"
+        },
+        {
+          "type": "null"
+        },
+        {
+          "type": "array",
+          "items": {
+            "$ref": "#/$defs/__schema0"
+          }
+        },
+        {
+          "type": "object",
+          "propertyNames": {
+            "type": "string"
+          },
+          "additionalProperties": {
+            "$ref": "#/$defs/__schema0"
+          }
+        }
+      ]
+    }
+  }
+}
+```
+
+来源:[`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
+### `stagehand_navigate`
+
+将 Stagehand 浏览器标签页导航至指定 URL。
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "type": "object",
+  "properties": {
+    "pageId": {
+      "type": "string",
+      "minLength": 1
+    },
+    "url": {
+      "type": "string",
+      "format": "uri"
+    }
+  },
+  "required": [
+    "url"
+  ],
+  "additionalProperties": false
+}
+```
+
+来源:[`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
+### `stagehand_observe`
+
+使用配置的 Stagehand 模型查找符合指令的浏览器操作。
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "type": "object",
+  "properties": {
+    "pageId": {
+      "type": "string",
+      "minLength": 1
+    },
+    "instruction": {
+      "type": "string",
+      "minLength": 1
+    }
+  },
+  "required": [
+    "instruction"
+  ],
+  "additionalProperties": false
+}
+```
+
+来源:[`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
+### `stagehand_screenshot`
+
+截取 Stagehand 标签页图像以供视觉检查。
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "type": "object",
+  "properties": {
+    "pageId": {
+      "type": "string",
+      "minLength": 1
+    },
+    "fullPage": {
+      "default": false,
+      "type": "boolean"
+    }
+  },
+  "required": [
+    "fullPage"
+  ],
+  "additionalProperties": false
+}
+```
+
+来源:[`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
+### `stagehand_tabs`
+
+列出、创建、选择或关闭 Stagehand 浏览器标签页。
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "oneOf": [
+    {
+      "type": "object",
+      "properties": {
+        "action": {
+          "type": "string",
+          "const": "list"
+        }
+      },
+      "required": [
+        "action"
+      ],
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "properties": {
+        "action": {
+          "type": "string",
+          "const": "new"
+        },
+        "url": {
+          "type": "string",
+          "format": "uri"
+        }
+      },
+      "required": [
+        "action"
+      ],
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "properties": {
+        "action": {
+          "type": "string",
+          "enum": [
+            "select",
+            "close"
+          ]
+        },
+        "pageId": {
+          "type": "string",
+          "minLength": 1
+        }
+      },
+      "required": [
+        "action",
+        "pageId"
+      ],
+      "additionalProperties": false
+    }
+  ],
+  "type": "object"
+}
+```
+
+来源:[`packages/experimental/browser-use-stagehand-native/src/index.ts`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+
 <a id="deepseek-aidsh-tool-ask-user"></a>
 
 ## `@deepseek-ai/dsh-tool-ask-user`

+ 5 - 0
package.json

@@ -195,6 +195,11 @@
   },
   "devDependencies": {
     "@deepseek-ai/dsh-agent": "workspace:^",
+    "@deepseek-ai/dsh-browser-use": "workspace:^",
+    "@deepseek-ai/dsh-experimental-browser-use-runtime": "workspace:^",
+    "@deepseek-ai/dsh-experimental-browser-use-playwright-mcp": "workspace:^",
+    "@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp": "workspace:^",
+    "@deepseek-ai/dsh-experimental-browser-use-stagehand-native": "workspace:^",
     "@deepseek-ai/dsh-computer-use": "workspace:^",
     "@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp": "workspace:^",
     "@deepseek-ai/dsh-experimental-computer-use-cua-driver-native": "workspace:^",

+ 2 - 2
packages/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/README.md
-README.md: 82fe5f2085cf5475a11b225f3d5ec437b3cd1089
-README.zh.md: 82729f08f92947609e6923b97a24175b567de414
+README.md: 113d5d11a0b108b84c0b8983602b7d99c0f79425
+README.zh.md: b86f98cc4c6ad8f748d3bd31d9a4bcaa7a10f27e

+ 1 - 0
packages/README.md

@@ -42,6 +42,7 @@ Every package lives in exactly one group; new packages join existing groups, and
 | [`terminal/`](terminal/README.md) | Persistent PTY capability family: owner-scoped sessions, local implementation, model-facing tools |
 | [`ptc-runtime/`](ptc-runtime/README.md) | PTC execution capability family: Service Definition + sandboxed Node provider + PTC mode Consumer |
 | [`computer-use/`](computer-use/README.md) | Exclusive named desktop-provider registration |
+| [`browser-use/`](browser-use/README.md) | Exclusive named browser-provider registration |
 | [`sandbox/`](sandbox/README.md) | Process-confinement seam; bwrap/Landlock/Seatbelt backends |
 | [`deliverables/`](deliverables/README.md) | Turn deliverables: explicit file delivery and recorded workspace changes |
 | [`fs/`](fs/README.md) | Filesystem capability family: seam, local impl, model-facing file tools, discovery tools |

+ 1 - 0
packages/README.zh.md

@@ -42,6 +42,7 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会
 | [`terminal/`](terminal/README.zh.md) | 持久 PTY 能力系列:限定所有者范围的会话、本地实现、面向模型的工具 |
 | [`ptc-runtime/`](ptc-runtime/README.zh.md) | PTC 执行能力族:Service Definition + 沙箱 Node 提供方 + PTC mode Consumer |
 | [`computer-use/`](computer-use/README.zh.md) | 按名称独占注册桌面提供方 |
+| [`browser-use/`](browser-use/README.zh.md) | 按名称独占注册浏览器提供方 |
 | [`sandbox/`](sandbox/README.zh.md) | 进程限制 seam;bwrap、Landlock、Seatbelt 后端 |
 | [`deliverables/`](deliverables/README.zh.md) | 轮次交付物:显式文件交付与记录的工作区改动 |
 | [`fs/`](fs/README.zh.md) | 文件系统能力系列:seam、本地实现、面向模型的文件工具、发现工具 |

+ 3 - 3
packages/api/session-controller/tests/agent.host.spec.ts

@@ -172,7 +172,7 @@ describe('ApiSession Agent lookup and recovery', () => {
   it('projects live Agent contexts and maps missing cold identities through Typert lookup failures', async () => {
     const { ctx } = await harness()
     const live = agent(ctx, header('live'))
-    ctx.agents.register(live)
+    await ctx.agents.register(live)
     providePersistence(ctx, {
       list: () => Promise.resolve([]),
       inspect: vi.fn(),
@@ -193,7 +193,7 @@ describe('ApiSession Agent lookup and recovery', () => {
     })
     const winner = agent(ordinary.ctx, ordinaryMeta)
     vi.spyOn(ordinary.ctx.agents, 'resume').mockImplementation(async () => {
-      ordinary.ctx.agents.register(winner)
+      await ordinary.ctx.agents.register(winner)
       throw new Error('raced publication')
     })
     await expect(ordinary.agents.resolveAgent(ordinaryMeta.id)).resolves.toEqual({ agent: winner })
@@ -326,7 +326,7 @@ describe('ApiSession create or adoption', () => {
     const ordinaryMeta = header('create-race', cwd)
     const winner = agent(ordinary.ctx, ordinaryMeta)
     vi.spyOn(ordinary.ctx.agents, 'create').mockImplementation(async () => {
-      ordinary.ctx.agents.register(winner)
+      await ordinary.ctx.agents.register(winner)
       throw new Error('raced creation')
     })
     await expect(ordinary.agents.ensureSession(ordinaryMeta.id, cwd, false))

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

@@ -82,7 +82,7 @@ async function commandHarness(
     followup: vi.fn(),
     cancel,
   } as unknown as Agent
-  ctx.agents.register(agent)
+  await ctx.agents.register(agent)
   ctx.provide('workspaceRegistry', { get: () => undefined, list: () => [] } as never)
   ctx.provide('agentDefaultModel', {
     currentSelection: () => ({ provider: 'fixture', model: 'fixture-model' }),

+ 7 - 7
packages/api/session-controller/tests/commands-upload-file.host.spec.ts

@@ -29,7 +29,7 @@ async function uploadHarness(origin?: 'subagent'): Promise<{
   saveFile: ReturnType<typeof vi.fn>
   saveFileStream: ReturnType<typeof vi.fn>
   saveImages: ReturnType<typeof vi.fn>
-  disposeAgent: () => void
+  disposeAgent: () => Promise<void>
   uploadRoute: (request: Request) => Promise<Response>
 }> {
   const ctx = new Context()
@@ -52,7 +52,7 @@ async function uploadHarness(origin?: 'subagent'): Promise<{
     cancel: vi.fn(),
   } as unknown as Agent
   ;(agent as { ctx: Context }).ctx = createScope(ctx, agent).ctx
-  const disposeAgent = ctx.agents.register(agent)
+  const disposeAgent = await ctx.agents.register(agent)
   const saveFile = vi.fn((input: SaveFileAttachment): Promise<FileAttachmentRef> => Promise.resolve({
     attachmentId: AttachmentId(`sha256:${'cd'.repeat(32)}`),
     name: input.name ?? 'file',
@@ -226,7 +226,7 @@ describe('Session file uploads', () => {
     saveFile.mockReturnValueOnce(saved.promise)
     const uploading = uploads.upload(agent, { data: 'AAAA', name: 'late.bin' }, new AbortController().signal)
     await vi.waitFor(() => { expect(saveFile).toHaveBeenCalledOnce() })
-    disposeAgent()
+    await disposeAgent()
     saved.resolve({
       attachmentId: AttachmentId(`sha256:${'ab'.repeat(32)}`), name: 'late.bin', bytes: 3,
     })
@@ -235,9 +235,9 @@ describe('Session file uploads', () => {
 
   it('resolves a cold ordinary Agent and releases the resolver registration', async () => {
     const { ctx, uploads, agent, disposeAgent } = await uploadHarness()
-    disposeAgent()
+    await disposeAgent()
     const resolveAgent = vi.fn(async () => {
-      ctx.agents.register(agent)
+      await ctx.agents.register(agent)
       return agent
     })
     const disposeResolver = uploads.registerAgentResolver(resolveAgent)
@@ -258,7 +258,7 @@ describe('Session file uploads', () => {
 
   it('rejects a cold upload when no Agent resolver is registered', async () => {
     const { uploads, disposeAgent } = await uploadHarness()
-    disposeAgent()
+    await disposeAgent()
     await expect(uploads.uploadStream({
       sessionId: SESSION,
       data: (async function* (): AsyncIterable<Uint8Array> {})(),
@@ -366,7 +366,7 @@ describe('Session file uploads', () => {
       { type: 'image', mediaType: 'image/png', data: 'AAAA' },
     ]))
     await vi.waitFor(() => { expect(saveImages).toHaveBeenCalledOnce() })
-    disposeAgent()
+    await disposeAgent()
     admitted.resolve([{
       attachmentId: AttachmentId('admitted-image'), mediaType: 'image/png', bytes: 3, width: 1, height: 1,
     }])

+ 1 - 1
packages/api/session-controller/tests/control-jobs.host.spec.ts

@@ -59,7 +59,7 @@ async function harness(withJobs: boolean): Promise<{
     runMaintenance: task => task(new AbortController().signal),
     whenIdle: () => Promise.resolve(),
   }
-  ctx.agents.register(agent)
+  await ctx.agents.register(agent)
   const control = new SessionControlController(ctx)
   await new Promise(resolve => setTimeout(resolve, 0))
   return { ctx, session, agent, control }

+ 1 - 1
packages/api/session-controller/tests/controller.host.spec.ts

@@ -74,7 +74,7 @@ describe('SessionController facade', () => {
       status: 'idle',
       ctx,
     } as Agent
-    ctx.agents.register(agent)
+    await ctx.agents.register(agent)
     const resolveUploadAgent = (id: SessionId): Promise<Agent> => {
       if (uploadResolver === undefined) throw new Error('file upload resolver was not registered')
       return uploadResolver(id)

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

@@ -202,7 +202,7 @@ describe('attached updatedAt tracks human prompts', () => {
       ],
       meta: { cwd: '/proj', createdAt: 500 },
     })
-    ctx.agents.register({ id: resumed.id, session: resumed, status: 'idle', ctx } as Agent)
+    await ctx.agents.register({ id: resumed.id, session: resumed, status: 'idle', ctx } as Agent)
     const boundary = resumed.snapshotEvents().at(-1)
     expect(boundary?.type).toBe('session/end-seed')
     expect(boundary?.time).toBeGreaterThan(worked)
@@ -330,7 +330,7 @@ describe('Remote Agent and Session lookup policy', () => {
       meta: { cwd: '/proj', parentSession: sid('session-parent'), origin: 'subagent' },
     })
     const liveAgent = { id: liveSession.id, session: liveSession, status: 'idle', ctx } as Agent
-    ctx.agents.register(liveAgent)
+    await ctx.agents.register(liveAgent)
     const resume = vi.spyOn(ctx.agents, 'resume')
     const defaultAgentLookup = ctx.typert.lookups.get('agent')
     const defaultSessionLookup = ctx.typert.lookups.get('session')
@@ -478,7 +478,7 @@ describe('subagent ownership fence', () => {
     await ctx.plugin(AgentRegistry)
     const parentSession = ctx.sessions.create(sid('session-parent'), { meta: { cwd: '/proj' } })
     const parent = { id: parentSession.id, session: parentSession, status: 'idle', ctx } as Agent
-    ctx.agents.register(parent)
+    await ctx.agents.register(parent)
 
     const originSession = ctx.sessions.create(sid('session-origin-child'), {
       meta: { cwd: '/proj', parentSession: parent.id, origin: 'subagent' },
@@ -493,7 +493,7 @@ describe('subagent ownership fence', () => {
       cancel,
       updateInbox,
     } as unknown as Agent
-    ctx.agents.register(originChild)
+    await ctx.agents.register(originChild)
 
     const startingSession = ctx.sessions.create(sid('session-starting-child'), {
       meta: { cwd: '/proj', parentSession: parent.id },
@@ -549,7 +549,7 @@ describe('subagent ownership fence', () => {
     const agent = {
       id: session.id, session, inbox: inboxFor(), status: 'idle', ctx, followup,
     } as unknown as Agent
-    ctx.agents.register(agent)
+    await ctx.agents.register(agent)
     const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
 
     const response = await remote.prompt(promptRequest({
@@ -570,7 +570,7 @@ describe('subagent ownership fence', () => {
     const agent = {
       id: session.id, session, inbox: inboxFor(), status: 'idle', ctx, followup,
     } as unknown as Agent
-    ctx.agents.register(agent)
+    await ctx.agents.register(agent)
     const remote = createSessionTestRemote(ctx, {
       defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
       cwd: '/tmp',
@@ -686,7 +686,7 @@ describe('sessions.prompt synchronous rejection', () => {
     const session = ctx.sessions.create(sid('session-empty-prompt'))
     const followup = vi.fn()
     const steer = vi.fn()
-    ctx.agents.register({
+    await ctx.agents.register({
       id: session.id,
       session,
       inbox: inboxFor(),
@@ -781,7 +781,7 @@ describe('sessions.prompt synchronous rejection', () => {
     const session = ctx.sessions.create(sid('session-throwing'))
     // A live structural stub whose delivery verbs throw synchronously, the
     // shape a disposed loop presents at this gateway boundary.
-    ctx.agents.register({
+    await ctx.agents.register({
       id: session.id,
       session,
       inbox: inboxFor(),
@@ -820,7 +820,7 @@ describe('sessions.prompt synchronous rejection', () => {
     // while the generic cold resume is in flight, so the resume collides.
     const parentSession = ctx.sessions.create(sid('race-parent'), { meta: { cwd: '/proj' } })
     const parent = { id: parentSession.id, session: parentSession, status: 'idle', ctx } as Agent
-    ctx.agents.register(parent)
+    await ctx.agents.register(parent)
     const childSession = ctx.sessions.create(sessionId, {
       meta: { cwd: '/proj', parentSession: parent.id, origin: 'subagent' },
     })
@@ -828,7 +828,7 @@ describe('sessions.prompt synchronous rejection', () => {
     vi.spyOn(ctx.agents, 'resume').mockImplementationOnce(async () => {
       // The parent's `enter()` wins the identity between the pre-resume
       // re-check and publication; the generic resume then collides.
-      ctx.agents.register(child)
+      await ctx.agents.register(child)
       throw new Error('session id already published')
     })
     const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })

+ 12 - 12
packages/api/session-controller/tests/session-fork.host.spec.ts

@@ -42,7 +42,7 @@ async function composed(workspaces: readonly Workspace[] = []): Promise<Context>
       const agentCtx = ownerCtx
       Object.assign(agent, { id: session.id, session, status: 'idle', ctx: agentCtx })
       await options.setup?.(agentCtx, agent)
-      ctx.agents.register(agent)
+      await ctx.agents.register(agent)
       return { agent, dispose: () => Promise.resolve() }
     },
     resume: () => Promise.reject(new Error('fork test sources are live')),
@@ -53,13 +53,13 @@ async function composed(workspaces: readonly Workspace[] = []): Promise<Context>
 /** Tail turn appended after the completed ones: left open, or closed as aborted (a stopped turn). */
 type Tail = 'none' | 'open' | 'aborted'
 
-function liveAgent(
+async function liveAgent(
   ctx: Context,
   id: string,
   turns: number,
   tail: Tail = 'none',
   lineage: { parentSession?: SessionId; origin?: 'subagent' } = {},
-): Session {
+): Promise<Session> {
   const session = ctx.sessions.create(sid(id), { meta: { cwd: '/proj', ...lineage } })
   for (let turn = 1; turn <= turns; turn++) {
     session.append('turn/start', { turn })
@@ -80,7 +80,7 @@ function liveAgent(
       reason: { kind: 'aborted', reason: { kind: 'user' } },
     })
   }
-  ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
+  await ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
   return session
 }
 
@@ -139,7 +139,7 @@ describe('sessions.fork', () => {
 
   it('cuts at the anchored completed turn and records lineage and cwd', async () => {
     const ctx = await composed()
-    const source = liveAgent(ctx, 'session-source', 2)
+    const source = await liveAgent(ctx, 'session-source', 2)
     const response = await remote(ctx).fork(request({ sessionId: source.id, atSeq: 1 }))
     expect(response.ok ? null : response.error).toBeNull()
     if (!response.ok) return
@@ -161,13 +161,13 @@ describe('sessions.fork', () => {
       attachSession,
     } as unknown as Workspace
     const ctx = await composed([workspace])
-    const owner = liveAgent(ctx, 'session-owner', 1)
+    const owner = await liveAgent(ctx, 'session-owner', 1)
     accounted.push(owner.id)
-    const child = liveAgent(ctx, 'session-child', 1, 'none', {
+    const child = await liveAgent(ctx, 'session-child', 1, 'none', {
       parentSession: owner.id,
       origin: 'subagent',
     })
-    const grandchild = liveAgent(ctx, 'session-grandchild', 1, 'none', {
+    const grandchild = await liveAgent(ctx, 'session-grandchild', 1, 'none', {
       parentSession: child.id,
       origin: 'subagent',
     })
@@ -253,7 +253,7 @@ describe('sessions.fork', () => {
 
   it('uses the last completed turn only for omitted and past-end anchors', async () => {
     const ctx = await composed()
-    const source = liveAgent(ctx, 'session-tail', 2, 'open')
+    const source = await liveAgent(ctx, 'session-tail', 2, 'open')
     const proxy = remote(ctx)
     const expectedTypes = [
       'turn/start', 'user/message', 'turn/end',
@@ -289,7 +289,7 @@ describe('sessions.fork', () => {
 
   it('cuts through an aborted turn: stopped is closed, not open', async () => {
     const ctx = await composed()
-    const source = liveAgent(ctx, 'session-aborted', 1, 'aborted')
+    const source = await liveAgent(ctx, 'session-aborted', 1, 'aborted')
     // What a stopped message's fork button anchors on: the frozen node sits
     // one event before its turn/end, floored client-side to that event's seq.
     const anchor = (source.snapshotEvents().at(-1)?.seq ?? 0) - 1
@@ -306,7 +306,7 @@ describe('sessions.fork', () => {
 
   it('rejects an in-log anchor whose turn is still open', async () => {
     const ctx = await composed()
-    const source = liveAgent(ctx, 'session-open', 1, 'open')
+    const source = await liveAgent(ctx, 'session-open', 1, 'open')
     const anchor = source.snapshotEvents().at(-1)?.seq ?? 0
     const response = await remote(ctx).fork(request({ sessionId: source.id, atSeq: anchor }))
     expect(response).toMatchObject({
@@ -319,7 +319,7 @@ describe('sessions.fork', () => {
 
   it('inherits model selection through the completed turn and excludes later changes', async () => {
     const ctx = await composed()
-    const source = liveAgent(ctx, 'session-routed', 0)
+    const source = await liveAgent(ctx, 'session-routed', 0)
     source.append('turn/start', { turn: 1 })
     source.append('request/header', {
       header: {

+ 5 - 5
packages/api/session-controller/tests/session-list-blank.host.spec.ts

@@ -19,15 +19,15 @@ import type {} from '@deepseek-ai/dsh-permission-presets'
 import type {} from '@deepseek-ai/dsh-sandbox-policy'
 import { createSessionTestRemote, type TestSessionRemote } from './test-remote.ts'
 
-async function harness(): Promise<{ ctx: Context; remote: TestSessionRemote; attach: (session: Session) => void }> {
+async function harness(): Promise<{ ctx: Context; remote: TestSessionRemote; attach: (session: Session) => Promise<void> }> {
   const ctx = new Context()
   await ctx.plugin(SessionStore)
   await ctx.plugin(AgentRegistry)
   return {
     ctx,
     remote: createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }),
-    attach: (session) => {
-      ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
+    attach: async (session) => {
+      await ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
     },
   }
 }
@@ -57,7 +57,7 @@ describe('summary blank = conversation not started', () => {
   it('standalone events (command lifecycle, plan/mode, title) keep the session blank', async () => {
     const { ctx, remote, attach } = await harness()
     const session = ctx.sessions.create()
-    attach(session)
+    await attach(session)
     expect(await listBlank(remote, session.id)).toBe(true)
     appendStandalone(session)
     expect(await listBlank(remote, session.id)).toBe(true)
@@ -66,7 +66,7 @@ describe('summary blank = conversation not started', () => {
   it('the first turn clears blank', async () => {
     const { ctx, remote, attach } = await harness()
     const session = ctx.sessions.create()
-    attach(session)
+    await attach(session)
     appendStandalone(session)
     session.append('turn/start', { turn: 0 })
     expect(await listBlank(remote, session.id)).toBe(false)

+ 1 - 1
packages/api/session-controller/tests/session-models.host.spec.ts

@@ -132,7 +132,7 @@ async function harness(logged?: {
     ctx,
     inbox: { nextTurn: [], nextStep: [] },
   } as unknown as Agent
-  ctx.agents.register(agent)
+  await ctx.agents.register(agent)
   return { ctx, agent, sessionId: session.id }
 }
 

+ 2 - 2
packages/api/session-controller/tests/session-presets.host.spec.ts

@@ -68,8 +68,8 @@ async function harness(presets?: readonly string[]) {
       const agent = stubAgent(session)
       ;(agent as { ctx?: Context }).ctx = ctx
       await options.setup?.(ctx, agent)
-      const unregister = ctx.agents.register(agent)
-      return { agent, dispose: () => { unregister(); return Promise.resolve() } }
+      const unregister = await ctx.agents.register(agent)
+      return { agent, dispose: async () => { await unregister() } }
     },
     async resume() {
       throw new Error('test harness has no persisted sessions')

+ 9 - 9
packages/api/session-controller/tests/session-rename.host.spec.ts

@@ -36,13 +36,13 @@ async function composed(withTitles = true): Promise<Context> {
   // forwarded seed/meta (the store validates the balanced prefix) and
   // registers an idle agent stub over it.
   ctx.agents.setFactory({
-    createAgent: (ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle> => {
+    createAgent: async (ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle> => {
       const session = ctx.sessions.create(options.sessionId, {
         ...options.seed === undefined ? {} : { seed: [...options.seed] },
         ...options.meta === undefined ? {} : { meta: options.meta },
       })
       const agent = { id: session.id, session, status: 'idle', ctx: ownerCtx } as Agent
-      ctx.agents.register(agent)
+      await ctx.agents.register(agent)
       return Promise.resolve({ agent, dispose: () => Promise.resolve() })
     },
     resume: () => Promise.reject(new Error('resume must not run: every source is attached')),
@@ -51,7 +51,7 @@ async function composed(withTitles = true): Promise<Context> {
 }
 
 /** Register one live agent whose log holds `turns` completed turns. */
-function liveAgent(ctx: Context, id: string, turns: number): Session {
+async function liveAgent(ctx: Context, id: string, turns: number): Promise<Session> {
   const session = ctx.sessions.create(sid(id), { meta: { cwd: '/proj' } })
   for (let turn = 1; turn <= turns; turn++) {
     session.append('turn/start', { turn })
@@ -61,7 +61,7 @@ function liveAgent(ctx: Context, id: string, turns: number): Session {
     }), { surfaceOp: 'append' })
     session.append('turn/end', { turn, reason: { kind: 'completed' } })
   }
-  ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
+  await ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
   return session
 }
 
@@ -70,7 +70,7 @@ const remote = (ctx: Context) => createSessionTestRemote(ctx, { defaultModelSele
 describe('sessions.rename', () => {
   it('accepts through the composed title service: normalized user-source event, echoed seq', async () => {
     const ctx = await composed()
-    const source = liveAgent(ctx, 'session-rename', 1)
+    const source = await liveAgent(ctx, 'session-rename', 1)
 
     const renamed = await remote(ctx).rename(request({ sessionId: source.id, title: '  new   name  ' }))
     expect(renamed.ok).toBe(true)
@@ -83,7 +83,7 @@ describe('sessions.rename', () => {
 
   it('maps only an empty-normalizing title to title-invalid, with a presentable message', async () => {
     const ctx = await composed()
-    const source = liveAgent(ctx, 'session-rename-bad', 1)
+    const source = await liveAgent(ctx, 'session-rename-bad', 1)
 
     // U+200B passes a client-side trim gate but normalizes to empty host-side.
     const response = await remote(ctx).rename(request({ sessionId: source.id, title: ' ​ ' }))
@@ -104,8 +104,8 @@ describe('sessions.rename', () => {
     // title service's liveness check throws a plain Error, which must not
     // read as the user's fault.
     const foreign = await composed(false)
-    const stale = liveAgent(foreign, 'session-rename-stale', 1)
-    ctx.agents.register({ id: stale.id, session: stale, status: 'idle', ctx } as Agent)
+    const stale = await liveAgent(foreign, 'session-rename-stale', 1)
+    await ctx.agents.register({ id: stale.id, session: stale, status: 'idle', ctx } as Agent)
 
     const response = await remote(ctx).rename(request({ sessionId: stale.id, title: 'name' }))
     expect(response.ok).toBe(false)
@@ -114,7 +114,7 @@ describe('sessions.rename', () => {
 
   it('answers internal when the composition mounts no session-title service', async () => {
     const ctx = await composed(false)
-    const source = liveAgent(ctx, 'session-no-titles', 1)
+    const source = await liveAgent(ctx, 'session-no-titles', 1)
 
     const response = await remote(ctx).rename(request({ sessionId: source.id, title: 'name' }))
     expect(response.ok).toBe(false)

+ 1 - 1
packages/api/session-controller/tests/session-skills.host.spec.ts

@@ -90,7 +90,7 @@ describe('SessionSkillCatalog', () => {
     const sessionId = SessionId('live-skills')
     const session = ctx.sessions.create(sessionId, { meta: { cwd: '/live/project' } })
     const agent = { id: sessionId, session, status: 'idle', ctx } as Agent
-    ctx.agents.register(agent)
+    await ctx.agents.register(agent)
     ctx.provide('sessionQuery', {
       observeSession: () => Promise.resolve(observation(sessionId, { cwd: '/live/project' })),
     } as never)

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/api/terminal-controller/README.md
-README.md: c302a2b7269bdb6a76d0c6056d5851a84a876e9a
-README.zh.md: dfc50e30f3a35b70c40bb76f9832cbd9548ba2cf
+README.md: 21bcfb335246dc4b95a3cd70993c8444ad7de057
+README.zh.md: 6ba88bcecc55c850cf477257c8055b5babae9b52

+ 1 - 1
packages/api/terminal-controller/README.md

@@ -32,7 +32,7 @@ Shell discovery lists the execution environment's declared default shell first.
 | Configuration | Default | Meaning |
 |---|---|---|
 | `shell` | omitted | Use the execution environment's default shell, or one explicit profile |
-| `shellCandidates` | `zsh`, `bash`, `fish`, `sh`, `ksh`, `tcsh`, `csh`, `pwsh`, `powershell`, `cmd`, `nu` | Additional executable names or paths offered when installed |
+| `shellCandidates` | `zsh`, `bash`, `fish`, `pwsh`, `powershell`, `cmd` | Additional executable names or paths offered when installed |
 | `maxTerminals` | `8` | Retained terminals and pending allocations per Session |
 | `maxCols`, `maxRows` | `500`, `200` | Maximum PTY dimensions |
 | `scrollback` | `1000` | Retained screen history rows |

+ 1 - 1
packages/api/terminal-controller/README.zh.md

@@ -32,7 +32,7 @@ Shell 探测结果首先列出执行环境声明的默认 shell。仅当 provide
 | 配置 | 默认值 | 含义 |
 |---|---|---|
 | `shell` | 省略 | 使用执行环境默认 shell,或指定一个 profile |
-| `shellCandidates` | `zsh`、`bash`、`fish`、`sh`、`ksh`、`tcsh`、`csh`、`pwsh`、`powershell`、`cmd`、`nu` | 已安装时供用户选择的额外可执行名称或路径 |
+| `shellCandidates` | `zsh`、`bash`、`fish`、`pwsh`、`powershell`、`cmd` | 已安装时供用户选择的额外可执行名称或路径 |
 | `maxTerminals` | `8` | 每个 Session 保留的终端和创建请求上限 |
 | `maxCols`、`maxRows` | `500`、`200` | PTY 最大尺寸 |
 | `scrollback` | `1000` | 屏幕历史行数 |

+ 1 - 1
packages/api/terminal-controller/src/index.ts

@@ -69,7 +69,7 @@ export class TerminalController extends TypertRemoteService {
     shell: z.union([z.object({
       path: z.string().required(), name: z.string().required(), args: z.array(z.string()).default([]),
     }), z.const(undefined)]),
-    shellCandidates: z.array(z.string().min(1)).default(['zsh', 'bash', 'fish', 'sh', 'ksh', 'tcsh', 'csh', 'pwsh', 'powershell', 'cmd', 'nu']),
+    shellCandidates: z.array(z.string().min(1)).default(['zsh', 'bash', 'fish', 'pwsh', 'powershell', 'cmd']),
     maxTerminals: z.number().step(1).min(1).default(8),
     maxCols: z.number().step(1).min(2).default(500),
     maxRows: z.number().step(1).min(1).default(200),

+ 6 - 0
packages/browser-use/README.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 packages/browser-use/README.md
+README.md: edac2b7756825984c61d14a08b991fc7c8965b0a
+README.zh.md: 35bc31f51e1e47b5750268522520025ddf4793b0

+ 40 - 0
packages/browser-use/README.md

@@ -0,0 +1,40 @@
+---
+description: "Browser-use capability packages for selecting and registering one browser backend."
+kind: "package-group"
+---
+
+# packages/browser-use
+
+English | [中文](README.zh.md)
+
+## Summary
+
+Browser-use providers let models inspect and operate web pages. This group owns exclusive provider registration. Experimental providers supply browser tools and manage each Session's browser resources.
+
+## Table of Contents
+
+- [Packages](#packages)
+- [Related documentation](#related-documentation)
+- [Dev Note](#dev-note)
+
+-----
+
+<a id="packages"></a>
+## Packages
+
+Choose one provider and mount the shared registration service.
+
+| Package | Role | ctx key |
+|---|---|---|
+| [`browser-use`](browser-use/README.md) | Exclusive named provider registration | `ctx.browserUse` |
+
+<a id="related-documentation"></a>
+## Related documentation
+
+- [Browser use](../../docs/subsystems/browser-use.md) — capability ownership and provider choices.
+- [Experimental packages](../experimental/README.md) — Playwright MCP, Chrome DevTools MCP, and Stagehand providers.
+
+<a id="dev-note"></a>
+## Dev Note
+
+None.

+ 40 - 0
packages/browser-use/README.zh.md

@@ -0,0 +1,40 @@
+---
+description: "浏览器操作能力包,用于选择与注册一个浏览器后端。"
+kind: "package-group"
+---
+
+# packages/browser-use
+
+[English](README.md) | 中文
+
+## 概述
+
+浏览器操作 提供方 让模型检查与操作网页。本组拥有独占 提供方 注册。实验性 提供方 提供浏览器工具,并管理各 Session 的浏览器资源。
+
+## 目录
+
+- [包](#packages)
+- [相关文档](#related-documentation)
+- [开发备注](#dev-note)
+
+-----
+
+<a id="packages"></a>
+## 包
+
+选择一个 提供方 并挂载共享注册服务。
+
+| 包 | 职责 | ctx 键 |
+|---|---|---|
+| [`browser-use`](browser-use/README.zh.md) | 独占具名 提供方 注册 | `ctx.browserUse` |
+
+<a id="related-documentation"></a>
+## 相关文档
+
+- [浏览器操作](../../docs/subsystems/browser-use.zh.md) — 能力所有权与 提供方 选择。
+- [实验包](../experimental/README.zh.md) — Playwright MCP、Chrome DevTools MCP 与 Stagehand 提供方。
+
+<a id="dev-note"></a>
+## 开发备注
+
+无。

+ 6 - 0
packages/browser-use/browser-use/README.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 packages/browser-use/browser-use/README.md
+README.md: d721f4f22e5a132f79e4428d6e306a0595057a95
+README.zh.md: fd1b2dc832a75800b6201a84c3c7be77686e53a8

+ 90 - 0
packages/browser-use/browser-use/README.md

@@ -0,0 +1,90 @@
+---
+description: "Browser-use provider registration for deployments that enable one browser backend at a time."
+kind: "package-reference"
+---
+
+# @deepseek-ai/dsh-browser-use
+
+English | [中文](README.zh.md)
+
+## Summary
+
+A deployment can enable one browser-use provider at a time. Loading another provider fails with the registered provider name. Each provider supplies its own tools and owns browser sessions. This package adds no model-visible tools or browser operations.
+
+## Table of Contents
+
+- [Use this package](#use-this-package)
+- [Understand the implementation](#understand-the-implementation)
+- [Further Exploration](#further-exploration)
+- [Model Experience](#model-experience)
+- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
+- [Dev Note](#dev-note)
+
+-----
+
+<a id="use-this-package"></a>
+## Use this package
+
+Mount the service once beside the chosen provider in a Cordis composition:
+
+```yaml
+- name: '@deepseek-ai/dsh-browser-use'
+```
+
+The service has no configuration. Provider plugins inject `browserUse` and call `ctx.browserUse.register(BrowserUseProviderName(name))`; the brand is exported from `@deepseek-ai/dsh-browser-use/brand`. The returned effect disposer releases that registration.
+
+Providers stop admitting tool calls, close their resources, and await owned work before releasing the registration. `ctx.browserUse.providerName` reports the registered name until release.
+
+-----
+
+<a id="understand-the-implementation"></a>
+## Understand the implementation
+
+<details>
+<summary>Implementation internals — click to expand</summary>
+
+One private name owns the slot. Cordis effects remove contributions when their plugin unloads; a repeated disposer cannot remove a later registration. The [source](src/index.ts) contains no browser object, operation interface, resource lifecycle, or provider selector.
+
+No runtime invariant companion is published: the registry has one authoritative field and exposes no independently maintained observation that can diverge. Duplicate rejection and plugin disposal are covered by the owning tests.
+
+</details>
+
+-----
+
+<a id="further-exploration"></a>
+## Further Exploration
+
+- [Browser use](../../../docs/subsystems/browser-use.md) — provider selection and Session ownership.
+- [Playwright MCP provider](../../experimental/browser-use-playwright-mcp/README.md) — Playwright browser tools.
+- [Chrome DevTools MCP provider](../../experimental/browser-use-chrome-devtools-mcp/README.md) — Chromium inspection and control.
+- [Stagehand provider](../../experimental/browser-use-stagehand-native/README.md) — native browser operations with AI assistance.
+
+-----
+
+<a id="model-experience"></a>
+## Model Experience
+
+None, as this registry only records provider names.
+
+#### KV Cache effect
+
+Registration does not alter model requests. Provider-owned tools and guidance determine their own request-prefix effects.
+
+## Known Limitations and Deferred Work
+
+<a id="known-limitations-and-deferred-work"></a>
+
+The service limits registrations within its Cordis service instance.
+
+- **Browser ownership** — providers own browser resources and enforce Session isolation; this service stores no browser state.
+- **Provider selection** — configuration selects the provider; the model cannot switch registered backends at runtime.
+
+<a id="dev-note"></a>
+### Dev Note
+
+<details>
+<summary>Working context for maintainers — click to expand</summary>
+
+None.
+
+</details>

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff