`(exe 产物)。协议字段 `serverInfo.name` 保持为 `deepseek-harness-sdk-runtime`(协议稳定值);Python 分发包名为 `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`,导入模块名仍为 `deepseek_harness` / `deepseek_harness_runtime`。
## 工作线程插件
@@ -62,7 +62,7 @@ exe 内支持 `dsh-workflow-workerthread` 与 `dsh-code-runtime-worker`。两个
## 测试
-验证面分三层。机制层:`--sea` 链路的实测结论内嵌在「决策」各节(VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都通过默认 SDK 路径、自定义配置和直接二进制协议,对 mock 端点完成一个轮次,并校验最终文本与 JSONL。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。同一构建任务还会经 Python SDK 运行一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个直接 spawn 的 subagent 和一个会通过 spawn 启动第二个 subagent 的工作流,随后卸载该插件。该 fixture(测试前置数据)会显式禁用组合包中未使用的 Bash 和本地 skill(技能)发现,使其工具集不依赖仓库外部状态;比较时会规范化以下各处的不透明消息 ID:SDK 结果与通知流,以及父会话和两个子会话的 JSONL 日志。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。随后把平台 wheel 包安装进干净的 venv,并在不传 `runtime_bin` 的情况下运行。
+验证面分三层。机制层:`--sea` 链路的实测结论内嵌在「决策」各节(VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都通过默认 SDK 路径、自定义配置、仓库内置的独立 minimal 组合和直接二进制协议,对 mock 端点完成一个轮次,并校验最终文本与 JSONL。minimal 运行会断言其精确系统提示词与双工具目录,跨调用保留 Bash 状态,并调用编辑器。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。同一构建任务还会经 Python SDK 运行一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个直接 spawn 的 subagent 和一个会通过 spawn 启动第二个 subagent 的工作流,随后卸载该插件。该 fixture(测试前置数据)会显式禁用组合包中未使用的 Bash 和本地 skill(技能)发现,使其工具集不依赖仓库外部状态;比较时会规范化以下各处的不透明消息 ID:SDK 结果与通知流,以及父会话和两个子会话的 JSONL 日志。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。随后把平台 wheel 包安装进干净的 venv,并在不传 `runtime_bin` 的情况下运行。
手工驱动注意:`bin` 将 stdin EOF 视为「客户端已离开」并立即 dispose,短命管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。
diff --git a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
index 91deb7153c..ce530420a5 100644
--- a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
+++ b/.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: 8903a2fefa83dba042f8105b182e590783a9adde
+2026-07-12-agent-scope-runtime-design.md: 5bee5f3f79be903848f8ecdf1ea84fe6fca60f6f
2026-07-12-agent-scope-runtime-design.zh.md: d86ab3d8cb9a051d4664aedac812b10b53521f22
diff --git a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
index 8903a2fefa..5bee5f3f79 100644
--- a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
+++ b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
@@ -25,7 +25,7 @@ The design can be skimmed as seven choices:
| Coordinate create/resume | One `AgentCreationTransaction` |
| Protect durable, queued, model, or wire data | Materialize once at that boundary |
| Pass typed values inside one process | Readonly borrowed contract |
-| Compose the model-visible prompt and tool surface | One shared tool view plus the authoritative assembly-waterfall result |
+| Compose the model-visible prompt and tool set | One shared tool view plus the authoritative assembly-waterfall result |
| Coordinate subagent, worker, and process shutdown | One cancellation signal plus the independent terminal/quiescence facts of that boundary |
The rest of this Agent Note expands those choices in dependency order: Cordis mechanics, scope routing, creation and session commit, tools and prompts, subagents and workflows, then executable checks.
diff --git a/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.i18n.yaml
index d4886965ad..bbecaabbbf 100644
--- a/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.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-scoped-layers-store.md
-2026-07-12-scoped-layers-store.md: c5186d1652bca617eed62ec02937f2d055ea727c
+2026-07-12-scoped-layers-store.md: 91df72cf18c3f28d49534793bee85094b299c9a5
2026-07-12-scoped-layers-store.zh.md: f5084426beef836d71e55c0898ec1edbdc7725f3
diff --git a/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md b/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md
index c5186d1652..91df72cf18 100644
--- a/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md
+++ b/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md
@@ -111,11 +111,11 @@ All seven facades keep validation and diagnostics in their owning registry and c
## Consequences
- Scope-aware registries express one aggregate layer and reuse the same construction, ownership, rollback, notification, and reclamation choreography. Domain-specific validation, diagnostics, filtering, evaluation, and observer policy remain in each registry.
-- The public read surface stays narrow: direct table iteration preserves explicitly live behavior, while `merge()` is the one shared materialized shadowing operation. A heterogeneous `ScopeLayer` has no layer-wide `values()` contract.
+- The public read API stays narrow: direct table iteration preserves explicitly live behavior, while `merge()` is the one shared materialized shadowing operation. A heterogeneous `ScopeLayer` has no layer-wide `values()` contract.
- The helper is deliberately synchronous. A future registration that needs asynchronous setup or several independently owned undos must identify its ownership and settlement boundaries before widening this contract.
- An action must throw before retaining a contribution or return an undo for everything it retained; the helper cannot repair mutation outside that contract. The provided entry operations are atomic, and migrated registries perform fallible validation before insertion.
- A scoped layer remains allocated until every table in its aggregate is empty. Disposing one facade therefore cannot discard sibling contributions owned by the same scope.
-- The four public symbols become a reusable package contract. Keeping `EntryValues` internal and consumer policy outside the helper limits the compatibility surface.
+- The four public symbols become a reusable package contract. Keeping `EntryValues` internal and consumer policy outside the helper limits the compatibility API.
- The migration changes no public registry behavior and no model-, human-, wire-, persistence-, configuration-, or dependency-graph output.
## Verification
diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml
index dd7ff64291..b1ffadacb3 100644
--- a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.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-14-provider-routed-llm-adapters.md
-2026-07-14-provider-routed-llm-adapters.md: 8bd34126d05140e486cd170bfc9255f0962dc413
-2026-07-14-provider-routed-llm-adapters.zh.md: 09d9032da5845d3e416dcfa047fde8e6527025b4
+2026-07-14-provider-routed-llm-adapters.md: df152a4436226ac5cb1941ce060a35bd4a0d496d
+2026-07-14-provider-routed-llm-adapters.zh.md: 181323c136a4f953d64928a8698720e164dd5ad1
diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md
index 8bd34126d0..df152a4436 100644
--- a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md
+++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md
@@ -48,7 +48,7 @@ This state is model-visible replay input and therefore follows the existing [rec
### Propagate the target through every request producer
-Every model-selection surface carries provider and model together: declarative agents, ACP and stdio app config, the JSON-RPC initialize request, subagent overrides and inheritance, workflow child overrides, and direct compaction summarization. Subagents inherit both fields from their parent before applying request overrides. The system-prompt variable set gains `provider` beside `model`.
+Every model-selection path carries provider and model together: declarative agents, ACP and stdio app config, the JSON-RPC initialize request, subagent overrides and inheritance, workflow child overrides, and direct compaction summarization. Subagents inherit both fields from their parent before applying request overrides. The system-prompt variable set gains `provider` beside `model`.
Compaction configuration gains `summarizationProvider` beside `summarizationModel`. Both are empty to inherit, or both are non-empty to select an explicit target; a half-configured pair fails load. Inheritance uses the last logged request target when one exists and falls back to the agent's creation options. `compact/summary` records both fields with the existing model-call envelope.
@@ -66,7 +66,7 @@ The on-disk session format remains the pre-release pinned version `0`, with no c
**Let `dsh-llm-pi-ai` automatically register every pi-ai provider.** This would claim ambient credentials and provider names the deployment never intended to expose, and would conflict with native adapters such as `dsh-llm-deepseek`. Explicit profiles make capability and credential scope reviewable.
-**Mount one pi-ai plugin instance per provider.** Separate instances isolate config but repeat plugin declarations and cannot make profile registration atomic. One adapter already receives provider on every request, so a validated profile map is the smaller lifecycle surface.
+**Mount one pi-ai plugin instance per provider.** Separate instances isolate config but repeat plugin declarations and cannot make profile registration atomic. One adapter already receives provider on every request, so a validated profile map is the smaller lifecycle API.
**Accept arbitrary inline pi-ai model descriptors.** This would support catalog-external private model ids, but it exposes pi-ai's model and compatibility schema as Harness configuration and makes the adapter responsible for validating protocol-specific combinations. The first version supports custom endpoints by overriding `baseURL` on catalog models; custom descriptors require a separate decision after a real catalog-external deployment is identified.
diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md
index 09d9032da5..181323c136 100644
--- a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md
@@ -48,7 +48,7 @@ pi-ai 回放状态是其成功 `AssistantMessage` 的带版本最小投影,包
### 在所有请求生产方中传播目标
-每个模型选择接口都同时携带 provider 与 model:声明式 agent、ACP(Agent Client Protocol)和 stdio 应用配置、JSON-RPC initialize 请求、subagent 覆盖与继承、工作流子 agent 覆盖,以及直接压缩摘要。subagent 先从父 agent 继承两个字段,再应用请求覆盖。系统提示词变量集合在 `model` 之外增加 `provider`。
+每条模型选择路径都同时携带 provider 与 model:声明式 agent、ACP(Agent Client Protocol)和 stdio 应用配置、JSON-RPC initialize 请求、subagent 覆盖与继承、工作流子 agent 覆盖,以及直接压缩摘要。subagent 先从父 agent 继承两个字段,再应用请求覆盖。系统提示词变量集合在 `model` 之外增加 `provider`。
压缩配置在 `summarizationModel` 之外增加 `summarizationProvider`。两个值均为空时继承,均非空时选择显式目标;只配置其中一个会导致加载失败。继承优先使用最近一次记录的请求目标,没有时回退到 agent 创建选项。`compact/summary` 使用现有模型调用 envelope 记录两个字段。
@@ -60,7 +60,7 @@ JSON-RPC 运行时显式接收 provider 与 model。仅当 `deepseek` 提供方
**继续以模型名称作为注册表键,并增加通配适配器。** 通配机制会在精确注册与兜底插件之间引入回退顺序,使重复所有权取决于监听器顺序;若不再增加其他约定,仍无法区分不同提供方中相同的模型 ID。
-**将提供方与模型编码到一个字符串中。** OpenRouter 的 `openai/gpt-*` 等值已经包含类似提供方的前缀和斜杠。分隔符约定会把路由语法泄漏到每个模型选择接口,并需要转义规则;两个显式字段更清晰,也可以分别记录日志。
+**将提供方与模型编码到一个字符串中。** OpenRouter 的 `openai/gpt-*` 等值已经包含类似提供方的前缀和斜杠。分隔符约定会把路由语法泄漏到每个模型选择器,并需要转义规则;两个显式字段更清晰,也可以分别记录日志。
**增加 `backend + provider + model`。** backend 键可以让 `dsh-llm-deepseek` 与 pi-ai 的 DeepSeek 实现共存,并按请求切换。最终采用的部署规则是一个提供方对应一个适配器所有者:同一上游的不同实现属于由插件组合选定的替代项。第三个路由维度会增加每个请求与配置的负担,却没有当前消费方。
diff --git a/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.i18n.yaml
index 02d531f642..0c81565bdd 100644
--- a/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.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-15-llm-model-catalog-and-acp-selection.md
-2026-07-15-llm-model-catalog-and-acp-selection.md: 77f8e379e2b07ecf4e67fa7197752543cfedd6dd
-2026-07-15-llm-model-catalog-and-acp-selection.zh.md: ce4a04a66e345834bc2b16b743be89dc7a0b9424
+2026-07-15-llm-model-catalog-and-acp-selection.md: bfd17c73b01319c10d5dc03333b3c726db5d6f33
+2026-07-15-llm-model-catalog-and-acp-selection.zh.md: aeddada5591bb2da2c0861acc368516eff148172
diff --git a/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md
index 77f8e379e2..bfd17c73b0 100644
--- a/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md
+++ b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md
@@ -26,17 +26,17 @@ Catalog membership is advisory. It drives selectors and diagnostics but never ch
`dsh-llm-pi-ai` maps the configured provider's installed `getModels(provider)` entries into the neutral catalog. Its existing request-time catalog lookup remains authoritative and still rejects unknown models with `UNKNOWN_MODEL`. `dsh-llm-deepseek` accepts an optional `models` config containing display entries, defaulting to `deepseek-v4-flash` named `DeepSeek-V4-Flash` and `deepseek-v4-pro` named `DeepSeek-V4-Pro`. An explicit list replaces those defaults and an empty list disables discovery. The entries improve selector UX for known public or private models, while every unlisted model id continues to pass through unchanged.
-### Per-session selection in the front door
+### Per-session selection in the front end
-A selection is owned by the front door that offers it (today the TUI `/model` selector), never by `LlmService` or `AgentOptions`: those are deployment-wide or creation-wide objects, and mutating them would couple concurrent sessions. Each opaque choice carries the full provider/model pair, because the same model id may appear under multiple routes.
+A selection is owned by the front end that offers it (today the TUI `/model` selector), never by `LlmService` or `AgentOptions`: those are deployment-wide or creation-wide objects, and mutating them would couple concurrent sessions. Each opaque choice carries the full provider/model pair, because the same model id may appear under multiple routes.
The ACP automation transport is not a catalog consumer. Its deployment config supplies one optional provider/model target for newly created agents, and it advertises no model selector or configuration-option interface.
### Prompt/request consistency and durability
-`installModelSelection` (in `dsh-agent`) installs scoped `system-prompt/assemble` and `agent/request` listeners for a front-door-owned selection. Prompt assembly snapshots the selected pair once per step, overwrites the assembled `provider` and `model` variables after downstream prompt listeners, and the request listener applies that same snapshot after downstream request listeners. A selection during asynchronous assembly therefore starts on the next step rather than splitting prompt text from routing. Other call-config fields remain untouched.
+`installModelSelection` (in `dsh-agent`) installs scoped `system-prompt/assemble` and `agent/request` listeners for a front-end-owned selection. Prompt assembly snapshots the selected pair once per step, overwrites the assembled `provider` and `model` variables after downstream prompt listeners, and the request listener applies that same snapshot after downstream request listeners. A selection during asynchronous assembly therefore starts on the next step rather than splitting prompt text from routing. Other call-config fields remain untouched.
-The request header remains the durable source of truth. When a selection is actually used, the existing full `request/header` snapshot records it, and a front door initializes its selection from the folded last request header before falling back to creation options. A selection that is never used by a request is intentionally in-memory only because it never became model-visible state.
+The request header remains the durable source of truth. When a selection is actually used, the existing full `request/header` snapshot records it, and a front end initializes its selection from the folded last request header before falling back to creation options. A selection that is never used by a request is intentionally in-memory only because it never became model-visible state.
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.zh.md b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.zh.md
index ce4a04a66e..aeddada559 100644
--- a/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.zh.md
@@ -26,17 +26,17 @@ ACP 选择还必须保留提供方维度。同一个模型 ID 可能存在于多
`dsh-llm-pi-ai` 将已配置提供方的安装目录 `getModels(provider)` 映射为中立目录。其现有请求时目录查询仍是权威依据,未知模型仍以 `UNKNOWN_MODEL` 失败。`dsh-llm-deepseek` 接受可选的 `models` 配置作为展示条目,默认包含名为 `DeepSeek-V4-Flash` 的 `deepseek-v4-flash` 和名为 `DeepSeek-V4-Pro` 的 `deepseek-v4-pro`。显式列表会替换这些默认值,空列表则关闭发现。这些条目改善已知公开或私有模型的选择体验,而所有未列出的模型 ID 仍会原样透传。
-### 前门内的会话级选择
+### 前端内的会话级选择
-选择由提供它的前门拥有(今天是 TUI 的 `/model` 选择器),而不由 `LlmService` 或 `AgentOptions` 拥有:它们是部署级或创建级对象,改动它们会把并发会话耦合在一起。每个不透明选项都携带完整的提供方/模型对,因为同一模型 ID 可能出现在多个路由下。
+选择由提供它的前端拥有(今天是 TUI 的 `/model` 选择器),而不由 `LlmService` 或 `AgentOptions` 拥有:它们是部署级或创建级对象,改动它们会把并发会话耦合在一起。每个不透明选项都携带完整的提供方/模型对,因为同一模型 ID 可能出现在多个路由下。
ACP 自动化传输层不是目录消费方。它通过部署配置为新创建的 agent 提供一个可选的提供方/模型目标,不展示模型选择器或配置选项接口。
### 提示词/请求一致性与持久化
-`installModelSelection`(位于 `dsh-agent`)为前门拥有的选择安装 agent 作用域的 `system-prompt/assemble` 与 `agent/request` 监听器。提示词组装在每个步骤对所选组合做一次快照,在下游提示词监听器之后覆写组装出的 `provider` 与 `model` 变量;请求监听器在下游请求监听器之后应用同一快照。因此,发生在异步组装期间的选择会从下一个步骤生效,而不会让提示词文本与路由分裂。其他调用配置字段保持不变。
+`installModelSelection`(位于 `dsh-agent`)为前端拥有的选择安装 agent 作用域的 `system-prompt/assemble` 与 `agent/request` 监听器。提示词组装在每个步骤对所选组合做一次快照,在下游提示词监听器之后覆写组装出的 `provider` 与 `model` 变量;请求监听器在下游请求监听器之后应用同一快照。因此,发生在异步组装期间的选择会从下一个步骤生效,而不会让提示词文本与路由分裂。其他调用配置字段保持不变。
-请求头仍是持久化的真源。当某个选择真正被使用时,现有的完整 `request/header` 快照会记录它;前门先从折叠后的最后一个请求头初始化其选择,然后才回退到创建选项。从未被请求使用的选择有意只保留在内存中,因为它从未成为模型可见状态。
+请求头仍是持久化的真源。当某个选择真正被使用时,现有的完整 `request/header` 快照会记录它;前端先从折叠后的最后一个请求头初始化其选择,然后才回退到创建选项。从未被请求使用的选择有意只保留在内存中,因为它从未成为模型可见状态。
## 考虑过的替代方案
diff --git a/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.i18n.yaml
index 91c9baf2a0..6fe5951d90 100644
--- a/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md
-2026-07-15-lsp-capability-seam.md: a461432fe5f218ea6c3ff3e031b52f7ebffbd7a5
+2026-07-15-lsp-capability-seam.md: c407de5275da0a8323b3c6186e178c8b2fafdc31
2026-07-15-lsp-capability-seam.zh.md: bfbd8d04f71b154d2833c86f2a6dc84d7e13fff4
diff --git a/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md b/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md
index a461432fe5..c407de5275 100644
--- a/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md
+++ b/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md
@@ -137,7 +137,7 @@ Navigation maps `Location` directly and `LocationLink` from `targetUri` plus `ta
Abort reaches every query phase and sends `$/cancelRequest` once an id exists. An unresponsive server is terminated and awaited without collateral active work because the instance is serialized. Disposal rejects and cancels work, attempts graceful shutdown, escalates through bounded termination, and awaits quiescence.
-## Deliberately deferred surface
+## Deliberately deferred API
Symbols are deferred because they need different schemas and overlap read/search; a future workspace-symbol tool must accept a search query. Call hierarchy is deferred because support is uneven, and `prepareCallHierarchy` remains an internal prerequisite rather than a model operation.
diff --git a/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.i18n.yaml
index 19f77344e1..08ddfd9b1c 100644
--- a/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.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-16-explicit-turn-cancellation.md
-2026-07-16-explicit-turn-cancellation.md: 5d7b5856ceebd3dc49564c1800004188da162fd8
+2026-07-16-explicit-turn-cancellation.md: 86faad9929d3eb5b00e66bb1c46a2e35b135d954
2026-07-16-explicit-turn-cancellation.zh.md: ba20b80f3c280087d71688ef4596fb075fa5782b
diff --git a/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md b/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md
index 5d7b5856ce..86faad9929 100644
--- a/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md
+++ b/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md
@@ -40,7 +40,7 @@ Initiator-scope tests assert that every hook still observes the exact Agent and
**Persist a free-form string reason.** Strings admit spelling drift, prevent exhaustive switching, and encourage consumers to parse presentation text. The runtime uses a closed discriminated union, while the terminal record needs only the stable aborted outcome.
-**Persist the typed caller cause in `turn/end`.** No production replay, UI, ACP, telemetry, or workflow consumer distinguishes `user` from `parent`. Copying the request source into the terminal result would conflate two facts and add Session-specific validation without a consumer; a future audit surface can record a separate cancellation-request event.
+**Persist the typed caller cause in `turn/end`.** No production replay, UI, ACP, telemetry, or workflow consumer distinguishes `user` from `parent`. Copying the request source into the terminal result would conflate two facts and add Session-specific validation without a consumer; a future audit trail can record a separate cancellation-request event.
**Define speculative `superseded`, `timeout`, and `shutdown` variants now.** No current Agent cancellation producer implements those semantics. `shutdown` is already lifecycle disposal, and timeout or supersession should enter the union only with an owning policy and unique terminal meaning.
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml
index 87c65807b8..d467b9c37b 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.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-19-gui-layering-and-rpc-protocol.md
-2026-07-19-gui-layering-and-rpc-protocol.md: 7997a682c8745f7b3d0a9721acfa9355603f9b2e
-2026-07-19-gui-layering-and-rpc-protocol.zh.md: cb36b5cb725128e1c5067e5e69e52aa851668e52
+2026-07-19-gui-layering-and-rpc-protocol.md: deba5e81c35e1572e2e6dc68b48234414ac9a7d5
+2026-07-19-gui-layering-and-rpc-protocol.zh.md: e506c6242794b1b065136c8190f5c46a46e0d069
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
index 7997a682c8..deba5e81c3 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
@@ -8,12 +8,12 @@ English | [中文](2026-07-19-gui-layering-and-rpc-protocol.zh.md)
## Problem
-We need a UI integration layer. Beyond the existing ACP/stdio baseline, more product UI shapes are coming — Web (server), Electron, and others. We call these shapes Clients, uniformly, and want the following capabilities:
+We need a UI integration layer. Beyond the existing ACP/stdio baseline, more product clients are coming — Web (server), Electron, and others. We call them Clients and want the following capabilities:
-- One `dsh` process supporting both `dsh web` (serve) and `dsh run` (headless) — one process, two modes (a design reservation)
-- Launching inside Electron with the same Web technology shape as `dsh web`
+- One `dsh` process supporting both `dsh web` (serve) and `dsh --profile headless` (headless) — one process, two modes (a design reservation)
+- Launching inside Electron with the same Web technologies as `dsh web`
-That demands a stable layered responsibility model in the engineering codebase, so future client shapes plug in cleanly.
+That demands a stable layered responsibility model in the engineering codebase, so future clients plug in cleanly.
At the same time the physical channels differ per consumer (browser HTTP/WebSocket, in-process fetch/SSE, IPC later), so we also need a channel-independent message model and a single contract source of truth — "adding a method" and "swapping a carrier" must not entangle each other, and every message on the wire must be type-validatable, observable, and reconcilable.
@@ -27,21 +27,21 @@ Directories layer as follows:
- the unified backend protocol (fetch, HTTP, streaming interfaces…) — definitions and support, see the "Message protocol" sections below
- `packages/client/*`: packages provide client-side capability only; every package stays single-sided. Three kinds live here (the axes are owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md)):
- **Pure libraries** (`ui-slots`, `web-react`, `ui-primitives`, plus the `loader` kernel package): ordinary root-index packages, statically bundled into the shell; the first three are seeded into the module table.
- - **Static-arrival entry packages** (`connection`, `runtime`, `ui-theme`, `i18n`, `hmr`): no `dshClient` key and no browser bundle — the shell bundles their `src/client/` half and registers it with `ctx.modules`; they are governed as entries of the host-authored graph like everything else.
- - **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dshClient` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
-- `apps/` holds the externally exported application shapes, assembled from Client / Host mixtures.
- - `apps/web` (`dsh-frontend`) is the vite application: a thin `main.ts` over the shell surface exported by `dsh-client-web`.
- - `apps/cli` (`@deepseek-ai/dsh`) dispatches shapes: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh run` = [a direct core Agent/Session front door](2026-08-09-headless-direct-core-front-door.md), with zero Host, HTTP, or browser layer.
- - A future Electron shape reuses the same web client packages over an IPC fetch carrier.
+ - **Static-arrival entry packages** (`connection`, `runtime`, `ui-theme`, `i18n`, `hmr`): no `dsh.client` key and no browser bundle — the shell bundles their `src/client/` half and registers it with `ctx.modules`; they are governed as entries of the host-authored graph like everything else.
+ - **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dsh.client` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
+- `apps/` holds the externally exported applications, assembled from Client / Host mixtures.
+ - `apps/web` (`dsh-frontend`) is the vite application: a thin `main.ts` over the shell API exported by `dsh-client-web`.
+ - `apps/cli` (`@deepseek-ai/dsh`) dispatches commands: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh --profile headless` = [a direct core Agent/Session entry point](2026-08-09-headless-direct-core-entry-point.md), with zero Host, HTTP, or browser layer.
+ - A future Electron application reuses the same web client packages over an IPC fetch carrier.
```
-apps/* (application shapes: apps/web = vite app, apps/cli = bin dispatch)
+apps/* (applications: apps/web = vite app, apps/cli = bin dispatch)
│ consume
▼
packages/host/* packages/client/*
apiproxy front layer: protocol pure libs: ui-slots / web-react / ui-primitives
- runtime assembly / host entity dshClient plugins ×8 (node half = empty apply,
- webserver web-shape HTTP carriage client half = src/client/)
+ runtime assembly / host entity dsh.client plugins ×8 (node half = empty apply,
+ webserver Web HTTP carriage client half = src/client/)
│ ctx.plugin(...) ▲ import only apiproxy's /api /client subpaths
▼ │ (type-only + the client base class)
harness core packages ──────────────────┘ (types reach the browser via import type)
@@ -51,35 +51,35 @@ Direction discipline (every rule auditable from package deps):
- `runtime → apiproxy` is one-way; apiproxy depends only on type definitions.
- Client-side packages **never import** host-side package runtime (they consume only the two browser-safe subpaths `/api` and `/client`).
-- `webserver` does not depend on `runtime`: it provides a `{ fetch }`-shaped implementation — "webserver ← runtime" is a runtime injection relationship, not a package dependency.
+- `webserver` does not depend on `runtime`: it provides an implementation of the `{ fetch }` interface — "webserver ← runtime" is a runtime injection relationship, not a package dependency.
- Cross-package client imports use the `/client` subpath for plugin packages, and between plugin packages they are type-only — a cross-plugin value import is a build error at the tsdown purity gate (value cooperation goes through cordis services; the [client plugin loading note](2026-07-23-client-plugin-loading-model.md) owns the edge rules).
TypeScript checks in **two aggregate programs** referenced by a solution root (`tsconfig.json` = solution; `tsconfig.host.json` = host side + tests, excluding `packages/client`; `tsconfig.client.json` = client packages and their tests): both sides merge the cordis `Context` interface under the same keys (`sessions`, `loader`) with different services, so one program would see both declaration merges and report a collision. Shared leaves (session/llm/tools/apiproxy…) build once and are referenced by both programs ([topology](../process/2026-07-22-tsconfig-solution-root-two-aggregates.md)).
-On the protocol side: TS interfaces (`packages/host/apiproxy/src/api/`, zero Node dependencies, browser-importable); wire messages unify under a **bidirectional model** — each logical message is shaped by "who initiates × request/response" (two axes, four cells, called the four quadrants below), decoupled from the physical channel; clients all inherit `AbstractApiClient` (protocol invariants live entirely in the base class, platform differences are just the `doFetch` transport aspect).
+On the protocol side: TS interfaces (`packages/host/apiproxy/src/api/`, zero Node dependencies, browser-importable); wire messages unify under a **bidirectional model** — each logical message is classified by "who initiates × request/response" (two axes, four cells, called the four quadrants below), decoupled from the physical channel; clients all inherit `AbstractApiClient` (protocol invariants live entirely in the base class, platform differences are just the `doFetch` transport aspect).
#### Layer roles
| Layer | Package | Responsibility | Key discipline |
|---|---|---|---|
| Front layer | `dsh-host-apiproxy` | TS/zod definitions (api/) + the fetch abstraction (fetch/: handler + client base class) | Keep it simple — every consumer needs it; importable from Node and browser alike; protocol content in the "Message protocol" sections below; clients must not bypass api through ctx |
-| Assembly layer | `dsh-host-runtime` | Plugin composition + ApiProxy integration + the web UI plugin mount (in-memory Loader tree over the eight dshClient packages); home of host-level configuration (defaults/persistenceRoot, future user profile) | Which plugins mount and with what defaults is decided only here; shells must not alter the assembly |
-| Carrier layer | `dsh-host-webserver` | Web-shape HTTP and upgrade: static serving + `/api/*`→handler forwarding + WebSocket upgrade route + close semantics; plugin bundle endpoint + `__DSH_BOOT__` manifest injection (fed by the web plugin registry) | Web (browser access) only; zero workspace dependencies (the registry arrives by structural injection); Electron does not reuse it |
+| Assembly layer | `dsh-host-runtime` | Plugin composition + ApiProxy integration + the web UI plugin mount (in-memory Loader tree over the eight dsh.client packages); home of host-level configuration (defaults/persistenceRoot, future user profile) | Which plugins mount and with what defaults is decided only here; shells must not alter the assembly |
+| Carrier layer | `dsh-host-webserver` | Web HTTP and upgrade: static serving + `/api/*`→handler forwarding + WebSocket upgrade route + close semantics; plugin bundle endpoint + `__DSH_BOOT__` manifest injection (fed by the web plugin registry) | Web (browser access) only; zero workspace dependencies (the registry arrives by structural injection); Electron does not reuse it |
| Client libraries | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | Slot registry core / ctx↔React glue / pure React atoms | Zero cordis runtime dependency in components; seeded into the loader module table by the shell |
| Client plugins | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | Browser-side cordis plugin tree (wire consumer, core services, theme, i18n, layout, sidebar, conversation, trajectory) — see the web client architecture note | Dual entry (node half = empty apply; implementation in `src/client/`); the consumption face goes exclusively through ApiProxy |
-| Application shape | `@deepseek-ai/dsh` (apps/cli) + `dsh-frontend` (apps/web, the vite application) | Coarse bin dispatch + one assembly module per shape (web.ts / headless.ts); the vite app is a thin main over the `dsh-client-web` shell surface | Shapes dynamic-import so they never load each other; workspace knowledge like dist location stays in the app |
+| Application | `@deepseek-ai/dsh` (apps/cli) + `dsh-frontend` (apps/web, the vite application) | Coarse bin dispatch + one assembly module per application (web.ts / headless.ts); the vite app is a thin main over the `dsh-client-web` shell surface | Applications use dynamic imports so they never load each other; workspace knowledge like dist location stays in the app |
#### Naming rule
Packages under `packages/host/*` and `packages/client/*` **must carry the directory-group prefix in the package name**: host/runtime → `dsh-host-runtime`, client/runtime → `dsh-client-runtime`. The directory name does not repeat the group prefix (host/ already expresses it). The package-name tail therefore ≠ the directory name, so the `dsh-*` wildcard in tsconfig.base.json (which resolves by directory name) misses them — **each package in these two groups needs an explicit paths entry**, including separate entries for the client packages' `/client` subpaths so source-level resolution matches the exports map.
-#### How to integrate a new shape (operational checklist)
+#### How to integrate a new application (operational checklist)
1. **Pick a fetch impersonation**: browser same-origin HTTP / in-process `host.handler.fetch` injection / your own transport-aspect subclass (e.g. future Electron IPC, see the "Subclass table" below).
-2. **Write an assembly module under `apps/`**: `startHost()` + a client subclass + the shape's private signal/print/exit semantics; a mixture never becomes a package — assembly is written in the app.
+2. **Write an assembly module under `apps/`**: `startHost()` + a client subclass + the application's private signal/print/exit semantics; a mixture never becomes a package — assembly is written in the app.
3. **Import `dsh-host-webserver` only if you need HTTP carriage**, otherwise zero ports.
-The two existing shapes preserve the boundary: the Web shape mounts Host, carrier, and browser composition, while `dsh run` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem, mount via `ctx.plugin(front-door plugin)` directly, and wear no fetch.
+The two existing applications preserve the division: the Web application mounts Host, carrier, and browser composition, while `dsh --profile headless` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem and mount directly via `ctx.plugin(entry-point plugin)` without fetch.
## Message protocol
@@ -116,7 +116,7 @@ Domain interface signatures perceive only the narrow forms: `RpcRequest = { r
### RpcReceipt: the carrier receipt
-The HTTP response body of a `ClientResponse` is `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }` — a carrier-layer receipt, **not** an RpcMessage (a response has no response); late/duplicate answers get `not-pending`, and the logical convergence surface is the `*/resolved` frames.
+The HTTP response body of a `ClientResponse` is `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }` — a carrier-layer receipt, **not** an RpcMessage (a response has no response); late/duplicate answers get `not-pending`, and the logical convergence point is the `*/resolved` frames.
## The type system: signatures are the source of truth
@@ -169,7 +169,7 @@ The remaining methods (`session.create`/`session.history`/`session.rename`/`sess
### Frames (server→client, named unions)
-Two logical streams: the mux stream (`/api/events.mux`, all-session aggregate) and the host stream (`/api/events.host`, host-level events). The browser consumes one downlink WebSocket per stream, while the in-process fetch carrier retains SSE to preserve the same shape; see the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) for the physical boundary. One example frame row:
+Two logical streams: the mux stream (`/api/events.mux`, all-session aggregate) and the host stream (`/api/events.host`, host-level events). The browser consumes one downlink WebSocket per stream, while the in-process fetch carrier retains SSE with the same event framing; see the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) for the physical boundary. One example frame row:
| frame type | payload | when |
|---|---|---|
@@ -215,8 +215,8 @@ All four quadrant full forms pass through `onEnvelope`; the base implementation
| Subclass | Package | doFetch | Purpose |
|---|---|---|---|
-| `InProcessApiClient` | apiproxy itself | the injected `{ fetch }` handler | **The isomorphic point**: `new InProcessApiClient(toFetchHandler(api))` never touches the network yet runs the real wire serialization/zod/SSE framing; carrier tests and callers can exercise the protocol without opening a port, while product `dsh run` drives core directly |
-| `WebApiClient` | dsh-client-connection | `globalThis.fetch` uplink + one same-origin WebSocket downlink per logical stream | the browser shape; physical boundary in the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) |
+| `InProcessApiClient` | apiproxy itself | the injected `{ fetch }` handler | **The isomorphic point**: `new InProcessApiClient(toFetchHandler(api))` never touches the network yet runs the real wire serialization/zod/SSE framing; carrier tests and callers can exercise the protocol without opening a port, while product `dsh --profile headless` drives core directly |
+| `WebApiClient` | dsh-client-connection | `globalThis.fetch` uplink + one same-origin WebSocket downlink per logical stream | the browser client; physical boundary in the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) |
| `FixtureApiClient` | dsh-client-connection | unused (protocol-layer override) | serverless UI development (`?fixture`): overrides the `callUnary`/`openMux`/`openHost`/`respond` virtuals and is itself the fake server (frame rpcIds minted by it, semantics self-consistent) |
| IPC bridge subclass (hypothetical example — no such shell exists) | an Electron shell | IPC serialization round trip | would swap only doFetch; contract and base class unchanged |
@@ -234,15 +234,15 @@ All four quadrant full forms pass through `onEnvelope`; the base implementation
## Consequences
-Every client shape consumes one contract: adding a unary method is a five-step mechanical change radiating from a single signature, swapping a carrier touches only a `doFetch` subclass, and every wire message is zod-validated, observable through the envelope tap, and reconcilable by rpcId. Ordinary unary calls remain bounded, while `host.pickDirectory` and `command.execute` may stay pending until the operation finishes or caller/connection cancellation arrives; this accepts that a non-cooperative user-paced operation can hang its request rather than treating valid operation duration as transport failure. The other accepted costs: two groups of packages need explicit tsconfig paths entries, and the reserved methods (fork/inject/task.list/listModels/hostInstanceId) stay dormant until a real consumer arrives.
+Every client consumes one contract: adding a unary method is a five-step mechanical change from a single signature, swapping a carrier touches only a `doFetch` subclass, and every wire message is zod-validated, observable through the envelope tap, and reconcilable by rpcId. Ordinary unary calls remain bounded, while `host.pickDirectory` and `command.execute` may stay pending until the operation finishes or caller/connection cancellation arrives; this accepts that a non-cooperative user-paced operation can hang its request rather than treating valid operation duration as transport failure. The other accepted costs: two groups of packages need explicit tsconfig paths entries, and the reserved methods (fork/inject/task.list/listModels/hostInstanceId) stay dormant until a real consumer arrives.
## Alternatives considered
| Rejected | One-line reason |
|---|---|
-| Packaging by "product shape" (a web family, an electron family) | What shapes share is host/client capability, not the shape itself; capability-provider layering means a new shape needs zero new packages |
+| Packaging by product (a web family, an electron family) | Products share host/client capabilities rather than an application implementation; capability-provider layering means a new application needs zero new packages |
| A package per mixture (e.g. a standalone headless package) | A mixture has exactly one consumer (its own app); packaging it is ownerless abstraction, while assembly in the app is readable and disposable |
-| Consuming clients connecting to ctx directly (skipping the apiproxy layer) | Client shapes require wire validation, observability, and multi-client consistency. Direct headless is a local front door with no client boundary and uses the public Agent/Session seams rather than a client command plane |
+| Consuming clients connecting to ctx directly (skipping the apiproxy layer) | Clients require wire validation, observability, and multi-client consistency. Direct headless is a local entry point with no client boundary and uses the public Agent/Session seams rather than a client command plane |
| webserver depending on runtime (saving the handler injection) | Structural-typing injection keeps webserver reusable by sidecars/tests with zero workspace deps; a package dependency would drag assembly knowledge into the carrier layer |
| Package names without the group prefix (continuing dsh-) | `dsh-runtime`/`dsh-web-ui` lose their belonging in the flat npm namespace; the cost is one explicit paths entry per package |
| Reusing the in-repo JSON-RPC 2.0 (dsh-jsonrpc) | Numeric error codes degrade to a single fallback code, contracts get aligned by hand in two copies, and naming drifts without a convention |
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md
index cb36b5cb72..e506c62427 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md
@@ -8,11 +8,11 @@ Status: implemented
## Problem
-需要提供 UI 对接层,除已有 ACP(Agent Client Protocol)/stdio 基线外,还需要 Web(server)、Electron 等其他产品 UI 形态。我们把这些形态统一称为 Client。希望具备以下能力:
-- 一个 `dsh` 进程同时支持 `dsh web`(启动)和 `dsh run`(headless),一个进程两种模式(设计预留)
-- 以与 `dsh web` 同构的 Web 技术形态,在 Electron 中启动
+需要提供 UI 对接层,除已有 ACP(Agent Client Protocol)/stdio 基线外,还需要 Web(server)、Electron 等其他产品客户端。我们把它们统一称为 Client。希望具备以下能力:
+- 一个 `dsh` 进程同时支持 `dsh web`(启动)和 `dsh --profile headless`(headless),一个进程两种模式(设计预留)
+- 在 Electron 中使用与 `dsh web` 相同的 Web 技术启动
-那么当前的工程代码需要稳定的分层职责模型,便于以后接入各类 client 形态。
+那么当前的工程代码需要稳定的分层职责模型,便于以后接入各类 client。
同时各消费端的物理通道不同(浏览器 HTTP/WebSocket、进程内 fetch/SSE、将来 IPC),还需要一个通道无关的消息模型和单一约定事实源,让「加一个方法」「换一种载体」互不牵连,且 wire 上的每条消息可类型校验、可观测、可对账。
@@ -25,21 +25,21 @@ Status: implemented
- 统一后端协议(fetch、HTTP、流式接口等)定义和支持,见本篇「消息协议」起各节
- `packages/client/*`:包只提供 Client 侧能力,每包单边不混。这里住三类包(两条轴归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有):
- **纯库**(`ui-slots`、`web-react`、`ui-primitives`,外加内核包 `loader`):普通根入口包,静态打包进壳;前三者播种进模块表。
- - **静态到达 entry 包**(`connection`、`runtime`、`ui-theme`、`i18n`、`hmr`):无 `dshClient` 键、无浏览器 bundle——壳把它们的 `src/client/` 半边打进自己的 bundle 并向 `ctx.modules` 登记;它们与其余单元一样,作为 host 独家撰写的图里的 entry 受治理。
- - **fetch 到达插件包**(`ui-layout`、`ui-sidebar`、`ui-conversation`、`ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dshClient` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布(tsdown 闭包工厂 bundle)。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。
-- `apps/` 作为对外导出的应用形态入口,可以由 Client / Host 混合组装。
- - `apps/web`(`dsh-frontend`)是 vite 应用:`dsh-client-web` 导出的壳表面之上的一层薄 `main.ts`。
- - `apps/cli`(`@deepseek-ai/dsh`)做形态分发:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist;`dsh run` = [直接使用核心 Agent/Session 的前门](2026-08-09-headless-direct-core-front-door.md),不含 Host、HTTP 或浏览器层。
- - 将来的 Electron 形态经由 IPC fetch 载体复用同一套 web client 包。
+ - **静态到达 entry 包**(`connection`、`runtime`、`ui-theme`、`i18n`、`hmr`):无 `dsh.client` 键、无浏览器 bundle——壳把它们的 `src/client/` 半边打进自己的 bundle 并向 `ctx.modules` 登记;它们与其余单元一样,作为 host 独家撰写的图里的 entry 受治理。
+ - **fetch 到达插件包**(`ui-layout`、`ui-sidebar`、`ui-conversation`、`ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dsh.client` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布(tsdown 闭包工厂 bundle)。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。
+- `apps/` 作为对外导出的应用入口,可以由 Client / Host 混合组装。
+ - `apps/web`(`dsh-frontend`)是 vite 应用:`dsh-client-web` 导出的壳 API 之上的一层薄 `main.ts`。
+ - `apps/cli`(`@deepseek-ai/dsh`)分发命令:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist;`dsh --profile headless` = [直接使用核心 Agent/Session 的入口](2026-08-09-headless-direct-core-entry-point.md),不含 Host、HTTP 或浏览器层。
+ - 将来的 Electron 应用经由 IPC fetch 载体复用同一套 web client 包。
```
-apps/* (application shapes: apps/web = vite app, apps/cli = bin dispatch)
+apps/* (applications: apps/web = vite app, apps/cli = bin dispatch)
│ consume
▼
packages/host/* packages/client/*
apiproxy front layer: protocol pure libs: ui-slots / web-react / ui-primitives
- runtime assembly / host entity dshClient plugins ×8 (node half = empty apply,
- webserver web-shape HTTP carriage client half = src/client/)
+ runtime assembly / host entity dsh.client plugins ×8 (node half = empty apply,
+ webserver Web HTTP carriage client half = src/client/)
│ ctx.plugin(...) ▲ import only apiproxy's /api /client subpaths
▼ │ (type-only + the client base class)
harness core packages ──────────────────┘ (types reach the browser via import type)
@@ -54,30 +54,30 @@ harness core packages ──────────────────┘
TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.json` = solution;`tsconfig.host.json` = host 侧 + 测试,排除 `packages/client`;`tsconfig.client.json` = client 各包及其测试):两侧在相同键(`sessions`、`loader`)下以不同服务合并 cordis `Context` 接口,单一 program 会同时看到两份声明合并而报冲突。共享叶子包(session/llm/tools/apiproxy 等)只构建一次,由两个 program 共同引用([拓扑](../process/2026-07-22-tsconfig-solution-root-two-aggregates.md))。
-协议侧:TS interface(`packages/host/apiproxy/src/api/`,零 Node 依赖,浏览器可 import);wire 消息统一为**双向模型**——每条逻辑消息由「谁发起 × request/response」定形(两轴四格,后文称四象限),与物理通道解耦;客户端统一继承 `AbstractApiClient`(协议不变量全在基类,平台差异只是 `doFetch` 传输切面)。
+协议侧:TS interface(`packages/host/apiproxy/src/api/`,零 Node 依赖,浏览器可 import);wire 消息统一为**双向模型**——每条逻辑消息按「谁发起 × request/response」分类(两轴四格,后文称四象限),与物理通道解耦;客户端统一继承 `AbstractApiClient`(协议不变量全在基类,平台差异只是 `doFetch` 传输切面)。
#### 分层角色
| 层 | 包 | 职责 | 关键纪律 |
|---|---|---|---|
| 前置层 | `dsh-host-apiproxy` | TS/zod 定义 (api/)+ fetch 抽象 (fetch/:handler + 客户端基类) | 做简单、所有接入方都要;Node/浏览器皆可 import;协议内容见下文「消息协议」起各节;client 不得经 ctx 绕开 api |
-| 装配层 | `dsh-host-runtime` | 插件组合 + ApiProxy 集成 + web UI 插件挂载(覆盖八个 dshClient 包的内存 Loader 树);host 级配置归属地(defaults/persistenceRoot,将来用户 profile) | 装什么插件、给什么默认值只在这里定;壳不得改装配 |
-| 承载层 | `dsh-host-webserver` | Web 形态 HTTP 与 upgrade:静态服务 + `/api/*`→handler 转发 + WebSocket upgrade route + close 语义;插件 bundle 端点 + `__DSH_BOOT__` manifest(元数据清单)注入(由 web 插件注册表供给) | Web(浏览器访问)专用;零 workspace 依赖(注册表经结构注入到达);Electron 不复用它 |
+| 装配层 | `dsh-host-runtime` | 插件组合 + ApiProxy 集成 + web UI 插件挂载(覆盖八个 dsh.client 包的内存 Loader 树);host 级配置归属地(defaults/persistenceRoot,将来用户 profile) | 装什么插件、给什么默认值只在这里定;壳不得改装配 |
+| 承载层 | `dsh-host-webserver` | Web HTTP 与 upgrade:静态服务 + `/api/*`→handler 转发 + WebSocket upgrade route + close 语义;插件 bundle 端点 + `__DSH_BOOT__` manifest(元数据清单)注入(由 web 插件注册表供给) | Web(浏览器访问)专用;零 workspace 依赖(注册表经结构注入到达);Electron 不复用它 |
| client 库 | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | slot 注册表核心 / ctx↔React 胶合 / 纯 React 原子组件 | 组件零 cordis 运行时依赖;由壳播种进 loader 模块表 |
| client 插件 | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | 浏览器侧 cordis 插件树(wire 消费者、核心服务、主题、i18n、布局、侧栏、对话、轨迹)——见 Web 客户端架构笔记 | 双入口(node 半边=空 apply;实现在 `src/client/`);消费面唯一经 ApiProxy |
-| 应用态 | `@deepseek-ai/dsh`(apps/cli)+ `dsh-frontend`(apps/web,vite 应用) | bin 粗分发 + 每形态一个拼装模块(web.ts / headless.ts);vite 应用是 `dsh-client-web` 壳表面之上的薄 main | 形态间动态 import 互不加载;dist 定位等 workspace 知识留在 app |
+| 应用 | `@deepseek-ai/dsh`(apps/cli)+ `dsh-frontend`(apps/web,vite 应用) | bin 粗分发 + 每个应用一个拼装模块(web.ts / headless.ts);vite 应用是 `dsh-client-web` 壳表面之上的薄 main | 各应用使用动态 import,因此不会互相加载;dist 定位等 workspace 知识留在 app |
#### 命名规则
`packages/host/*` 与 `packages/client/*` 下的包名**必须含目录组前缀**:host/runtime → `dsh-host-runtime`、client/runtime → `dsh-client-runtime`。目录名不重复组前缀(host/ 已表达)。因此包名尾段 ≠ 目录名,tsconfig.base.json 的 `dsh-*` 通配(按目录名解析)命不中——**这两组的每包需显式 paths 条目**,且 client 各包的 `/client` 子路径要单列条目,使源码级解析与 exports map 一致。
-#### 怎么接入一个新形态(操作清单)
+#### 怎么接入一个新应用(操作清单)
1. **选 fetch 伪造方式**:浏览器同源 HTTP / 进程内 `host.handler.fetch` 注入 / 自写传输切面子类(如将来 Electron IPC,见下文「子类表」)。
-2. **在 `apps/` 下写拼装模块**:`startHost()` + 客户端子类 + 该形态私有的信号/打印/退出语义;混合体不建包,拼装写在 app 里。
+2. **在 `apps/` 下写拼装模块**:`startHost()` + 客户端子类 + 该应用私有的信号/打印/退出语义;混合体不建包,拼装写在 app 里。
3. **需要 HTTP 承载才 import `dsh-host-webserver`**,否则零端口。
-现有两种形态保持这一边界:Web 形态挂载 Host、载体与浏览器组合,而 `dsh run` 挂载直接使用核心服务的 runner,不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(前门插件)` 挂载,不套 fetch。
+现有两个应用保持这一区分:Web 应用挂载 Host、载体与浏览器组合,而 `dsh --profile headless` 挂载直接使用核心服务的 runner,不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(入口插件)` 挂载,不使用 fetch。
## 消息协议
@@ -114,7 +114,7 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
### RpcReceipt:载体回执
-`ClientResponse` 的 HTTP 应答体是 `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }`——载体层回执,**不是** RpcMessage(response 不再有 response);迟到/重复应答收 `not-pending`,逻辑收敛面是 `*/resolved` 帧。
+`ClientResponse` 的 HTTP 应答体是 `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }`——载体层回执,**不是** RpcMessage(response 不再有 response);迟到/重复应答收 `not-pending`,逻辑收敛点是 `*/resolved` 帧。
## 类型体系:函数签名即事实源
@@ -213,8 +213,8 @@ export type ResponseValue =
| 子类 | 所在包 | doFetch | 用途 |
|---|---|---|---|
-| `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**:`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧;载体测试与调用方可以在不打开端口的情况下运行这套协议,而产品 `dsh run` 直接驱动 core |
-| `WebApiClient` | dsh-client-connection | `globalThis.fetch` 上行 + 每逻辑流一条同源 WebSocket 下行 | 浏览器形态;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.md) |
+| `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**:`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧;载体测试与调用方可以在不打开端口的情况下运行这套协议,而产品 `dsh --profile headless` 直接驱动 core |
+| `WebApiClient` | dsh-client-connection | `globalThis.fetch` 上行 + 每逻辑流一条同源 WebSocket 下行 | 浏览器客户端;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.md) |
| `FixtureApiClient` | dsh-client-connection | 不用(协议层覆写) | 无 server 的 UI 开发(`?fixture`):覆写 `callUnary`/`openMux`/`openHost`/`respond` 虚方法,自己就是假 server(帧 rpcId 由它 mint,语义自洽) |
| IPC 桥子类(假想示例——尚无此形态) | Electron 壳 | IPC 序列化往返 | 只需换 doFetch,约定/基类零改 |
@@ -232,15 +232,15 @@ export type ResponseValue =
## Consequences
-所有 client 形态消费同一约定:加一个 unary 方法是从单一签名辐射的五步机械改动,换载体只动一个 `doFetch` 子类,wire 上每条消息可 zod 校验、可经 envelope tap 观测、可按 rpcId 对账。普通 unary 调用仍受时限约束,而 `host.pickDirectory` 与 `command.execute` 可保持挂起,直到操作完成或调用方/连接取消到来;若由用户掌控节奏的操作不自行结束,请求可能一直挂起,这是为避免把合理的操作时长视为传输失败而接受的代价。其余接受的代价:两组包需要显式 tsconfig paths 条目;预留方法(fork/inject/task.list/listModels/hostInstanceId)在真实消费者出现前保持休眠。
+所有 client 使用同一约定:加一个 unary 方法是从单一签名出发的五步机械改动,换载体只动一个 `doFetch` 子类,wire 上每条消息可 zod 校验、可经 envelope tap 观测、可按 rpcId 对账。普通 unary 调用仍受时限约束,而 `host.pickDirectory` 与 `command.execute` 可保持挂起,直到操作完成或调用方/连接取消到来;若由用户掌控节奏的操作不自行结束,请求可能一直挂起,这是为避免把合理的操作时长视为传输失败而接受的代价。其余接受的代价:两组包需要显式 tsconfig paths 条目;预留方法(fork/inject/task.list/listModels/hostInstanceId)在真实消费者出现前保持休眠。
## Alternatives considered
| 放弃项 | 一句话理由 |
|---|---|
-| 按「产品形态」分包(web 一族、electron 一族) | 形态间共享的是 host/client 两侧能力而非形态本身;能力支持方分层让新形态零新包 |
+| 按产品分包(web 一族、electron 一族) | 产品共享的是 host/client 两侧能力,而不是某个应用实现;能力支持方分层让新应用零新包 |
| 混合体建包(如 headless 独立包) | 混合体只有一个消费者(它自己的 app),建包是无主抽象;拼装写在 app 里可读可弃 |
-| 消费型 client 直连 ctx(省 apiproxy 一层) | client 形态需要 wire 校验、观测与多 client 一致性。直接 headless 是没有 client 边界的本地前门,使用公开的 Agent/Session seam,而不是 client 命令面 |
+| 消费型 client 直连 ctx(省 apiproxy 一层) | client 需要 wire 校验、观测与多 client 一致性。直接 headless 是没有 client 边界的本地入口,使用公开的 Agent/Session seam,而不是 client 命令面 |
| webserver 依赖 runtime(省 handler 注入) | 结构 typing 注入让 webserver 可被 sidecar/测试复用且零 workspace 依赖;包依赖会把装配知识拖进承载层 |
| 包名不带组前缀(沿用 dsh-<尾段>) | `dsh-runtime`/`dsh-web-ui` 在扁平 npm 命名空间里失去归属信息;代价只是每包一条显式 paths |
| 复用仓内 JSON-RPC 2.0(dsh-jsonrpc) | 数字错误码退化成单码兜底、约定双份人肉对齐、命名无 convention 自然漂移 |
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml
index 269933c1a7..8714bf9cf5 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-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 .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
-2026-07-19-gui-web-client-architecture.md: 82b2f85708c423748954644d4991e2d54d42874a
-2026-07-19-gui-web-client-architecture.zh.md: c37252d1db291cae11db2a615c9e4005ece717da
+2026-07-19-gui-web-client-architecture.md: 070b857f14007f429826ab83b17b5c8fbd3d3d0b
+2026-07-19-gui-web-client-architecture.zh.md: 1f5bafe1dff878b5ca5ffcbdb9ed8ca38a863c9f
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
index 82b2f85708..070b857f14 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
@@ -30,7 +30,7 @@ Both ends run cordis. The host is a cordis plugin tree; the browser runs a secon
## The client cordis tree and the loading chain
-The loading chain — the two package kinds (plain vs dshClient plugin), the module-system/plugin-governor split, the two-phase boot over the host-authored entry graph with revisions, and hot reload — is owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md). The load-bearing facts for this document: the browser boots the same vendored `@cordisjs/plugin-loader` as the host with a client module system (`ctx.modules`, `packages/client/modules`) filling its `internal` contract; every unit with product behavior is an entry in the host-authored `__DSH_BOOT__` graph — every production plugin package (infrastructure included) carries the `dshClient` declaration and arrives as a fetched `./client` tsdown closure bundle, `immediately` rows differing only in boot phase-one prefetch, while plain packages (react family, cordis, the not-yet-promoted libraries) stay shell-bundled, seeded, and invisible to the graph; bundles execute `window.__ModuleLoader__.load({ id, factory })` and their `require` is answered from the lazy CJS module table (seed words + registered factories, materialized and memoized on first require — cross-plugin value imports are a build error, cooperation goes through cordis services); plugin CSS is inlined in the bundle and injected as `
-
\ No newline at end of file
+
diff --git a/apps/web/tests/agent-preset-authoring.e2e.ts b/apps/web/tests/agent-preset-authoring.e2e.ts
new file mode 100644
index 0000000000..1a27f96c6e
--- /dev/null
+++ b/apps/web/tests/agent-preset-authoring.e2e.ts
@@ -0,0 +1,280 @@
+// Web e2e scenario: the agent-preset settings section as copy-only authoring.
+// The browser never edits composition text — a shipped preset opens in a
+// read-only viewer, the copy dialog collects an id and an optional display
+// name, and the host copies the whole directory. The section's other job is
+// getting the user TO the files: this lane pins `nativeOpen: false` (see the
+// overlay), so the location affordance answers the preset directory as text —
+// the deterministic branch a golden can hold on every platform.
+//
+// Zero model calls: no replay fixture mounts, so a stray stream fails loud.
+import { existsSync } from 'node:fs'
+import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { fileURLToPath } from 'node:url'
+import { join } from 'node:path'
+import type { Browser, Page } from 'playwright'
+import { chromium } from 'playwright'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import type { Locator } from 'playwright'
+import {
+ captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole,
+ webSnapshotMode, type WebScaffold,
+} from './scaffold.ts'
+import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
+
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/agent-preset-authoring', import.meta.url))
+const SECTION_EXPECTED = join(SNAPSHOT_DIR, 'section.expected.md')
+const COPY_DIALOG_EXPECTED = join(SNAPSHOT_DIR, 'copy-dialog.expected.md')
+const CREATED_EXPECTED = join(SNAPSHOT_DIR, 'created.expected.md')
+const DAMAGED_EXPECTED = join(SNAPSHOT_DIR, 'damaged.expected.md')
+/** The shipped roster, beside the composition that names it. */
+const SHIPPED_PRESETS = fileURLToPath(new URL('../../cli/config/agent-presets', import.meta.url))
+const OVERLAY = fileURLToPath(new URL('./agent-preset-authoring.overlay.yml', import.meta.url))
+const MODE = webSnapshotMode()
+
+describe('web e2e: agent-preset authoring is a host-side copy', () => {
+ let scaffold: WebScaffold
+ let browser: Browser
+ let page: Page
+ let tripwire: ReturnType
+ let userRoot: string
+
+ /** The settings dialog, opened on the Agent-presets section. */
+ function settingsDialog(): Locator {
+ return page.getByRole('dialog', { name: '设置' })
+ }
+
+ /** Tokenize the lane-owned preset root after general aria normalization. */
+ function withPresetRoot(snapshot: string): string {
+ const rootSuffix = `/${userRoot.split('/').pop()!}`
+ return snapshot.split('\n').map((line) => {
+ const rootStart = line.indexOf(rootSuffix)
+ if (rootStart === -1) return line
+ const pathStart = line.lastIndexOf(' ', rootStart) + 1
+ return `${line.slice(0, pathStart)}{{presetRoot}}${line.slice(rootStart + rootSuffix.length)}`
+ }).join('\n')
+ }
+
+ beforeAll(async () => {
+ userRoot = await realpath(await mkdtemp(join(tmpdir(), 'dsh-web-e2e-presets-')))
+ scaffold = await launchWebScaffold({
+ extraOverlayPath: OVERLAY,
+ agentPresets: {
+ roots: [
+ { path: SHIPPED_PRESETS, trust: 'system' },
+ { path: userRoot, trust: 'user' },
+ ],
+ default: 'standard',
+ },
+ })
+ browser = await chromium.launch()
+ // The scenario asserts the shipped Chinese copy, so the browser asks for it.
+ page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
+ tripwire = watchConsole(page)
+ await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ }, 120_000)
+
+ afterAll(async () => {
+ await browser?.close()
+ await scaffold?.close()
+ })
+
+ it('offers the roster with copy as the only way to create', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-preset-authoring-section'))
+ await page.getByRole('button', { name: '设置', exact: true }).click()
+ const dialog = settingsDialog()
+ await dialog.waitFor({ timeout: 10_000 })
+ await dialog.getByRole('button', { name: 'Agent 预设' }).click()
+ await dialog.getByRole('heading', { name: 'Agent 预设' }).waitFor({ timeout: 10_000 })
+ await dialog.getByText('标准模式').first().waitFor({ timeout: 10_000 })
+
+ const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
+
+ await compareOrRefreshGolden(SECTION_EXPECTED, snapshot, MODE)
+ // The intro carries the guidance a create button used to imply, and the
+ // shipped rows offer view/copy but never delete or a location — their
+ // install is overwritten by upgrades and is not the user's to manage.
+ expect(snapshot).toContain('或用「创造模式」让 Agent 帮你创建')
+ expect(snapshot).not.toContain('新建预设')
+ expect(snapshot).toContain('查看: 标准模式')
+ expect(snapshot).not.toContain('删除: 标准模式')
+ expect(snapshot).not.toContain('打开目录')
+ }, 60_000)
+
+ it('views a shipped composition read-only instead of editing it', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-preset-authoring-view'))
+ const dialog = settingsDialog()
+ await dialog.getByRole('button', { name: '查看: 标准模式' }).click()
+ const viewer = page.getByRole('dialog', { name: '查看 · 标准模式' })
+ await viewer.waitFor({ timeout: 10_000 })
+
+ // The real shipped composition, not a golden: the viewer shows whatever
+ // the deployment ships, and this lane only asserts it is shown read-only.
+ const shipped = await readFile(join(SHIPPED_PRESETS, 'standard', 'agent.cordis.yml'), 'utf8')
+ expect(await viewer.locator('pre').textContent()).toBe(shipped)
+ expect(await viewer.getByRole('textbox').count()).toBe(0)
+ // The header X and the footer button share the 关闭 name; the footer one
+ // is last in the dialog.
+ await viewer.getByRole('button', { name: '关闭' }).last().click()
+ await viewer.waitFor({ state: 'detached', timeout: 10_000 })
+ }, 60_000)
+
+ it('copies 极简模式 whole under a new id and lands in its files', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-preset-authoring-copy'))
+ const dialog = settingsDialog()
+ await dialog.getByRole('button', { name: '复制: 极简模式' }).click()
+ const copyDialog = page.getByRole('dialog', { name: '复制预设 · 复制自 极简模式' })
+ await copyDialog.waitFor({ timeout: 10_000 })
+
+ const dialogSnapshot = await captureStableAria(
+ page, '[role="dialog"][aria-label^="复制预设"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(COPY_DIALOG_EXPECTED, dialogSnapshot, MODE)
+ // Two fields and nothing else: the id is the directory name the host
+ // needs up front; description and composition live in the files.
+ expect(dialogSnapshot).toContain('标识符')
+ expect(dialogSnapshot).not.toContain('描述')
+
+ await copyDialog.getByPlaceholder('my-agent').fill('my-agent')
+ await copyDialog.getByPlaceholder('选择器中显示的名字,缺省用标识符').fill('我的模式')
+ await copyDialog.getByRole('button', { name: '创建' }).click()
+ await copyDialog.waitFor({ state: 'detached', timeout: 10_000 })
+
+ // The new row lands in the custom group, and — with no desktop opener —
+ // its directory is revealed as text right away: landing in the files is
+ // the completion of a copy, not a follow-up.
+ await dialog.getByText('我的模式').first().waitFor({ timeout: 10_000 })
+ await dialog.getByText('预设文件:').waitFor({ timeout: 10_000 })
+ // The copy dialog is detached, so the settings dialog is the only one
+ // left (it names itself via aria-labelledby, which a CSS attribute
+ // selector cannot address).
+ const snapshot = withPresetRoot(
+ await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd))
+ await compareOrRefreshGolden(CREATED_EXPECTED, snapshot, MODE)
+ expect(snapshot).toContain('{{presetRoot}}/my-agent')
+
+ // The host copied the whole directory and rewrote only the display
+ // metadata: the composition is byte-identical to the shipped source, the
+ // description rides along for the user to edit in place, and neither the
+ // source's name nor its roster order survives into the copy.
+ const composition = await readFile(join(userRoot, 'my-agent', 'agent.cordis.yml'), 'utf8')
+ expect(composition).toBe(await readFile(join(SHIPPED_PRESETS, 'minimal', 'agent.cordis.yml'), 'utf8'))
+ const metadata = await readFile(join(userRoot, 'my-agent', 'preset.yml'), 'utf8')
+ expect(metadata).toContain('name: 我的模式')
+ expect(metadata).toContain('description: 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。')
+ expect(metadata).not.toContain('order:')
+ }, 60_000)
+
+ it('deletes the copy after confirmation and reclaims the roster', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-preset-authoring-delete'))
+ const dialog = settingsDialog()
+ await dialog.getByRole('button', { name: '删除: 我的模式' }).click()
+ const confirm = page.getByRole('dialog', { name: '删除该预设?' })
+ await confirm.waitFor({ timeout: 10_000 })
+ await confirm.getByRole('button', { name: '删除', exact: true }).click()
+ await confirm.waitFor({ state: 'detached', timeout: 10_000 })
+
+ await expect.poll(async () => dialog.getByText('我的模式').count(), { timeout: 10_000 }).toBe(0)
+ expect(existsSync(join(userRoot, 'my-agent'))).toBe(false)
+ // The custom group outlives its only member: the heading stays with the
+ // creator entry so the place to author a preset never disappears.
+ expect(await dialog.getByRole('heading', { name: '自定义' }).count()).toBe(1)
+ expect(await dialog.getByRole('button', { name: '用「创造模式」创作自定义预设' }).count()).toBe(1)
+ expect(await dialog.getByText('标准模式').count()).toBeGreaterThan(0)
+ }, 60_000)
+
+ it('marks damaged presets broken and clears a ghost through delete', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-preset-authoring-damaged'))
+ // The two hand-edit damage shapes: a composition that no longer parses,
+ // and a directory whose composition file was deleted outright.
+ await mkdir(join(userRoot, 'broken-yaml'), { recursive: true })
+ await writeFile(join(userRoot, 'broken-yaml', 'agent.cordis.yml'), '- id: x\n name: [unclosed\n')
+ await mkdir(join(userRoot, 'ghost'), { recursive: true })
+ await writeFile(join(userRoot, 'ghost', 'preset.yml'), 'name: 幽灵预设\ndescription: composition 已被手动删除。\n')
+
+ // The section reads the roster when it mounts; hop away and back.
+ const dialog = settingsDialog()
+ await dialog.getByRole('button', { name: '通用设置' }).click()
+ await dialog.getByRole('button', { name: 'Agent 预设' }).click()
+ await dialog.getByText('加载失败').first().waitFor({ timeout: 10_000 })
+
+ const snapshot = withPresetRoot(
+ await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd))
+ await compareOrRefreshGolden(DAMAGED_EXPECTED, snapshot, MODE)
+ // Both damage shapes surface as marked, unselectable, uncopyable cards
+ // that still carry their metadata and the discovery-reported reason.
+ expect(snapshot).toContain('加载失败: broken-yaml')
+ expect(snapshot).toContain('加载失败: 幽灵预设')
+ expect(snapshot).toContain('not valid YAML')
+ expect(snapshot).toContain('agent.cordis.yml is missing')
+ expect(await dialog.getByRole('button', { name: '加载失败: broken-yaml' }).isDisabled()).toBe(true)
+ expect(await dialog.getByRole('button', { name: '复制: 幽灵预设' }).isDisabled()).toBe(true)
+ // A broken card offers no "set default" affordance at all — the aria name
+ // IS the broken marking, so the picking name must not exist.
+ expect(await dialog.getByRole('button', { name: '设为默认: broken-yaml' }).count()).toBe(0)
+
+ // The ghost's way out is the card's own delete — and the id it blocked
+ // is claimable again immediately afterwards.
+ await dialog.getByRole('button', { name: '删除: 幽灵预设' }).click()
+ const confirm = page.getByRole('dialog', { name: '删除该预设?' })
+ await confirm.waitFor({ timeout: 10_000 })
+ await confirm.getByRole('button', { name: '删除', exact: true }).click()
+ await confirm.waitFor({ state: 'detached', timeout: 10_000 })
+ await expect.poll(async () => dialog.getByText('幽灵预设').count(), { timeout: 10_000 }).toBe(0)
+ expect(existsSync(join(userRoot, 'ghost'))).toBe(false)
+
+ await dialog.getByRole('button', { name: '复制: 极简模式' }).click()
+ const copyDialog = page.getByRole('dialog', { name: '复制预设 · 复制自 极简模式' })
+ await copyDialog.waitFor({ timeout: 10_000 })
+ await copyDialog.getByPlaceholder('my-agent').fill('ghost')
+ await copyDialog.getByRole('button', { name: '创建' }).click()
+ await copyDialog.waitFor({ state: 'detached', timeout: 10_000 })
+ await dialog.getByRole('button', { name: '设为默认: ghost' }).waitFor({ timeout: 10_000 })
+
+ // Leave the roster as the earlier tests shaped it.
+ await dialog.getByRole('button', { name: '删除: ghost' }).click()
+ const cleanup = page.getByRole('dialog', { name: '删除该预设?' })
+ await cleanup.waitFor({ timeout: 10_000 })
+ await cleanup.getByRole('button', { name: '删除', exact: true }).click()
+ await cleanup.waitFor({ state: 'detached', timeout: 10_000 })
+ await rm(join(userRoot, 'broken-yaml'), { recursive: true, force: true })
+ }, 60_000)
+
+ it('starts a creator-mode session from the section', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-preset-authoring-creator'))
+ // Without a workspace the flow only stages (there is no session to land
+ // in until one is connected); connect first so the gesture carries all
+ // the way to a composed host session.
+ await settingsDialog().getByRole('button', { name: '关闭' }).last().click()
+ await connectFreshWorkspaceZh(page, scaffold.workspaceCwd)
+ await page.getByRole('button', { name: '设置', exact: true }).click()
+ const dialog = settingsDialog()
+ await dialog.waitFor({ timeout: 10_000 })
+ await dialog.getByRole('button', { name: 'Agent 预设' }).click()
+ await dialog.getByRole('button', { name: '用「创造模式」创作自定义预设' }).click()
+
+ // Leaving settings is part of the gesture: the flow lands on the
+ // new-session screen with the self-referential preset staged, and the
+ // blank session the flow produces composes from it on the host.
+ await dialog.waitFor({ state: 'detached', timeout: 10_000 })
+ await page.getByRole('button', { name: '创造模式' }).waitFor({ timeout: 10_000 })
+ await expect.poll(async () => {
+ const response = await fetch(`${scaffold.baseUrl}/api/session.list`, {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({
+ type: 'client-request', rpcId: 'creator-draft-stage', method: 'session.list', payload: {},
+ }),
+ })
+ const body = await response.json() as {
+ result: { value?: { sessions: unknown[] } }
+ }
+ return JSON.stringify(body.result.value?.sessions ?? body.result)
+ }, { timeout: 15_000 }).toContain('"agentPreset":"cordis"')
+ }, 60_000)
+
+ it('drove every surface without a page error or a stream warning', () => {
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ })
+})
diff --git a/apps/web/tests/agent-preset-authoring.overlay.yml b/apps/web/tests/agent-preset-authoring.overlay.yml
new file mode 100644
index 0000000000..6644752bc2
--- /dev/null
+++ b/apps/web/tests/agent-preset-authoring.overlay.yml
@@ -0,0 +1,12 @@
+# The authoring lane drives the location affordance. A real desktop open
+# would pop a file manager on the machine running the tests and the
+# capability itself is platform-detected (macOS yes, headless Linux CI no),
+# so the gateway is pinned headless: `hasDocument` is false everywhere and
+# `openDocument` answers the directory as text — the same branch on every
+# host, and the one whose rendering a golden can hold. A patch replaces the
+# row's complete config, so the shipped routing defaults ride along.
+- id: api-gateway
+ config:
+ provider: deepseek-official
+ model: deepseek-v4-flash
+ nativeOpen: false
diff --git a/apps/web/tests/agent-preset-selection.e2e.ts b/apps/web/tests/agent-preset-selection.e2e.ts
new file mode 100644
index 0000000000..4a7f6eb819
--- /dev/null
+++ b/apps/web/tests/agent-preset-selection.e2e.ts
@@ -0,0 +1,295 @@
+// Web e2e scenario: agent-preset selection. The roster's `roots` is an
+// assembly fact the CLI entry resolves and patches in, so every other lane
+// boots with an empty roster and no preset surface at all; this is the one
+// lane that mounts the SHIPPED presets and puts them in front of a browser.
+//
+// Two surfaces, one host rule: a session's composition is fixed when the
+// session starts. Before that, the new-session chip stages the choice beside
+// the workspace picker — the only screen where it still works. After it, the
+// session header names what the session runs and offers no control at all,
+// because the host answers `agent-preset-locked` to anything else.
+//
+// Zero model calls: no replay fixture mounts, so a stray stream fails loud.
+import { fileURLToPath } from 'node:url'
+import { mkdir, writeFile } from 'node:fs/promises'
+import { join } from 'node:path'
+import type { Browser, Page } from 'playwright'
+import { chromium } from 'playwright'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import {
+ SESSION_FORMAT_VERSION, SessionId as sessionId, type SessionEvent, type SessionId,
+} from '@deepseek-ai/dsh-session'
+import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
+import {
+ captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole,
+ webSnapshotMode, type WebScaffold,
+} from './scaffold.ts'
+import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
+
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/agent-preset-selection', import.meta.url))
+const HERO_EXPECTED = join(SNAPSHOT_DIR, 'hero.expected.md')
+const MENU_EXPECTED = join(SNAPSHOT_DIR, 'menu.expected.md')
+const HEADER_EXPECTED = join(SNAPSHOT_DIR, 'header.expected.md')
+/** The shipped roster, beside the composition that names it. */
+const SHIPPED_PRESETS = fileURLToPath(new URL('../../cli/config/agent-presets', import.meta.url))
+const MODE = webSnapshotMode()
+const SEED_ID = 'agent-preset-selection-web-e2e'
+/** A project skill only a preset that mounts `skill-local` can discover. */
+const SKILL_NAME = 'preset-catalog-demo'
+
+/**
+ * Seed one project skill under the connected workspace.
+ *
+ * Local skill discovery is a PRESET row, so this file is visible through
+ * `standard` and invisible through `minimal` — which makes the '/' menu's
+ * skill group a statement about the session's composition.
+ * @param workspaceCwd - the scaffold's temp project parent.
+ */
+async function seedWorkspaceSkill(workspaceCwd: string): Promise {
+ const directory = join(workspaceCwd, 'workspace', '.agents', 'skills', SKILL_NAME)
+ await mkdir(directory, { recursive: true })
+ await writeFile(join(directory, 'SKILL.md'), [
+ '---',
+ `name: ${SKILL_NAME}`,
+ 'description: Prove the slash catalog follows the session composition',
+ '---',
+ '',
+ 'Body.',
+ '',
+ ].join('\n'))
+}
+
+/**
+ * A settled one-turn session with no model content: this lane asserts chrome
+ * around a conversation, not a conversation, and a recorded turn would tie
+ * the golden to a provider's wording for no gain.
+ * @returns a tokenized session log ending on a closed turn.
+ */
+function seedLog(): string {
+ const time = 1784974100000
+ const at = (index: number, event: Record): string =>
+ JSON.stringify({ ...event, seq: index, time: time + index })
+ return [
+ JSON.stringify({ type: 'session', version: 0, id: '{{sessionId}}', createdAt: time, cwd: '{{cwd}}/workspace' }),
+ at(0, { type: 'turn/start', data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user', rpcId: 'seed' } } } }),
+ at(1, {
+ type: 'user/message',
+ data: { content: [{ type: 'text', text: 'Seeded turn.' }], source: { kind: 'user', rpcId: 'seed' } },
+ surfaceOp: 'append',
+ }),
+ at(2, { type: 'session/title', data: { title: 'Seeded turn', messageSeqs: [1], source: { kind: 'fallback' } } }),
+ at(3, { type: 'turn/end', data: { turn: 1, reason: { kind: 'completed' } } }),
+ ].join('\n')
+}
+
+/**
+ * Persist one child so the assembled header snapshot exercises both action
+ * contributors whose relative order is the product contract under test.
+ * @param scaffold - the booted Web scaffold.
+ * @param parentId - the seeded session whose header the browser opens.
+ */
+async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise {
+ const childId = sessionId('agent-preset-selection-child')
+ const createdAt = 1784974100100
+ await scaffold.ctx.sessionPersistence.create({
+ version: SESSION_FORMAT_VERSION,
+ id: childId,
+ createdAt,
+ cwd: scaffold.workspaceCwd,
+ parentSession: parentId,
+ origin: 'subagent',
+ delegationDepth: 1,
+ agentPreset: 'minimal',
+ })
+ await scaffold.ctx.sessionPersistence.append(childId, [
+ {
+ type: 'turn/start',
+ seq: 0,
+ time: createdAt,
+ data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
+ },
+ {
+ type: 'user/message',
+ seq: 1,
+ time: createdAt + 1,
+ data: {
+ content: [{ type: 'text', text: 'Check the session-header action order.' }],
+ source: { kind: 'user' },
+ },
+ surfaceOp: 'append',
+ },
+ {
+ type: 'subagent/descriptor',
+ seq: 2,
+ time: createdAt + 2,
+ data: snapshotSubagentDescriptor({
+ mode: 'one-shot', provider: 'spawn', label: 'header order probe',
+ }),
+ },
+ {
+ type: 'turn/end',
+ seq: 3,
+ time: createdAt + 3,
+ data: { turn: 1, reason: { kind: 'completed' } },
+ },
+ ] as SessionEvent[])
+ await scaffold.ctx.sessionProjectionCache.coldSnapshot(childId)
+}
+
+/**
+ * The preset the host reports for the blank session the workspace connect
+ * produced. Addressed by id rather than by scanning the serialized list: the
+ * seeded session records `minimal` too, so a substring match over the whole
+ * list answers before the switch has landed.
+ * @param baseUrl - the scaffold's origin.
+ * @returns the live session's preset, or undefined before it is listed.
+ */
+async function livePreset(baseUrl: string): Promise {
+ const response = await fetch(`${baseUrl}/api/session.list`, {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({
+ type: 'client-request', rpcId: 'agent-preset-live', method: 'session.list', payload: {},
+ }),
+ })
+ const body = await response.json() as {
+ result: { value?: { items: { sessionId: string; agentPreset?: string }[] } }
+ }
+ return body.result.value?.items.find(item => item.sessionId !== SEED_ID)?.agentPreset
+}
+
+/** Every option label the trigger menu currently lists. */
+async function menuOptions(page: Page): Promise {
+ const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
+ await menu.waitFor({ timeout: 10_000 })
+ return await menu.getByRole('option').allTextContents()
+}
+
+describe('web e2e: agent-preset selection', () => {
+ let scaffold: WebScaffold
+ let browser: Browser
+ let page: Page
+ let tripwire: ReturnType
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold({
+ agentPresets: { roots: [{ path: SHIPPED_PRESETS, trust: 'system' }], default: 'standard' },
+ })
+ // A resumed session runs what it was created with; seeding one that
+ // records `minimal` is what makes the header label a claim about the
+ // session rather than an echo of the current default.
+ const seededId = await seedSession(scaffold, seedLog(), SEED_ID, 'minimal')
+ await seedSubagent(scaffold, seededId)
+ await seedWorkspaceSkill(scaffold.workspaceCwd)
+ browser = await chromium.launch()
+ page = await newEnglishPage(browser)
+ tripwire = watchConsole(page)
+ await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ }, 120_000)
+
+ afterAll(async () => {
+ await browser?.close()
+ await scaffold?.close()
+ })
+
+ it('offers the chip on the new-session screen, beside the workspace picker', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-hero'))
+ await connectFreshWorkspace(page, scaffold.workspaceCwd)
+
+ const snapshot = await captureStableAria(page, '[class*="heroWorkspaceRow"]', scaffold.workspaceCwd)
+
+ await compareOrRefreshGolden(HERO_EXPECTED, snapshot, MODE)
+ // The chip opens on the deployment default, by the name that preset
+ // publishes rather than its directory name.
+ expect(snapshot).toContain('Standard mode')
+ })
+
+ it('names every preset and what it is for', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-menu'))
+ await page.getByRole('button', { name: 'Standard mode' }).click()
+ const menu = page.getByRole('menu')
+ await menu.waitFor({ timeout: 10_000 })
+
+ const snapshot = await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd)
+
+ await compareOrRefreshGolden(MENU_EXPECTED, snapshot, MODE)
+ // Every shipped preset, each with the sentence saying what it composes —
+ // the id alone never said what a preset does.
+ expect(snapshot).toContain('Minimal mode')
+ expect(snapshot).toContain('Creator mode')
+ await page.keyboard.press('Escape')
+ })
+
+ it('applies the staged pick to the blank session, and the host honors it', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-stage'))
+ await page.getByRole('button', { name: 'Standard mode' }).click()
+ await page.getByRole('menuitem', { name: /Minimal mode/ }).click()
+
+ // The chip stages; the blank session the workspace connect produced is
+ // what the stage lands on. The host's own answer is what comes back.
+ await expect.poll(() => livePreset(scaffold.baseUrl), { timeout: 15_000 }).toBe('minimal')
+ })
+
+ it('re-reads the slash catalog through the composition the switch installed', async () => {
+ // Continues the previous case: the chip has already applied `minimal` to
+ // the blank session, and this one reads the menu that switch left behind.
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-slash-catalog'))
+ const composer = page.locator('textarea:enabled').last()
+
+ // `minimal` mounts neither the compaction group nor plan mode nor local
+ // skill discovery, so the catalog the composer warmed under the
+ // deployment default must not survive the switch.
+ await composer.fill('/')
+ await expect.poll(() => menuOptions(page), { timeout: 15_000 })
+ .not.toEqual(expect.arrayContaining([expect.stringContaining(SKILL_NAME)]))
+ const onMinimal = await menuOptions(page)
+ expect(onMinimal.some(option => option.startsWith('compact'))).toBe(false)
+ expect(onMinimal.some(option => option.startsWith('plan'))).toBe(false)
+ // The host-plane commands and the client's own contribution are the
+ // floor: they belong to no preset and never move.
+ expect(onMinimal.some(option => option.startsWith('goal'))).toBe(true)
+ expect(onMinimal.some(option => option.startsWith('model'))).toBe(true)
+ await composer.fill('')
+
+ // Switching back up reaches the host at all — the chip compares the pick
+ // against its list row, so a row that never reprojected the first switch
+ // answers "already standard" and sends nothing — and restores the catalog
+ // instead of leaving the session reading the narrower composition.
+ await page.getByRole('button', { name: 'Minimal mode' }).click()
+ await page.getByRole('menuitem', { name: /^Standard mode/ }).first().click()
+ await expect.poll(() => livePreset(scaffold.baseUrl), { timeout: 15_000 }).toBe('standard')
+
+ await composer.fill('/')
+ await expect.poll(() => menuOptions(page), { timeout: 15_000 })
+ .toEqual(expect.arrayContaining([expect.stringContaining(SKILL_NAME)]))
+ const onStandard = await menuOptions(page)
+ expect(onStandard.some(option => option.startsWith('compact'))).toBe(true)
+ expect(onStandard.some(option => option.startsWith('plan'))).toBe(true)
+ await composer.fill('')
+ }, 90_000)
+
+ it('labels a resumed session with the preset it was created under', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-header'))
+ // The seeded session's cwd is the scaffold root rather than the connected
+ // workspace, so it lists under Ungrouped; the group collapses by default.
+ await page.getByRole('treeitem', { name: /^Ungrouped/ }).click()
+ await page.locator('[role="treeitem"]').last().click()
+ await page.getByText('Seeded turn.').waitFor({ timeout: 15_000 })
+
+ const snapshot = await captureStableAria(page, '[class*="titleRow"]', scaffold.workspaceCwd)
+
+ await compareOrRefreshGolden(HEADER_EXPECTED, snapshot, MODE)
+ expect(snapshot).toContain('Minimal mode')
+ expect(snapshot).toContain('button "1 subagent"')
+ expect(snapshot.indexOf('Minimal mode')).toBeLessThan(snapshot.indexOf('button "1 subagent"'))
+ // Static chrome, not a control: the header can only report a composition
+ // the host would refuse to change.
+ expect(snapshot).not.toContain('button "Minimal mode"')
+ })
+
+ it('drove every surface without a page error or a stream warning', () => {
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ })
+})
diff --git a/apps/web/tests/approval-composer.e2e.ts b/apps/web/tests/approval-composer.e2e.ts
index 9911742633..e57367ee46 100644
--- a/apps/web/tests/approval-composer.e2e.ts
+++ b/apps/web/tests/approval-composer.e2e.ts
@@ -35,9 +35,9 @@ const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
const MODE = webSnapshotMode()
// Irreducible payload: the command has to be long enough to pass the card's
-// height cap, which is the only shape that reproduces an action row pushed off
-// screen. Unrelated tokens, not a repeated word — the model compresses a
-// repeated word into `printf 'alpha %.0s' {1..400}` when recording, and a
+// height cap, which is the only command length that reproduces an action row pushed off
+// screen. Unrelated tokens, not a repeated word — a repeated word is what the
+// model compressed into `printf 'alpha %.0s' {1..400}` while recording, and a
// short command proves nothing here. The formula keeps the source small; the
// model receives the expanded literal it has to put in the command.
const TOKENS = Array.from({ length: 220 }, (_, index) => `tok${((index + 1) * 7919 % 99991).toString(36)}`).join(' ')
diff --git a/apps/web/tests/assembled-boot.ts b/apps/web/tests/assembled-boot.ts
index 631196c652..eb22d8319c 100644
--- a/apps/web/tests/assembled-boot.ts
+++ b/apps/web/tests/assembled-boot.ts
@@ -27,6 +27,7 @@ const PLUGINS: readonly (WebBootEntry & { bundlePath: string })[] = [
{ id: '@deepseek-ai/dsh-client-ui-sidebar', bundlePath: 'packages/client/ui-sidebar/lib/client.js', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
{ id: '@deepseek-ai/dsh-client-ui-conversation', bundlePath: 'packages/client/ui-conversation/lib/client.js', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
{ id: '@deepseek-ai/dsh-client-ui-tool', bundlePath: 'packages/client/ui-tool/lib/client.js', url: '/plugins/ui-tool.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-locale', '@deepseek-ai/dsh-client-ui-conversation'] },
+ { id: '@deepseek-ai/dsh-client-ui-workflow-run', bundlePath: 'packages/client/ui-workflow-run/lib/client.js', url: '/plugins/ui-workflow-run.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-locale', '@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-conversation'] },
{
id: '@deepseek-ai/dsh-client-ui-workspace',
bundlePath: 'packages/client/ui-workspace/lib/client.js',
@@ -70,7 +71,12 @@ let unmount: (() => void) | undefined
export function installAssembledBootEnv(): void {
beforeEach(() => {
localStorage.clear()
- localStorage.setItem('dsh.locale', 'en')
+ // The locale service derives its provisional locale from the browser and
+ // takes an explicit choice only from Host settings, which this lane's
+ // fixture transport does not serve; pinning the navigator is what selects
+ // English here.
+ Object.defineProperty(navigator, 'languages', { value: ['en-US'], configurable: true })
+ Object.defineProperty(navigator, 'language', { value: 'en-US', configurable: true })
document.title = 'DeepSeek Harness'
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
@@ -88,6 +94,11 @@ export function installAssembledBootEnv(): void {
document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
document.title = ''
history.replaceState(null, '', '/')
+ // Deleting the own properties uncovers jsdom's own accessors again
+ // (Navigator declares both readonly, hence the erased receiver).
+ const ownNavigator = navigator as unknown as Record
+ delete ownNavigator.languages
+ delete ownNavigator.language
vi.unstubAllGlobals()
})
}
diff --git a/apps/web/tests/background-task-list.e2e.ts b/apps/web/tests/background-task-list.e2e.ts
new file mode 100644
index 0000000000..734d43131a
--- /dev/null
+++ b/apps/web/tests/background-task-list.e2e.ts
@@ -0,0 +1,133 @@
+// Web e2e scenario: the session-header background-task list over the real
+// host. No model call is involved — a genuine `run_in_background` bash call
+// registers with `ctx.tasks`, and the assertion chain is the whole delivery
+// path: registry change feed → api-proxy `session/tasks` frame → the client's
+// `tasksBySession` mirror → the header action.
+import { readFile } from 'node:fs/promises'
+import { fileURLToPath } from 'node:url'
+import { join } from 'node:path'
+import type { Browser, Page } from 'playwright'
+import { chromium } from 'playwright'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import type { Agent } from '@deepseek-ai/dsh-agent'
+import { CallId } from '@deepseek-ai/dsh-llm'
+import { SessionId } from '@deepseek-ai/dsh-session'
+import { TaskId } from '@deepseek-ai/dsh-tasks'
+import {
+ assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
+ launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
+} from './scaffold.ts'
+import { newEnglishPage, saveFailureShot } from './support.ts'
+
+const FIXTURE = fileURLToPath(new URL('./snapshots/fresh-round-trip/session.jsonl', import.meta.url))
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/background-task-list', import.meta.url))
+const RUNNING_EXPECTED = join(SNAPSHOT_DIR, 'running.expected.md')
+const SETTLED_EXPECTED = join(SNAPSHOT_DIR, 'settled.expected.md')
+const MODE = webSnapshotMode()
+const SEED_ID = 'background-task-list-web-e2e'
+// Long enough that the running assertions never race the process exiting on
+// their own; the test kills it explicitly to reach the settled state.
+const COMMAND = 'sleep 45'
+
+/**
+ * Wait for the Host to publish the live Agent that opening a session resumes.
+ * @param scaffold - the booted web scaffold.
+ * @param sessionId - the opened session's identity.
+ * @returns the registered Agent instance.
+ */
+async function liveAgent(scaffold: WebScaffold, sessionId: SessionId): Promise {
+ const deadline = Date.now() + 30_000
+ for (;;) {
+ const found = scaffold.ctx.agents.get(sessionId)
+ if (found !== undefined) return found
+ if (Date.now() > deadline) throw new Error(`opening session "${sessionId}" published no live Agent`)
+ await new Promise(resolve => setTimeout(resolve, 100))
+ }
+}
+
+describe.skipIf(MODE === 'record')('web e2e: background task list', () => {
+ let scaffold: WebScaffold
+ let browser: Browser
+ let page: Page
+ let tripwire: ReturnType
+ let agent: Agent
+ let taskId: TaskId
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold({})
+ await seedSession(scaffold, await readFile(FIXTURE, 'utf8'), SEED_ID)
+ browser = await chromium.launch()
+ page = await newEnglishPage(browser)
+ tripwire = watchConsole(page)
+ await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+
+ const groupRow = page.locator('[role="treeitem"]').first()
+ await groupRow.waitFor({ timeout: 15_000 })
+ await groupRow.click()
+ const sessionRow = page.locator('[role="treeitem"]').nth(1)
+ await sessionRow.waitFor({ timeout: 10_000 })
+ await sessionRow.click()
+
+ // Opening the session drives the Host's ordinary Agent resolution; the
+ // task owner must be that exact live instance, never a second one.
+ // `expect.poll` is test-scoped, so this hook polls by hand.
+ agent = await liveAgent(scaffold, SessionId(SEED_ID))
+ }, 120_000)
+
+ afterAll(async () => {
+ await browser?.close()
+ await scaffold?.close()
+ })
+
+ it('shows a running background task in the session header without a refresh', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-background-task-running'))
+ // Point assertion, not a poll: `expect.poll` retries until a predicate
+ // holds, so polling for zero passes at t=0 and proves nothing. The
+ // "renders nothing without a task" branch is owned by the component suite.
+ const trigger = page.getByRole('button', { name: '1 background task running' })
+ expect(await trigger.count()).toBe(0)
+
+ const started = await scaffold.ctx.tools.execute({
+ signal: new AbortController().signal,
+ callId: CallId('background-task-list-e2e'),
+ name: 'bash',
+ arguments: { command: COMMAND, description: 'Hold a background slot open', run_in_background: true },
+ agent,
+ })
+ const reported = started.content.map(block => block.type === 'text' ? block.text : '').join('')
+ const matched = /\bbash-\d+\b/.exec(reported)
+ if (matched === null) throw new Error(`background bash reported no task id: ${reported}`)
+ taskId = TaskId(matched[0])
+
+ await trigger.waitFor({ timeout: 15_000 })
+ await trigger.click()
+ const row = page.getByRole('list', { name: 'Background tasks' }).getByRole('listitem').first()
+ await row.waitFor({ timeout: 10_000 })
+ await expect.poll(() => row.textContent()).toContain(COMMAND)
+
+ const snapshot = await captureStableAria(page, '[class*="menu"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(RUNNING_EXPECTED, snapshot, MODE)
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ }, 60_000)
+
+ it('flips the open list to the cancelled outcome when the registry settles it', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-background-task-settled'))
+ expect(scaffold.ctx.tasks.kill(taskId, agent, 'web e2e cancellation')).toBe('requested')
+
+ // The trigger drops its live count once the task leaves running/stopping,
+ // which is also the proof that settlement reached the browser unprompted.
+ const idle = page.getByRole('button', { name: '1 background task' })
+ await idle.waitFor({ timeout: 20_000 })
+
+ const snapshot = await captureStableAria(page, '[class*="menu"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(SETTLED_EXPECTED, snapshot, MODE)
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ }, 60_000)
+
+ it('keeps its snapshot inventory closed', async () => {
+ await assertFixtureInventory(SNAPSHOT_DIR, ['running.expected.md', 'settled.expected.md'])
+ })
+})
diff --git a/apps/web/tests/chat-long-interactions.e2e.ts b/apps/web/tests/chat-long-interactions.e2e.ts
index ad9d6a6874..aca85146b5 100644
--- a/apps/web/tests/chat-long-interactions.e2e.ts
+++ b/apps/web/tests/chat-long-interactions.e2e.ts
@@ -186,7 +186,7 @@ describe('web e2e: long Chat interaction contract', () => {
const boundary = source.session.events.find((event): event is SessionEvent<'turn/end'> => (
event.type === 'turn/end' && event.data.turn === BRANCH_TURN
))
- if (boundary === undefined) throw new Error(`turn ${String(BRANCH_TURN)} has no completed boundary`)
+ if (boundary === undefined) throw new Error(`turn ${String(BRANCH_TURN)} has no turn/end event`)
const expectedUserText = textContent(branchUserEvent.data.content)
await wheelUntilMounted(page, `[data-chat-call-id="${TARGET_CALL_2}"]`, -1_100)
diff --git a/apps/web/tests/complex-history.perf.ts b/apps/web/tests/complex-history.perf.ts
index 2daed2c0f6..d3c27f0ce0 100644
--- a/apps/web/tests/complex-history.perf.ts
+++ b/apps/web/tests/complex-history.perf.ts
@@ -1,7 +1,7 @@
// Opt-in browser benchmark for high-cardinality workspace and history
// rendering. It reports measurements without timing assertions because host
-// speed is not a correctness contract; structural assertions keep the load
-// shape from silently shrinking.
+// speed is not a correctness contract; structural assertions keep the number
+// of workspaces and history entries from silently shrinking.
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
diff --git a/apps/web/tests/composer-draft-scroll.e2e.ts b/apps/web/tests/composer-draft-scroll.e2e.ts
index 687419dc53..0ec3ef63e1 100644
--- a/apps/web/tests/composer-draft-scroll.e2e.ts
+++ b/apps/web/tests/composer-draft-scroll.e2e.ts
@@ -64,12 +64,12 @@ const DRAFT = Array.from({ length: DRAFT_LINES }, (_unused, index) => {
}).join('\n')
/**
- * A draft ending in a newline: the shape where the two layers reserve their
+ * A draft ending in a newline, where the two layers reserve their
* final line box on different terms. A textarea keeps one for the caret after a
* final newline; `white-space: pre-wrap` collapses a text node's trailing
* newline and generates none. The hidden auto-grow mirror carries the newline
* and so decides the height for both, which is why the backdrop needs no
- * padding of its own — but only a draft of this shape can show it.
+ * padding of its own — but only a draft with a trailing newline can show it.
*/
const DRAFT_TRAILING_NEWLINE = `${DRAFT}\n`
@@ -381,7 +381,7 @@ describe('web e2e: composer draft scrolling', () => {
const data = new DataTransfer()
data.setData('text/plain', text)
el.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true }))
- // Ending in a newline is the shape the engines disagree on: the caret
+ // The engines disagree when the draft ends in a newline: the caret
// lands on a line with nothing on it, where chromium reports no client
// rects at all for the collapsed position.
}, `\n${DRAFT}\n`)
@@ -401,7 +401,7 @@ describe('web e2e: composer draft scrolling', () => {
it('a draft ending in a newline scrolls to its true end, not a line above it', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-composer-draft-scroll-trailing-newline'))
- // The layers reserve a final line box on different terms, so this shape is
+ // The layers reserve a final line box on different terms, so the trailing-newline case is
// the one that separates a height every layer agrees on from a box measured
// one line short of the caret's own last position.
const input = page.locator('textarea:enabled').first()
@@ -453,7 +453,7 @@ describe('web e2e: composer draft scrolling', () => {
const data = new DataTransfer()
data.setData('text/plain', text)
el.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true }))
- // The ordinary shape — not ending in a newline — so the collapsed branch
+ // The ordinary case, without a trailing newline, so the collapsed branch
// of the reveal keeps a real engine under it; the case above owns the
// after-newline branch.
}, `\n${DRAFT}`)
diff --git a/apps/web/tests/core-web-profile.snapshot.ts b/apps/web/tests/core-web-profile.snapshot.ts
deleted file mode 100644
index 1178390837..0000000000
--- a/apps/web/tests/core-web-profile.snapshot.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { writeFile } from 'node:fs/promises'
-import { join } from 'node:path'
-import { fileURLToPath } from 'node:url'
-import { afterAll, beforeAll, describe, expect, it } from 'vitest'
-import type { AgentHandle } from '@deepseek-ai/dsh-agent'
-import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
-import { SessionId } from '@deepseek-ai/dsh-session'
-import { assertFixtureInventory, launchWebScaffold, type WebScaffold } from './scaffold.ts'
-
-const CORE_WEB_OVERLAY = fileURLToPath(new URL('../../cli/config/core-web.cordis.yml', import.meta.url))
-const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/core-web-profile', import.meta.url))
-const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
-const PROMPT = 'Reply exactly CORE_WEB_REQUEST_OK and stop.'
-
-describe('core Web profile', () => {
- let scaffold: WebScaffold
- let agentHandle: AgentHandle
-
- beforeAll(async () => {
- const systemPrompt = process.env.DSH_SYSTEM_PROMPT
- Reflect.deleteProperty(process.env, 'DSH_SYSTEM_PROMPT')
- try {
- scaffold = await launchWebScaffold({ extraOverlayPath: CORE_WEB_OVERLAY, replayFixture: FIXTURE })
- } finally {
- if (systemPrompt !== undefined) process.env.DSH_SYSTEM_PROMPT = systemPrompt
- }
- agentHandle = await scaffold.ctx.agents.create({
- sessionId: SessionId('core-web-profile-smoke'),
- meta: { cwd: scaffold.workspaceCwd },
- agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
- })
- })
-
- afterAll(async () => {
- const failures: unknown[] = []
- await agentHandle?.dispose().catch((error: unknown) => failures.push(error))
- await scaffold?.close().catch((error: unknown) => failures.push(error))
- if (failures.length === 1) throw failures[0]
- if (failures.length > 1) throw new AggregateError(failures, 'core Web profile smoke teardown failed')
- })
-
- it('sends the RL prompt and tool schemas through a real request, then executes both tools', async () => {
- agentHandle.agent.followup(createUserMessage({
- content: [{ type: 'text', text: PROMPT }],
- source: { kind: 'user' },
- }))
- await agentHandle.agent.whenIdle()
-
- const requestHeader = agentHandle.agent.session.requestHeader()
- if (requestHeader === undefined) throw new Error('the core Web agent issued no model request')
-
- const seedPath = join(scaffold.workspaceCwd, 'profile-smoke.txt')
- await writeFile(seedPath, 'CORE_WEB_EDITOR_OK\n')
- const signal = new AbortController().signal
- const bash = await scaffold.ctx.tools.execute({
- signal,
- callId: CallId('core-web-bash-smoke'),
- name: 'bash',
- arguments: { command: "printf 'CORE_WEB_BASH_OK\\n'" },
- agent: agentHandle.agent,
- })
- const editor = await scaffold.ctx.tools.execute({
- signal,
- callId: CallId('core-web-editor-smoke'),
- name: 'str_replace_editor',
- arguments: { command: 'view', path: seedPath },
- agent: agentHandle.agent,
- })
-
- const text = (result: typeof bash): string => result.content
- .filter(block => block.type === 'text')
- .map(block => block.text)
- .join('')
- .replaceAll(scaffold.workspaceCwd, '{{cwd}}')
- .trimEnd()
-
- expect({
- prompt: requestHeader.system,
- tools: requestHeader.tools?.map(tool => tool.name),
- bash: text(bash),
- editor: text(editor),
- }).toMatchInlineSnapshot(`
- {
- "bash": "CORE_WEB_BASH_OK",
- "editor": "Here's the content of {{cwd}}/profile-smoke.txt with line numbers (which has a total of 2 lines):
- 1 CORE_WEB_EDITOR_OK
- 2",
- "prompt": "You are a helpful software engineer assistant.",
- "tools": [
- "bash",
- "str_replace_editor",
- ],
- }
- `)
- expect(requestHeader.tools).toEqual(scaffold.ctx.tools.schemas(agentHandle.agent))
-
- const entries = [...scaffold.ctx.loader.entries()]
- expect(entries.find(entry => entry.options.id === 'persistent-bash')?.fiber).toBeDefined()
- expect(entries.find(entry => entry.options.id === 'pty-local')?.fiber).toBeDefined()
- expect(entries.find(entry => entry.options.id === 'str-replace-editor')?.fiber).toBeDefined()
- expect(entries.find(entry => entry.options.id === 'web-runtime')?.fiber).toBeDefined()
- expect(entries.find(entry => entry.options.id === 'workspace-context')?.fiber).toBeUndefined()
- await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl'])
- })
-
- it('uses DSH_SYSTEM_PROMPT as the complete prompt when configured', async () => {
- const previous = process.env.DSH_SYSTEM_PROMPT
- process.env.DSH_SYSTEM_PROMPT = 'RL prompt override'
- let overrideScaffold: WebScaffold | undefined
- let overrideAgent: AgentHandle | undefined
- try {
- overrideScaffold = await launchWebScaffold({ extraOverlayPath: CORE_WEB_OVERLAY, replayFixture: FIXTURE })
- overrideAgent = await overrideScaffold.ctx.agents.create({
- sessionId: SessionId('core-web-profile-override'),
- meta: { cwd: overrideScaffold.workspaceCwd },
- agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
- })
- overrideAgent.agent.followup(createUserMessage({
- content: [{ type: 'text', text: PROMPT }],
- source: { kind: 'user' },
- }))
- await overrideAgent.agent.whenIdle()
- expect(overrideAgent.agent.session.requestHeader()?.system).toBe('RL prompt override')
- } finally {
- try {
- await overrideAgent?.dispose()
- } finally {
- try {
- await overrideScaffold?.close()
- } finally {
- if (previous === undefined) Reflect.deleteProperty(process.env, 'DSH_SYSTEM_PROMPT')
- else process.env.DSH_SYSTEM_PROMPT = previous
- }
- }
- }
- })
-})
diff --git a/apps/web/tests/details-session-lifecycle.e2e.ts b/apps/web/tests/details-session-lifecycle.e2e.ts
index c88a823fb7..7f507c59d2 100644
--- a/apps/web/tests/details-session-lifecycle.e2e.ts
+++ b/apps/web/tests/details-session-lifecycle.e2e.ts
@@ -42,7 +42,7 @@ function appFrame(page: Page) {
return page.locator('[style*="grid-template-columns"]').first()
}
-/** Render the two boundary affordances without platform-dependent coordinates. */
+/** Render the two column-resize handles without platform-dependent coordinates. */
async function handleSnapshot(page: Page): Promise {
const handles = await page.locator('[class*="handle"]').evaluateAll(elements =>
elements.map(element => ({
diff --git a/apps/web/tests/feedback-command.e2e.ts b/apps/web/tests/feedback-command.e2e.ts
new file mode 100644
index 0000000000..577e77a97a
--- /dev/null
+++ b/apps/web/tests/feedback-command.e2e.ts
@@ -0,0 +1,101 @@
+// Keyless assembled-browser coverage for the /feedback command over the
+// shipped Web bundles and the real host wire. The command plane settles
+// without a model turn: the host appends the log-only command/run +
+// feedback/record + command/done lifecycle, and the transcript renders the
+// acknowledgement — the recorded session id plus the session-sharing
+// disclosure — as a persistent command row. The scaffold mounts the shipped
+// telemetry row in FULL mode against a local dead endpoint (no record leaves
+// the process), so the golden pins the shipped default sentence
+// `Session sharing is enabled.`; the per-status sentences are pinned by the
+// package and OTel unit tests.
+import { readFile } from 'node:fs/promises'
+import { fileURLToPath } from 'node:url'
+import { join } from 'node:path'
+import type { Browser, Page } from 'playwright'
+import { chromium } from 'playwright'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import {
+ assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
+ launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
+} from './scaffold.ts'
+import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
+
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/feedback-command', import.meta.url))
+const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
+const ACK_EXPECTED = join(SNAPSHOT_DIR, 'ack.expected.md')
+const MODE = webSnapshotMode()
+// Discard port: loopback listener never binds, so FULL telemetry discloses
+// the shipped default policy without any record reaching a collector.
+const TELEMETRY_URL = 'http://127.0.0.1:9/v1/logs'
+
+const PROMPT = 'Reply with the single word LIGHTHOUSE and stop.'
+
+describe('web e2e: /feedback command acknowledgement', () => {
+ let scaffold: WebScaffold
+ let browser: Browser
+ let page: Page
+ let tripwire: ReturnType
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold({
+ telemetryUrl: TELEMETRY_URL,
+ ...(MODE === 'record' ? {} : { replayFixture: FIXTURE }),
+ })
+ browser = await chromium.launch()
+ page = await newEnglishPage(browser)
+ tripwire = watchConsole(page)
+ await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ // Fresh world: connecting a workspace births the blank session whose
+ // live composer accepts the slash line.
+ await connectFreshWorkspace(page, scaffold.workspaceCwd)
+ }, 120_000)
+
+ afterAll(async () => {
+ await browser?.close()
+ await scaffold?.close()
+ })
+
+ it('drives the recorded prompt to a settled turn (all modes)', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-feedback-drive'))
+ if (MODE !== 'record') {
+ // Drift guard: the committed fixture must carry exactly the drive prompt.
+ expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT])
+ }
+ const input = page.locator('textarea').first()
+ await input.waitFor({ timeout: 10_000 })
+ // Arm the turn-boundary waiter BEFORE sending, so a burst replay cannot
+ // miss the turn/end that settles the recorded turn.
+ const settled = scaffold.whenTurnSettled()
+ await input.fill(PROMPT)
+ await input.press('Enter')
+ const sessionId = await settled
+ if (MODE === 'record') {
+ await recordFixture(scaffold, sessionId, FIXTURE)
+ }
+ }, 60_000)
+
+ it.skipIf(MODE === 'record')('records feedback and renders the acknowledgement with session id and sharing status', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-feedback-command'))
+ // The drive test settled the recorded turn: the transcript is active (a
+ // command row does not render while a fresh session is still blank) and
+ // the replayed reply is on screen.
+ await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
+ const input = page.locator('textarea').first()
+ await input.fill('/feedback the diff view is unreadable')
+ await input.press('Enter')
+ // The command plane settles without a model turn: the ack row names the
+ // recorded session and the mounted FULL backend's disclosure.
+ await page.getByText(/Feedback recorded for session/).waitFor({ timeout: 10_000 })
+ expect(await page.getByText(/Session sharing is enabled/).count()).toBe(1)
+ const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(ACK_EXPECTED, snapshot, MODE)
+
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ }, 60_000)
+
+ it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
+ await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl', 'ack.expected.md'])
+ })
+})
diff --git a/apps/web/tests/goal-command-presentation.e2e.ts b/apps/web/tests/goal-command-presentation.e2e.ts
new file mode 100644
index 0000000000..6117fb8ace
--- /dev/null
+++ b/apps/web/tests/goal-command-presentation.e2e.ts
@@ -0,0 +1,123 @@
+// Web e2e: /goal opts its command input into the human transcript while the
+// command remains log-only. The shipped composition runs with no model adapter,
+// so an accidental turn fails loud in addition to the event-level assertions.
+import { fileURLToPath } from 'node:url'
+import type { Browser, Page } from 'playwright'
+import { chromium } from 'playwright'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
+import type {} from '@deepseek-ai/dsh-commands/types'
+import {
+ acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria,
+ compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode,
+ type WebScaffold,
+} from './scaffold.ts'
+import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
+
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/goal-command-presentation', import.meta.url))
+const UI_EXPECTED = fileURLToPath(new URL(
+ './snapshots/goal-command-presentation/ui.expected.md', import.meta.url,
+))
+const MODE = webSnapshotMode()
+
+describe('web e2e: /goal human transcript presentation', () => {
+ let scaffold: WebScaffold
+ let browser: Browser
+ let page: Page
+ let tripwire: ReturnType
+ const events: SessionEvent[] = []
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold()
+ scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { events.push(event) })
+ browser = await chromium.launch()
+ page = await newEnglishPage(browser)
+ tripwire = watchConsole(page)
+ await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ await connectFreshWorkspace(page, scaffold.workspaceCwd)
+ }, 120_000)
+
+ afterAll(async () => {
+ await browser?.close()
+ await scaffold?.close()
+ })
+
+ it('shows the bare input and result from a fresh session without a model turn', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-goal-command-presentation'))
+ await expect.poll(() => page.getByText('Into the Unknown', { exact: false }).count(), {
+ timeout: 15_000,
+ }).toBe(1)
+ const input = page.locator('textarea').first()
+ await input.fill('/goal')
+ await input.press('Enter')
+ await expect.poll(() => input.inputValue()).toBe('/goal ')
+ await input.press('Enter')
+
+ const commandInput = page.locator('[data-command-input]')
+ await commandInput.waitFor({ timeout: 10_000 })
+ await expect.poll(() => commandInput.textContent()).toBe('/goal')
+ expect(await commandInput.getAttribute('role')).toBe('group')
+ expect(await commandInput.getAttribute('aria-label')).toBe('Command input')
+ expect(await commandInput.getByRole('button').count()).toBe(0)
+ const typography = await commandInput.evaluate((element) => {
+ const bubble = element.firstElementChild?.firstElementChild
+ if (!(bubble instanceof HTMLElement)) throw new Error('command input bubble is missing')
+ const rootStyle = getComputedStyle(element)
+ const bubbleStyle = getComputedStyle(bubble)
+ return {
+ fontFamily: bubbleStyle.fontFamily,
+ parentFontFamily: rootStyle.fontFamily,
+ fontSize: bubbleStyle.fontSize,
+ lineHeight: bubbleStyle.lineHeight,
+ }
+ })
+ expect(typography).toMatchObject({ fontSize: '14px', lineHeight: '22px' })
+ expect(typography.fontFamily).not.toBe(typography.parentFontFamily)
+ const resultRow = page.locator('[data-variant="others"]').filter({ hasText: 'No goal is currently set.' })
+ await expect.poll(() => resultRow.count(), { timeout: 10_000 }).toBe(1)
+ expect(await resultRow.getByText('goal', { exact: true }).count()).toBe(1)
+ await expect.poll(() => page.locator('[data-phase="active"]').count()).toBe(1)
+ expect(await page.getByText('Into the Unknown', { exact: false }).count()).toBe(0)
+
+ const run = events.find(event => event.type === 'command/run')
+ expect(run).toMatchObject({
+ type: 'command/run',
+ data: { name: 'goal', args: ' ', source: { kind: 'user' } },
+ })
+ expect(events.some(event => event.type === 'command/done')).toBe(true)
+ expect(events.some(event => event.type === 'user/message')).toBe(false)
+ expect(events.some(event => event.type === 'turn/start')).toBe(false)
+ expect(events.some(event => event.type === 'step/start')).toBe(false)
+ expect(events.some(event => event.type === 'request/header')).toBe(false)
+
+ const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
+ }, 60_000)
+
+ it('reloads the same bubble and result from the persisted command lifecycle', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-goal-command-presentation-reload'))
+ const warningStart = tripwire.warnings.length
+ await page.reload({ waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ acknowledgeReloadConnectionLoss(tripwire, warningStart)
+
+ await expect.poll(() => page.locator('[data-command-input]').textContent(), { timeout: 15_000 }).toBe('/goal')
+ const resultRow = page.locator('[data-variant="others"]').filter({ hasText: 'No goal is currently set.' })
+ await expect.poll(() => resultRow.count(), { timeout: 10_000 }).toBe(1)
+ await expect.poll(() => page.locator('[data-phase="active"]').count()).toBe(1)
+
+ const sessions = scaffold.ctx.sessions.list()
+ expect(sessions).toHaveLength(1)
+ const persisted = sessions[0]?.events ?? []
+ expect(persisted.filter(event => event.type === 'command/run' || event.type === 'command/done')
+ .map(event => event.type)).toEqual(['command/run', 'command/done'])
+ expect(persisted.some(event => event.type === 'user/message')).toBe(false)
+ expect(persisted.some(event => event.type === 'turn/start')).toBe(false)
+ expect(persisted.some(event => event.type === 'step/start')).toBe(false)
+ expect(persisted.some(event => event.type === 'request/header')).toBe(false)
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
+ }, 90_000)
+})
diff --git a/apps/web/tests/hmr-live.e2e.ts b/apps/web/tests/hmr-live.e2e.ts
index cafd0fb474..fc12fae810 100644
--- a/apps/web/tests/hmr-live.e2e.ts
+++ b/apps/web/tests/hmr-live.e2e.ts
@@ -1,4 +1,4 @@
-/** Published dsh web --dev + pnpm dev:web → browser HMR, with no page reload. */
+/** Published dsh web + pnpm dev:web → browser HMR, with no page reload. */
import { existsSync } from 'node:fs'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
@@ -6,8 +6,8 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { chromium } from 'playwright'
import { expect, it } from 'vitest'
-import { Context } from 'cordis'
-import type { Fiber } from 'cordis'
+import { Context } from '@deepseek-ai/cordis'
+import type { Fiber } from '@deepseek-ai/cordis'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import { REPO_ROOT } from './support.ts'
@@ -92,14 +92,14 @@ it('hot-reloads a real client-plugin source edit without refreshing the page', a
watcher = subprocessCtx.subprocess.spawn(spawnSpec(['pnpm', 'run', 'dev:web'], REPO_ROOT))
await waitForOutput(watcher, /dev-web: watching/, 'pnpm run dev:web')
host = subprocessCtx.subprocess.spawn(spawnSpec(
- [process.execPath, binPath, 'web', '--dev', '--port', '0'],
+ [process.execPath, binPath, 'web', '--port', '0'],
world,
{
DEEPSEEK_API_KEY: 'keyless-hmr-no-call',
DSH_HOME: join(world, '.dsh'),
},
))
- const baseUrl = await waitForOutput(host, /dsh web: (http:\/\/[^\s]+)/, 'built dsh web --dev')
+ const baseUrl = await waitForOutput(host, /dsh web: (http:\/\/[^\s]+)/, 'built dsh web')
browser = await chromium.launch()
const page = await browser.newPage()
const pageErrors: string[] = []
diff --git a/apps/web/tests/image-display.snapshot.ts b/apps/web/tests/image-display.snapshot.ts
new file mode 100644
index 0000000000..df286ec1c3
--- /dev/null
+++ b/apps/web/tests/image-display.snapshot.ts
@@ -0,0 +1,136 @@
+// @vitest-environment jsdom
+// Multimodal image surfaces over the BUILT client graph (the code-mode-fixture
+// idiom: real bundles via AppWebEntry, keyless FixtureApiClient transport).
+// Opens the fixture history session whose turn 72 carries an image in BOTH a
+// user message and an assistant message, and pins the product surfaces: the
+// history ImageGallery loading real fixture bytes through the authorized
+// sessions.attachment route, the double-click ImageLightbox, and the composer
+// intake chain (paste → ordered thumbnail rail → image-only send enablement → remove).
+import { fireEvent, screen, waitFor, within } from '@testing-library/react'
+import { expect, it } from 'vitest'
+import { installAssembledBootEnv, mountAssembledApp } from './assembled-boot.ts'
+
+installAssembledBootEnv()
+
+/** Open the fixture history session (the alpha log carrying the turn-72 image pair) and wait for its gallery. */
+async function openFixtureSession(): Promise {
+ const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
+ const group = (await within(tree).findAllByText('fixture'))
+ .map(el => el.closest('[role="treeitem"]'))
+ .find(el => el?.getAttribute('aria-expanded') !== null)
+ if (group === null || group === undefined) throw new Error('fixture Workspace group missing')
+ if (group.getAttribute('aria-expanded') === 'false') {
+ fireEvent.click(within(group).getByText('fixture'))
+ await waitFor(() => {
+ expect(group.getAttribute('aria-expanded')).toBe('true')
+ })
+ }
+ const session = await within(tree).findByText('Fixture 历史会话')
+ fireEvent.click(session)
+ await waitFor(() => {
+ expect(document.querySelectorAll('[data-align] img').length).toBeGreaterThan(0)
+ }, { timeout: 10_000 })
+}
+
+it('renders the history image pair through the authorized attachment route and opens the lightbox', async () => {
+ mountAssembledApp()
+ await openFixtureSession()
+
+ // Both the user-side (align=end) and assistant-side (align=start) galleries
+ // load real fixture bytes over sessions.attachment. jsdom provides
+ // createObjectURL, so this environment MUST take the object-URL path — a
+ // data: src here would mean the fallback ran where it should not.
+ await waitFor(() => {
+ if (document.querySelector('[data-align="end"] img') === null
+ || document.querySelector('[data-align="start"] img') === null) {
+ throw new Error('history image galleries missing')
+ }
+ }, { timeout: 10_000 })
+ const galleryShape = (align: string) => [...document.querySelectorAll(`[data-align="${align}"] img`)]
+ .map(img => ({ alt: img.getAttribute('alt'), scheme: img.getAttribute('src')?.split(':')[0] }))
+ expect({ user: galleryShape('end'), assistant: galleryShape('start') }).toMatchInlineSnapshot(`
+ {
+ "assistant": [
+ {
+ "alt": "fixture-image.png",
+ "scheme": "blob",
+ },
+ ],
+ "user": [
+ {
+ "alt": "fixture-image.png",
+ "scheme": "blob",
+ },
+ ],
+ }
+ `)
+ const userImage = document.querySelector('[data-align="end"] img')!
+
+ // Double-click opens the original-size lightbox; Escape/close dismisses it.
+ const frame = userImage.closest('button')
+ if (frame === null) throw new Error('image frame button missing')
+ fireEvent.doubleClick(frame)
+ const lightbox = await screen.findByRole('dialog')
+ expect(within(lightbox).getByRole('img').getAttribute('src')?.split(':')[0]).toBe('blob')
+ fireEvent.click(within(lightbox).getByRole('button', { name: /Close/ }))
+ await waitFor(() => {
+ expect(screen.queryByRole('dialog')).toBeNull()
+ })
+})
+
+it('accepts pasted images into the composer rail in order and removes them', async () => {
+ mountAssembledApp()
+
+ const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
+ const start = tree.querySelector('button[aria-label="New session in fixture"]')
+ if (start === null) throw new Error('fixture Workspace new-session action missing')
+ fireEvent.click(start)
+
+ // Image-only send arming is pinned at package level (input-bar.spec.tsx);
+ // this assembled lane pins the intake chain over the built graph.
+ const textarea = await screen.findByPlaceholderText('Describe what you want to build', {}, { timeout: 10_000 })
+ const image = new File([new Uint8Array([137, 80, 78, 71])], 'pasted.png', { type: 'image/png' })
+ fireEvent.paste(textarea, {
+ clipboardData: {
+ items: [{ kind: 'file', type: 'image/png', getAsFile: () => image }],
+ getData: () => '',
+ },
+ })
+
+ // The rail is an accessible group holding the draft thumbnail (queried via
+ // DOM: jsdom's a11y-visibility computation hides the composer subtree).
+ const rail = await waitFor(() => {
+ const el = document.querySelector('[role="group"][aria-label="Pending images"]')
+ if (el === null) throw new Error('attachment rail missing')
+ return el
+ }, { timeout: 5_000 })
+ expect([...rail.querySelectorAll('img')].map(img => ({
+ alt: img.getAttribute('alt'), scheme: img.getAttribute('src')?.split(':')[0],
+ }))).toMatchInlineSnapshot(`
+ [
+ {
+ "alt": "pasted.png",
+ "scheme": "blob",
+ },
+ ]
+ `)
+
+ const second = new File([new Uint8Array([137, 80, 78, 71])], 'second.png', { type: 'image/png' })
+ fireEvent.paste(textarea, {
+ clipboardData: {
+ items: [{ kind: 'file', type: 'image/png', getAsFile: () => second }],
+ getData: () => '',
+ },
+ })
+ await waitFor(() => {
+ expect([...rail.querySelectorAll('img')].map(img => img.getAttribute('alt')))
+ .toEqual(['pasted.png', 'second.png'])
+ })
+
+ const remove = [...rail.querySelectorAll('button[aria-label^="Remove image"]')]
+ if (remove.length !== 2) throw new Error('remove buttons missing')
+ for (const button of remove) fireEvent.click(button)
+ await waitFor(() => {
+ expect(document.querySelector('[role="group"][aria-label="Pending images"]')).toBeNull()
+ })
+})
diff --git a/apps/web/tests/math-rendering.e2e.ts b/apps/web/tests/math-rendering.e2e.ts
index b183fe7df5..de24c1ca76 100644
--- a/apps/web/tests/math-rendering.e2e.ts
+++ b/apps/web/tests/math-rendering.e2e.ts
@@ -119,6 +119,10 @@ describe('web e2e: settled Markdown math rendering', () => {
await expect.poll(() => page.locator('.katex').count(), { timeout: 10_000 }).toBe(6)
await expect.poll(() => page.locator('.katex-display').count(), { timeout: 10_000 }).toBe(2)
expect(await page.locator('.katex-error').count()).toBe(0)
+ await expect.poll(
+ () => page.getByText('Input 0 tok · Output 0 tok', { exact: false }).count(),
+ { timeout: 10_000 },
+ ).toBe(1)
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
.split(SEED_ID).join('{{seededId}}')
diff --git a/apps/web/tests/message-actions.e2e.ts b/apps/web/tests/message-actions.e2e.ts
index 6c4f450529..4284d3d71a 100644
--- a/apps/web/tests/message-actions.e2e.ts
+++ b/apps/web/tests/message-actions.e2e.ts
@@ -128,6 +128,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-actions-aria'))
await page.getByRole('button', { name: /^Select model, current/ })
.waitFor({ timeout: 10_000 })
+ await page.getByText(/Cache hit \d+%/u).first().waitFor({ timeout: 10_000 })
// Keep a footer focused so opacity-hidden actions stay in the a11y tree
// as an active/focused control during the capture.
await page.getByRole('button', { name: 'Copy' }).first().focus()
@@ -176,6 +177,12 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
() => page.locator('[role="treeitem"][aria-selected="true"]').count(),
{ timeout: 10_000 },
).toBe(1)
+ // The child row is published before its inherited title rename settles;
+ // wait for that second RPC projection before freezing the ARIA tree.
+ await expect.poll(
+ () => page.locator('[role="treeitem"][aria-selected="true"]').textContent(),
+ { timeout: 10_000 },
+ ).toContain('Use the read tool twice (2)')
const tree = await captureStableAria(
page,
'[role="tree"][aria-label="Sessions"]',
diff --git a/apps/web/tests/message-feedback-protocol.snapshot.ts b/apps/web/tests/message-feedback-protocol.snapshot.ts
new file mode 100644
index 0000000000..9fbc556f77
--- /dev/null
+++ b/apps/web/tests/message-feedback-protocol.snapshot.ts
@@ -0,0 +1,115 @@
+import { readFile } from 'node:fs/promises'
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { afterAll, beforeAll, describe, expect, it } from 'vitest'
+import {
+ assertFixtureInventory,
+ compareOrRefreshGolden,
+ launchWebScaffold,
+ seedSession,
+ type WebScaffold,
+} from './scaffold.ts'
+
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/message-feedback-protocol', import.meta.url))
+const SESSION_FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
+const PROTOCOL_EXPECTED = join(SNAPSHOT_DIR, 'protocol.expected.json')
+const SESSION_ID = 'message-feedback-protocol'
+const MESSAGE_ID = '11111111-1111-4111-8111-111111111111'
+
+interface ProtocolExchange {
+ readonly endpoint: string
+ readonly request: unknown
+ readonly status: number
+ readonly response: unknown
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null
+}
+
+/** Extract the opaque item version while keeping every surrounding wire field snapshot-owned. */
+function createdVersion(response: unknown): string {
+ if (!isRecord(response) || !isRecord(response.result) || response.result.ok !== true
+ || !isRecord(response.result.value) || response.result.value.ok !== true
+ || !isRecord(response.result.value.value)
+ || typeof response.result.value.value.version !== 'string') {
+ throw new Error('messageFeedback.put did not return a successful versioned item')
+ }
+ return response.result.value.value.version
+}
+
+/** Replace only run-owned UUID/time values; all protocol names and business fields stay exact. */
+function normalizeProtocol(exchanges: readonly ProtocolExchange[], version: string): string {
+ return JSON.stringify(exchanges, (key, value: unknown) => {
+ if ((key === 'version' || key === 'ifVersion') && value === version) return '{{version}}'
+ if ((key === 'createdAt' || key === 'updatedAt') && typeof value === 'number') return '{{timestamp}}'
+ return value
+ }, 2)
+}
+
+describe('message feedback Host Remote protocol', () => {
+ let scaffold: WebScaffold
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold()
+ await seedSession(scaffold, await readFile(SESSION_FIXTURE, 'utf8'), SESSION_ID)
+ })
+
+ afterAll(async () => {
+ await scaffold?.close()
+ })
+
+ it('snapshots strict list, put, conflict, and delete calls through the shipped Web Host', async () => {
+ const exchanges: ProtocolExchange[] = []
+ const invoke = async (rpcId: string, endpoint: string, request: unknown): Promise => {
+ const payload = { args: { request } }
+ const response = await fetch(`${scaffold.baseUrl}/api/${endpoint}`, {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({
+ type: 'client-request',
+ rpcId,
+ method: endpoint,
+ payload,
+ }),
+ })
+ const body: unknown = await response.json()
+ exchanges.push({ endpoint: `/api/${endpoint}`, request: payload, status: response.status, response: body })
+ return body
+ }
+
+ await invoke('feedback-invalid', 'messageFeedback/put', {
+ sessionId: SESSION_ID,
+ messageId: MESSAGE_ID,
+ rating: 'invalid-rating',
+ ifVersion: null,
+ })
+ await invoke('feedback-list-empty', 'messageFeedback/list', { sessionId: SESSION_ID })
+ const created = await invoke('feedback-put', 'messageFeedback/put', {
+ sessionId: SESSION_ID,
+ messageId: MESSAGE_ID,
+ rating: 'positive',
+ note: 'Useful answer',
+ ifVersion: null,
+ })
+ const version = createdVersion(created)
+ expect(version).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
+ await invoke('feedback-list-created', 'messageFeedback/list', { sessionId: SESSION_ID })
+ await invoke('feedback-conflict', 'messageFeedback/put', {
+ sessionId: SESSION_ID,
+ messageId: MESSAGE_ID,
+ rating: 'negative',
+ ifVersion: null,
+ })
+ await invoke('feedback-delete', 'messageFeedback/delete', {
+ sessionId: SESSION_ID,
+ messageId: MESSAGE_ID,
+ ifVersion: version,
+ })
+ await invoke('feedback-list-deleted', 'messageFeedback/list', { sessionId: SESSION_ID })
+
+ expect(exchanges.every(exchange => exchange.status === 200)).toBe(true)
+ await compareOrRefreshGolden(PROTOCOL_EXPECTED, normalizeProtocol(exchanges, version), scaffold.mode)
+ await assertFixtureInventory(SNAPSHOT_DIR, ['protocol.expected.json', 'session.jsonl'])
+ })
+})
diff --git a/apps/web/tests/minimal-preset.snapshot.ts b/apps/web/tests/minimal-preset.snapshot.ts
new file mode 100644
index 0000000000..0c8c6fa765
--- /dev/null
+++ b/apps/web/tests/minimal-preset.snapshot.ts
@@ -0,0 +1,115 @@
+import { mkdir, writeFile } from 'node:fs/promises'
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { afterAll, beforeAll, describe, expect, it } from 'vitest'
+import type { AgentHandle } from '@deepseek-ai/dsh-agent'
+import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
+import { SessionId } from '@deepseek-ai/dsh-session'
+import type {} from '@deepseek-ai/dsh-agent-presets'
+import type {} from '@deepseek-ai/dsh-system-prompt'
+import { assertFixtureInventory, launchWebScaffold, type WebScaffold } from './scaffold.ts'
+
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/minimal-preset', import.meta.url))
+const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
+const PROMPT = 'Reply exactly MINIMAL_PRESET_REQUEST_OK and stop.'
+
+describe('minimal agent preset', () => {
+ let scaffold: WebScaffold
+ let agentHandle: AgentHandle
+ let disposeInjectedPrompt: () => void
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold({ replayFixture: FIXTURE })
+ disposeInjectedPrompt = scaffold.ctx.systemPrompt.section({
+ name: 'test:injected-prompt',
+ order: 999,
+ text: 'THIS TEXT MUST NOT REACH THE MODEL.',
+ })
+ agentHandle = await scaffold.ctx.agents.create({
+ sessionId: SessionId('minimal-preset-smoke'),
+ meta: { cwd: scaffold.workspaceCwd, agentPreset: 'minimal' },
+ agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
+ setup: agentCtx => scaffold.ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
+ })
+ })
+
+ afterAll(async () => {
+ const failures: unknown[] = []
+ await agentHandle?.dispose().catch((error: unknown) => failures.push(error))
+ try {
+ disposeInjectedPrompt?.()
+ } catch (error: unknown) {
+ failures.push(error)
+ }
+ await scaffold?.close().catch((error: unknown) => failures.push(error))
+ if (failures.length === 1) throw failures[0]
+ if (failures.length > 1) throw new AggregateError(failures, 'minimal preset smoke teardown failed')
+ })
+
+ it('sends the exact RL prompt and schemas, then executes the persistent shell and editor', async () => {
+ agentHandle.agent.followup(createUserMessage({
+ content: [{ type: 'text', text: PROMPT }],
+ source: { kind: 'user' },
+ }))
+ await agentHandle.agent.whenIdle()
+
+ const requestHeader = agentHandle.agent.session.requestHeader()
+ if (requestHeader === undefined) throw new Error('the minimal agent issued no model request')
+
+ const stateDir = join(scaffold.workspaceCwd, 'persistent-state')
+ await mkdir(stateDir)
+ const signal = new AbortController().signal
+ await scaffold.ctx.tools.execute({
+ signal,
+ callId: CallId('minimal-bash-state-setup'),
+ name: 'bash',
+ arguments: { command: `cd ${JSON.stringify(stateDir)} && export DSH_MINIMAL_STATE=PERSISTED` },
+ agent: agentHandle.agent,
+ })
+ const bash = await scaffold.ctx.tools.execute({
+ signal,
+ callId: CallId('minimal-bash-state-read'),
+ name: 'bash',
+ arguments: { command: 'printf \'%s:%s\n\' "$DSH_MINIMAL_STATE" "$PWD"' },
+ agent: agentHandle.agent,
+ })
+ const seedPath = join(scaffold.workspaceCwd, 'preset-smoke.txt')
+ await writeFile(seedPath, 'MINIMAL_EDITOR_OK\n')
+ const editor = await scaffold.ctx.tools.execute({
+ signal,
+ callId: CallId('minimal-editor-smoke'),
+ name: 'str_replace_editor',
+ arguments: { command: 'view', path: seedPath },
+ agent: agentHandle.agent,
+ })
+
+ const text = (result: typeof bash): string => result.content
+ .filter(block => block.type === 'text')
+ .map(block => block.text)
+ .join('')
+ .replaceAll(scaffold.workspaceCwd, '{{cwd}}')
+ .trimEnd()
+
+ expect({
+ prompt: requestHeader.system,
+ tools: requestHeader.tools?.map(tool => tool.name),
+ bash: text(bash),
+ editor: text(editor),
+ }).toMatchInlineSnapshot(`
+ {
+ "bash": "PERSISTED:{{cwd}}/persistent-state",
+ "editor": "Here's the content of {{cwd}}/preset-smoke.txt with line numbers (which has a total of 2 lines):
+ 1 MINIMAL_EDITOR_OK
+ 2",
+ "prompt": "You are a helpful software engineer assistant.",
+ "tools": [
+ "bash",
+ "str_replace_editor",
+ ],
+ }
+ `)
+ expect(requestHeader.tools?.toSorted((left, right) => left.name.localeCompare(right.name)))
+ .toEqual(scaffold.ctx.tools.schemas(agentHandle.agent).toSorted((left, right) => left.name.localeCompare(right.name)))
+ await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl'])
+ })
+})
diff --git a/apps/web/tests/models-settings.e2e.ts b/apps/web/tests/models-settings.e2e.ts
index 364b00f9e5..e07426cce1 100644
--- a/apps/web/tests/models-settings.e2e.ts
+++ b/apps/web/tests/models-settings.e2e.ts
@@ -4,8 +4,9 @@
// stores it write-only under the derived reference (`MINIMAX_CN_API_KEY`)
// while the settings document records only that reference. Each saved row
// appears after route topology invalidation without presenting liveness as
-// provider status. The customized-settings fold writes the curated
-// reasoning field as a merge patch. Zero model calls: configuration is pure
+// provider status. The customized-settings fold writes its curated fields —
+// the endpoint, and a declared route's own name and protocol — as merge
+// patches against the stored profile. Zero model calls: configuration is pure
// settings/credentials/llm-domain traffic, so there is no fixture and a
// stray stream would fail loud because the adapter registry is empty. The provider under test is
// minimax-cn so a developer's real ANTHROPIC/OPENAI environment keys can
@@ -28,6 +29,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/models-settings', import
const EMPTY_EXPECTED = join(SNAPSHOT_DIR, 'empty.expected.md')
const CONFIGURED_EXPECTED = join(SNAPSHOT_DIR, 'configured.expected.md')
const DECLARED_EXPECTED = join(SNAPSHOT_DIR, 'declared.expected.md')
+const DECLARED_EDIT_EXPECTED = join(SNAPSHOT_DIR, 'declared-edit.expected.md')
const NATIVE_DELETE_EXPECTED = join(SNAPSHOT_DIR, 'native-delete.expected.md')
const DELETE_EXPECTED = join(SNAPSHOT_DIR, 'delete.expected.md')
const MODE = webSnapshotMode()
@@ -209,6 +211,40 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
+ it('reopens the name and protocol a declared route was created with', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-models-declared-identity'))
+ const dialog = page.getByRole('dialog', { name: '设置' })
+ await dialog.getByRole('button', { name: '编辑 Acme Gateway (acme-gateway)' }).click()
+ await dialog.getByText('自定义设置').click()
+ // The create card asked this route for a name and a protocol because
+ // nothing can default them; the editor reaches the same two fields rather
+ // than sending the user to settings.yaml for what only this route names.
+ const protocol = dialog.getByLabel('API 协议')
+ await protocol.waitFor({ timeout: 10_000 })
+ expect(await protocol.inputValue()).toBe('openai-completions')
+ const name = dialog.getByLabel('显示名称', { exact: true })
+ expect(await name.inputValue()).toBe('Acme Gateway')
+ const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(DECLARED_EDIT_EXPECTED, snapshot, MODE)
+
+ await protocol.selectOption('anthropic-messages')
+ await name.fill('Acme 网关')
+ await dialog.getByRole('button', { name: '保存', exact: true }).click()
+ await expect.poll(async () => dialog.getByLabel('API 协议').count(), { timeout: 10_000 }).toBe(0)
+ // The adapter re-resolved the route under the new protocol and re-registered
+ // it under the new name: an unserviceable profile would have been refused
+ // at the write instead, and a rename that did not re-register would leave
+ // the old label on the row.
+ await dialog.getByText('Acme 网关', { exact: true }).first().waitFor({ timeout: 10_000 })
+ // The status line names the route as the refreshed directory reports it;
+ // the target captured when the card opened still carries the old name.
+ await dialog.getByText('已保存 Acme 网关 (acme-gateway)。', { exact: true }).waitFor({ timeout: 10_000 })
+ const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ expect(document).toContain('api: anthropic-messages')
+ expect(document).toContain('displayName: Acme 网关')
+ expect(tripwire.pageErrors).toEqual([])
+ }, 60_000)
+
it('confirms an identified provider deletion before removing its profile and key', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-delete'))
const settingsDialog = page.getByRole('dialog', { name: '设置' })
@@ -243,8 +279,8 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, [
- 'configured.expected.md', 'declared.expected.md', 'delete.expected.md',
- 'empty.expected.md', 'native-delete.expected.md',
+ 'configured.expected.md', 'declared-edit.expected.md', 'declared.expected.md',
+ 'delete.expected.md', 'empty.expected.md', 'native-delete.expected.md',
])
})
})
diff --git a/apps/web/tests/navigation-panes.e2e.ts b/apps/web/tests/navigation-panes.e2e.ts
index cc6d007d82..c8b6455296 100644
--- a/apps/web/tests/navigation-panes.e2e.ts
+++ b/apps/web/tests/navigation-panes.e2e.ts
@@ -11,6 +11,7 @@ import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import type { Browser, Page, Response } from 'playwright'
import { chromium } from 'playwright'
+import { strFromU8, unzipSync } from 'fflate'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, onTestFailed } from 'vitest'
import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
@@ -78,8 +79,8 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
beforeAll(async () => {
scaffold = await launchWebScaffold({})
// The workspace-aware flow runs sessions in /workspace;
- // the read targets must live in that session cwd (pre-creation is safe:
- // create-by-name adopts an existing directory).
+ // the read targets must live in that session cwd (pre-creation is safe
+ // because the picker adopts an existing directory by path).
const sessionCwd = join(scaffold.workspaceCwd, 'workspace')
await mkdir(sessionCwd, { recursive: true })
await writeFile(join(sessionCwd, 'nav-a.md'), '# alpha nav\n')
@@ -164,8 +165,8 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
sessionId = await settled
}
await recordFixture(scaffold, sessionId!, SEED)
- // Fixture honesty: the recording must carry the shape the replay
- // scenarios assert on — three calls in turn 1 and two closed turns.
+ // Fixture honesty: the recording must contain the events the replay
+ // scenarios assert on: three calls in turn 1 and two closed turns.
const recorded = parseSessionLog(await readFile(SEED, 'utf8'))
expect(recorded.filter(e => e.type === 'turn/end')).toHaveLength(2)
const calls = recorded.filter((e): e is SessionEvent & { data: { name: string } } => e.type === 'tool/call')
@@ -274,6 +275,23 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
await details.getByRole('button', { name: 'Close details' }).click()
}, 60_000)
+ it.skipIf(MODE === 'record')('downloads the session-log ZIP from the trajectory toolbar', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-export'))
+ await ensureSeedOpen(page)
+ await page.getByRole('tab', { name: 'Trajectory' }).click()
+ const downloadPromise = page.waitForEvent('download', { timeout: 30_000 })
+ await page.getByRole('button', { name: 'Export session log' }).click()
+ const download = await downloadPromise
+ expect(download.suggestedFilename()).toMatch(/^dsh-session-.+\.zip$/)
+ // The real host streamed the ZIP; its root entry is the persisted log
+ // text verbatim (the assembled seam: real route, real persistence read).
+ const files = unzipSync(await readFile(await download.path()))
+ expect(Object.keys(files)).toEqual(['session.jsonl'])
+ const content = strFromU8(files['session.jsonl'] as Uint8Array)
+ expect(content.split('\n')[0]).toContain(SEED_ID)
+ expect(content).toContain('FIRST_DONE')
+ }, 60_000)
+
it.skipIf(MODE === 'record')('focuses the ledger by dragging an overview interval', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-timeline'))
await ensureSeedOpen(page)
diff --git a/apps/web/tests/pwa-manifest.e2e.ts b/apps/web/tests/pwa-manifest.e2e.ts
index 696e1c7797..fe97e42da9 100644
--- a/apps/web/tests/pwa-manifest.e2e.ts
+++ b/apps/web/tests/pwa-manifest.e2e.ts
@@ -25,3 +25,11 @@ it('ships install metadata with the built web application', async () => {
}],
})
})
+
+it('ships a favicon that switches to a light mark under dark color scheme', async () => {
+ const favicon = await readFile(join(DIST_ROOT, 'favicon.svg'), 'utf8')
+ // The light fill must live inside the dark-scheme media query, so the icon
+ // stays black in light mode and only turns white under a dark scheme.
+ expect(favicon).toMatch(/@media \(prefers-color-scheme: dark\)\s*{\s*path\s*{[^}]*fill:\s*#fff/i)
+ expect(favicon).toContain('fill="#000"')
+})
diff --git a/apps/web/tests/pwsh-terminal.e2e.ts b/apps/web/tests/pwsh-terminal.e2e.ts
index e1b92ed2e3..85b52a734f 100644
--- a/apps/web/tests/pwsh-terminal.e2e.ts
+++ b/apps/web/tests/pwsh-terminal.e2e.ts
@@ -1,7 +1,7 @@
// Keyless browser regression for pwsh UI parity with bash: a seeded session
// whose pwsh call/result is presented by the REAL tool-pwsh on replay (the
// api-proxy recomputes presentation views from logged args/result content)
-// must render as a bash-shaped terminal card with the parsed exit-status
+// must render with the same terminal card layout as bash and show the parsed exit-status
// pill — not a generic console-fenced card. The seed is authored, not
// recorded: its header line carries no `cwd`
// field (seedSession writes the session cwd itself, and a Windows temp path
@@ -43,7 +43,7 @@ const HAS_PWSH = MODE === 'record' ? false : spawnSync(
{ encoding: 'utf8' },
).status === 0
-describe.skipIf(MODE === 'record' || !HAS_PWSH)('web e2e: pwsh calls render as bash-shaped terminal cards', () => {
+describe.skipIf(MODE === 'record' || !HAS_PWSH)('web e2e: pwsh calls use the bash terminal-card layout', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
@@ -75,7 +75,7 @@ describe.skipIf(MODE === 'record' || !HAS_PWSH)('web e2e: pwsh calls render as b
await expect.poll(() => result.count(), { timeout: 15_000 }).toBe(1)
await result.click()
await page.getByRole('tab', { name: 'Chat', exact: true }).waitFor({ timeout: 15_000 })
- // The tool row is expand-gated: the settled bash-shaped row carries the
+ // The tool row is expand-gated: the settled row uses the bash layout and carries the
// shell-family variant, and the terminal card lives in the expanded body.
const row = page.locator('[data-tool="pwsh"]').first()
await row.waitFor({ timeout: 15_000 })
diff --git a/apps/web/tests/question-composer.e2e.ts b/apps/web/tests/question-composer.e2e.ts
index 6f865567bb..5e16453a53 100644
--- a/apps/web/tests/question-composer.e2e.ts
+++ b/apps/web/tests/question-composer.e2e.ts
@@ -31,7 +31,7 @@ const ANSWERED_EXPECTED = join(SNAPSHOT_DIR, 'answered.expected.md')
const MODE = webSnapshotMode()
// The options carry long descriptions on purpose: the squeeze assertion below
-// needs option copy that WRAPS, which is the only shape that reproduces a
+// needs option copy that WRAPS, which is the only text layout that reproduces a
// collapsed row painting its copy outside its own box.
const PROMPT = 'Use the ask_user_question tool to ask me exactly one multi-select question with id "color", question "Which color do you prefer?", header "Pick one", and two options: label "Blue" with description "A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.", and label "Green" with description "A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions." Set multi_select to true. After I answer, reply with the single word DONE and stop.'
@@ -116,7 +116,7 @@ describe('web e2e: resident question composer round trip', () => {
return {
rows: rows.length,
spill: Math.max(...spill),
- // Wrapped copy is the shape that overflows a collapsed row, and a
+ // Wrapped option text is what overflows a collapsed row, and a
// scrolling list proves the seat is genuinely capped. Without both,
// the spill assertion would hold vacuously.
wrappedRows: rows.filter(row => row.getBoundingClientRect().height > 42).length,
diff --git a/apps/web/tests/replay-round-trip.e2e.ts b/apps/web/tests/replay-round-trip.e2e.ts
index 0bb0bc8176..f55cb0e7b5 100644
--- a/apps/web/tests/replay-round-trip.e2e.ts
+++ b/apps/web/tests/replay-round-trip.e2e.ts
@@ -101,14 +101,14 @@ describe('web e2e: fresh round trip through the real assembly', () => {
callId: CallId('web-url-probe'),
name: 'bash',
arguments: {
- command: 'printf \'%s\\n%s\\n\' "$DSH_WEB_URL" "$DSH_WEB_MODE"',
+ command: 'printf \'%s\\n\' "$DSH_WEB_URL"',
description: 'Print current Web runtime',
},
agent,
})
expect(result.isError).toBe(false)
expect(result.content.filter(block => block.type === 'text').map(block => block.text).join(''))
- .toBe(`${scaffold.baseUrl}\nproduction\n`)
+ .toBe(`${scaffold.baseUrl}\n`)
})
it.skipIf(MODE === 'record')('rendered the settled turn: markdown, tool row, composer restore', async () => {
diff --git a/apps/web/tests/scaffold-hermetic.e2e.ts b/apps/web/tests/scaffold-hermetic.e2e.ts
index 6e14eebfa5..c504913b9b 100644
--- a/apps/web/tests/scaffold-hermetic.e2e.ts
+++ b/apps/web/tests/scaffold-hermetic.e2e.ts
@@ -3,6 +3,8 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expect, it } from 'vitest'
import type {} from '@deepseek-ai/dsh-skill'
+import { SessionId } from '@deepseek-ai/dsh-session'
+import type {} from '@deepseek-ai/dsh-agent-presets'
import { launchWebScaffold, type WebScaffold } from './scaffold.ts'
async function writeSkill(root: string, name: string): Promise {
@@ -37,10 +39,25 @@ it('isolates replay skill discovery from every ambient host root', async () => {
let scaffold: WebScaffold | undefined
try {
scaffold = await launchWebScaffold()
- const names = (await scaffold.ctx.skills.list({ cwd: scaffold.workspaceCwd })).map(skill => skill.name)
- expect(names).not.toContain('ambient-dsh')
- expect(names).not.toContain('ambient-agents')
- expect(names).not.toContain('ambient-bundled')
+ const ctx = scaffold.ctx
+ // Local skill discovery belongs to the agent's preset LAYER of the host
+ // registry, so the roots under test are only reachable through a composed
+ // agent's view — the same scope the gateway's `skill.list` resolves for a
+ // browser request about a session.
+ const handle = await ctx.agents.create({
+ sessionId: SessionId('hermetic-skills'),
+ setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined),
+ })
+ try {
+ const skills = ctx.get('skills')
+ if (skills === undefined) throw new Error('the composition mounts no skill registry')
+ const names = (await skills.list({ cwd: scaffold.workspaceCwd, scope: handle.agent })).map(skill => skill.name)
+ expect(names).not.toContain('ambient-dsh')
+ expect(names).not.toContain('ambient-agents')
+ expect(names).not.toContain('ambient-bundled')
+ } finally {
+ await handle.dispose()
+ }
} finally {
try {
await scaffold?.close()
diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts
index 4a922b9353..0f13282b49 100644
--- a/apps/web/tests/scaffold.ts
+++ b/apps/web/tests/scaffold.ts
@@ -29,12 +29,12 @@ import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import type { Page } from 'playwright'
import { expect } from 'vitest'
-import { Context } from 'cordis'
-import Loader from '@cordisjs/plugin-loader'
-import Include, { type PatchOptions } from '@cordisjs/plugin-include'
+import { Context } from '@deepseek-ai/cordis'
+import Loader from '@deepseek-ai/cordis-plugin-loader'
+import Include, { type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
+import Group from '@deepseek-ai/cordis-plugin-group'
import { scrubRequestHeaders, stabilizeFixtureMessageIds } from '@deepseek-ai/dsh-acp-snapshot'
import {
- addHarnessSourceSection,
assertEntriesLoaded,
composeEntries,
healProfilesModuleFallback,
@@ -64,6 +64,7 @@ import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
// Empty type imports carry the httpServer/agents/sessionPersistence Context merges.
import type {} from '@deepseek-ai/dsh-host-webserver'
import type {} from '@deepseek-ai/dsh-agent'
+import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
import { REPO_ROOT, requireDist } from './support.ts'
/** Snapshot mode for the lane, from $DSH_SNAPSHOT (same vocabulary as the other snapshot suites). */
@@ -85,6 +86,8 @@ const BASE_PATCH_PATH = join(REPO_ROOT, 'packages/bundle/base/cordis.patch.yml')
const WEB_PATCH_PATH = join(REPO_ROOT, 'packages/bundle/web-app/cordis.patch.yml')
/** The installation anchor whose dependency surface the profile module fallback mirrors. */
const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json')
+/** The deployment's own agent-preset root, shipped beside the app's config. */
+const SHIPPED_PRESET_DIR = join(REPO_ROOT, 'apps/cli/config/agent-presets')
// Replay publishes the provider catalog the gateway routes to (providers
// mode, never catch-all: with llm-deepseek disabled no adapter exists, so a
@@ -226,14 +229,37 @@ export interface LaunchOptions {
/** Credential reference resolved by the shipped search provider. */
apiKeyEnv: string
}
+ /**
+ * Replace the roster the scaffold mounts by default (the shipped directory
+ * at `system` trust, default `standard`). Supply this only to change WHICH
+ * presets a scenario sees — a writable user root, a different default —
+ * never to turn the roster on: without one every session composes an agent
+ * with no tools, no persona, and no token meter, which is not a shape the
+ * product ever boots in. The patch lands after the default, so it wins.
+ */
+ agentPresets?: {
+ /** Roots to discover, in precedence order; the shipped directory is `system`. */
+ roots: { path: string; trust: 'system' | 'user' }[]
+ /** The preset a session that names none is composed from. */
+ default: string
+ }
/** Leave the current welcome notice unacknowledged; ordinary scenarios publish it as complete before browser boot. */
welcomeNoticePending?: boolean
+ /**
+ * Mount the shipped telemetry row in FULL mode against this exporter URL
+ * instead of disabling it. Used to pin a real backend disclosure in
+ * assembled coverage; point the URL at a local dead endpoint so no record
+ * leaves the process.
+ */
+ telemetryUrl?: string
/**
* Browse through a trusted non-loopback hostname that the browser resolves
* to loopback (for example `*.localhost`). The test server stays bound to
* 127.0.0.1; a non-resolving authority fails before Host trust is exercised.
*/
remoteAuthority?: string
+ /** Reuse an existing harness home so a second Host can verify user settings across origins. */
+ harnessHome?: string
}
/** Dispose the booted tree and remove both owned temp roots, reporting every independent cleanup failure. */
@@ -280,13 +306,42 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise [key, process.env[key]]),
+ )
+ let skillRootEnvironmentRestored = false
+ const restoreSkillRootEnvironment = (): void => {
+ if (skillRootEnvironmentRestored) return
+ skillRootEnvironmentRestored = true
+ for (const [key, value] of Object.entries(originalSkillRootEnvironment)) {
+ if (value === undefined) Reflect.deleteProperty(process.env, key)
+ else process.env[key] = value
+ }
+ }
+ Object.assign(process.env, skillRootEnvironment)
let persistenceRoot: string
try {
persistenceRoot = await mkdtemp(join(tmpdir(), 'dsh-web-e2e-sessions-'))
} catch (error) {
const failures: unknown[] = [error]
await rm(workspaceCwd, { recursive: true, force: true }).catch((cleanupError: unknown) => failures.push(cleanupError))
+ restoreSkillRootEnvironment()
if (failures.length > 1) throw new AggregateError(failures, 'web scaffold temp-root setup failed')
throw error
}
@@ -310,6 +365,18 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise/profiles.
healProfilesModuleFallback(INSTALL_ANCHOR, harnessHome)
@@ -397,14 +470,26 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise {
+ throw new Error(`web e2e scaffold: the web app requested exit ${String(code)} with no arguments to reject`)
+ },
+ })
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
+ // `cordis:group` beside it, exactly as `boot()` registers it: a group row is
+ // how a preset gives one `isolate` realm to a provider and its consumers,
+ // and a preset resolving package names from its own directory cannot reach
+ // `@deepseek-ai/cordis-plugin-group` by name.
+ ctx.loader.builtins.group = Group
// The shipped CLI deliberately has no dependency on this opt-in package.
// Keep the Loader row real without broadening the product installation.
if (options.cordisTools === true) ctx.loader.builtins['tool-cordis'] = ToolCordis
- if (surfaceContext) {
- ctx.inject(['systemPrompt'], (promptCtx) => { addHarnessSourceSection(promptCtx, REPO_ROOT) })
- }
await ctx.loader.create({
name: 'cordis:include',
config: { path: pathToFileURL(rootConfig).href, patches },
@@ -449,6 +534,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise 0) {
throw new AggregateError([error, ...cleanupFailures], 'web scaffold setup failed and cleanup was incomplete')
}
@@ -496,6 +582,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise 0) throw new AggregateError(failures, 'web scaffold teardown failed')
},
@@ -557,11 +644,13 @@ export function fixtureUserPrompts(fixtureText: string): string[] {
* through the REAL backend API (throwaway Context + SessionStore + JSONL
* plugin — the semantic-checkpoint precedent), never raw file writes: no
* knowledge of bucket hashing, filename encoding, or compression, and
- * malformed shapes fail loud at seed time. The fixture's tokenized identity
+ * malformed session events fail loud at seed time. The fixture's tokenized identity
* ({{sessionId}}/{{cwd}}) is realized for this world before parsing.
* @param scaffold - the target scaffold.
* @param fixtureText - raw recorded session.jsonl contents.
* @param id - the seeded session id (stable for deterministic goldens).
+ * @param agentPreset - the preset the recorded session was composed from,
+ * for scenarios asserting what a resumed session reports running.
* @returns the seeded id.
*/
/**
@@ -585,7 +674,12 @@ export function realizeSeedFixture(scaffold: WebScaffold, fixtureText: string, i
: realized.split(fixtureCwd).join(scaffold.workspaceCwd)
}
-export async function seedSession(scaffold: WebScaffold, fixtureText: string, id: string): Promise {
+export async function seedSession(
+ scaffold: WebScaffold,
+ fixtureText: string,
+ id: string,
+ agentPreset?: string,
+): Promise {
const events = parseSessionLog(realizeSeedFixture(scaffold, fixtureText, id))
if (events.length === 0) throw new Error('seed fixture has no events')
const last = events[events.length - 1]!
@@ -598,6 +692,7 @@ export async function seedSession(scaffold: WebScaffold, fixtureText: string, id
createdAt: Date.now() - 60_000,
cwd: scaffold.workspaceCwd,
delegationDepth: 0,
+ ...agentPreset === undefined ? {} : { agentPreset },
}
const seeder = new Context()
try {
@@ -653,7 +748,7 @@ function normalizeAria(snapshot: string, workspaceCwd: string): string {
// between local worktrees and CI scratch directories.
.replace(/(Compacted \d+ history items \(~)\d+( tokens\))/g, '$1{{tokens}}$2')
// Message IconActions clocks widen by calendar day/year; collapse every
- // shape so goldens stay stable across midnight and year boundaries.
+ // format so goldens stay stable across midnight and year changes.
.replace(/\d{4}年\d{1,2}月\d{1,2}日 \d{2}:\d{2}/g, '{{clock}}')
.replace(/\d{1,2}月\d{1,2}日 \d{2}:\d{2}/g, '{{clock}}')
.replace(/(? {
}, { timeout: 10_000 })
// `data-tool` sits on the ToolRow root; the collapsed row is the expand
- // toggle. Click it so the card and its recovery footer mount, then shape the
+ // toggle. Click it so the card and its recovery footer mount, then serialize the
// whole row (the card lives inside ToolRow's body wrapper).
const grepRow = document.querySelector('[data-tool="grep"]')!
act(() => { fireEvent.click(grepRow.querySelector('[data-expandable]') ?? grepRow) })
diff --git a/apps/web/tests/seeded-history.e2e.ts b/apps/web/tests/seeded-history.e2e.ts
index 14de0e8d8f..9ec978fb97 100644
--- a/apps/web/tests/seeded-history.e2e.ts
+++ b/apps/web/tests/seeded-history.e2e.ts
@@ -3,12 +3,11 @@
// else covers: sidebar cold listing, the implicit resume/attach inside the
// history RPC, history-page tool views, and the client's log-ordered transcript
// events — with ZERO model calls in replay (no replay fixture; a stray stream
-// fails loud on the open llm seam). The cold session also carries the one
-// keyless command-row surfaces: the seeded manual `/compact` lifecycle folds
-// into its checkpoint, while an Access-chip pick later runs `/permission` on
-// the host. The seed is a recorded
-// fixture under the
-// same record discipline as every other: DSH_SNAPSHOT=record drives the turn
+// fails loud on the open llm seam). The cold session also carries keyless
+// command-row surfaces: the seeded manual `/compact` lifecycle folds into its
+// checkpoint, an Access-chip pick later runs `/permission` on the host, and
+// `/feedback` pins its expandable correlation ids. The seed is a recorded
+// fixture under the same record discipline as every other: DSH_SNAPSHOT=record drives the turn
// live through the composer (real read tool against seeded workspace files)
// and harvests seed.jsonl; replay/refresh seed it cold and only render.
import { readFile, writeFile, mkdir } from 'node:fs/promises'
@@ -31,9 +30,9 @@ import { newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/seeded-history', import.meta.url))
const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
const UI_EXPECTED = fileURLToPath(new URL('./snapshots/seeded-history/ui.expected.md', import.meta.url))
-// The command-row golden: the same conversation after one /permission switch,
-// which is the only surface that shows a settled command row's copy.
+// Command-row goldens over the same conversation after direct host commands.
const COMMAND_ROW_EXPECTED = fileURLToPath(new URL('./snapshots/seeded-history/command-row.expected.md', import.meta.url))
+const FEEDBACK_ROW_EXPECTED = fileURLToPath(new URL('./snapshots/seeded-history/feedback-row.expected.md', import.meta.url))
const MODE = webSnapshotMode()
const SEED_ID = 'seeded-history-web-e2e'
@@ -102,7 +101,7 @@ function withCompaction(raw: string, meter: TokenMeterService): string {
})
// Load-bearing exactness: the projections subtract this count verbatim, so
// it must equal what the host's fold prices for these nodes. The estimator
- // prices message CONTENT only, so a minimal wrapper per storage shape is
+ // prices message CONTENT only, so a minimal wrapper for each stored event format is
// exact — pre-identity rows carry bare `content` (the persistence read path
// upgrades them), a current row carries the full `message` envelope.
const priceRow = (row: (typeof events)[number]): number => {
@@ -169,7 +168,7 @@ function withCompaction(raw: string, meter: TokenMeterService): string {
},
})
// The persistence seed helper requires a terminal turn/end. Keep the manual
- // command standalone, then add a closed zero-step fixture boundary after it.
+ // command standalone, then add a closed zero-step turn after it.
const closureTurn = lastTurn + 1
at({ type: 'turn/start', data: { turn: closureTurn } })
at({ type: 'turn/end', data: { turn: closureTurn, reason: { kind: 'completed' } } })
@@ -186,8 +185,8 @@ describe('web e2e: seeded history renders through cold resume', () => {
scaffold = await launchWebScaffold({})
// The workspace-aware flow runs sessions in /workspace
// (the composer's default draft name); the read-tool targets must live in
- // that session cwd. Pre-creating the directory is safe: create-by-name
- // adopts an existing directory.
+ // that session cwd. Pre-creating the directory is safe because the picker
+ // adopts an existing directory by path.
const sessionCwd = join(scaffold.workspaceCwd, 'workspace')
await mkdir(sessionCwd, { recursive: true })
await writeFile(join(sessionCwd, 'a.txt'), 'alpha\n')
@@ -195,10 +194,12 @@ describe('web e2e: seeded history renders through cold resume', () => {
if (MODE !== 'record') {
const raw = await readFile(SEED, 'utf8')
expect(fixtureUserPrompts(raw), 'seed fixture must carry exactly the drive prompt').toEqual([PROMPT])
+ // The meter is host-plane — it takes no configuration and keys every
+ // fold by Session — so pricing fixture content needs no agent at all.
const meter = scaffold.ctx.get('tokenMeter')
- if (meter === undefined) throw new Error('seeded-history requires the composed token meter')
- const realized = realizeSeedFixture(scaffold, raw, SEED_ID)
- await seedSession(scaffold, withCompaction(realized, meter), SEED_ID)
+ if (meter === undefined) throw new Error('seeded-history requires the host token meter')
+ const realizedWithCompaction = withCompaction(realizeSeedFixture(scaffold, raw, SEED_ID), meter)
+ await seedSession(scaffold, realizedWithCompaction, SEED_ID)
}
browser = await chromium.launch()
page = await newEnglishPage(browser)
@@ -245,10 +246,15 @@ describe('web e2e: seeded history renders through cold resume', () => {
const projections = body.result.value?.projections
expect(projections).toBeDefined()
expect(projections?.asOfSeq).toBeGreaterThanOrEqual(0)
- // The seed carries a session/title event: the title unit must serve it.
+ // The seed carries a session/title event: the title unit is host-plane, so
+ // it folds the detached log and serves the value with nothing composed.
expect(typeof projections?.values.title).toBe('string')
- // tool-todo is composed but the seed has no todo/write: whole-value null,
- // key PRESENT (absence would mean the unit never registered).
+ // `todos` IS here, as its empty fold (null). Its unit is registered by
+ // `tool-todo` inside the default preset's STANDING mount, which the read
+ // itself ensures — deterministically, not because some unrelated session
+ // happens to be composed. A present-but-null key is what keeps the
+ // client's "omitted key = capability absent → clear the row" rule from
+ // wiping preset-owned projections on cold reads.
expect(projections?.values).toHaveProperty('todos', null)
})
@@ -428,6 +434,44 @@ describe('web e2e: seeded history renders through cold resume', () => {
await compareOrRefreshGolden(COMMAND_ROW_EXPECTED, snapshot, MODE)
}, 60_000)
+ it.skipIf(MODE === 'record')('reports full feedback correlation ids in an expandable two-line row', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-seeded-feedback-row'))
+ const previousDshHome = process.env.DSH_HOME
+ process.env.DSH_HOME = scaffold.harnessHome
+ try {
+ const input = page.locator('textarea').first()
+ await input.fill('/feedback the diff view is unreadable')
+ await input.press('Enter')
+ const row = page.locator('[data-variant="others"]').filter({
+ hasText: `Feedback recorded for session ${SEED_ID}`,
+ })
+ await row.waitFor({ timeout: 10_000 })
+ const disclosure = row.locator('[data-expandable]')
+ expect(await disclosure.getAttribute('aria-expanded')).toBe('false')
+ await disclosure.click()
+ await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('true')
+
+ const agent = scaffold.ctx.agents.get(SessionId(SEED_ID))
+ if (agent === undefined) throw new Error('seeded session did not attach an agent')
+ const done = agent.session.events.filter(event => event.type === 'command/done').at(-1)
+ if (done?.type !== 'command/done') throw new Error('feedback command did not settle')
+ const [sessionLine, userLine, extraLine] = done.data.text?.split('\n') ?? []
+ expect(sessionLine).toBe(`Feedback recorded for session ${SEED_ID}`)
+ expect(userLine).toMatch(/^User: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\./i)
+ expect(extraLine).toBeUndefined()
+ const userId = userLine?.match(/^User: ([0-9a-f-]+)/i)?.[1]
+ if (userId === undefined) throw new Error('feedback command omitted the user id')
+
+ const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
+ .split(SEED_ID).join('{{seededId}}')
+ .split(userId).join('{{userId}}')
+ await compareOrRefreshGolden(FEEDBACK_ROW_EXPECTED, snapshot, MODE)
+ } finally {
+ if (previousDshHome === undefined) delete process.env.DSH_HOME
+ else process.env.DSH_HOME = previousDshHome
+ }
+ }, 60_000)
+
it.skipIf(MODE === 'record')('fits short logged context without a scrollport', async () => {
const agent = scaffold.ctx.agents.get(SessionId(SEED_ID))
if (agent === undefined) throw new Error('seeded session did not attach an agent')
@@ -455,6 +499,6 @@ describe('web e2e: seeded history renders through cold resume', () => {
// stream would have failed the turn loudly. Cleanliness pins the wire.
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
- await assertFixtureInventory(SNAPSHOT_DIR, ['command-row.expected.md', 'seed.jsonl', 'ui.expected.md'])
+ await assertFixtureInventory(SNAPSHOT_DIR, ['command-row.expected.md', 'feedback-row.expected.md', 'seed.jsonl', 'ui.expected.md'])
})
})
diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts
index c49b1ecc1f..a3b387b660 100644
--- a/apps/web/tests/settings-chrome.e2e.ts
+++ b/apps/web/tests/settings-chrome.e2e.ts
@@ -1,11 +1,10 @@
// Web e2e scenarios: the settings surface — the modal shell (trigger, nav,
// section switching, both close paths), the Appearance preference row (the
-// real theme gesture — click 深色 and the whole cascade runs: ThemeService preference -> localStorage dsh.theme
+// real theme gesture — click 深色 and the whole cascade runs: ThemeService preference -> Host settings
// -> theme/change -> ui-layout's presenter -> body attribute -> alias token +
// browser theme-color metadata)
-// the Language row (settings-scoped localization + persisted dsh.locale),
-// the busy-state Enter preference, plus Permission as the persisted default
-// for subsequently created sessions.
+// the Language row and busy-state Enter preference (both Host-backed), plus
+// Permission as the persisted default for subsequently created sessions.
// Zero model calls: everything is pure client + persistence state on a blank
// frame, so there is no fixture and a stray stream would fail loud on the
// open llm seam.
@@ -153,23 +152,85 @@ describe('web e2e: settings modal and General preferences', () => {
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
- it('flips the theme through the Appearance cubes and persists across reload', async () => {
+ it('uses the persisted dark preference while plugins are still loading', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-boot-theme'))
+ await page.emulateMedia({ colorScheme: 'light' })
+ await page.getByRole('button', { name: '设置', exact: true }).click()
+ const initialDialog = page.getByRole('dialog', { name: '设置' })
+ const darkCube = initialDialog.getByRole('button', { name: '深色' })
+ await darkCube.click()
+ await expect.poll(() => darkCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
+ .toMatch(/ui-theme:\n\s+preference: dark/)
+ await page.keyboard.press('Escape')
+
+ // Hold real plugin bundles so the shell-owned loading page remains observable.
+ const pluginPattern = '**/plugins/**'
+ let releaseBundles = (): void => {}
+ const bundlesReleased = new Promise((resolve) => { releaseBundles = resolve })
+ await page.route(pluginPattern, async (route) => {
+ await bundlesReleased
+ await route.continue()
+ })
+
+ const warningStart = tripwire.warnings.length
+ let reload: ReturnType | undefined
+ try {
+ reload = page.reload({ waitUntil: 'domcontentloaded' })
+ const loading = page.getByText('Loading plugins…', { exact: true })
+ await loading.waitFor({ timeout: 10_000 })
+ const state = await loading.evaluate((element) => {
+ const boot = element.parentElement?.parentElement
+ if (boot === undefined || boot === null) throw new Error('loading hint is detached from the boot page')
+ return {
+ attr: document.body.hasAttribute('data-ds-dark-theme'),
+ background: getComputedStyle(boot).backgroundColor,
+ colorScheme: document.documentElement.style.colorScheme,
+ }
+ })
+ expect(state).toEqual({
+ attr: true,
+ background: 'rgb(21, 21, 23)',
+ colorScheme: 'dark',
+ })
+ } finally {
+ releaseBundles()
+ await reload
+ await page.unroute(pluginPattern)
+ }
+
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ acknowledgeReloadConnectionLoss(tripwire, warningStart)
+ await page.getByRole('button', { name: '设置', exact: true }).click()
+ const restoredDialog = page.getByRole('dialog', { name: '设置' })
+ const systemCube = restoredDialog.getByRole('button', { name: '跟随系统' })
+ await systemCube.click()
+ await expect.poll(() => systemCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
+ await expect.poll(() => page.evaluate(() => document.body.hasAttribute('data-ds-dark-theme')), {
+ timeout: 5_000,
+ }).toBe(false)
+ await page.keyboard.press('Escape')
+ expect(tripwire.pageErrors).toEqual([])
+ }, 90_000)
+
+ it('flips the theme through the Appearance cubes and persists across reload and a distinct port', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-appearance'))
interface ThemeState {
attr: boolean
background: string
- stored: string | null
+ /** Pre-migration localStorage key; the Host-backed world never writes it. */
+ legacy: string | null
themeColor: string | null
themeColorCount: number
token: string
}
- const readState = async (): Promise => await page.evaluate(() => {
+ const readState = async (target: Page = page): Promise => await target.evaluate(() => {
const metas = document.head.querySelectorAll('meta[name="theme-color"]')
const computed = getComputedStyle(document.body)
return {
attr: document.body.hasAttribute('data-ds-dark-theme'),
background: computed.backgroundColor,
- stored: localStorage.getItem('dsh.theme'),
+ legacy: localStorage.getItem('dsh.theme'),
themeColor: metas[0]?.content ?? null,
themeColorCount: metas.length,
token: computed.getPropertyValue('--dsw-alias-bg-base').trim(),
@@ -193,27 +254,51 @@ describe('web e2e: settings modal and General preferences', () => {
const darkCube = dialog.getByRole('button', { name: '深色' })
expect(await darkCube.getAttribute('aria-pressed')).toBe('false')
await darkCube.click()
- // The full cascade: pressed state, persisted preference, body attribute,
+ // The full cascade: pressed state, Host-backed preference, body attribute,
// alias token flip — all from one real user gesture.
await expect.poll(() => darkCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
const dark = await readState()
expect(dark.attr).toBe(true)
- expect(dark.stored).toBe('dark')
+ expect(dark.legacy).toBeNull()
expect(dark.token).not.toBe(light.token)
expectThemeColorSynchronized(dark)
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
+ .toMatch(/ui-theme:\n\s+preference: dark/)
await page.keyboard.press('Escape')
- // Reload: the preference survives boot (restore + presenter initial apply).
+ // Reload: the preference survives the background Host read + presenter update.
const warningStart = tripwire.warnings.length
await page.reload({ waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
acknowledgeReloadConnectionLoss(tripwire, warningStart)
await page.emulateMedia({ colorScheme: 'light' })
+ await expect.poll(async () => (await readState()).attr, { timeout: 5_000 }).toBe(true)
const reloaded = await readState()
- expect(reloaded.attr).toBe(true)
- expect(reloaded.stored).toBe('dark')
+ expect(reloaded.legacy).toBeNull()
expectThemeColorSynchronized(reloaded)
+ // A second live Host binds another ephemeral port but shares the same
+ // user-settings home. Its fresh origin has no theme localStorage and still
+ // converges to dark before the settings dialog opens.
+ const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome })
+ const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
+ const secondTripwire = watchConsole(secondPage)
+ try {
+ expect(second.baseUrl).not.toBe(scaffold.baseUrl)
+ await secondPage.emulateMedia({ colorScheme: 'light' })
+ await secondPage.goto(second.baseUrl, { waitUntil: 'load' })
+ await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ await expect.poll(async () => (await readState(secondPage)).attr, { timeout: 5_000 }).toBe(true)
+ const secondState = await readState(secondPage)
+ expect(secondState.legacy).toBeNull()
+ expectThemeColorSynchronized(secondState)
+ expect(secondTripwire.pageErrors).toEqual([])
+ expect(secondTripwire.warnings).toEqual([])
+ } finally {
+ await secondPage.close()
+ await second.close()
+ }
+
// `system` follows the emulated OS scheme (dark stays dark, light clears).
await page.getByRole('button', { name: '设置', exact: true }).click()
const systemCube = page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '跟随系统' })
@@ -233,7 +318,7 @@ describe('web e2e: settings modal and General preferences', () => {
expect(tripwire.pageErrors).toEqual([])
}, 90_000)
- it('persists the busy-state Enter behavior across reload and restores Queue', async () => {
+ it('persists the busy-state Enter behavior across reload and a distinct port', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-enter-behavior'))
await page.getByRole('button', { name: '设置', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '设置' })
@@ -241,7 +326,9 @@ describe('web e2e: settings modal and General preferences', () => {
await dialog.getByRole('button', { name: '排队发送' }).click()
await page.getByRole('menuitem', { name: '插话发送' }).click()
await dialog.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
- expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBe('steer')
+ expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
+ .toMatch(/ui-conversation:\n\s+busyEnter: steer/)
await page.keyboard.press('Escape')
const warningStart = tripwire.warnings.length
@@ -251,15 +338,36 @@ describe('web e2e: settings modal and General preferences', () => {
await page.getByRole('button', { name: '设置', exact: true }).click()
const reloaded = page.getByRole('dialog', { name: '设置' })
await reloaded.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
+
+ const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome })
+ const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
+ const secondTripwire = watchConsole(secondPage)
+ try {
+ expect(second.baseUrl).not.toBe(scaffold.baseUrl)
+ await secondPage.goto(second.baseUrl, { waitUntil: 'load' })
+ await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ await secondPage.getByRole('button', { name: '设置', exact: true }).click()
+ await secondPage.getByRole('dialog', { name: '设置' })
+ .getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
+ expect(await secondPage.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
+ expect(secondTripwire.pageErrors).toEqual([])
+ expect(secondTripwire.warnings).toEqual([])
+ } finally {
+ await secondPage.close()
+ await second.close()
+ }
+
await reloaded.getByRole('button', { name: '插话发送' }).click()
await page.getByRole('menuitem', { name: '排队发送' }).click()
await reloaded.getByRole('button', { name: '排队发送' }).waitFor({ timeout: 10_000 })
- expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBe('queue')
+ expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
+ .toMatch(/ui-conversation:\n\s+busyEnter: queue/)
await page.keyboard.press('Escape')
expect(tripwire.pageErrors).toEqual([])
}, 90_000)
- it('switches the settings surface language and persists dsh.locale', async () => {
+ it('persists the settings language across reload and a distinct port', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-language'))
await page.getByRole('button', { name: '设置', exact: true }).click()
const zhDialog = page.getByRole('dialog', { name: '设置' })
@@ -276,7 +384,9 @@ describe('web e2e: settings modal and General preferences', () => {
await enDialog.waitFor({ timeout: 10_000 })
expect(await enDialog.getByRole('button', { name: 'General' }).getAttribute('aria-current')).toBe('true')
await expect.poll(() => enDialog.getByText('Appearance', { exact: true }).count(), { timeout: 5_000 }).toBe(1)
- expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBe('en')
+ expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
+ .toMatch(/locale:\n\s+preference: en/)
// Reload keeps English; then restore zh so shared page state (and the
// other specs' 设置-anchored selectors + goldens) see the default again.
const warningStart = tripwire.warnings.length
@@ -285,24 +395,47 @@ describe('web e2e: settings modal and General preferences', () => {
acknowledgeReloadConnectionLoss(tripwire, warningStart)
const enTrigger = page.getByRole('button', { name: 'Settings' })
await enTrigger.waitFor({ timeout: 10_000 })
+
+ // A Chinese browser on another port still receives the explicit English
+ // preference from the shared Host settings document.
+ const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome })
+ const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
+ const secondTripwire = watchConsole(secondPage)
+ try {
+ expect(second.baseUrl).not.toBe(scaffold.baseUrl)
+ await secondPage.goto(second.baseUrl, { waitUntil: 'load' })
+ await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ await secondPage.getByRole('button', { name: 'Settings', exact: true }).click()
+ await secondPage.getByRole('dialog', { name: 'Settings' })
+ .getByRole('button', { name: 'English' }).waitFor({ timeout: 10_000 })
+ expect(await secondPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
+ expect(secondTripwire.pageErrors).toEqual([])
+ expect(secondTripwire.warnings).toEqual([])
+ } finally {
+ await secondPage.close()
+ await second.close()
+ }
+
await enTrigger.click()
await page.getByRole('dialog', { name: 'Settings' }).getByRole('button', { name: 'English' }).click()
await page.getByRole('menuitem', { name: '中文' }).click()
await page.getByRole('dialog', { name: '设置' }).waitFor({ timeout: 10_000 })
- expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBe('zh')
+ expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
+ .toMatch(/locale:\n\s+preference: zh/)
await page.keyboard.press('Escape')
expect(tripwire.pageErrors).toEqual([])
}, 90_000)
it('opens an English browser in English without any stored preference', async () => {
- // A second page under a different browser language: nothing is persisted
- // for it, so the settings surface must follow the browser rather than the
- // product fallback the shared zh page shows.
+ // A fresh Host home has no locale preference, so its surface follows the
+ // browser rather than the product fallback.
+ const fresh = await launchWebScaffold({})
const enPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'en-US' })
const enTripwire = watchConsole(enPage)
onTestFailed(() => saveFailureShot(enPage, 'web-e2e-settings-browser-language'))
try {
- await enPage.goto(scaffold.baseUrl, { waitUntil: 'load' })
+ await enPage.goto(fresh.baseUrl, { waitUntil: 'load' })
await enPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
expect(await enPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
await enPage.getByRole('button', { name: 'Settings', exact: true }).click()
@@ -315,6 +448,7 @@ describe('web e2e: settings modal and General preferences', () => {
expect(enTripwire.warnings).toEqual([])
} finally {
await enPage.close()
+ await fresh.close()
}
}, 90_000)
diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts
index 11bbdcfd82..ee00353ef8 100644
--- a/apps/web/tests/shipped-composition.e2e.ts
+++ b/apps/web/tests/shipped-composition.e2e.ts
@@ -5,6 +5,7 @@
// surface itself.
import { tmpdir } from 'node:os'
import { afterEach, expect, it } from 'vitest'
+import { CallId } from '@deepseek-ai/dsh-llm'
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
import { SessionId } from '@deepseek-ai/dsh-session'
// Empty type imports carry the tools/sandboxPolicy/approval Context merges.
@@ -12,6 +13,7 @@ import type {} from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-sandbox-policy'
import type {} from '@deepseek-ai/dsh-user-approval'
import type {} from '@deepseek-ai/dsh-permission'
+import type {} from '@deepseek-ai/dsh-agent-presets'
import type {} from '@deepseek-ai/dsh-commands'
import { launchWebScaffold, type WebScaffold } from './scaffold.ts'
@@ -34,9 +36,9 @@ const EXPECTED_TOOLS = [
'list_agents',
'ralph',
'read',
+ 'read_image',
'send_message',
'skill',
- 'str_replace_editor',
'subagent',
'subagent_fork',
'task_kill',
@@ -66,15 +68,30 @@ afterEach(async () => {
it('assembles the shipped Web catalog with the confined access default', async () => {
scaffold = await launchWebScaffold()
- const names = scaffold.ctx.tools.schemas().map(schema => schema.name).sort()
- expect(names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TOOLS)
- // The packaged ripgrep binary ships with the dependency, so the pair is a
- // fixed roster member on every host.
- expect(names.filter(name => RIPGREP_TOOLS.includes(name))).toEqual(RIPGREP_TOOLS)
+ const ctx = scaffold.ctx
+ // The catalog belongs to an AGENT, not to the process: every model-facing row
+ // now lives in a preset mounted under one session's scope, so the global
+ // layer holds nothing and a caller must name the agent to see anything. This
+ // composes from the deployment default — what a session that names no preset
+ // gets — which is the shape this test has always been about.
+ expect(ctx.tools.schemas().map(schema => schema.name)).toEqual([])
+ const handle = await ctx.agents.create({
+ sessionId: SessionId('shipped-composition'),
+ setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined),
+ })
+ try {
+ const names = ctx.tools.schemas(handle.agent).map(schema => schema.name).sort()
+ expect(names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TOOLS)
+ // The packaged ripgrep binary ships with the dependency, so the pair is a
+ // fixed roster member on every host.
+ expect(names.filter(name => RIPGREP_TOOLS.includes(name))).toEqual(RIPGREP_TOOLS)
+ } finally {
+ await handle.dispose()
+ }
// `workspace-write` is not "the workspace and nothing else": the shared roots
// helper always admits the temp directories too. Pinning it against an
// explicit mode keeps the claim independent of this surface's default, and
- // keeps a future boundary test from being run inside /tmp — where an
+ // keeps a future sandbox-confinement test from being run inside /tmp — where an
// "escape" write succeeds by design and reads as a sandbox failure.
expect(writableRoots(scaffold.ctx.sandboxPolicy.resolve({ mode: 'workspace-write' }))).toEqual(
expect.arrayContaining([canonicalPath('/tmp'), canonicalPath(tmpdir())]),
@@ -83,17 +100,78 @@ it('assembles the shipped Web catalog with the confined access default', async (
expect(scaffold.ctx.approval.config.policy).toBe('ask')
expect(scaffold.ctx.permission.defaultPreset).toBe('workspace-write')
- const handle = await scaffold.ctx.agents.create({
+ const commandHandle = await scaffold.ctx.agents.create({
sessionId: SessionId('shipped-command-catalog'),
meta: { cwd: scaffold.workspaceCwd },
agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
})
try {
- expect(scaffold.ctx.commands.list(handle.agent)).toContainEqual({
+ expect(scaffold.ctx.commands.list(commandHandle.agent)).toContainEqual({
name: 'feedback',
description: 'record feedback about this session',
input: { hint: '' },
})
+ } finally {
+ await commandHandle.dispose()
+ }
+}, 120_000)
+
+it('lets a preset producer reach the background-task registry', async () => {
+ scaffold = await launchWebScaffold()
+ const ctx = scaffold.ctx
+ const handle = await ctx.agents.create({
+ sessionId: SessionId('shipped-background-task'),
+ meta: { cwd: scaffold.workspaceCwd },
+ setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined),
+ })
+ try {
+ const signal = new AbortController().signal
+ // `tool-bash` is a preset row and `tasks` is a host registry; the producer
+ // resolves it with `ctx.get`, so a registry hidden behind a preset realm
+ // fails here — with every task control still listed in the catalog above.
+ const started = await ctx.tools.execute({
+ signal,
+ callId: CallId('shipped-bash-background'),
+ name: 'bash',
+ arguments: {
+ command: 'printf SHIPPED_BACKGROUND_OK',
+ description: 'shipped background probe',
+ run_in_background: true,
+ },
+ agent: handle.agent,
+ })
+ expect({ isError: started.isError, content: started.content }).toEqual({
+ isError: false,
+ content: [{ type: 'text', text: 'started background task bash-1' }],
+ })
+
+ // The controller reads what the producer started: same registry, one
+ // owner. A per-preset registry would list nothing here even on success.
+ const listed = await ctx.tools.execute({
+ signal,
+ callId: CallId('shipped-task-list'),
+ name: 'task_list',
+ arguments: {},
+ agent: handle.agent,
+ })
+ expect(listed.isError).toBe(false)
+ expect(listed.content).toEqual([
+ { type: 'text', text: expect.stringContaining('bash-1 [bash]') as unknown as string },
+ ])
+
+ // The full round trip: the output a host-plane producer wrote is collected
+ // through a preset-plane control, which is the linkage the realm severed.
+ const collected = await ctx.tools.execute({
+ signal,
+ callId: CallId('shipped-task-output'),
+ name: 'task_output',
+ arguments: { task_id: 'bash-1', wait: true },
+ agent: handle.agent,
+ })
+ expect(collected.isError).toBe(false)
+ expect(collected.content).toEqual([
+ { type: 'text', text: expect.stringContaining('SHIPPED_BACKGROUND_OK') as unknown as string },
+ ])
} finally {
await handle.dispose()
}
diff --git a/apps/web/tests/smoke-real.e2e.ts b/apps/web/tests/smoke-real.e2e.ts
index b0acf074b4..87001230d1 100644
--- a/apps/web/tests/smoke-real.e2e.ts
+++ b/apps/web/tests/smoke-real.e2e.ts
@@ -27,7 +27,7 @@ import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { REPO_ROOT, connectFreshWorkspace, newEnglishPage, probeFreePort, requireDist, saveFailureShot } from './support.ts'
-const DEVELOPMENT_PROMPT = fileURLToPath(new URL('./snapshots/web-runtime-context/development-prompt.expected.md', import.meta.url))
+const WEB_SURFACE_PROMPT = fileURLToPath(new URL('./snapshots/web-runtime-context/web-surface-prompt.expected.md', import.meta.url))
function waitForReadyLine(child: ChildProcess): Promise {
return new Promise((resolveReady, reject) => {
@@ -187,7 +187,7 @@ describe('dsh web keyless CLI smoke', () => {
}
})
- it('routes --dev runtime context and workspace instructions through the real CLI request', async () => {
+ it('routes web runtime context and workspace instructions through the real CLI request', async () => {
requireDist()
const workspace = mkdtempSync(join(tmpdir(), 'dsh-web-workspace-'))
mkdirSync(join(workspace, '.git'))
@@ -226,7 +226,7 @@ describe('dsh web keyless CLI smoke', () => {
const tsxLoader = pathToFileURL(createRequire(join(REPO_ROOT, 'package.json')).resolve('tsx')).href
const child = spawn(
process.execPath,
- ['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', '0', '--dev'],
+ ['--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', '0'],
{
cwd: workspace,
env: {
@@ -261,7 +261,7 @@ describe('dsh web keyless CLI smoke', () => {
const workspaceMessage = captured.messages?.find(message =>
message.role === 'user' && message.content?.includes('web-workspace-context-probe'))
const systemMessage = captured.messages?.find(message => message.role === 'system')
- const expectedWebSection = readFileSync(DEVELOPMENT_PROMPT, 'utf8').trimEnd()
+ const expectedWebSection = readFileSync(WEB_SURFACE_PROMPT, 'utf8').trimEnd()
.replace('{{webUrl}}', baseUrl)
expect(systemMessage?.content).toContain(expectedWebSection)
expect(workspaceMessage).toMatchInlineSnapshot(`
@@ -478,17 +478,20 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
requireDist()
sessionsDir = mkdtempSync(join(tmpdir(), 'dsh-web-w5-'))
const port = await probeFreePort()
- // tsx boot mirrors demo:web — lib/ may be unbuilt in this worktree. Isolate
+ // tsx boot mirrors the runtime half of the root dsh script. Isolate
// the host-level Harness and shared-agent homes inside the temp world; tsx
// also needs the repo's loader and tsconfig paths pointed at explicitly.
const tsxLoader = pathToFileURL(createRequire(join(REPO_ROOT, 'package.json')).resolve('tsx')).href
child = spawn(
process.execPath,
[
- '--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', String(port),
+ '--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web',
+ // Launcher flags come first: the first token the launcher does not own
+ // starts the web app's own arguments.
// Pin the in-browser picker: the shipped `-auto` row would resolve to
// the native OS chooser on this bind, and no page can drive that.
'--patch', fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)),
+ '--port', String(port),
],
{
cwd: sessionsDir,
diff --git a/apps/web/tests/snapshots/agent-preset-authoring/copy-dialog.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/copy-dialog.expected.md
new file mode 100644
index 0000000000..dc4045c5cc
--- /dev/null
+++ b/apps/web/tests/snapshots/agent-preset-authoring/copy-dialog.expected.md
@@ -0,0 +1,14 @@
+- dialog "复制预设 · 复制自 极简模式":
+ - heading "复制预设 · 复制自 极简模式" [level=2]
+ - button "关闭":
+ - img
+ - paragraph: 整个预设会在本机复制一份。标识符将成为目录名,事后无法更改;其余内容之后直接在预设自己的文件里编辑。
+ - text: 标识符
+ - textbox "标识符":
+ - /placeholder: my-agent
+ - text: 名称
+ - textbox "名称":
+ - /placeholder: 选择器中显示的名字,缺省用标识符
+ - alert: 请填写标识符。
+ - button "取消"
+ - button "创建" [disabled]
diff --git a/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md
new file mode 100644
index 0000000000..0ddc4dd77b
--- /dev/null
+++ b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md
@@ -0,0 +1,81 @@
+- dialog "设置":
+ - navigation:
+ - text: 设置
+ - button "通用设置":
+ - img
+ - text: 通用设置
+ - button "模型":
+ - img
+ - text: 模型
+ - button "Agent 预设":
+ - img
+ - text: Agent 预设
+ - button "打开配置文件"
+ - button "关闭":
+ - img
+ - text: 关闭
+ - heading "Agent 预设" [level=2]
+ - paragraph: 预设即一个会话的 Agent 所运行的插件组装 —— 它的工具、提示词与能力。复制一份既有预设改成自己的,或用「创造模式」让 Agent 帮你创建。
+ - heading "内置" [level=3]
+ - list:
+ - listitem:
+ - 'button "当前使用: 标准模式" [disabled] [pressed]':
+ - text: 标准模式 内置 当前使用 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
+ - code: standard
+ - 'button "查看: 标准模式"':
+ - img
+ - text: 查看
+ - 'button "复制: 标准模式"':
+ - img
+ - text: 复制
+ - listitem:
+ - 'button "设为默认: 代码模式"':
+ - text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
+ - code: code
+ - 'button "查看: 代码模式"':
+ - img
+ - text: 查看
+ - 'button "复制: 代码模式"':
+ - img
+ - text: 复制
+ - listitem:
+ - 'button "设为默认: 极简模式"':
+ - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
+ - code: minimal
+ - 'button "查看: 极简模式"':
+ - img
+ - text: 查看
+ - 'button "复制: 极简模式"':
+ - img
+ - text: 复制
+ - listitem:
+ - 'button "设为默认: 创造模式"':
+ - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
+ - code: cordis
+ - 'button "查看: 创造模式"':
+ - img
+ - text: 查看
+ - 'button "复制: 创造模式"':
+ - img
+ - text: 复制
+ - heading "自定义" [level=3]
+ - list:
+ - listitem:
+ - 'button "设为默认: 我的模式"':
+ - text: 我的模式 自定义 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
+ - code: my-agent
+ - 'button "查看路径: 我的模式"':
+ - img
+ - text: 查看路径
+ - 'button "复制: 我的模式"':
+ - img
+ - text: 复制
+ - 'button "删除: 我的模式"':
+ - img
+ - text: 删除
+ - paragraph:
+ - text: 预设文件:
+ - code: {{presetRoot}}/my-agent
+ - button "用「创造模式」创作自定义预设":
+ - img
+ - text: 用「创造模式」创作自定义预设
diff --git a/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md
new file mode 100644
index 0000000000..f853cacf24
--- /dev/null
+++ b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md
@@ -0,0 +1,93 @@
+- dialog "设置":
+ - navigation:
+ - text: 设置
+ - button "通用设置":
+ - img
+ - text: 通用设置
+ - button "模型":
+ - img
+ - text: 模型
+ - button "Agent 预设":
+ - img
+ - text: Agent 预设
+ - button "打开配置文件"
+ - button "关闭":
+ - img
+ - text: 关闭
+ - heading "Agent 预设" [level=2]
+ - paragraph: 预设即一个会话的 Agent 所运行的插件组装 —— 它的工具、提示词与能力。复制一份既有预设改成自己的,或用「创造模式」让 Agent 帮你创建。
+ - heading "内置" [level=3]
+ - list:
+ - listitem:
+ - 'button "当前使用: 标准模式" [disabled] [pressed]':
+ - text: 标准模式 内置 当前使用 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
+ - code: standard
+ - 'button "查看: 标准模式"':
+ - img
+ - text: 查看
+ - 'button "复制: 标准模式"':
+ - img
+ - text: 复制
+ - listitem:
+ - 'button "设为默认: 代码模式"':
+ - text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
+ - code: code
+ - 'button "查看: 代码模式"':
+ - img
+ - text: 查看
+ - 'button "复制: 代码模式"':
+ - img
+ - text: 复制
+ - listitem:
+ - 'button "设为默认: 极简模式"':
+ - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
+ - code: minimal
+ - 'button "查看: 极简模式"':
+ - img
+ - text: 查看
+ - 'button "复制: 极简模式"':
+ - img
+ - text: 复制
+ - listitem:
+ - 'button "设为默认: 创造模式"':
+ - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
+ - code: cordis
+ - 'button "查看: 创造模式"':
+ - img
+ - text: 查看
+ - 'button "复制: 创造模式"':
+ - img
+ - text: 复制
+ - heading "自定义" [level=3]
+ - list:
+ - listitem:
+ - 'button "加载失败: broken-yaml" [disabled]':
+ - text: broken-yaml 加载失败 自定义 暂无描述。
+ - alert: "the composition is not valid YAML: unexpected end of the stream within a flow collection (3:1)"
+ - code: broken-yaml
+ - 'button "查看路径: broken-yaml"':
+ - img
+ - text: 查看路径
+ - 'button "复制: broken-yaml" [disabled]':
+ - img
+ - text: 预设加载失败,不能复制
+ - 'button "删除: broken-yaml"':
+ - img
+ - text: 删除
+ - listitem:
+ - 'button "加载失败: 幽灵预设" [disabled]':
+ - text: 幽灵预设 加载失败 自定义 composition 已被手动删除。
+ - alert: the composition file agent.cordis.yml is missing — the directory still occupies the id; delete it or restore the file
+ - code: ghost
+ - 'button "查看路径: 幽灵预设"':
+ - img
+ - text: 查看路径
+ - 'button "复制: 幽灵预设" [disabled]':
+ - img
+ - text: 预设加载失败,不能复制
+ - 'button "删除: 幽灵预设"':
+ - img
+ - text: 删除
+ - button "用「创造模式」创作自定义预设":
+ - img
+ - text: 用「创造模式」创作自定义预设
diff --git a/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md
new file mode 100644
index 0000000000..ad2bf86389
--- /dev/null
+++ b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md
@@ -0,0 +1,64 @@
+- dialog "设置":
+ - navigation:
+ - text: 设置
+ - button "通用设置":
+ - img
+ - text: 通用设置
+ - button "模型":
+ - img
+ - text: 模型
+ - button "Agent 预设":
+ - img
+ - text: Agent 预设
+ - button "打开配置文件"
+ - button "关闭":
+ - img
+ - text: 关闭
+ - heading "Agent 预设" [level=2]
+ - paragraph: 预设即一个会话的 Agent 所运行的插件组装 —— 它的工具、提示词与能力。复制一份既有预设改成自己的,或用「创造模式」让 Agent 帮你创建。
+ - heading "内置" [level=3]
+ - list:
+ - listitem:
+ - 'button "当前使用: 标准模式" [disabled] [pressed]':
+ - text: 标准模式 内置 当前使用 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
+ - code: standard
+ - 'button "查看: 标准模式"':
+ - img
+ - text: 查看
+ - 'button "复制: 标准模式"':
+ - img
+ - text: 复制
+ - listitem:
+ - 'button "设为默认: 代码模式"':
+ - text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
+ - code: code
+ - 'button "查看: 代码模式"':
+ - img
+ - text: 查看
+ - 'button "复制: 代码模式"':
+ - img
+ - text: 复制
+ - listitem:
+ - 'button "设为默认: 极简模式"':
+ - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
+ - code: minimal
+ - 'button "查看: 极简模式"':
+ - img
+ - text: 查看
+ - 'button "复制: 极简模式"':
+ - img
+ - text: 复制
+ - listitem:
+ - 'button "设为默认: 创造模式"':
+ - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
+ - code: cordis
+ - 'button "查看: 创造模式"':
+ - img
+ - text: 查看
+ - 'button "复制: 创造模式"':
+ - img
+ - text: 复制
+ - heading "自定义" [level=3]
+ - button "用「创造模式」创作自定义预设":
+ - img
+ - text: 用「创造模式」创作自定义预设
diff --git a/apps/web/tests/snapshots/agent-preset-selection/header.expected.md b/apps/web/tests/snapshots/agent-preset-selection/header.expected.md
new file mode 100644
index 0000000000..5a78c6461e
--- /dev/null
+++ b/apps/web/tests/snapshots/agent-preset-selection/header.expected.md
@@ -0,0 +1,7 @@
+- navigation "Session hierarchy":
+ - button "Seeded turn" [disabled]
+- img
+- text: Minimal mode
+- button "1 subagent":
+ - text: 1 subagent
+ - img
diff --git a/apps/web/tests/snapshots/agent-preset-selection/hero.expected.md b/apps/web/tests/snapshots/agent-preset-selection/hero.expected.md
new file mode 100644
index 0000000000..a320fa3e2f
--- /dev/null
+++ b/apps/web/tests/snapshots/agent-preset-selection/hero.expected.md
@@ -0,0 +1,8 @@
+- button "Choose workspace":
+ - img
+ - text: workspace
+ - img
+- button "Standard mode":
+ - img
+ - text: Standard mode
+ - img
diff --git a/apps/web/tests/snapshots/agent-preset-selection/menu.expected.md b/apps/web/tests/snapshots/agent-preset-selection/menu.expected.md
new file mode 100644
index 0000000000..7e0d1ae032
--- /dev/null
+++ b/apps/web/tests/snapshots/agent-preset-selection/menu.expected.md
@@ -0,0 +1,7 @@
+- menu:
+ - menuitem "Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.":
+ - text: Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.
+ - img
+ - menuitem "Code mode All Standard mode capabilities, with tools exposed through the Code Mode SDK so the model can combine multi-step operations in one TypeScript program."
+ - menuitem "Minimal mode Two-tool coding agent with persistent bash and str_replace_editor."
+ - menuitem "Creator mode Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance."
diff --git a/apps/web/tests/snapshots/background-task-list/running.expected.md b/apps/web/tests/snapshots/background-task-list/running.expected.md
new file mode 100644
index 0000000000..1adaa965e2
--- /dev/null
+++ b/apps/web/tests/snapshots/background-task-list/running.expected.md
@@ -0,0 +1,2 @@
+- list "Background tasks":
+ - listitem: bash sleep 45 running {{duration}}
diff --git a/apps/web/tests/snapshots/background-task-list/settled.expected.md b/apps/web/tests/snapshots/background-task-list/settled.expected.md
new file mode 100644
index 0000000000..c97ff5026c
--- /dev/null
+++ b/apps/web/tests/snapshots/background-task-list/settled.expected.md
@@ -0,0 +1,2 @@
+- list "Background tasks":
+ - listitem: "bash sleep 45 signal: SIGTERM {{duration}}"
diff --git a/apps/web/tests/snapshots/code-mode-round/ui.expected.md b/apps/web/tests/snapshots/code-mode-round/ui.expected.md
index 99b6bac89b..9d9ea8ab0e 100644
--- a/apps/web/tests/snapshots/code-mode-round/ui.expected.md
+++ b/apps/web/tests/snapshots/code-mode-round/ui.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- 'button "Using ONE run_code program: run" [disabled]'
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md
index 72d0a79756..42197364ae 100644
--- a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md
+++ b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Use only Cordis tools. First" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/feedback-command/ack.expected.md b/apps/web/tests/snapshots/feedback-command/ack.expected.md
new file mode 100644
index 0000000000..89d40acb3b
--- /dev/null
+++ b/apps/web/tests/snapshots/feedback-command/ack.expected.md
@@ -0,0 +1,39 @@
+- banner:
+ - navigation "Session hierarchy":
+ - button "Reply with the single word" [disabled]
+ - img
+ - text: Standard mode
+ - tablist:
+ - tab "Chat" [selected]
+ - tab "Trajectory"
+- text: Reply with the single word LIGHTHOUSE and stop. {{clock}}
+- button "Copy":
+ - img
+- button "Context injection @deepseek-ai/dsh-system-prompt":
+ - img
+ - img
+ - text: Context injection @deepseek-ai/dsh-system-prompt
+- button "Think The user wants me to reply with a single word. Let me comply.":
+ - img
+ - img
+ - text: Think The user wants me to reply with a single word. Let me comply.
+- paragraph: LIGHTHOUSE
+- button "Copy":
+ - img
+- button "Branch into a new conversation":
+ - img
+- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
+- 'button "feedback Feedback recorded for session session-{{uuid}} User: {{uuid}}. Session sharing is enabled."':
+ - img
+ - img
+ - text: "feedback Feedback recorded for session session-{{uuid}} User: {{uuid}}. Session sharing is enabled."
+- textbox "Message the agent"
+- button "Commands":
+ - img
+- 'button "Access mode, current: Workspace Write"': Workspace Write
+- button "Select model, current DeepSeek-V4-Flash":
+ - text: DeepSeek-V4-Flash
+ - img
+- button "6% of context used"
+- button "Send message" [disabled]
+- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 7.8K tok · Output 21 tok
diff --git a/apps/web/tests/snapshots/feedback-command/session.jsonl b/apps/web/tests/snapshots/feedback-command/session.jsonl
new file mode 100644
index 0000000000..d528f36c0e
--- /dev/null
+++ b/apps/web/tests/snapshots/feedback-command/session.jsonl
@@ -0,0 +1,17 @@
+{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785015039278,"cwd":"{{cwd}}/workspace"}
+{"type":"turn/start","seq":0,"time":1785015039291,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
+{"type":"user/message","seq":1,"time":1785015039292,"data":{"content":[{"type":"text","text":"Reply with the single word LIGHTHOUSE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
+{"type":"session/title","seq":2,"time":1785015039294,"data":{"title":"Reply with the single word","messageSeqs":[1],"source":{"kind":"fallback"}}}
+{"type":"step/start","seq":3,"time":1785015039362,"data":{"turn":1,"step":1}}
+{"type":"request/header","seq":4,"time":1785015039363,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
+{"type":"assistant/chunk","seq":5,"time":1785015039930,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":6,"time0":1785015039930,"data":{"turn":1,"step":1,"index":0,"dt":[162,28,1,0,0,46,1,0,0,0,11,0,0,30],"texts":["The"," user"," wants"," me"," to"," reply"," with"," a"," single"," word","."," Let"," me"," comply","."]}}
+{"type":"assistant/chunk","seq":21,"time":1785015040209,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"text-chunks","seq0":22,"time0":1785015040209,"data":{"turn":1,"step":1,"index":1,"dt":[1,0,30,1],"texts":["L","IGH","TH","O","USE"]}}
+{"type":"assistant/chunk","seq":27,"time":1785015040241,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with a single word. Let me comply."}}}}
+{"type":"assistant/chunk","seq":28,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"LIGHTHOUSE"}}}}
+{"type":"assistant/chunk","seq":29,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15}}}}
+{"type":"assistant/chunk","seq":30,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":31,"time":1785015040244,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with a single word. Let me comply."},{"type":"text","text":"LIGHTHOUSE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30],"surfaceOp":"append"}
+{"type":"step/end","seq":32,"time":1785015040246,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":33,"time":1785015040247,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/apps/web/tests/snapshots/fresh-round-trip/system-prompt.expected.md b/apps/web/tests/snapshots/fresh-round-trip/system-prompt.expected.md
index e2113428e1..651695a5f6 100644
--- a/apps/web/tests/snapshots/fresh-round-trip/system-prompt.expected.md
+++ b/apps/web/tests/snapshots/fresh-round-trip/system-prompt.expected.md
@@ -2,6 +2,6 @@ You are an AI agent powered by the DeepSeek Harness SDK.
The DeepSeek Harness implementation checkout is at {{sourceRoot}}. The checkout location and current working directory are separate values and may differ; never infer the working directory from this path. Use pwd to determine the current working directory. Use this checkout only to inspect or extend DSH itself.
-You are interacting with the user through the DeepSeek Harness Web GUI at {{webUrl}}. When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. The browser provides no implicit DOM, route, or screenshot context. This Web process was launched without `--dev`, so HMR is inactive: rebuild the affected Web artifacts and verify this existing URL after a page refresh. If the user wants no-refresh client-plugin updates, explain that this GUI must be restarted with `dsh web --dev` and `pnpm run dev:web` must also run from this same checkout; do not present either command alone as sufficient. Starting another server does not update this GUI. The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. Do not start a replacement server unless the user asks; if one is needed, use a managed background task and verify its exact URL.
+You are interacting with the user through the DeepSeek Harness Web GUI at {{webUrl}}. When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. The browser provides no implicit DOM, route, or screenshot context. The client-plugin HMR receiver is active, but client-plugin changes reload without a refresh only while `pnpm run dev:web` is also running from this same checkout to rebuild their bundles; verify that watcher before promising automatic updates. Every other change — the apps/web shell and plain packages — requires rebuilding the affected Web artifacts and verifying this existing URL after a page refresh. Starting another server does not update this GUI. The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. Do not start a replacement server unless the user asks; if one is needed, use a managed background task and verify its exact URL.
You are a coding agent powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.
diff --git a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md
index 92183ee6ea..d360f936bd 100644
--- a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md
+++ b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Use the bash tool to" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/goal-command-presentation/ui.expected.md b/apps/web/tests/snapshots/goal-command-presentation/ui.expected.md
new file mode 100644
index 0000000000..e3f026066f
--- /dev/null
+++ b/apps/web/tests/snapshots/goal-command-presentation/ui.expected.md
@@ -0,0 +1,21 @@
+- banner:
+ - navigation "Session hierarchy":
+ - button "workspace" [disabled]
+ - img
+ - text: Standard mode
+ - tablist:
+ - tab "Chat" [selected]
+ - tab "Trajectory"
+- group "Command input": /goal
+- 'button "goal No goal is currently set. Usage: /goal [|clear|edit |pause|resume]"':
+ - img
+ - img
+ - text: "goal No goal is currently set. Usage: /goal [|clear|edit |pause|resume]"
+- textbox "Message the agent"
+- button "Commands":
+ - img
+- 'button "Access mode, current: Workspace Write"': Workspace Write
+- button "Select model, current DeepSeek-V4-Flash":
+ - text: DeepSeek-V4-Flash
+ - img
+- button "Send message" [disabled]
diff --git a/apps/web/tests/snapshots/goal-multi-turn-actions/ui.expected.md b/apps/web/tests/snapshots/goal-multi-turn-actions/ui.expected.md
index b1c0cb52ca..737bd5b591 100644
--- a/apps/web/tests/snapshots/goal-multi-turn-actions/ui.expected.md
+++ b/apps/web/tests/snapshots/goal-multi-turn-actions/ui.expected.md
@@ -1,9 +1,12 @@
- banner:
- navigation "Session hierarchy":
- button "workspace" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
+- group "Command input": /goal 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的
- 'button "goal Goal created Status: active Objective: 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的 Rounds: 0/256 Activation: armed Commands: /goal edit , /goal pause, /goal clear"':
- img
- img
diff --git a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md
index dfa23ca508..c666681b0b 100644
--- a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md
+++ b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md
@@ -25,6 +25,10 @@
- img
- text: workspace
- img
+- button "Standard mode":
+ - img
+ - text: Standard mode
+ - img
- textbox "Describe what you want to build"
- button "Commands":
- img
diff --git a/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md
index 4ccab18ac7..2f9c701936 100644
--- a/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md
+++ b/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md
@@ -25,6 +25,10 @@
- img
- text: workspace
- img
+- button "Standard mode":
+ - img
+ - text: Standard mode
+ - img
- textbox "Describe what you want to build"
- button "Commands":
- img
diff --git a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md
index bf32465f2b..19283ae51d 100644
--- a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md
+++ b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Reply with the single word" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/live-interactions/cancel.expected.md b/apps/web/tests/snapshots/live-interactions/cancel.expected.md
index 01a8343313..4fa0394693 100644
--- a/apps/web/tests/snapshots/live-interactions/cancel.expected.md
+++ b/apps/web/tests/snapshots/live-interactions/cancel.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md
index f75432e2e4..83be4dc961 100644
--- a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md
+++ b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/live-interactions/loading.expected.md b/apps/web/tests/snapshots/live-interactions/loading.expected.md
index 6c36405064..c475c05e05 100644
--- a/apps/web/tests/snapshots/live-interactions/loading.expected.md
+++ b/apps/web/tests/snapshots/live-interactions/loading.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/live-interactions/retry.expected.md b/apps/web/tests/snapshots/live-interactions/retry.expected.md
index a281ca26b2..7bb61c5b27 100644
--- a/apps/web/tests/snapshots/live-interactions/retry.expected.md
+++ b/apps/web/tests/snapshots/live-interactions/retry.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/message-feedback-protocol/protocol.expected.json b/apps/web/tests/snapshots/message-feedback-protocol/protocol.expected.json
new file mode 100644
index 0000000000..302e4876df
--- /dev/null
+++ b/apps/web/tests/snapshots/message-feedback-protocol/protocol.expected.json
@@ -0,0 +1,203 @@
+[
+ {
+ "endpoint": "/api/messageFeedback/put",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol",
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "invalid-rating",
+ "ifVersion": null
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-invalid",
+ "result": {
+ "ok": false,
+ "error": {
+ "code": "internal",
+ "message": "typert gateway: messageFeedback/put: wire field \"request\" failed boundary validation",
+ "details": {}
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/list",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol"
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-list-empty",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "items": []
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/put",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol",
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "positive",
+ "note": "Useful answer",
+ "ifVersion": null
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-put",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "positive",
+ "note": "Useful answer",
+ "version": "{{version}}",
+ "createdAt": "{{timestamp}}",
+ "updatedAt": "{{timestamp}}"
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/list",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol"
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-list-created",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "items": [
+ {
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "positive",
+ "note": "Useful answer",
+ "version": "{{version}}",
+ "createdAt": "{{timestamp}}",
+ "updatedAt": "{{timestamp}}"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/put",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol",
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "negative",
+ "ifVersion": null
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-conflict",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": false,
+ "error": {
+ "code": "version-conflict",
+ "current": {
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "positive",
+ "note": "Useful answer",
+ "version": "{{version}}",
+ "createdAt": "{{timestamp}}",
+ "updatedAt": "{{timestamp}}"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/delete",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol",
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "ifVersion": "{{version}}"
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-delete",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "absent": true
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/list",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol"
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-list-deleted",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "items": []
+ }
+ }
+ }
+ }
+ }
+]
diff --git a/apps/web/tests/snapshots/message-feedback-protocol/session.jsonl b/apps/web/tests/snapshots/message-feedback-protocol/session.jsonl
new file mode 100644
index 0000000000..d970fb8cb4
--- /dev/null
+++ b/apps/web/tests/snapshots/message-feedback-protocol/session.jsonl
@@ -0,0 +1,7 @@
+{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1786406400000,"cwd":"{{cwd}}"}
+{"type":"turn/start","seq":0,"time":1786406400001,"data":{"turn":1}}
+{"type":"user/message","seq":1,"time":1786406400002,"data":{"role":"user","content":[{"type":"text","text":"Give one useful answer."}],"source":{"kind":"user"},"id":"22222222-2222-4222-8222-222222222222"},"surfaceOp":"append"}
+{"type":"step/start","seq":2,"time":1786406400003,"data":{"turn":1,"step":1}}
+{"type":"assistant/message","seq":3,"time":1786406400004,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"A useful answer."}],"source":{"kind":"model","provider":"fixture","model":"fixture"},"id":"11111111-1111-4111-8111-111111111111"},"usage":{"inputTokens":4,"outputTokens":4}},"surfaceOp":"append"}
+{"type":"step/end","seq":4,"time":1786406400005,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":5,"time":1786406400006,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/apps/web/tests/snapshots/core-web-profile/session.jsonl b/apps/web/tests/snapshots/minimal-preset/session.jsonl
similarity index 73%
rename from apps/web/tests/snapshots/core-web-profile/session.jsonl
rename to apps/web/tests/snapshots/minimal-preset/session.jsonl
index 04f0d62d15..49977be802 100644
--- a/apps/web/tests/snapshots/core-web-profile/session.jsonl
+++ b/apps/web/tests/snapshots/minimal-preset/session.jsonl
@@ -1,7 +1,7 @@
-{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785974400000,"cwd":"{{cwd}}"}
-{"type":"user/message","seq":0,"time":1785974400001,"data":{"content":[{"type":"text","text":"Reply exactly CORE_WEB_REQUEST_OK and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
+{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785974400000,"cwd":"{{cwd}}","agentPreset":"minimal"}
+{"type":"user/message","seq":0,"time":1785974400001,"data":{"content":[{"type":"text","text":"Reply exactly MINIMAL_PRESET_REQUEST_OK and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":1,"time":1785974400002,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":2,"time":1785974400003,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CORE_WEB_REQUEST_OK"}}}
-{"type":"assistant/chunk","seq":3,"time":1785974400004,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CORE_WEB_REQUEST_OK"}}}}
+{"type":"assistant/chunk","seq":2,"time":1785974400003,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"MINIMAL_PRESET_REQUEST_OK"}}}
+{"type":"assistant/chunk","seq":3,"time":1785974400004,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}}}}
{"type":"assistant/chunk","seq":4,"time":1785974400005,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}}}
{"type":"assistant/chunk","seq":5,"time":1785974400006,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
diff --git a/apps/web/tests/snapshots/models-settings/configured.expected.md b/apps/web/tests/snapshots/models-settings/configured.expected.md
index 036630c2d5..02746c7f76 100644
--- a/apps/web/tests/snapshots/models-settings/configured.expected.md
+++ b/apps/web/tests/snapshots/models-settings/configured.expected.md
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
+ - button "Agent 预设":
+ - img
+ - text: Agent 预设
- button "打开配置文件"
- button "关闭":
- img
diff --git a/apps/web/tests/snapshots/models-settings/declared-edit.expected.md b/apps/web/tests/snapshots/models-settings/declared-edit.expected.md
new file mode 100644
index 0000000000..e36c7ff2e8
--- /dev/null
+++ b/apps/web/tests/snapshots/models-settings/declared-edit.expected.md
@@ -0,0 +1,65 @@
+- dialog "设置":
+ - navigation:
+ - text: 设置
+ - button "通用设置":
+ - img
+ - text: 通用设置
+ - button "模型":
+ - img
+ - text: 模型
+ - button "Agent 预设":
+ - img
+ - text: Agent 预设
+ - button "打开配置文件"
+ - button "关闭":
+ - img
+ - text: 关闭
+ - heading "模型" [level=2]
+ - paragraph: 填入各提供方的 API 密钥即可使用其模型。
+ - list:
+ - listitem:
+ - text: minimax-cn
+ - img "API 密钥已配置"
+ - button "编辑 minimax-cn": 编辑
+ - button "删除 minimax-cn": 删除
+ - listitem:
+ - text: Acme Gateway 自定义
+ - button "编辑 Acme Gateway (acme-gateway)": 编辑
+ - button "删除 Acme Gateway (acme-gateway)": 删除
+ - text: Acme Gateway acme-gateway API 密钥
+ - textbox "API 密钥":
+ - /placeholder: 输入 API 密钥,或留空使用环境认证
+ - group:
+ - text: 自定义设置 显示名称
+ - textbox "显示名称":
+ - /placeholder: acme-gateway
+ - text: Acme Gateway
+ - text: API 地址
+ - textbox "API 地址":
+ - /placeholder: https://gateway.acme.example/v1
+ - text: https://gateway.acme.example/v1
+ - text: API 协议
+ - combobox "API 协议":
+ - option "openai-completions" [selected]
+ - option "openai-responses"
+ - option "anthropic-messages"
+ - region "模型目录":
+ - text: 模型目录 已自定义模型目录
+ - button "恢复默认模型"
+ - button "获取可用模型"
+ - textbox "模型 ID 1":
+ - /placeholder: 模型 ID
+ - text: acme-large
+ - textbox "显示名称 1":
+ - /placeholder: 显示名称
+ - button "容量 1"
+ - button "删除模型 1"
+ - button "添加模型"
+ - button "取消"
+ - button "保存"
+ - button "添加提供方":
+ - img
+ - text: 添加提供方
+ - button "添加自定义提供方":
+ - img
+ - text: 添加自定义提供方
diff --git a/apps/web/tests/snapshots/models-settings/declared.expected.md b/apps/web/tests/snapshots/models-settings/declared.expected.md
index df47e186c3..857bfaf13e 100644
--- a/apps/web/tests/snapshots/models-settings/declared.expected.md
+++ b/apps/web/tests/snapshots/models-settings/declared.expected.md
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
+ - button "Agent 预设":
+ - img
+ - text: Agent 预设
- button "打开配置文件"
- button "关闭":
- img
diff --git a/apps/web/tests/snapshots/models-settings/empty.expected.md b/apps/web/tests/snapshots/models-settings/empty.expected.md
index ab0a25b780..03169f8e72 100644
--- a/apps/web/tests/snapshots/models-settings/empty.expected.md
+++ b/apps/web/tests/snapshots/models-settings/empty.expected.md
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
+ - button "Agent 预设":
+ - img
+ - text: Agent 预设
- button "打开配置文件"
- button "关闭":
- img
diff --git a/apps/web/tests/snapshots/navigation-panes/trajectory.expected.md b/apps/web/tests/snapshots/navigation-panes/trajectory.expected.md
index a9b5dbb982..3476255bab 100644
--- a/apps/web/tests/snapshots/navigation-panes/trajectory.expected.md
+++ b/apps/web/tests/snapshots/navigation-panes/trajectory.expected.md
@@ -2,6 +2,7 @@
- button "Use actual duration": Duration
- button "Collapse turns": Turns
- button "Collapse calls": Calls
+ - button "Export session log": Export
- img
- searchbox "Search trajectory"
- region "Trajectory timeline":
diff --git a/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md b/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md
index 931caf0acb..e50c347966 100644
--- a/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md
+++ b/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
+ - button "Agent 预设":
+ - img
+ - text: Agent 预设
- button "打开配置文件"
- button "关闭":
- img
diff --git a/apps/web/tests/snapshots/plan-review/approved.expected.md b/apps/web/tests/snapshots/plan-review/approved.expected.md
index c1cae54bb5..23664b4a1d 100644
--- a/apps/web/tests/snapshots/plan-review/approved.expected.md
+++ b/apps/web/tests/snapshots/plan-review/approved.expected.md
@@ -1,11 +1,13 @@
- banner:
- navigation "Session hierarchy":
- 'button "Plan a small change: add" [disabled]'
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
- img
-- text: "plan Plan mode on. Use /plan off to leave. Interjection Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop. {{clock}}"
+- text: "plan Plan mode on. Use /plan off to leave. Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop. {{clock}}"
- button "Copy":
- img
- button "Context injection @deepseek-ai/dsh-system-prompt":
diff --git a/apps/web/tests/snapshots/question-composer/answered.expected.md b/apps/web/tests/snapshots/question-composer/answered.expected.md
index a524a02e23..d15b2af3a1 100644
--- a/apps/web/tests/snapshots/question-composer/answered.expected.md
+++ b/apps/web/tests/snapshots/question-composer/answered.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Use the ask_user_question tool to" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/queue-actions/collapsed.expected.md b/apps/web/tests/snapshots/queue-actions/collapsed.expected.md
index 18b40d976a..5438f084ae 100644
--- a/apps/web/tests/snapshots/queue-actions/collapsed.expected.md
+++ b/apps/web/tests/snapshots/queue-actions/collapsed.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
@@ -14,7 +16,7 @@
- paragraph: partial
- status: Deep diving...
- button "2 queued messages"
-- textbox "Message the agent"
+- textbox "Cmd/Ctrl+Enter steers all queued messages"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
diff --git a/apps/web/tests/snapshots/queue-actions/editing.expected.md b/apps/web/tests/snapshots/queue-actions/editing.expected.md
index 7dc4f38f86..186f0b0169 100644
--- a/apps/web/tests/snapshots/queue-actions/editing.expected.md
+++ b/apps/web/tests/snapshots/queue-actions/editing.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
@@ -30,7 +32,7 @@
- tooltip "Save queued message"
- button "Cancel editing":
- img
-- textbox "Message the agent"
+- textbox "Cmd/Ctrl+Enter steers all queued messages"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
diff --git a/apps/web/tests/snapshots/queue-actions/layout.expected.md b/apps/web/tests/snapshots/queue-actions/layout.expected.md
index 7370a15264..49db39a1a5 100644
--- a/apps/web/tests/snapshots/queue-actions/layout.expected.md
+++ b/apps/web/tests/snapshots/queue-actions/layout.expected.md
@@ -1,9 +1,12 @@
- banner:
- navigation "Session hierarchy":
- button "workspace" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
+- group "Command input": /goal Keep the composer context panels aligned
- 'button "goal Goal created Status: active Objective: Keep the composer context panels aligned Rounds: 0/256 Activation: armed Commands: /goal edit , /goal pause, /goal clear"':
- img
- img
@@ -29,7 +32,7 @@
- button "Clear goal":
- img
- button "2 queued messages"
-- textbox "Message the agent"
+- textbox "Cmd/Ctrl+Enter steers all queued messages"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
diff --git a/apps/web/tests/snapshots/queue-actions/preserved.expected.md b/apps/web/tests/snapshots/queue-actions/preserved.expected.md
index e1b1cf9084..54410743dc 100644
--- a/apps/web/tests/snapshots/queue-actions/preserved.expected.md
+++ b/apps/web/tests/snapshots/queue-actions/preserved.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/queue-actions/ui.expected.md b/apps/web/tests/snapshots/queue-actions/ui.expected.md
index 0d9ae5fcf3..1467c50d97 100644
--- a/apps/web/tests/snapshots/queue-actions/ui.expected.md
+++ b/apps/web/tests/snapshots/queue-actions/ui.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
@@ -23,7 +25,7 @@
- img
- button "Steer queued message":
- img
-- textbox "Message the agent"
+- textbox "Cmd/Ctrl+Enter steers all queued messages"
- button "Commands":
- img
- 'button "Access mode, current: Workspace Write"': Workspace Write
diff --git a/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md b/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md
new file mode 100644
index 0000000000..6928b95777
--- /dev/null
+++ b/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md
@@ -0,0 +1,53 @@
+- banner:
+ - navigation "Session hierarchy":
+ - button "Use the read tool twice" [disabled]
+ - tablist:
+ - tab "Chat" [selected]
+ - tab "Trajectory"
+- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. 7/25 {{clock}}"
+- button "Copy":
+ - img
+- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.":
+ - img
+ - img
+ - text: Think The user wants me to read a.txt and b.txt, then reply with "DONE". Let me do both reads in parallel.
+- button "Read a.txt":
+ - img
+ - img
+ - text: Read
+ - button "a.txt"
+- button "Read b.txt":
+ - img
+ - img
+ - text: Read
+ - button "b.txt"
+- button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed.":
+ - img
+ - img
+ - text: Think Both files have been read. a.txt contains "alpha" and b.txt contains "beta". I'll now reply with DONE as instructed.
+- paragraph: DONE
+- button "Copy":
+ - img
+- button "Branch into a new conversation":
+ - img
+- text: 7/25 {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
+- button "compact Compacted 5 history items (~{{tokens}} tokens)"
+- button "Context injection AGENTS.md":
+ - img
+ - img
+ - text: Context injection AGENTS.md
+- img
+- text: permission preset read-only
+- 'button "feedback Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured." [expanded]':
+ - img
+ - text: "feedback Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured."
+- text: "Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured."
+- textbox "Message the agent"
+- button "Commands":
+ - img
+- 'button "Access mode, current: Read Only"': Read Only
+- button "Select model, current DeepSeek-V4-Flash":
+ - text: DeepSeek-V4-Flash
+ - img
+- button "Send message" [disabled]
+- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 98% Input 15.8K tok · Output 135 tok
diff --git a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md
index f358ff26f5..cf87f5acbd 100644
--- a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md
+++ b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md
@@ -7,10 +7,17 @@
- button "模型":
- img
- text: 模型
+ - button "Agent 预设":
+ - img
+ - text: Agent 预设
- button "打开配置文件"
- button "关闭":
- img
- text: 关闭
+ - text: Agent 预设 对此后新建的会话生效。运行中的会话保持它开始时的预设。
+ - button "标准模式":
+ - text: 标准模式
+ - img
- text: 权限 选择新会话的默认权限模式
- button "Workspace Write":
- text: Workspace Write
diff --git a/apps/web/tests/snapshots/skill-user-invoke/ui.expected.md b/apps/web/tests/snapshots/skill-user-invoke/ui.expected.md
index c77081584a..1f1cd0ea18 100644
--- a/apps/web/tests/snapshots/skill-user-invoke/ui.expected.md
+++ b/apps/web/tests/snapshots/skill-user-invoke/ui.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "/user-invoke-demo and confirm the fixtur" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/steer-all/mid-steer.expected.md b/apps/web/tests/snapshots/steer-all/mid-steer.expected.md
new file mode 100644
index 0000000000..ce2d4a66e6
--- /dev/null
+++ b/apps/web/tests/snapshots/steer-all/mid-steer.expected.md
@@ -0,0 +1,35 @@
+- banner:
+ - navigation "Session hierarchy":
+ - button "Use the ask_user_question tool to" [disabled]
+ - img
+ - text: Standard mode
+ - tablist:
+ - tab "Chat" [selected]
+ - tab "Trajectory"
+- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
+- button "Copy":
+ - img
+- button "Context injection @deepseek-ai/dsh-system-prompt":
+ - img
+ - img
+ - text: Context injection @deepseek-ai/dsh-system-prompt
+- text: Running
+- button "Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that.":
+ - img
+ - img
+ - text: Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that.
+- status: Deep diving...
+- text: "Interjection: include the word BANANA in your final reply."
+- button "Copy":
+ - img
+- text: "Interjection: include the word ORANGE in your final reply."
+- button "Copy":
+ - img
+- textbox "Message the agent"
+- button "Commands":
+ - img
+- 'button "Access mode, current: Workspace Write"': Workspace Write
+- button "Select model, current DeepSeek-V4-Flash":
+ - text: DeepSeek-V4-Flash
+ - img
+- button "Stop generating"
diff --git a/apps/web/tests/snapshots/steer-all/replay.override.json b/apps/web/tests/snapshots/steer-all/replay.override.json
new file mode 100644
index 0000000000..6a1c133faf
--- /dev/null
+++ b/apps/web/tests/snapshots/steer-all/replay.override.json
@@ -0,0 +1,47 @@
+[
+ {
+ "kind": "chunks",
+ "chunks": [
+ { "type": "block-start", "index": 0, "blockType": "reasoning" },
+ { "type": "reasoning-delta", "index": 0, "text": "The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that." },
+ { "type": "block-start", "index": 1, "blockType": "tool-call" },
+ {
+ "type": "tool-call-delta",
+ "index": 1,
+ "id": "call_00_steer_all",
+ "name": "ask_user_question",
+ "argumentsDelta": "{\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]}"
+ },
+ {
+ "type": "block-end",
+ "index": 0,
+ "block": {
+ "type": "reasoning",
+ "text": "The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that."
+ }
+ },
+ {
+ "type": "block-end",
+ "index": 1,
+ "block": {
+ "type": "tool-call",
+ "id": "call_00_steer_all",
+ "name": "ask_user_question",
+ "arguments": "{\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]}"
+ }
+ },
+ { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 10, "cacheReadTokens": 0, "reasoningTokens": 0 } },
+ { "type": "finish", "reason": { "kind": "tool-calls" } }
+ ]
+ },
+ {
+ "kind": "chunks",
+ "chunks": [
+ { "type": "block-start", "index": 0, "blockType": "text" },
+ { "type": "text-delta", "index": 0, "text": "Got it: BANANA and ORANGE." },
+ { "type": "block-end", "index": 0, "block": { "type": "text", "text": "Got it: BANANA and ORANGE." } },
+ { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 10, "cacheReadTokens": 0, "reasoningTokens": 0 } },
+ { "type": "finish", "reason": { "kind": "stop" } }
+ ]
+ }
+]
diff --git a/apps/web/tests/snapshots/steer-all/settled.expected.md b/apps/web/tests/snapshots/steer-all/settled.expected.md
new file mode 100644
index 0000000000..b20e590686
--- /dev/null
+++ b/apps/web/tests/snapshots/steer-all/settled.expected.md
@@ -0,0 +1,45 @@
+- banner:
+ - navigation "Session hierarchy":
+ - button "Use the ask_user_question tool to" [disabled]
+ - img
+ - text: Standard mode
+ - tablist:
+ - tab "Chat" [selected]
+ - tab "Trajectory"
+- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
+- button "Copy":
+ - img
+- button "Context injection @deepseek-ai/dsh-system-prompt":
+ - img
+ - img
+ - text: Context injection @deepseek-ai/dsh-system-prompt
+- button "Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that.":
+ - img
+ - img
+ - text: Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that.
+- button "Ask question 1/1 answered":
+ - img
+ - img
+ - text: Ask question 1/1 answered
+- text: "Interjection: include the word BANANA in your final reply. {{clock}}"
+- button "Copy":
+ - img
+- text: "Interjection: include the word ORANGE in your final reply. {{clock}}"
+- button "Copy":
+ - img
+- paragraph: "Got it: BANANA and ORANGE."
+- button "Copy":
+ - img
+- button "Branch into a new conversation":
+ - img
+- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
+- textbox "Message the agent"
+- button "Commands":
+ - img
+- 'button "Access mode, current: Workspace Write"': Workspace Write
+- button "Select model, current DeepSeek-V4-Flash":
+ - text: DeepSeek-V4-Flash
+ - img
+- button "0% of context used"
+- button "Send message" [disabled]
+- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 20 tok · Output 20 tok
diff --git a/apps/web/tests/snapshots/steering/mid-steer.expected.md b/apps/web/tests/snapshots/steering/mid-steer.expected.md
index 5f3f24f709..30d4d7ac4b 100644
--- a/apps/web/tests/snapshots/steering/mid-steer.expected.md
+++ b/apps/web/tests/snapshots/steering/mid-steer.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Use the ask_user_question tool to" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
@@ -21,7 +23,7 @@
- img
- text: Ask question waiting
- status: Deep diving...
-- text: "Interjection Interjection: include the word BANANA in your final reply."
+- text: "Interjection: include the word BANANA in your final reply."
- button "Copy":
- img
- region "Ready to continue?":
diff --git a/apps/web/tests/snapshots/steering/settled.expected.md b/apps/web/tests/snapshots/steering/settled.expected.md
index d598613fa3..93c311cce4 100644
--- a/apps/web/tests/snapshots/steering/settled.expected.md
+++ b/apps/web/tests/snapshots/steering/settled.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Use the ask_user_question tool to" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
@@ -19,7 +21,7 @@
- img
- img
- text: Ask question 1/1 answered
-- text: "Interjection Interjection: include the word BANANA in your final reply. {{clock}}"
+- text: "Interjection: include the word BANANA in your final reply. {{clock}}"
- button "Copy":
- img
- button "Think The user selected \"Yes\" and wants me to include the word \"BANANA\" in my final reply. Let me acknowledge their answer.":
diff --git a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md
index 27c7ec092e..15ceebba6b 100644
--- a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md
+++ b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md
@@ -3,6 +3,8 @@
- button "Ask a research subagent to"
- text: /
- button "event-sourcing researcher" [disabled]
+ - img
+ - text: Standard mode
- button "1 subagent":
- text: 1 subagent
- img
@@ -41,7 +43,7 @@
- textbox "Message the agent"
- button "Commands":
- img
-- 'button "Access mode, current: Workspace Write"': Workspace Write
+- 'button "Access mode, current: Custom"': Custom
- button "6% of context used"
- button "Send message" [disabled]
- text: 2 turns · 2 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 15.6K tok · Output 158 tok
diff --git a/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md b/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md
index a977afbbea..8f4554e47b 100644
--- a/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md
+++ b/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md
@@ -3,6 +3,8 @@
- button "Ask a research subagent to"
- text: /
- button "event-sourcing researcher" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
@@ -18,6 +20,6 @@
- textbox "Parent session offline; sending is unavailable but you can still stop the run" [disabled]
- button "Commands" [disabled]:
- img
-- 'button "Access mode, current: Workspace Write" [disabled]': Workspace Write
+- 'button "Access mode, current: Custom" [disabled]': Custom
- button "Stop generating"
- button "Send message" [disabled]
diff --git a/apps/web/tests/snapshots/turn-tail-actions/running.expected.md b/apps/web/tests/snapshots/turn-tail-actions/running.expected.md
index 0dd1189e3c..8e5d4c5858 100644
--- a/apps/web/tests/snapshots/turn-tail-actions/running.expected.md
+++ b/apps/web/tests/snapshots/turn-tail-actions/running.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Begin your reply with the" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/turn-tail-actions/settled.expected.md b/apps/web/tests/snapshots/turn-tail-actions/settled.expected.md
index 828350b846..47203f70eb 100644
--- a/apps/web/tests/snapshots/turn-tail-actions/settled.expected.md
+++ b/apps/web/tests/snapshots/turn-tail-actions/settled.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Begin your reply with the" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/web-runtime-context/development-prompt.expected.md b/apps/web/tests/snapshots/web-runtime-context/development-prompt.expected.md
deleted file mode 100644
index 58157d4437..0000000000
--- a/apps/web/tests/snapshots/web-runtime-context/development-prompt.expected.md
+++ /dev/null
@@ -1 +0,0 @@
-You are interacting with the user through the DeepSeek Harness Web GUI at {{webUrl}}. When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. The browser provides no implicit DOM, route, or screenshot context. This Web process was launched with `dsh web --dev`, so its client-plugin HMR receiver is active. No-refresh updates occur only when `pnpm run dev:web` is also running from this same checkout to rebuild client-plugin bundles; verify that watcher before promising automatic updates. Client-plugin changes then reload automatically, while apps/web shell and other plain-package changes still require a rebuild and page refresh. Starting another server does not update this GUI. The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. Do not start a replacement server unless the user asks; if one is needed, use a managed background task and verify its exact URL.
diff --git a/apps/web/tests/snapshots/web-runtime-context/web-surface-prompt.expected.md b/apps/web/tests/snapshots/web-runtime-context/web-surface-prompt.expected.md
new file mode 100644
index 0000000000..327b34dd71
--- /dev/null
+++ b/apps/web/tests/snapshots/web-runtime-context/web-surface-prompt.expected.md
@@ -0,0 +1 @@
+You are interacting with the user through the DeepSeek Harness Web GUI at {{webUrl}}. When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. The browser provides no implicit DOM, route, or screenshot context. The client-plugin HMR receiver is active, but client-plugin changes reload without a refresh only while `pnpm run dev:web` is also running from this same checkout to rebuild their bundles; verify that watcher before promising automatic updates. Every other change — the apps/web shell and plain packages — requires rebuilding the affected Web artifacts and verifying this existing URL after a page refresh. Starting another server does not update this GUI. The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. Do not start a replacement server unless the user asks; if one is needed, use a managed background task and verify its exact URL.
diff --git a/apps/web/tests/snapshots/web-search-round/ui.expected.md b/apps/web/tests/snapshots/web-search-round/ui.expected.md
index 0281d242f4..9e86cdcf2c 100644
--- a/apps/web/tests/snapshots/web-search-round/ui.expected.md
+++ b/apps/web/tests/snapshots/web-search-round/ui.expected.md
@@ -1,6 +1,8 @@
- banner:
- navigation "Session hierarchy":
- button "Use web_search to search exactly" [disabled]
+ - img
+ - text: Standard mode
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
diff --git a/apps/web/tests/snapshots/workflow-run/ui.expected.md b/apps/web/tests/snapshots/workflow-run/ui.expected.md
new file mode 100644
index 0000000000..be377da995
--- /dev/null
+++ b/apps/web/tests/snapshots/workflow-run/ui.expected.md
@@ -0,0 +1,32 @@
+- text: "Use the workflow tool exactly once, with args omitted, meta set to { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }, and this EXACT script body (copy it verbatim): phase('Run') const reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.') return { reply } After the workflow returns, reply with the single word WORKFLOW_DONE and stop. Do not use any other tool. {{clock}}"
+- button "Copy":
+ - img
+- button "Context injection @deepseek-ai/dsh-system-prompt":
+ - img
+ - img
+ - text: Context injection @deepseek-ai/dsh-system-prompt
+- button "Think The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:":
+ - img
+ - img
+ - text: "Think The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:"
+- button "Tool call workflow ·":
+ - img
+ - img
+ - text: Tool call workflow ·
+- button "snapshot-flow 1 member Completed" [expanded]:
+ - img
+ - text: snapshot-flow 1 member Completed
+- button "Run 1 member Completed 1" [expanded]:
+ - img
+ - text: Run 1 member Completed 1
+- text: Reply with exactly the word WF_CHILD_OK and not… Completed
+- button "Think The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop.":
+ - img
+ - img
+ - text: Think The workflow returned successfully with the reply "WF_CHILD_OK". Now I need to reply with exactly "WORKFLOW_DONE" and stop.
+- paragraph: WORKFLOW_DONE
+- button "Copy":
+ - img
+- button "Branch into a new conversation":
+ - img
+- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
diff --git a/apps/web/tests/startup-auto-selection.e2e.ts b/apps/web/tests/startup-auto-selection.e2e.ts
index bf5f920458..bbb064c119 100644
--- a/apps/web/tests/startup-auto-selection.e2e.ts
+++ b/apps/web/tests/startup-auto-selection.e2e.ts
@@ -17,11 +17,10 @@
// replacing those nodes.
//
// The round-trip against a loopback host is far too fast to observe, so this
-// scenario HOLDS the `session.history` response open at the browser's network
-// boundary and asserts the visible frame while it is in flight. That gate is
-// what makes the assertions non-vacuous: without the phase exemption, the
-// held window is exactly when `settling` would be painted and the composer
-// hidden.
+// scenario HOLDS the `session.history` response open in the browser's network
+// handler and asserts the visible frame while it is in flight. That wait is
+// what makes the assertions non-vacuous: without the phase exemption, the held
+// window is exactly when `settling` would be painted and the composer hidden.
//
// Zero model calls: registering a workspace and opening its blank session are
// host RPCs with no model involvement. A stray stream would fail loud with
diff --git a/apps/web/tests/steering.e2e.ts b/apps/web/tests/steering.e2e.ts
index 6c96f9b6aa..01b3a85b56 100644
--- a/apps/web/tests/steering.e2e.ts
+++ b/apps/web/tests/steering.e2e.ts
@@ -34,6 +34,18 @@ const REPLAY_PACE_MS = 100
const PROMPT = 'Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop.'
const STEER = 'Interjection: include the word BANANA in your final reply.'
+// Empty-draft flush scenario: an override-only fixture. The whole-script
+// replacement answers both model calls of a FRESH session (no recorded
+// session.jsonl exists — call 0 keeps the turn open with a question-tool
+// call, call 1 is the reply after both steerings drain).
+const STEER_ALL_DIR = fileURLToPath(new URL('./snapshots/steer-all', import.meta.url))
+const STEER_ALL_FIXTURE = join(STEER_ALL_DIR, 'session.jsonl')
+const STEER_ALL_OVERRIDE = join(STEER_ALL_DIR, 'replay.override.json')
+const STEER_ALL_MID = join(STEER_ALL_DIR, 'mid-steer.expected.md')
+const STEER_ALL_SETTLED = join(STEER_ALL_DIR, 'settled.expected.md')
+const STEER_ONE = 'Interjection: include the word BANANA in your final reply.'
+const STEER_TWO = 'Interjection: include the word ORANGE in your final reply.'
+
/** Concatenated assistant text deltas — the model-visible reply body. */
function assistantText(events: SessionEvent[]): string {
return events
@@ -278,3 +290,103 @@ describe('web e2e: composer shortcut follows the swapped busy behavior', () => {
expect(tripwire.warnings).toEqual([])
}, 90_000)
})
+
+describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
+ let scaffold: WebScaffold
+ let browser: Browser
+ let page: Page
+ let tripwire: ReturnType
+ const sessionEvents: SessionEvent[] = []
+
+ beforeAll(async () => {
+ // The scenario boots a fresh session against the override-only fixture;
+ // the replay.override.json sidecar replaces the derived script, so the
+ // (deliberately absent) session.jsonl is never read.
+ scaffold = await launchWebScaffold({
+ replayFixture: STEER_ALL_FIXTURE,
+ replayOverride: STEER_ALL_OVERRIDE,
+ paceMs: REPLAY_PACE_MS,
+ })
+ scaffold.ctx.on('session/event', (_session, event) => { sessionEvents.push(event) })
+ browser = await chromium.launch()
+ page = await newEnglishPage(browser)
+ tripwire = watchConsole(page)
+ await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ await connectFreshWorkspace(page, scaffold.workspaceCwd)
+ await page.getByText('Standard mode', { exact: true }).waitFor({ timeout: 10_000 })
+ }, 120_000)
+
+ afterAll(async () => {
+ await browser?.close()
+ await scaffold?.close()
+ })
+
+ it.skipIf(MODE === 'record')('queues two messages, then flushes both with an empty-draft Cmd+Enter', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-steer-all'))
+ const input = page.locator('textarea').first()
+ await input.waitFor({ timeout: 10_000 })
+ const settled = scaffold.whenTurnSettled(30_000)
+
+ // Call 0 streams a question-tool call; the fills must land inside the
+ // first replay window, before the question composer replaces the textarea.
+ await input.fill(PROMPT)
+ await input.press('Enter')
+ await input.fill(STEER_ONE)
+ await input.press('Enter')
+ await input.fill(STEER_TWO)
+ await input.press('Enter')
+ const dock = page.locator('[data-queue-dock]')
+ // Both messages queued: the two-row dock shows a collapsed count header,
+ // and Playwright text matching skips the hidden rows — expand the list,
+ // then assert each row's content.
+ await dock.getByText('2 queued messages').waitFor({ timeout: 10_000 })
+ await dock.getByRole('button').click()
+ await dock.getByText(STEER_ONE, { exact: true }).waitFor({ timeout: 10_000 })
+ await dock.getByText(STEER_TWO, { exact: true }).waitFor({ timeout: 10_000 })
+ expect(await page.locator('[data-pending-steering]').count()).toBe(0)
+
+ // Empty draft + Cmd+Enter: both queued rows steer in FIFO order, the dock
+ // empties, and the pending steering renders at the conversation tail.
+ await input.press('Meta+Enter')
+ await expect.poll(
+ () => page.locator('[data-pending-steering]').filter({ hasText: /BANANA|ORANGE/ }).count(),
+ { timeout: 10_000 },
+ ).toBe(2)
+ expect(await page.locator('[data-queue-dock]').count()).toBe(0)
+ // The reasoning row streams independently of the steering handoff; wait
+ // for it so the mid snapshot pins the assistant step, not the pre-render
+ // gap a fast machine can catch between steering acceptance and the block.
+ await page.locator('[data-variant="think"]').first().waitFor({ timeout: 10_000 })
+ const mid = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(STEER_ALL_MID, mid, MODE)
+
+ // Answer the question; the step closes, the loop drains both steerings
+ // into one next-step request, and the final reply obeys both markers.
+ const composer = page.locator('[data-question-key]')
+ await composer.waitFor({ timeout: 30_000 })
+ await composer.getByRole('radio', { name: 'Yes' }).click()
+ await composer.getByRole('radio', { name: 'Yes' }).press('Enter')
+ await settled
+
+ const first = claimedMessages(sessionEvents, STEER_ONE)
+ const second = claimedMessages(sessionEvents, STEER_TWO)
+ expect(first).toHaveLength(1)
+ expect(second).toHaveLength(1)
+ expect(assistantText(sessionEvents)).toContain('BANANA')
+ expect(assistantText(sessionEvents)).toContain('ORANGE')
+ await expect.poll(() => page.getByText(STEER_ONE, { exact: true }).count(), { timeout: 15_000 }).toBe(1)
+ await expect.poll(() => page.getByText(STEER_TWO, { exact: true }).count(), { timeout: 15_000 }).toBe(1)
+ expect(await page.locator('[data-pending-steering]').count()).toBe(0)
+ const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(STEER_ALL_SETTLED, snapshot, MODE)
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ }, 200_000)
+
+ it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
+ await assertFixtureInventory(STEER_ALL_DIR, [
+ 'replay.override.json', 'mid-steer.expected.md', 'settled.expected.md',
+ ])
+ })
+})
diff --git a/apps/web/tests/subagent-conversation.e2e.ts b/apps/web/tests/subagent-conversation.e2e.ts
index 324cde1227..6756b093d2 100644
--- a/apps/web/tests/subagent-conversation.e2e.ts
+++ b/apps/web/tests/subagent-conversation.e2e.ts
@@ -395,7 +395,8 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
expect([
Math.round(clickAreaBox!.x - treeBox!.x),
Math.round(treeBox!.x + treeBox!.width - clickAreaBox!.x - clickAreaBox!.width),
- ]).toEqual([5, 5])
+ // Menu padding alone insets the rows now that the border is gone.
+ ]).toEqual([4, 4])
await compareOrRefreshGolden(
BRANCHLESS_EXPECTED,
await captureStableAria(page, '[role="tree"][aria-label="Subagent sessions"]', scaffold.workspaceCwd),
@@ -446,7 +447,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
).toBe(3)
expect(await page.getByText('Ungrouped', { exact: true }).count()).toBe(0)
const hierarchy = page.getByRole('navigation', { name: 'Session hierarchy' })
- expect(await hierarchy.getByRole('button').count()).toBe(1)
+ await expect.poll(() => hierarchy.getByRole('button').count()).toBe(1)
await compareOrRefreshGolden(
FORK_EXPECTED,
await captureStableAria(page, '[role="tree"][aria-label="Sessions"]', scaffold.workspaceCwd),
diff --git a/apps/web/tests/support.ts b/apps/web/tests/support.ts
index 1b7b67aab3..ee2a1a1a62 100644
--- a/apps/web/tests/support.ts
+++ b/apps/web/tests/support.ts
@@ -18,18 +18,17 @@ export const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
export const ZH_BROWSER_LOCALE = 'zh-CN'
/**
- * Open the standard browser-test page with English selected before client
- * boot. This keeps role locators and goldens deterministic across localized
- * component migrations; the scenarios asserting the Chinese surface bypass
- * this helper and advertise {@link ZH_BROWSER_LOCALE} instead.
+ * Open the standard browser-test page advertising English before client boot.
+ * This keeps role locators and goldens deterministic while leaving the Host
+ * settings document free to override the provisional browser-derived locale;
+ * scenarios asserting the Chinese surface advertise
+ * {@link ZH_BROWSER_LOCALE} instead.
* @param browser - Playwright browser owning the page.
* @param height - Viewport height; width is fixed to the lane baseline.
* @returns the initialized page.
*/
export async function newEnglishPage(browser: Browser, height = 1000): Promise {
- const page = await browser.newPage({ viewport: { width: 1680, height } })
- await page.addInitScript(() => { localStorage.setItem('dsh.locale', 'en') })
- return page
+ return await browser.newPage({ viewport: { width: 1680, height }, locale: 'en-US' })
}
/** Fail loud on a stale checkout instead of testing yesterday's bundle. */
@@ -58,9 +57,9 @@ export function probeFreePort(): Promise {
/**
* Drive the hero's workspace picker through the composed directory dialog
* until the live composer unlocks. A fresh world has no Workspace, so the boot
- * lands in the locked view state (startup auto-selection has nothing to
+ * lands in the Workspace-trigger view state (startup auto-selection has nothing to
* select); every scenario that types into the composer must connect one
- * first. With nothing to list, the chip gesture raises the dialog directly —
+ * first. With nothing to list, activating the textarea raises the dialog directly —
* adding a workspace is the picker's only entry. The directory is staged here
* and adopted through the path editor, which is idempotent across the repeated
* connects a scenario may make; creating a folder from inside the dialog (the
@@ -74,7 +73,7 @@ export function probeFreePort(): Promise {
*/
export async function connectFreshWorkspace(page: Page, root: string, name = 'workspace'): Promise {
mkdirSync(join(root, name), { recursive: true })
- await page.getByRole('button', { name: 'Choose workspace' }).click()
+ await page.getByRole('textbox', { name: 'Choose workspace' }).click()
const dialog = page.getByRole('dialog', { name: 'Select Workspace Directory' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: 'Edit path' }).click()
@@ -98,7 +97,7 @@ export async function connectFreshWorkspace(page: Page, root: string, name = 'wo
*/
export async function connectFreshWorkspaceZh(page: Page, root: string, name = 'workspace'): Promise {
mkdirSync(join(root, name), { recursive: true })
- await page.getByRole('button', { name: '选择工作区' }).click()
+ await page.getByRole('textbox', { name: '选择工作区' }).click()
const dialog = page.getByRole('dialog', { name: '选择工作区目录' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: '编辑路径' }).click()
diff --git a/apps/web/tests/todo-row.snapshot.ts b/apps/web/tests/todo-row.snapshot.ts
index c5ded143b3..8186905f62 100644
--- a/apps/web/tests/todo-row.snapshot.ts
+++ b/apps/web/tests/todo-row.snapshot.ts
@@ -2,7 +2,7 @@
// Assembled todo snapshot: boots the real built `packages/client/*/lib/
// client.js` bundles through AppWebEntry's ModuleLoader path against the
// keyless FixtureApiClient transport, opens the fixture session, and pins the
-// two surfaces the fixture's parallel plan (turn 72, two items `in_progress`)
+// two surfaces the fixture's parallel plan (turn 73, two items `in_progress`)
// reaches — the `todo_write` tool row and the dock's plan strip.
//
// The row is pinned as three separate fields on purpose. `summary=` is the
@@ -20,7 +20,7 @@ const EXPECTED = join(process.cwd(), 'apps/web/tests/snapshots/todo-row/parallel
installAssembledBootEnv()
-/** Normalize the todo row and the plan strip to a stable text shape: the row's
+/** Normalize the todo row and the plan strip to stable text fields: the row's
* title, its truncatable summary, its non-shrinking suffix, then the panel's
* per-status header and every list item with its status. */
function todoShape(row: Element, panel: Element): string {
diff --git a/apps/web/tests/turn-tail-actions.e2e.ts b/apps/web/tests/turn-tail-actions.e2e.ts
index d68aef4700..4822a254cd 100644
--- a/apps/web/tests/turn-tail-actions.e2e.ts
+++ b/apps/web/tests/turn-tail-actions.e2e.ts
@@ -1,7 +1,7 @@
// Web e2e scenario: assistant IconActions belong to the settled answer, so
// they arrive with `turn/end` and not before. The recorded turn narrates in
-// plain text before its tool call, which is the shape that would hand the
-// footer to mid-turn narration for the seconds a tool runs and then move it
+// plain text before its tool call, which is the event order that would show the
+// footer beside mid-turn narration for the seconds a tool runs and then move it
// down. A `hang` sidecar on the SECOND model call parks the turn after the
// narration and the tool result are durable, so the running state is stable by
// construction rather than by timing; stopping from that park writes the
diff --git a/apps/web/tests/workflow-run.e2e.ts b/apps/web/tests/workflow-run.e2e.ts
new file mode 100644
index 0000000000..eafb78223f
--- /dev/null
+++ b/apps/web/tests/workflow-run.e2e.ts
@@ -0,0 +1,181 @@
+// Keyless shipped-Web acceptance for the durable workflow Conversation Node.
+// Reuses the existing recorded workflow parent/child model fixtures; the real
+// workflow tool, worker, subagent provider, Session log, browser plugin graph,
+// and navigation all execute during replay.
+import { readFile } from 'node:fs/promises'
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import type { Browser, Page } from 'playwright'
+import { chromium } from 'playwright'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
+import {
+ assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
+ fixtureUserPrompts, launchWebScaffold, watchConsole, webSnapshotMode,
+ type WebScaffold,
+} from './scaffold.ts'
+import {
+ connectFreshWorkspace, newEnglishPage, REPO_ROOT, saveFailureShot,
+} from './support.ts'
+
+const MODE = webSnapshotMode()
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/workflow-run', import.meta.url))
+const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
+const PARENT_FIXTURE = join(REPO_ROOT, 'examples/acp-agent/tests/snapshots/workflow-run/session.jsonl')
+const CHILD_FIXTURE = join(REPO_ROOT, 'examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl')
+const CHILD_PROMPT = 'Reply with exactly the word WF_CHILD_OK and nothing else.'
+
+describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () => {
+ let scaffold: WebScaffold
+ let browser: Browser
+ let page: Page
+ let tripwire: ReturnType
+ let prompt: string
+
+ const waitForParentSettlement = (): Promise => new Promise((resolve, reject) => {
+ let dispose = (): void => {}
+ dispose = scaffold.ctx.on('session/event', (session: Session, event: SessionEvent) => {
+ if (event.type !== 'turn/end' || session.header.origin === 'subagent') return
+ dispose()
+ void (async () => {
+ await scaffold.ctx.agents.get(session.id)?.whenIdle()
+ await scaffold.ctx.sessions.flush(session)
+ resolve(session.id)
+ })().catch(reject)
+ })
+ })
+
+ beforeAll(async () => {
+ const prompts = fixtureUserPrompts(await readFile(PARENT_FIXTURE, 'utf8'))
+ expect(prompts).toHaveLength(1)
+ prompt = prompts[0]!
+ scaffold = await launchWebScaffold({
+ replayFixture: PARENT_FIXTURE,
+ replayChildFixtures: [CHILD_FIXTURE],
+ paceMs: 25,
+ })
+ browser = await chromium.launch()
+ page = await newEnglishPage(browser)
+ tripwire = watchConsole(page)
+ await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ await connectFreshWorkspace(page, scaffold.workspaceCwd)
+ }, 120_000)
+
+ afterAll(async () => {
+ await browser?.close()
+ await scaffold?.close()
+ })
+
+ it('shows the live member, opens its local child, then retains the settled record beside the tool row', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-workflow-run-live'))
+ const settled = waitForParentSettlement()
+ const input = page.locator('textarea').first()
+ await input.fill(prompt)
+ await input.press('Enter')
+
+ const workflow = page.getByRole('button', { name: /^snapshot-flow/ })
+ await workflow.waitFor({ timeout: 30_000 })
+ expect(await workflow.getAttribute('aria-expanded')).toBe('true')
+ const phase = page.getByRole('button', { name: /^Run/ })
+ await phase.waitFor({ timeout: 15_000 })
+ await phase.click()
+ const member = page.getByRole('button', { name: /^Open Reply with exactly the word/ })
+ await member.waitFor({ timeout: 15_000 })
+ await member.focus()
+
+ const lightColor = await member.locator('[data-member-label]').evaluate(element => getComputedStyle(element).color)
+ await page.setViewportSize({ width: 560, height: 800 })
+ await page.evaluate(() => { document.body.setAttribute('data-ds-dark-theme', '') })
+ const darkNarrow = await page.locator('[data-workflow-run]').evaluate((element) => {
+ const panel = element as HTMLElement
+ panel.style.width = '356px'
+ const label = element.querySelector('[data-member-label]')
+ const labelWrap = element.querySelector('[data-member-label-wrap]')
+ const status = element.querySelector('[data-member-status-text]')
+ const disclosures = element.querySelectorAll('[data-disclosure-row]')
+ const runHeader = disclosures[0]
+ const phaseHeader = disclosures[1]
+ const phaseTitle = phaseHeader?.children.item(1) as HTMLElement | null
+ const phaseStatus = element.querySelector('[data-phase-status-text]')
+ const originalPhaseTitle = phaseTitle?.textContent ?? ''
+ if (phaseTitle !== null) phaseTitle.textContent = 'A phase name long enough to require ellipsis in the narrow layout'
+ const phaseTitleRight = phaseTitle?.getBoundingClientRect().right ?? 0
+ const phaseStatusLeft = phaseStatus?.getBoundingClientRect().left ?? 0
+ if (phaseTitle !== null) phaseTitle.textContent = originalPhaseTitle
+ return {
+ clientWidth: element.clientWidth,
+ scrollWidth: element.scrollWidth,
+ color: label === null ? '' : getComputedStyle(label).color,
+ decoration: label === null ? '' : getComputedStyle(label).textDecorationLine,
+ focusWidth: labelWrap === null ? '' : getComputedStyle(labelWrap).outlineWidth,
+ statusWidth: status?.getBoundingClientRect().width ?? 0,
+ statusFontSize: status === null ? '' : getComputedStyle(status).fontSize,
+ runHeight: runHeader?.getBoundingClientRect().height ?? 0,
+ phaseHeight: phaseHeader?.getBoundingClientRect().height ?? 0,
+ phaseTitleRight,
+ phaseStatusLeft,
+ }
+ })
+ expect(darkNarrow.clientWidth).toBe(356)
+ expect(darkNarrow.scrollWidth).toBeLessThanOrEqual(darkNarrow.clientWidth)
+ expect(darkNarrow.color).not.toBe(lightColor)
+ expect(darkNarrow.decoration).toContain('underline')
+ expect(Number.parseFloat(darkNarrow.focusWidth)).toBeGreaterThanOrEqual(2)
+ expect(darkNarrow.statusWidth).toBe(64)
+ expect(darkNarrow.statusFontSize).toBe('13px')
+ expect(darkNarrow.runHeight).toBe(32)
+ expect(darkNarrow.phaseHeight).toBe(32)
+ expect(darkNarrow.phaseTitleRight).toBeLessThanOrEqual(darkNarrow.phaseStatusLeft)
+ await page.locator('[data-workflow-run]').evaluate((element) => {
+ (element as HTMLElement).style.removeProperty('width')
+ document.body.removeAttribute('data-ds-dark-theme')
+ })
+ await page.setViewportSize({ width: 1280, height: 800 })
+
+ await member.click()
+ await page.getByText(CHILD_PROMPT, { exact: true }).waitFor({ timeout: 15_000 })
+
+ const sessions = page.getByRole('tree', { name: 'Sessions' })
+ await sessions.getByRole('treeitem', { name: /Use the workflow tool exactly/ }).click()
+ await settled
+
+ expect(await page.locator('[data-chat-flow-kind="tool-call"]').count()).toBeGreaterThanOrEqual(1)
+ expect(await page.locator('[data-chat-flow-kind="workflow-run"]').count()).toBe(1)
+ const terminalWorkflow = page.getByRole('button', { name: /^snapshot-flow/ })
+ await terminalWorkflow.waitFor()
+ if (await terminalWorkflow.getAttribute('aria-expanded') !== 'true') await terminalWorkflow.click()
+ const terminalPhase = page.getByRole('button', { name: /^Run/ })
+ await terminalPhase.waitFor()
+ if (await terminalPhase.getAttribute('aria-expanded') !== 'true') await terminalPhase.click()
+ await page.getByText(CHILD_PROMPT, { exact: false }).waitFor()
+ await expect.poll(
+ () => page.getByRole('button', { name: /^Open Reply with exactly the word/ }).count(),
+ { timeout: 10_000 },
+ ).toBe(0)
+ }, 90_000)
+
+ it('rebuilds the terminal record from history after reload', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-workflow-run-history'))
+ await page.reload({ waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ const workflow = page.getByRole('button', { name: /^snapshot-flow/ })
+ await workflow.waitFor({ timeout: 15_000 })
+ expect(await workflow.getAttribute('aria-expanded')).toBe('false')
+ await workflow.click()
+ const phase = page.getByRole('button', { name: /^Run/ })
+ await phase.waitFor()
+ await phase.click()
+ await page.getByText(CHILD_PROMPT, { exact: false }).waitFor()
+ expect(await page.getByRole('button', { name: /^Open Reply with exactly the word/ }).count()).toBe(0)
+
+ const snapshot = await captureStableAria(page, '[data-chat-flow]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
+ }, 60_000)
+
+ it('stays clean and owns only its one golden', async () => {
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
+ })
+})
diff --git a/apps/web/tests/workspace-management.e2e.ts b/apps/web/tests/workspace-management.e2e.ts
index a8e238422b..a435592da5 100644
--- a/apps/web/tests/workspace-management.e2e.ts
+++ b/apps/web/tests/workspace-management.e2e.ts
@@ -31,8 +31,8 @@ const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', impo
const MODE = webSnapshotMode()
const BROWSER_EXPECTED = join(SNAPSHOT_DIR, 'directory-browser.expected.md')
const SEED_ID = 'workspace-management-web-e2e'
-// Both waits exceed ui-primitives' 200ms POINTER_GRACE_MS. Keep them coupled
-// to that contract if the shared grace tuning changes.
+// Both waits exceed ui-primitives' 200ms POINTER_GRACE_MS. Keep them above
+// that value if the shared setting changes.
const POINTER_TRANSIT_MS = 300
const POINTER_HOLD_MS = 600
@@ -99,6 +99,19 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
}
}
+ /**
+ * Reveal and click a row action, re-hovering if a projection update replaces
+ * the row before its hover-only button becomes visible.
+ */
+ async function clickHoverAction(row: Locator, name: string): Promise {
+ const button = row.getByRole('button', { name })
+ await expect.poll(async () => {
+ await row.hover()
+ return await button.isVisible()
+ }, { timeout: 10_000 }).toBe(true)
+ await button.click()
+ }
+
beforeAll(async () => {
scaffold = await launchWebScaffold({})
// Seed one cold session (Ungrouped bucket) for the flat view + hover card.
@@ -137,10 +150,8 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
it('renames a workspace over the wire with a duplicate-name pre-check', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-ws-rename'))
- // The actions button is display:none until its row hovers — hover the
- // group row first, then the revealed button becomes actionable.
- await page.locator('[role="treeitem"]').filter({ hasText: 'alpha-ws' }).first().hover()
- await page.getByRole('button', { name: 'Workspace actions for alpha-ws' }).click()
+ const alphaRow = page.locator('[role="treeitem"]').filter({ hasText: 'alpha-ws' }).first()
+ await clickHoverAction(alphaRow, 'Workspace actions for alpha-ws')
await page.getByRole('menuitem', { name: 'Rename' }).click()
const dialog = page.getByRole('dialog', { name: 'Rename workspace' })
await dialog.waitFor({ timeout: 10_000 })
@@ -214,17 +225,19 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
// The header row is wrapped by its HoverCard anchor span, so the section
// is the nearest groupSection ancestor, not the immediate parent.
const groupSection = groupRow.locator('xpath=ancestor::*[contains(@class, "groupSection")][1]')
- if (await groupSection.locator('[role="treeitem"]').count() < 2) await groupRow.click()
- await expect.poll(
- () => groupSection.locator('[role="treeitem"]').count(),
- { timeout: 10_000 },
- ).toBeGreaterThanOrEqual(2)
+ await expect.poll(async () => {
+ const count = await groupSection.locator('[role="treeitem"]').count()
+ if (count < 2 && await groupRow.getAttribute('aria-expanded') !== 'true') {
+ await groupRow.click()
+ await page.waitForTimeout(50)
+ }
+ return await groupSection.locator('[role="treeitem"]').count()
+ }, { timeout: 10_000 }).toBeGreaterThanOrEqual(2)
const seededRow = groupSection.locator('[role="treeitem"]').nth(1)
await seededRow.click()
await expect.poll(() => seededRow.getAttribute('aria-selected'), { timeout: 10_000 }).toBe('true')
- await groupRow.hover()
- await page.getByRole('button', { name: `Workspace actions for ${workspace.title}` }).click()
+ await clickHoverAction(groupRow, `Workspace actions for ${workspace.title}`)
await page.getByRole('menuitem', { name: 'Delete workspace' }).click()
const dialog = page.getByRole('dialog', { name: 'Delete workspace' })
await dialog.waitFor({ timeout: 10_000 })
@@ -338,8 +351,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
if (oldWorkspace === undefined) throw new Error('old same-name Workspace was not registered')
const oldRow = page.locator('[role="treeitem"]').filter({ hasText: title }).first()
- await oldRow.hover()
- await page.getByRole('button', { name: `Workspace actions for ${title}` }).click()
+ await clickHoverAction(oldRow, `Workspace actions for ${title}`)
await page.getByRole('menuitem', { name: 'Delete workspace' }).click()
await page.getByRole('dialog', { name: 'Delete workspace' })
.getByRole('button', { name: 'Delete workspace' }).click()
@@ -509,9 +521,10 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
onTestFailed(() => saveFailureShot(page, 'web-e2e-ws-row-menu'))
const sessionRow = await seededSessionRow()
// The trigger is display:none until its row hovers.
- await sessionRow.hover()
const trigger = sessionRow.locator('button[aria-label^="Session actions for "]')
- await trigger.click()
+ const triggerName = await trigger.getAttribute('aria-label')
+ if (triggerName === null) throw new Error('seeded Session row has no actions label')
+ await clickHoverAction(sessionRow, triggerName)
const item = page.getByRole('menuitem', { name: 'Rename' })
await item.waitFor({ timeout: 5_000 })
// Into the list, then back up to the trigger across the 4px gap below it:
@@ -559,8 +572,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
const rowTitle = await sessionRow.locator('[class*="title"]').innerText()
// Row menu: hover reveals the actions button; Archive session commits
// without a confirmation dialog (non-destructive: log + accounting stay).
- await sessionRow.hover()
- await sessionRow.getByRole('button', { name: `Session actions for ${rowTitle}` }).click()
+ await clickHoverAction(sessionRow, `Session actions for ${rowTitle}`)
await page.getByRole('menuitem', { name: 'Archive session' }).click()
// The row disappears on the archive-set echo; with no other visible
// stray, the whole Ungrouped bucket withdraws.
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 14cc4840f1..c2a3cd4671 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -24,7 +24,8 @@
"exclude": [
"tests/scaffold.ts",
"tests/scaffold-hermetic.e2e.ts",
- "tests/core-web-profile.snapshot.ts",
+ "tests/minimal-preset.snapshot.ts",
+ "tests/message-feedback-protocol.snapshot.ts",
"tests/live-interactions.e2e.ts",
"tests/question-composer.e2e.ts",
"tests/approval-composer.e2e.ts",
@@ -61,16 +62,21 @@
"tests/skill-user-invoke.e2e.ts",
"tests/permission-policy-context.e2e.ts",
"tests/access-confirmation.e2e.ts",
+ "tests/agent-preset-selection.e2e.ts",
+ "tests/agent-preset-authoring.e2e.ts",
"tests/shipped-composition.e2e.ts",
"tests/schedule-after.e2e.ts",
+ "tests/feedback-command.e2e.ts",
"tests/startup-auto-selection.e2e.ts",
"tests/produced-files.e2e.ts",
"tests/produced-file-mentions.e2e.ts",
"tests/goal-bar.e2e.ts",
+ "tests/goal-command-presentation.e2e.ts",
"tests/subagent-conversation.e2e.ts",
"tests/subagent-interrupt.e2e.ts",
"tests/subagent-interrupt-ui.e2e.ts",
"tests/sidebar-subagent-activity.e2e.ts",
+ "tests/background-task-list.e2e.ts",
"tests/bash-abort-row.e2e.ts",
"tests/skill-tool-row.e2e.ts",
"tests/turn-tail-actions.e2e.ts",
@@ -81,7 +87,8 @@
"tests/chat-continuous-conversation.e2e.ts",
"tests/composer-tab-geometry.e2e.ts",
"tests/complex-history.perf.ts",
- "tests/pwsh-terminal.e2e.ts"
+ "tests/pwsh-terminal.e2e.ts",
+ "tests/workflow-run.e2e.ts"
],
"references": [
{
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index ec34ec435a..08e12b2e97 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -5,8 +5,8 @@ import react from '@vitejs/plugin-react'
const src = (rel: string): string => fileURLToPath(new URL(rel, import.meta.url))
const STANDALONE_ERROR = 'apps/web is not a standalone application: bare Vite cannot inject window.__DSH_BOOT__. '
- + 'Build with `pnpm run build && pnpm run build:web`, then run `dsh web` (repository checkout: `pnpm run dsh -- web`). '
- + 'For client-plugin HMR, run `pnpm run dsh -- web --dev` together with `pnpm run dev:web`.'
+ + 'From a repository checkout, run `pnpm dsh web`; an installed package uses `dsh web`. '
+ + 'For client-plugin HMR, run `pnpm dsh web` together with `pnpm run dev:web`.'
/** Fail before a Vite dev or preview server can expose the boot-manifest-free shell. */
function rejectStandaloneServe(): Plugin {
@@ -31,7 +31,7 @@ function rejectStandaloneServe(): Plugin {
* editing shell code re-hashes only index and returning clients keep the
* cached vendor chunk.
*
- * Boundary invariant: every member must be react-free. A package that
+ * Every member must be React-free. A package that
* imports react/jsx-runtime must never be listed — rollup folds a module
* shared between the entry and a manual chunk into the manual chunk, so one
* react-importing member would drag the single shared react copy into
@@ -77,8 +77,8 @@ const BOOT_GRAMMAR_FILES: readonly string[] = [
const FONT_EXTENSIONS: readonly string[] = ['.woff2', '.woff', '.ttf']
/**
- * npm package name of a resolved module id (the segment after the LAST
- * `node_modules/` — pnpm nests the real package under an inner node_modules).
+ * npm package name of a resolved module id: the segment after the last
+ * `node_modules/`. pnpm nests the real package under an inner node_modules.
*/
function npmPackageOf(id: string): string | undefined {
const parts = id.split('/node_modules/')
@@ -130,7 +130,7 @@ export default defineConfig({
// Workspace packages resolve to SOURCE: package.json exports point at lib
// for Node/type consumers, but the browser bundle must compile src directly
// so CSS rides vite's pipeline instead of the CSS-externalized lib bundle.
- // Only the shell's normal-package surface is aliased — plugin packages are
+ // Only the shell's normal package entry is aliased — plugin packages are
// NEVER bundled here (shell self-sufficiency — see
// packages/client/web/README.md); they arrive as runtime
// bundles through the client module system. Order matters — subpath
diff --git a/bin/dsh b/bin/dsh
deleted file mode 100755
index c578d78e74..0000000000
--- a/bin/dsh
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/sh
-# dsh launcher: runs the apps/cli `dsh` bin FROM SOURCE through the tsx ESM
-# hook, so a symlink from anywhere (e.g. ~/.local/bin/dsh) always executes the
-# current working tree without a build step.
-set -eu
-
-# Resolve symlink chains without readlink -f (not on every macOS).
-script=$0
-while [ -L "$script" ]; do
- target=$(readlink "$script")
- case $target in
- /*) script=$target ;;
- *) script=$(dirname "$script")/$target ;;
- esac
-done
-root=$(CDPATH='' cd -- "$(dirname -- "$script")/.." && pwd)
-
-# The ESM-only tsx hook transforms TypeScript and projects this checkout's
-# tsconfig paths into Node resolution (the CJS hook stays off: the graph is
-# ESM-only and the CJS resolver costs ~0.4s of startup). Absolute paths keep
-# both the hook and the tsconfig anchored to this checkout when the launcher
-# runs from any cwd, where bare `tsx/esm` would not resolve.
-NODE_USE_ENV_PROXY=1 \
- TSX_TSCONFIG_PATH="$root/tsconfig.json" \
- exec node --import "$root/node_modules/tsx/dist/esm/index.mjs" \
- "$root/apps/cli/src/bin.ts" "$@"
diff --git a/docs/AGENTS.md b/docs/AGENTS.md
index 04cd0f22f0..96c9b36352 100644
--- a/docs/AGENTS.md
+++ b/docs/AGENTS.md
@@ -6,7 +6,7 @@ This file defines document structure, Markdown tiers, writing rules, and `verify
These rules apply to human-facing documentation; [Agent Notes](../.agents/notes/README.md) remain outside their scope. A [postmortem](postmortem/README.md) is an incident-scoped reference; chronology records evidence, not a teaching sequence. A document's subject and tree position fix its scope: describe its own subject at appropriate detail and direct children only by purpose, responsibility, and high-level behavior; link to the owning descendant for lower-level detail. Document type does not widen that scope. A reference may be exhaustive only about its own subject. Testing mechanisms, fixtures, and harnesses belong at the lowest owning level; higher documents link there.
-Classify every in-scope document as a tutorial or reference. A tutorial follows an ordered path to an outcome and introduces only what each step needs. A reference defines a lookup scope and describes current behavior without depending on a teaching sequence. Separate substantial tutorial and reference content; use a clear structural boundary when either part is small.
+Classify every in-scope document as a tutorial or reference. Tutorials follow an ordered path to an outcome and introduce only what each step needs. References define a lookup scope and current behavior without a teaching sequence. Separate substantial tutorial and reference content; label a section when either part is small.
Before writing a tutorial, privately classify the reader's starting knowledge and each concept as beginner, intermediate, or advanced. Establish prerequisites before dependent concepts, increase difficulty gradually, and move unnecessary advanced material to a later tutorial or reference.
@@ -20,18 +20,18 @@ Each fact has one home: the tier whose job it is; elsewhere, link there.
|---|---|---|
| Root `AGENTS.md` | Standing orders: rules an agent needs in context in every session, one to three lines each, linking its home | Stories, worked examples, situational procedures, anything restated from a linked home |
| Subtree `AGENTS.md` (`packages/`, `examples/`, `docs/`, `.agents/notes/`) | Orders specific to that subtree | Repo-wide rules the root file already carries |
-| [architecture.md](architecture.md) | System map: services, loop, capability seams, extension points — read before changing `packages/` | Type shapes (→ subsystems), per-package detail (→ package READMEs), decision rationale (→ Agent Notes), implementation-status annotations |
-| [subsystems/](subsystems/README.md) | One reference page per subsystem: type shapes, semantics, and the generated Cordis surface | Behavior narration (→ architecture.md) |
-| [Agent Notes](../.agents/notes/README.md) | Active decision records: the why, what-was-given-up, and verification contract; `implemented/` notes describe shipped reality in present tense | Migration plans, acceptance-task checklists, fixture walkthroughs, and spec-speak ("should…") once the decision has shipped; archived notes are frozen history, never current authority |
+| [architecture.md](architecture.md) | System map: services, loop, capability seams, extension points — read before changing `packages/` | Type definitions (→ subsystems), per-package detail (→ package READMEs), decision rationale (→ Agent Notes), implementation-status annotations |
+| [subsystems/](subsystems/README.md) | One reference page per subsystem: type definitions, semantics, and the generated Cordis API | Behavior narration (→ architecture.md) |
+| [Agent Notes](../.agents/notes/README.md) | Active decision records: the why, what-was-given-up, and required verification; `implemented/` notes describe shipped reality in present tense | Migration plans, acceptance-task checklists, fixture walkthroughs, and spec-speak ("should…") once the decision has shipped; archived notes are frozen history, never current authority |
| [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — |
| [cookbook/](cookbook/adding-a-package.md) | Step-by-step how-tos with numbered verify steps | Design rationale (→ the Agent Note each guide links) |
| [user/](user/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history |
| Package README | The per-package contract: config, semantics, limitations, extension points, and [Model Experience](cookbook/adding-a-package.md#4-write-the-package-readme) | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns |
-| [development.md](development.md) | First-stop contributor onboarding: local setup, daily workflow, and CI shape at summary level; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ Agent Notes), gate-by-gate enumerations that drift from `package.json` scripts |
+| [development.md](development.md) | Contributor setup, daily workflow, and a summary of CI; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ Agent Notes), check-by-check lists that drift from `package.json` scripts |
| Generated reference: the per-page `cordis-surface` regions in [subsystems/](subsystems/README.md), the [Cordis core API + inherited tier](cordis-api/context.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive English sources regenerated from source and freshness-gated; reviewed Chinese counterparts follow the [pairing workflow](i18n/README.md#scope-and-exclusions) | Hand edits to generated English sources or regions; Chinese counterparts update through pairing only |
| Skills (`.agents/skills/`) | Reusable workflows and specialized decision standards | Product and runtime contracts (→ docs or source) |
-Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookbooks; type shapes → subsystems; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link.
+Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookbooks; type definitions → subsystems; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link.
## Writing rules
@@ -40,9 +40,9 @@ Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookb
- **One physical line per paragraph** (`verify-md-wrap`): use editor soft-wrap. Code blocks, tables, and list structure keep their formatting; code comments stay under the linter's column limit.
- **Fenced `ts` blocks must compile** (`doc-typecheck`); a pasted type declaration and its original JSDoc use ` ```ts type-equiv `, while a body-stripped public class declaration uses ` ```ts public-api `; register either in the manifest so neither can drift ([mechanics](development.md#documenting-types-verbatim-ts-type-equiv)).
- **The owning [subsystems page](subsystems/README.md) updates in the same change** that reshapes a documented type. `verify-type-equiv` catches drifted pastes, not never-documented new types; a type is documented on its declaring package group's page ([page scoping](../.agents/notes/implemented/process/2026-08-03-package-anchored-subsystem-pages.md)).
-- **Bilingual pairs update together**: editing either side obligates the counterpart and a re-record in the same change ([i18n contract](i18n/README.md)).
-- **Comments and JSDoc state complete contracts, not reasoning transcripts.** Preserve behavior, timing, modality, exceptions, consequences, and non-obvious orientation; delete narration, test walkthroughs, review analysis, and code restatement. Keep the local contract and link its rationale. Use [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for details.
-- Write directly: name actors and facts plainly ([decision](../.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.md)). Reserve `seam` for the defined capability; avoid metaphorical "gate", "vocabulary", and "surface".
+- **Pairs update together**: [Terminology-guided](i18n/terminology.md), single-pass active-agent work repositions first-use annotations, preserves untouched prose, and re-records; `dsh-translate-docs` remains user-invoked ([contract](i18n/README.md)).
+- **Comments and JSDoc state complete contracts, not reasoning transcripts.** Preserve behavior, failure, timing, ownership, modality, exceptions, consequences, and non-obvious orientation; delete narration, test walkthroughs, review analysis, and code restatement. Keep the local contract and link its rationale. Use [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for details.
+- Write directly: name actors and facts ([decision](../.agents/notes/implemented/process/2026-08-09-concrete-prose-names-actors-and-recorded-facts.md)). Reserve `seam` for the defined capability. Name the exact check, type, API, operation, or behavior instead of metaphorical "gate", "vocabulary", or "surface".
## Wordcount Budgets
@@ -54,7 +54,7 @@ When the gate goes red:
2. **Condense** content that belongs here but can be shorter.
3. **Raise** the ceiling only when the words need the space; justify the manifest diff in the PR. A too-low ceiling is a budget bug.
-Ceilings are guardrails, not reduction targets. At or below target, retain at least 5% headroom; above target, freeze the ceiling until relocation or condensation brings the document under target. Lower a ceiling only when the contract still has room, and raise it when content would otherwise be deleted. Targets: root `AGENTS.md` ≤ 1,600 words; `architecture.md` ≤ 1,800; subtree `AGENTS.md` ≤ 600, except `packages/AGENTS.md` ≤ 650 and this file ≤ 1,250; `packages/README.md` ≤ 600. Review governs unbudgeted tiers.
+Ceilings are guardrails, not reduction targets. At or below target, retain at least 5% headroom; above target, freeze the ceiling until relocation or condensation brings the document under target. Lower a ceiling only when the document still has room, and raise it when content would otherwise be deleted. Targets: root `AGENTS.md` ≤ 1,600 words; `architecture.md` ≤ 1,800; subtree `AGENTS.md` ≤ 600, except `packages/AGENTS.md` ≤ 650 and this file ≤ 1,250; `packages/README.md` ≤ 600. Review governs unbudgeted tiers.
## The slop checklist
diff --git a/docs/agent-lifecycle.i18n.yaml b/docs/agent-lifecycle.i18n.yaml
index 144d77b8c2..f9ac3b6f30 100644
--- a/docs/agent-lifecycle.i18n.yaml
+++ b/docs/agent-lifecycle.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/agent-lifecycle.md
-agent-lifecycle.md: 7e3939b7fd6e7730918e4bc05e5bfcd655422e04
-agent-lifecycle.zh.md: 259ec78431ff0ead61c66e1d8edae4d22f1bcfd0
+agent-lifecycle.md: 99b7823a3824b695d227abb85c3c773749154376
+agent-lifecycle.zh.md: a54547f486c447fe83fbe6017d016102d66b8612
diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md
index 7e3939b7fd..99b7823a38 100644
--- a/docs/agent-lifecycle.md
+++ b/docs/agent-lifecycle.md
@@ -75,8 +75,8 @@ The `assistant/message` event records every successful provider call, including
`dsh-compact-basic` uses `agent/pre-step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and failed turn close, and opens a fresh retry turn only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.
-The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages unless replacement is intentional. Steering and injected context pass through the same waterfall after a later boundary claims their next-step batch.
+The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages unless replacement is intentional. Steering and injected context pass through the same waterfall after a later claim operation takes their next-step batch.
-SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request shaping, steering, continuation, and errors.
+SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination API for queue/status, prompt interception, request construction, steering, continuation, and errors.
Maintenance mode: curated Mermaid sequence; exact event signatures live in the generated Cordis catalog.
diff --git a/docs/agent-lifecycle.zh.md b/docs/agent-lifecycle.zh.md
index 259ec78431..a54547f486 100644
--- a/docs/agent-lifecycle.zh.md
+++ b/docs/agent-lifecycle.zh.md
@@ -77,8 +77,8 @@ sequenceDiagram
`dsh-compact-basic` 在派生请求之前通过 `agent/pre-step` 处理压力,而 `agent/request-error` 仅用于规范的上下文溢出。任一触发条件满足后,系统都会先执行可选的工具结果剪枝,再选择摘要。恢复发生在失败步骤结束之后、失败轮次结束之前;只有当剪枝或摘要生成推进了 surface replacement generation 时,系统才会开启一个全新的重试轮次,否则仍以原始请求错误为准。
-以返回的 `agent/pre-step` 决策为准;通过包装 `next()` 的监听器会保留下游消息,除非有意替换这些消息。steering(中途引导)和注入的上下文在后续边界认领其下一步骤批次后,会经过同一 waterfall(瀑布式事件)。
+以返回的 `agent/pre-step` 决策为准;通过包装 `next()` 的监听器会保留下游消息,除非有意替换这些消息。steering(中途引导)和注入的上下文在后续的认领操作取得其下一步骤批次后,会经过同一 waterfall(瀑布式事件)。
-需要可回放 transcript(文本记录)数据的 SDK 用户应当消费 `session/event`;`agent/*` 是用于队列与状态、提示词拦截、请求整形、steering、继续执行和错误处理的实时协调接口。
+需要可回放 transcript(文本记录)数据的 SDK 用户应当消费 `session/event`;`agent/*` 是用于队列与状态、提示词拦截、请求构造、steering、继续执行和错误处理的实时协调接口。
维护模式:英文源文件包含人工维护的 Mermaid 时序图,并由生成器写出;本中文文件作为经评审对侧通过双语配对维护。确切的事件签名位于生成的 Cordis 目录中。
diff --git a/docs/api-gateway.i18n.yaml b/docs/api-gateway.i18n.yaml
index 0d82ebdd37..be48f9ef42 100644
--- a/docs/api-gateway.i18n.yaml
+++ b/docs/api-gateway.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/api-gateway.md
-api-gateway.md: 1fdbe256afb7e870e953f05bd29923a4e1c7c22b
-api-gateway.zh.md: b9e18bf78bb5887725a5278e0b28efb86d17b944
+api-gateway.md: c60793532d621585fc9878b6197497b45015856b
+api-gateway.zh.md: daf46f6ad208f4cb959028ff59d98d9de7a072d6
diff --git a/docs/api-gateway.md b/docs/api-gateway.md
index 1fdbe256af..c60793532d 100644
--- a/docs/api-gateway.md
+++ b/docs/api-gateway.md
@@ -17,7 +17,7 @@ Services normally extend `GatewayService` so the constructor explicitly binds th
```ts
import type { Agent } from '@deepseek-ai/dsh-agent'
import { GatewayService, Remote, RemoteScope } from '@deepseek-ai/dsh-type-meta'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export interface CreateGoalRequest {
objective: string
@@ -60,7 +60,7 @@ The Client uses concrete functions on ordinary objects, not a JavaScript Proxy.
```ts ignore-check
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { AgentContext } from '@deepseek-ai/dsh-client-runtime/client'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-api-remotes/client'
export const inject = ['remote', 'remote.goals']
@@ -85,7 +85,7 @@ The `api-remotes` assembly and the `ctx.remote` contract are React-independent;
| Build | `@deepseek-ai/dsh-typert-generator` | Strictly analyzes Remote signatures, the type graph, lookups, Contexts, and source locations from the Host `ts.Program`, then generates Host and Host-for-Client artifacts |
| Host | `@deepseek-ai/dsh-typert-registry` and Loader | Places generated Host descriptors, schemas, and business-package registrations in `ctx.typert`, and holds lookup and Context providers |
| Host | `@deepseek-ai/dsh-api-remotes` | Owns the application Agent/Session identity policy and configures the corresponding TypeRT lookups |
-| Host | `@deepseek-ai/dsh-api-gateway` | Provides `ctx.typertGateway`, claims Remote endpoints, resolves objects or Contexts, invokes live Cordis services, and validates boundaries |
+| Host | `@deepseek-ai/dsh-api-gateway` | Provides `ctx.typertGateway`, claims Remote endpoints, resolves objects or Contexts, invokes live Cordis services, and validates request and return values |
| Client | `@deepseek-ai/dsh-api-gateway/client` | Provides `ctx.remote` and `remote.` child Services, mounts generated descriptors as concrete methods, and initiates, validates, and cancels calls through the Connection |
| Client | `@deepseek-ai/dsh-api-remotes/client` | Explicitly selects and mounts the `/remote` contributions allowed by the application and brings the corresponding declaration merges into business code |
| Both | `@deepseek-ai/dsh-client-connection` | Provides the RPC carrier, request correlation, trust boundary, cancellation, response envelope, and the `/api` HTTP bridge |
@@ -114,7 +114,7 @@ Business packages expose the Host Loader entry through `./typert` and the Host-f
Parameter names in Remote Client declarations come from wire fields, while parameter and return types reference Client-safe types exported by the original business package. The declaration map resolves the generated property behind `ctx.remote.goals.create` back to the Host source method marked with `@Remote`, so editors that support declaration maps can navigate from a Client call to the real implementation instead of stopping at the generated `.d.ts`.
-Strict analysis requires a Remote to be a public, non-static instance method with a concrete implementation. The method cannot be generic; parameters must be required, named simple identifiers and cannot use destructuring, default values, rest parameters, or optional parameters. TypeRT generates strict schemas for ordinary JSON-representable types; complex objects such as workspace classes must have a unique `TypeRTLookupMap` declaration. Lookup and Context packages are responsible for both static declaration merges and runtime provider registration; if either side is missing, the build or earliest resolvable runtime boundary fails.
+Strict analysis requires a Remote to be a public, non-static instance method with a concrete implementation. The method cannot be generic; parameters must be required, named simple identifiers and cannot use destructuring, default values, rest parameters, or optional parameters. TypeRT generates strict schemas for ordinary JSON-representable types; complex objects such as workspace classes must have a unique `TypeRTLookupMap` declaration. Lookup and Context packages are responsible for both static declaration merges and runtime provider registration; if either side is missing, the build fails or the first call that needs the provider fails.
## Runtime invocation
@@ -122,7 +122,7 @@ Remote and API Proxy share the Connection's `/api` route. The Client Remote call
The Connection performs the unified trust check for `/api` before the HTTP bridge, then dispatches inside the shared FetchHandler in interceptor order. The TypeRT Gateway claims only two-segment endpoints that have a strict descriptor or active SRC marker; unclaimed requests fall back to the existing API Proxy. The Connection owns transport, RPC ids, response envelopes, and request cancellation, while the Gateway owns only the Remote data protocol and business dispatch. Replacing the Connection carrier in the future does not require changes to Remote descriptors or the Client programming interface.
-For every call, the Gateway resolves the descriptor and live service from the current registries instead of caching business objects. It requires the fields in `args` to match the descriptor exactly, validates wire values with codecs, resolves objects or receivers through registered lookup or Context providers, invokes the service method targeted by the binding, and validates the return value. A missing provider, unknown identity, binding mismatch, missing or extra argument, schema failure, or missing method fails at the boundary before entering or after leaving business code.
+For every call, the Gateway resolves the descriptor and live service from the current registries instead of caching business objects. It requires the fields in `args` to match the descriptor exactly, validates wire values with codecs, resolves objects or receivers through registered lookup or Context providers, invokes the service method targeted by the binding, and validates the return value. A missing provider, unknown identity, binding mismatch, missing or extra argument, schema failure, or missing method fails before entering or after leaving business code.
The lookup provider's `register()` supplies both the stable declaration and the default resolver; `configure()` supplies a resolver owned by Host composition that may execute asynchronously and is scoped to an effect lifetime. Configuration may precede provider mounting; without a provider, invocation still fails with `lookup-unavailable`, and unloading the configuration restores the provider's default policy. API Remotes owns the standard `agentFor()` semantics for `agent` and `session`: it reuses a live Agent, automatically resumes ordinary cold sessions, deduplicates concurrent resumes, and rejects identities owned by subagent routing; the `session` lookup returns that Agent's Session. The Web API Proxy supplies its Agent defaults and scope setup, then consumes the same resolver for legacy methods. Resume failures and ownership fences pass through unchanged as existing RPC errors rather than being collapsed into the Gateway's `internal` error.
@@ -138,20 +138,14 @@ SRC solves only dispatch for a Host process running from source. The Client does
## Development mode
-A complete build generates Host contracts before compiling the Host, Client, and Web, so it is the deterministic entry for creating or refreshing all artifacts:
+The repository `dsh` script completes the Host, Client, and Web build before starting the source Host. Web development runs that command and the Client plugin watcher in separate terminals:
```sh
-pnpm run build
-```
-
-Web development normally starts the source Host after one complete build and runs the Client plugin watcher in another terminal:
-
-```sh
-pnpm run dsh -- web --dev
+pnpm dsh web
pnpm run dev:web
```
-`dsh` starts the Host source through tsx, so the Host can use the SRC fallback; `dev:web` watches only Client plugins with a `dshClient` declaration and rewrites their `lib/client.js`. It does not analyze Host decorators or generate Remote Client DTS.
+`dsh` starts the Host source through tsx, so the Host can use the SRC fallback; `dev:web` watches only Client plugins with a `dsh.client` declaration and rewrites their `lib/client.js`. It does not analyze Host decorators or generate Remote Client DTS.
Changing only a Remote method's implementation body without changing its contract does not require regenerating the TypeRT files. After adding or removing a decorator or changing an export name, namespace, parameter, return value, lookup, Context, or cancellation signature, rerun the ordered lib build so the Host generates the strict contract before the Client compiles and bundles the new contribution:
diff --git a/docs/api-gateway.zh.md b/docs/api-gateway.zh.md
index b9e18bf78b..daf46f6ad2 100644
--- a/docs/api-gateway.zh.md
+++ b/docs/api-gateway.zh.md
@@ -17,7 +17,7 @@ Service 通常继承 `GatewayService`,让 Cordis service key 与默认 Remote
```ts
import type { Agent } from '@deepseek-ai/dsh-agent'
import { GatewayService, Remote, RemoteScope } from '@deepseek-ai/dsh-type-meta'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export interface CreateGoalRequest {
objective: string
@@ -60,7 +60,7 @@ Client 使用普通对象上的具体函数,不使用 JavaScript Proxy。直
```ts ignore-check
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { AgentContext } from '@deepseek-ai/dsh-client-runtime/client'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-api-remotes/client'
export const inject = ['remote', 'remote.goals']
@@ -85,7 +85,7 @@ Client 应用只装配 `@deepseek-ai/dsh-api-remotes`。该包以运行时值导
| 构建 | `@deepseek-ai/dsh-typert-generator` | 从 Host `ts.Program` 严格分析 Remote 签名、类型图、lookup、Context 与源码位置,并生成 Host 和 Host-for-Client 产物 |
| Host | `@deepseek-ai/dsh-typert-registry` 与 Loader | 把生成的 Host 描述符、schema 及业务包注册项放入 `ctx.typert`,并持有 lookup 与 Context 提供方 |
| Host | `@deepseek-ai/dsh-api-remotes` | 负责应用的 Agent/Session 身份策略,并配置对应的 TypeRT lookup |
-| Host | `@deepseek-ai/dsh-api-gateway` | 提供 `ctx.typertGateway`,认领 Remote endpoint,解析对象或 Context,调用实时 Cordis Service 并校验边界 |
+| Host | `@deepseek-ai/dsh-api-gateway` | 提供 `ctx.typertGateway`,认领 Remote endpoint,解析对象或 Context,调用实时 Cordis Service,并校验请求值和返回值 |
| Client | `@deepseek-ai/dsh-api-gateway/client` | 提供 `ctx.remote` 与 `remote.` 子 Service,把生成的描述符挂成具体方法,并通过 Connection 发起、校验和取消调用 |
| Client | `@deepseek-ai/dsh-api-remotes/client` | 显式选择并挂载本应用允许使用的 `/remote` 贡献,向业务代码带入对应的声明合并 |
| 双侧 | `@deepseek-ai/dsh-client-connection` | 提供 RPC carrier、请求关联、信任边界、取消、响应 envelope 与 `/api` HTTP bridge |
@@ -114,7 +114,7 @@ API Gateway 包同时拥有 Host dispatcher 与 Client Remote endpoint 两个对
Remote Client 声明中的参数名来自 wire 字段,参数和返回类型则引用原业务包导出的 Client-safe 类型。声明 map 把 `ctx.remote.goals.create` 最终解析到的生成属性映射到带 `@Remote` 的 Host 源方法,因此支持 declaration-map 的编辑器可以从 Client 调用跳到真实实现,而不是停在生成的 `.d.ts`。
-严格分析要求 Remote 是公开、非静态、有具体实现的实例方法。方法不能是泛型;参数必须是具名且必填的简单标识符,不能使用解构、默认值、rest 或可选参数。可 JSON 表示的普通类型由 TypeRT 生成严格 schema;工作区 class 等复杂对象必须具有唯一的 `TypeRTLookupMap` 声明。lookup 与 Context 包同时负责静态声明合并和运行时提供方注册,缺少任一侧都会在构建或最早可解析的运行时边界报错。
+严格分析要求 Remote 是公开、非静态、有具体实现的实例方法。方法不能是泛型;参数必须是具名且必填的简单标识符,不能使用解构、默认值、rest 或可选参数。可 JSON 表示的普通类型由 TypeRT 生成严格 schema;工作区 class 等复杂对象必须具有唯一的 `TypeRTLookupMap` 声明。lookup 与 Context 包同时负责静态声明合并和运行时提供方注册;缺少任一侧都会导致构建失败,或者首次调用需要该提供方时失败。
## 运行时调用
@@ -122,7 +122,7 @@ Remote 与 API Proxy 共用 Connection 的 `/api` 路由。Client Remote 调用
Connection 在 HTTP bridge 之前执行 `/api` 的统一信任检查,再在共享 FetchHandler 内按 interceptor 顺序分发。TypeRT Gateway 只认领存在严格描述符或活跃 SRC marker 的两段式 endpoint;未认领的请求回退到既有 API Proxy。Connection 拥有传输、RPC id、响应 envelope 和 request cancellation,Gateway 只拥有 Remote 数据协议和业务分发。未来替换 Connection carrier 不要求改变 Remote 描述符或 Client 编程界面。
-Gateway 每次调用都从当前注册表解析描述符和实时 Service,不缓存业务对象。它要求 `args` 的字段集合与描述符完全一致,先用 codec 校验 wire 值,再通过注册的 lookup 或 Context provider 解析对象或接收者,最后调用 binding 指向的 Service 方法并校验返回值。缺少 provider、identity 未命中、binding 不一致、参数多缺、schema 失败和方法不存在都在进入或离开业务边界时失败。
+Gateway 每次调用都从当前注册表解析描述符和实时 Service,不缓存业务对象。它要求 `args` 的字段集合与描述符完全一致,先用 codec 校验 wire 值,再通过注册的 lookup 或 Context provider 解析对象或接收者,最后调用 binding 指向的 Service 方法并校验返回值。缺少 provider、identity 未命中、binding 不一致、参数多缺、schema 失败和方法不存在都会在进入业务代码前或离开业务代码后失败。
lookup provider 的 `register()` 同时提供稳定声明和默认 resolver;`configure()` 提供由 Host 组合拥有、可异步执行且受 effect 生命周期约束的 resolver。配置可以先于 provider 挂载;没有 provider 时调用仍以 `lookup-unavailable` 失败,配置卸载后则恢复 provider 默认策略。API Remotes 负责 `agent` 与 `session` 的标准 `agentFor()` 语义:复用 live Agent,自动恢复普通冷会话,对并发恢复去重,并拒绝由 subagent routing 拥有的 identity;`session` lookup 返回该 Agent 的 Session。Web API Proxy 提供 Agent 默认值与 scope 设置,再让旧方法使用同一个 resolver。恢复失败和 ownership fence 通过既有 RPC error 原样返回,不折叠为 Gateway 的 `internal` 错误。
@@ -138,20 +138,14 @@ SRC 只解决 Host 源码进程的分发问题。Client 不会从运行中的 Ho
## 开发模式
-完整构建会先生成 Host 约定,再编译 Host、Client 与 Web,因此是建立或刷新所有产物的确定性入口:
+仓库的 `dsh` 脚本会先完成 Host、Client 与 Web 构建,再启动源码 Host。Web 开发需要在两个终端中分别运行该命令和 Client plugin watcher:
```sh
-pnpm run build
-```
-
-Web 开发通常在完成一次构建后启动源码 Host,并在另一个终端运行 Client plugin watcher:
-
-```sh
-pnpm run dsh -- web --dev
+pnpm dsh web
pnpm run dev:web
```
-`dsh` 通过 tsx 启动 Host 源码,所以 Host 可以使用 SRC 回退;`dev:web` 只监听带 `dshClient` 声明的 Client plugin 并重写其 `lib/client.js`,它不会分析 Host decorator,也不会生成 Remote Client DTS。
+`dsh` 通过 tsx 启动 Host 源码,所以 Host 可以使用 SRC 回退;`dev:web` 只监听带 `dsh.client` 声明的 Client plugin 并重写其 `lib/client.js`,它不会分析 Host decorator,也不会生成 Remote Client DTS。
只修改 Remote 方法实现体而不改变约定时,无需重新生成 TypeRT 文件。新增或删除 decorator、修改导出名、namespace、参数、返回值、lookup、Context 或取消签名时,重新执行有序 lib 构建,让 Host 先生成严格约定,再让 Client 编译并打包新的贡献:
diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml
index 9124489815..d8caa93760 100644
--- a/docs/architecture.i18n.yaml
+++ b/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: 73e1cb70d97f4754113c9d0ed289063226b82b9f
-architecture.zh.md: e34ac28839f1e11917a2fcae1ae25f4199e4a5af
+architecture.md: f5ebff879929079870c7936b424f03a02089c9d5
+architecture.zh.md: 1769f6febc4f156f6abccc5a19363f6eb55b6139
diff --git a/docs/architecture.md b/docs/architecture.md
index 73e1cb70d9..f5ebff8799 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -17,7 +17,7 @@ Harnesses are [Cordis](cordis-primer.md) contexts; packages contribute services,
| `ctx.systemPrompt` | `dsh-system-prompt` | ordered prompt sections, tool schemas, and variables |
| `ctx.tools` | `dsh-tools` | tool registry and [execution pipeline](tool-execution-pipeline.md) |
| `ctx.agents` | `dsh-agent` | live agents, delegated creation, `agent/*` events, process-local initiator scope |
-| `ctx.agentDefaultModel` | [`dsh-agent-default-model`](../packages/core/agent-default-model/README.md) | Settings-backed model selection shared by Agent front doors |
+| `ctx.agentDefaultModel` | [`dsh-agent-default-model`](../packages/core/agent-default-model/README.md) | Settings-backed model selection shared by Agent entry points |
| `ctx.agentLoop` | `dsh-agent-loop` | concrete `Agent` driver |
### Capability Services
@@ -42,6 +42,7 @@ Harnesses are [Cordis](cordis-primer.md) contexts; packages contribute services,
| `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | background task registry, generic `task_*` controls |
| `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | script-driven multi-agent orchestration |
| `ctx.goals` | [`goal/`](../packages/goal/README.md) | persisted same-session goals |
+| `ctx.messageFeedback` | [`feedback/`](../packages/feedback/README.md) | lifecycle-bound editable feedback for individual assistant messages and its Host Remote contract |
| `ctx.sessionPersistence` | [`session/`](../packages/session/README.md) | durable session-log storage |
| `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | live-preferred exact/filter/trace queries over SQLite FTS, workspace-authorized model tools |
| `ctx.sessionTitle` | [`session/session-title`](../packages/session/README.md) | log-backed fallbacks, one optional asynchronous provider |
@@ -123,7 +124,7 @@ Pruning precedes summaries; overflow retries require durable progress. `agent/re
Adapter selection, dispatch, and iteration failures become terminal error or aborted `finish` chunks. `agent/request-error` receives request coordinates, normalized `LlmFailure`, available retry policy, and signal; middleware and consumer errors remain outside recovery. Failed chunks commit neither messages nor tool calls.
-Other failures use `agent/error`; cancellation and disposal beat recovery. Before request-header commit, the turn signal cancels capability preparation; undispatched tools get synthetic `tool/call`/`ABORTED_BEFORE_DISPATCH` pairs. Effective `cancel(cause)` reports its cause before clearing and aborting; idle calls emit nothing. Waking input that lands after the abort fires but before convergence runs at the driver's convergence boundary, while a `disposed` cancel leaves it parked ([cancel-convergence wake latch](../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md)). Durability distinguishes `aborted` cancellation from `disposed` teardown, which awaits quiescence ([decision](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)).
+Other failures use `agent/error`; cancellation and disposal beat recovery. Before request-header commit, the turn signal cancels capability preparation; undispatched tools get synthetic `tool/call`/`ABORTED_BEFORE_DISPATCH` pairs. Effective `cancel(cause)` reports its cause before clearing and aborting; idle calls emit nothing. The driver processes waking input received after abort starts but before convergence; a `disposed` cancel leaves it parked ([cancel-convergence wake latch](../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md)). Durability distinguishes `aborted` cancellation from `disposed` teardown, which awaits quiescence ([decision](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)).
Turn and step events are turn-enclosed; the loop appends `user/message` events only from entered batches inside a turn. A turn opens before the initial claim and pre-step, so rejection, empty input, cancellation, or failure closes a durable turn without any step events. Standalone `compact/* { turn: null }` events consume no turn, and their lock-time markers may interleave with inbox splices. Reload synthesizes interrupted turn ends; `session/end-seed` distinguishes stale compaction orphans from live locks. After close, only `agent/error` reports failures. Each turn has one [TurnEndReason](subsystems/session.md#why-a-turn-ended-turnendreasonmap).
@@ -143,7 +144,7 @@ The session log is authoritative. `deriveMessages()` projects model history; raw
**Model-visible ⟺ logged**: messages entering at `step/start` plus the folded `request/header` reconstruct every request. The header marks adapter defaults so later proposals discard them and re-resolve the route without losing explicit settings. `request/context` separately records registration-bound provider, model, and capacity metadata when the route changes; it does not participate in request reconstruction or header equality. `dsh-agent-loop/invariant` asserts reconstructability through `ctx.invariants` ([reconstructability](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)).
-Durability is a plugin concern. Backends copy synchronous `session/event` notifications into fixed-window durable batches; `session/flush` bypasses the wait before requests and top-level tool dispatch, and after `turn/end` before another turn or idle. `SessionPersistence` stores events and header metadata; JSONL defaults to checksummed Zstandard and SQLite shares the contract ([checkpoint decision](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md), [batching decision](../.agents/notes/implemented/architecture/2026-08-08-bounded-session-persistence-write-batching.md)).
+Durability is a plugin concern. Backends copy synchronous `session/event` notifications into fixed-window durable batches; `session/flush` bypasses the wait before requests and top-level tool dispatch, and after `turn/end` before another turn or idle. `SessionPersistence` stores events and header metadata; JSONL defaults to checksummed Zstandard, and SQLite uses the same checkpoint and batching rules ([checkpoint decision](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md), [batching decision](../.agents/notes/implemented/architecture/2026-08-08-bounded-session-persistence-write-batching.md)).
Between turns, owners append log-only events through `Session`, flushing only for durability. `session/title` relies on bounded background persistence and lifecycle drains; manual compaction flushes its bracket before the operation completes. Title work never delays responses; the latest title event wins, and it records the source message seqs and whether the user, fallback, or provider supplied it. Title records are inherited fork boundaries ([decision](../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md)).
@@ -165,7 +166,11 @@ Exceptions combine LLM Service Definition/Consumer roles, filesystem policy, web
### Bundles And Apps
-`dsh-agent-spine-demo` bundles a spine and optional goals. App packages own CLI, ACP automation, and JSON-RPC front doors ([README](../packages/examples/agent-spine-demo/README.md), [acp/](../packages/acp/README.md), [interaction/](../packages/interaction/README.md)). `dsh-jsonrpc-agent` boots external `cordis.yml`; the Python SDK defaults when config is absent ([Python SDK](../python/README.md)). Thin deployments use swappable backends and optional tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)).
+`dsh-agent-spine-demo` bundles a spine and optional goals. App packages own CLI, ACP automation, and JSON-RPC entry points ([README](../packages/examples/agent-spine-demo/README.md), [acp/](../packages/acp/README.md), [interaction/](../packages/interaction/README.md)). `dsh-jsonrpc-agent` boots external `cordis.yml`; the Python SDK defaults when config is absent ([Python SDK](../python/README.md)). Thin deployments use swappable backends and optional tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)).
+
+### Agent Presets
+
+A deployment may compose each session's model-facing plugin set separately. An **agent preset** is a directory holding one `agent.cordis.yml`, mounted as an `include` subtree under that agent's scope during `setup(agentCtx)`, so its tool and prompt registrations file into that agent's layer and unwind with it — no new tier in the registries. The host composition keeps what must be shared: the registries themselves, cross-session facilities, the sandbox and approval stack, the model route. `ctx.agentPresets` owns discovery and the guarded mount, rejecting a row that never activates or that publishes into the root service realm. Details: [per-session agent presets](../.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md), [preset/](../packages/preset/README.md).
### Where New Behavior Goes
@@ -175,13 +180,14 @@ New behavior attaches to a documented extension point; a loop change updates thi
|---|---|
| Add a model provider | register its adapter on `ctx.llm` |
| Add a model-facing capability | register on `ctx.tools`; schemas join prompt assembly |
+| Give one session a different capability set | compose it in an agent preset; a service row there needs an `isolate` realm |
| Add shell execution | implement and register a `ctx.bash` backend; the local backend spawns through `ctx.subprocess` |
| Add persistent terminal execution | register a `ctx.pty` backend plus `dsh-tool-pty` |
| Add a human command | register on `ctx.commands`; adapters discover and dispatch without a model turn |
| Add background work | register on `ctx.tasks`; generic `task_*` tools collect or stop it |
| Add filesystem access or policy | implement a `ctx.fs` provider or listen to `fs/*` policy events |
| Confine spawned processes | use a `ctx.sandbox` backend; consumers wrap argv before spawning |
-| Intercept a request, tool, or turn | use its `agent/*` or `tools/*` event; `agent/turn-stopping` is the stop boundary |
+| Intercept a request, tool, or turn | use its `agent/*` or `tools/*` event; `agent/turn-stopping` is the event that stops a turn |
| Add model-facing context | call `agent.inject()` to queue sourced context for the next admitted request |
| Add UI or editor integration | drive `ctx.agents` and render from `session/event` |
| Web Client Chat node | register a `ConversationNodeDefinition` + keyed renderer |
diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md
index e34ac28839..1769f6febc 100644
--- a/docs/architecture.zh.md
+++ b/docs/architecture.zh.md
@@ -42,6 +42,7 @@
| `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | 后台任务注册表和通用 `task_*` 控制 |
| `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | 脚本驱动的多 agent 编排 |
| `ctx.goals` | [`goal/`](../packages/goal/README.md) | 持久化的同会话目标 |
+| `ctx.messageFeedback` | [`feedback/`](../packages/feedback/README.md) | 绑定生命周期的单条 assistant 消息可编辑反馈及其 Host Remote 契约 |
| `ctx.sessionPersistence` | [`session/`](../packages/session/README.md) | 会话日志的持久化存储 |
| `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | 基于 SQLite 全文搜索的实时优先精确检索/过滤/追踪、经工作区授权的模型工具 |
| `ctx.sessionTitle` | [`session/session-title`](../packages/session/README.md) | 基于日志的回退标题和单个可选异步提供方 |
@@ -123,7 +124,7 @@ idle inject:
适配器选择、分发与迭代失败会成为 error 或 aborted 类型的终止 `finish` 分片。`agent/request-error` 接收请求坐标、标准化 `LlmFailure`、可用的重试策略和信号;middleware 与消费方错误仍在恢复之外。失败分片既不提交消息,也不提交工具调用。
-其他故障使用 `agent/error`;取消和 dispose(资源释放)优先于恢复。在提交请求头之前,轮次信号会取消能力准备;尚未分派的工具会得到合成的 `tool/call`/`ABORTED_BEFORE_DISPATCH` 对。实际生效的 `cancel(cause)` 会在清空队列和中止前报告原因;空闲调用不发事件。abort 触发后、收敛前到达的唤醒输入会在 driver 的收敛边界执行,而 `disposed` 取消则将其停放([取消收敛窗口唤醒锁存](../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md))。持久性以 `aborted` 区分取消,以 `disposed` 区分会等待完全停稳的拆卸([决策](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md))。
+其他故障使用 `agent/error`;取消和 dispose(资源释放)优先于恢复。在提交请求头之前,轮次信号会取消能力准备;尚未分派的工具会得到合成的 `tool/call`/`ABORTED_BEFORE_DISPATCH` 对。实际生效的 `cancel(cause)` 会在清空队列和中止前报告原因;空闲调用不发事件。driver 会处理在 abort 开始后、收敛前收到的唤醒输入;`disposed` 取消会让该输入保持待处理状态([取消收敛窗口唤醒锁存](../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md))。持久性以 `aborted` 区分取消,以 `disposed` 区分会等待完全停稳的拆卸([决策](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md))。
轮次和步骤事件均位于轮次边界内;loop 只会在轮次内从进入步骤的批次追加 `user/message`。轮次会在首次领取与 pre-step 之前打开,因此拒绝、空输入、取消或失败会关闭一个不包含任何步骤事件的持久轮次。独立的 `compact/* { turn: null }` 事件不占用轮次,其锁定时刻标记可以与 inbox splice 交错。重新加载会为中断的轮次合成结束事件;`session/end-seed` 区分陈旧的压缩遗留项与活跃锁。关闭后仅由 `agent/error` 报告故障。每个轮次有一个 [TurnEndReason](subsystems/session.md#why-a-turn-ended-turnendreasonmap)。
@@ -143,7 +144,7 @@ idle inject:
**模型可见 ⟺ 已记录**:在 `step/start` 进入的消息加上折叠后的 `request/header` 可以重建每个请求。该 header 会标记适配器默认值,使后续提议丢弃这些值并重新解析路由,同时不丢失显式设置。`request/context` 会在路由变化时另行记录与注册项绑定的提供方、模型及容量元数据;它不参与请求重建或 header 相等性判断。`dsh-agent-loop/invariant` 通过 `ctx.invariants` 断言可重建性([可重建性](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md))。
-持久性由插件负责。后端会将同步的 `session/event` 通知复制到固定窗口的持久化批次中;`session/flush` 会绕过等待,在请求与顶层工具分发之前执行,并在 `turn/end` 之后、另一个轮次或空闲状态之前执行。`SessionPersistence` 存储事件和 header 元数据;JSONL 默认采用带校验和的 Zstandard,SQLite 遵循同一约定([检查点决策](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md)、[批处理决策](../.agents/notes/implemented/architecture/2026-08-08-bounded-session-persistence-write-batching.md))。
+持久性由插件负责。后端会将同步的 `session/event` 通知复制到固定窗口的持久化批次中;`session/flush` 会绕过等待,在请求与顶层工具分发之前执行,并在 `turn/end` 之后、另一个轮次或空闲状态之前执行。`SessionPersistence` 存储事件和 header 元数据;JSONL 默认采用带校验和的 Zstandard,SQLite 使用同样的检查点与批处理规则([检查点决策](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md)、[批处理决策](../.agents/notes/implemented/architecture/2026-08-08-bounded-session-persistence-write-batching.md))。
在轮次之间,事件所有方通过 `Session` 追加纯日志事件,仅为持久性而刷写。`session/title` 依赖有界后台持久化与生命周期排空;手动压缩会在操作完成前 flush 其标记对。标题工作绝不延迟响应;最新的标题事件生效,并记录来源消息 seq,以及标题由用户、后备逻辑还是提供方提供。标题记录是可继承的 fork 边界([决策](../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md))。
@@ -167,6 +168,10 @@ idle inject:
`dsh-agent-spine-demo` 组合一套主干和可选目标。应用包负责 CLI(命令行界面)、ACP 自动化入口和 JSON-RPC 入口([README](../packages/examples/agent-spine-demo/README.md)、[acp/](../packages/acp/README.md)、[interaction/](../packages/interaction/README.md))。`dsh-jsonrpc-agent` 启动外部 `cordis.yml`;Python SDK 在配置缺失时提供默认项([Python SDK](../python/README.md))。轻量部署使用可替换后端和可选工具([examples/](../examples/AGENTS.md)、[可运行接线](cookbook/extension-cookbook.md#runnable-wirings)、[图谱](graph-atlas.md))。
+### Agent Preset
+
+部署可为每个会话分别组装面向模型的插件集合。**agent preset** 是一个含 `agent.cordis.yml` 的目录,在 `setup(agentCtx)` 期间作为 `include` 子树挂到该 agent 的 scope 之下,其工具与提示词注册因而归档进该 agent 的分层并随之卸载,注册表无需新增层级。宿主组装保留必须共享的部分:注册表本身、跨会话设施、沙箱与审批栈、模型路由。`ctx.agentPresets` 负责发现与把关,拒绝未激活的行和把服务发布进根 realm 的行。详见 [按会话组装 agent preset](../.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md)、[preset/](../packages/preset/README.md)。
+
### 新行为的归属位置
新行为附加到已有文档记录的扩展点;循环发生变更时,本架构图随之更新。
@@ -175,13 +180,14 @@ idle inject:
|---|---|
| 添加模型提供方 | 在 `ctx.llm` 上注册其适配器 |
| 添加面向模型的能力 | 在 `ctx.tools` 上注册;schema 加入提示词组装 |
+| 让某个会话拥有不同的能力集合 | 在 agent preset 中组装它;其中的 service 行需要 `isolate` realm |
| 添加 shell 执行 | 实现并注册 `ctx.bash` 后端;本地后端通过 `ctx.subprocess` spawn 进程 |
| 添加持久化终端执行 | 注册 `ctx.pty` 后端和 `dsh-tool-pty` |
| 添加用户命令 | 在 `ctx.commands` 上注册;适配器无需模型轮次即可发现并分派 |
| 添加后台工作 | 在 `ctx.tasks` 上注册;通用 `task_*` 工具负责收集或停止 |
| 添加文件系统访问或策略 | 实现 `ctx.fs` 提供方,或监听 `fs/*` 策略事件 |
-| 限制 spawn 出的进程 | 使用 `ctx.sandbox` 后端;消费方在 spawn 前包装 argv |
-| 拦截请求、工具或轮次 | 使用相应的 `agent/*` 或 `tools/*` 事件;`agent/turn-stopping` 是停止边界 |
+| 限制所启动的进程 | 使用 `ctx.sandbox` 后端;消费方在启动进程前包装 argv |
+| 拦截请求、工具或轮次 | 使用相应的 `agent/*` 或 `tools/*` 事件;`agent/turn-stopping` 是停止轮次的事件 |
| 添加模型可见上下文 | 调用 `agent.inject()`,将带来源的上下文排入下一次获准请求 |
| 添加 UI 或编辑器集成 | 驱动 `ctx.agents` 并从 `session/event` 渲染 |
| Web Client Chat 节点 | 注册 `ConversationNodeDefinition` + keyed renderer |
diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml
index aa09cb3036..6d2f07e5f8 100644
--- a/docs/capability-seams.i18n.yaml
+++ b/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: af9f8ba48e67074a485019a4ad9dddd08b2faf81
-capability-seams.zh.md: 7dff963646991d8b1f763ed789109e93cce2ddb8
+capability-seams.md: d56b8964a69d98b840749ff98b1be3605d95ec20
+capability-seams.zh.md: 8d2cf846a64c254f5ff0ab617eda25be3031f970
diff --git a/docs/capability-seams.md b/docs/capability-seams.md
index af9f8ba48e..d56b8964a6 100644
--- a/docs/capability-seams.md
+++ b/docs/capability-seams.md
@@ -7,10 +7,14 @@ A service can be a core spine service, a swappable capability seam, or a bundle/
```mermaid
flowchart LR
+ pkg_attachment["attachment"]
+ svc_attachments["ctx.attachments
Durable binary attachment storage"]
+ pkg_attachment_local["attachment-local"]
+ pkg_host_runtime["host-runtime"]
+ pkg_llm_pi_ai["llm-pi-ai"]
pkg_llm["llm"]
svc_llm["ctx.llm
LLM adapter registry"]
pkg_llm_deepseek["llm-deepseek"]
- pkg_llm_pi_ai["llm-pi-ai"]
pkg_llm_replay["llm-replay"]
pkg_agent_loop["agent-loop"]
pkg_compact_basic["compact-basic"]
@@ -26,6 +30,7 @@ flowchart LR
pkg_session_query_sqlite["session-query-sqlite"]
pkg_subagent_inprocess["subagent-inprocess"]
pkg_invariants["invariants"]
+ pkg_message_feedback["message-feedback"]
svc_invariants["ctx.invariants
Package-owned invariant registry"]
pkg_scope["scope"]
pkg_typert_registry["typert-registry"]
@@ -56,6 +61,7 @@ flowchart LR
pkg_storage_domain["storage-domain"]
svc_storageDomain["ctx.storageDomain
Domain data facility"]
pkg_workspace["workspace"]
+ svc_messageFeedback["ctx.messageFeedback
Lifecycle-bound message feedback"]
svc_workspace["ctx.workspace
Workspace entity registry"]
svc_sessionQuery["ctx.sessionQuery
Session reads, traces, filters, and search"]
pkg_session_reference["session-reference"]
@@ -81,6 +87,8 @@ flowchart LR
svc_userInteraction["ctx.userInteraction
Human question/answer seam"]
pkg_plan_mode["plan-mode"]
svc_planMode["ctx.planMode
Plan collaboration state"]
+ pkg_agent_presets["agent-presets"]
+ svc_agentPresets["ctx.agentPresets
Per-session agent composition"]
pkg_commands["commands"]
svc_commands["ctx.commands
Human command registry"]
pkg_session_projection["session-projection"]
@@ -181,8 +189,11 @@ flowchart LR
pkg_agent --> svc_agents
pkg_agent_default_model --> svc_agentDefaultModel
pkg_agent_loop --> svc_agentLoop
+ pkg_agent_presets --> svc_agentPresets
pkg_api_gateway --> svc_typertGateway
pkg_approval --> svc_approval
+ pkg_attachment --> svc_attachments
+ pkg_attachment_local --> svc_attachments
pkg_bash --> svc_bash
pkg_bash_env --> svc_bashEnv
pkg_bash_local --> svc_bash
@@ -209,6 +220,7 @@ flowchart LR
pkg_llm_deepseek --> svc_llm
pkg_llm_pi_ai --> svc_llm
pkg_llm_replay --> svc_llm
+ pkg_message_feedback --> svc_messageFeedback
pkg_modules --> svc_clientModuleHost
pkg_permission --> svc_permission
pkg_plan_mode --> svc_planMode
@@ -277,6 +289,8 @@ flowchart LR
svc_agents --> pkg_subagent_inprocess
svc_approval --> pkg_tool_bash
svc_approval --> pkg_tools
+ svc_attachments --> pkg_host_runtime
+ svc_attachments --> pkg_llm_pi_ai
svc_bash --> pkg_hooks_claude
svc_bash --> pkg_hooks_codex
svc_bash --> pkg_tool_bash
@@ -311,6 +325,7 @@ flowchart LR
svc_sessionPersistence --> pkg_agent_loop
svc_sessionPersistence --> pkg_hooks_claude
svc_sessionPersistence --> pkg_hooks_codex
+ svc_sessionPersistence --> pkg_message_feedback
svc_sessionPersistence --> pkg_session_query
svc_sessionPersistence --> pkg_session_query_sqlite
svc_sessionPersistence --> pkg_tool_bash
@@ -323,6 +338,7 @@ flowchart LR
svc_sessions --> pkg_agent
svc_sessions --> pkg_agent_loop
svc_sessions --> pkg_invariants
+ svc_sessions --> pkg_message_feedback
svc_sessions --> pkg_session_persistence
svc_sessions --> pkg_session_query
svc_sessions --> pkg_session_query_sqlite
@@ -333,6 +349,7 @@ flowchart LR
svc_skills --> pkg_tool_skill
svc_spillStore --> pkg_spill_policy
svc_storage --> pkg_storage_domain
+ svc_storageDomain --> pkg_message_feedback
svc_storageDomain --> pkg_workspace
svc_subagents --> pkg_tool_ralph
svc_subagents --> pkg_tool_subagent
@@ -377,40 +394,43 @@ flowchart LR
| ctx key | Role | Owner | Implementations | Direct consumers | Companion plugins | Note |
| --- | --- | --- | --- | --- | --- | --- |
+| `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | `host-runtime`, [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | The host commits accepted images before session events; provider adapters resolve authorized durable references into provider-native content. |
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compact-basic`](../packages/compact/compact-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. |
| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
-| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. |
+| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | Owns append-only Session instances and emits the durable session event feed. |
| `ctx.invariants` | `core` | [`invariants`](../packages/support/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. |
| `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges. |
| `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. |
-| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. |
+| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`message-feedback`](../packages/feedback/message-feedback) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. |
| `ctx.settings` | `seam` | [`settings`](../packages/settings/settings) | [`settings-local`](../packages/settings/settings-local) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | Plugins register namespace schemas and resolve layered values; providers store the raw document. The LLM adapters register their entry config as the composition base under the user section; the web gateway serves redacted layered descriptors and writes the user layer. |
| `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | Configuration carries references to secrets; providers own the values. Consumers resolve per operation, so a rotated credential reaches the very next request; the web gateway exposes value-free views and write-only storage. |
| `ctx.telemetry` | `seam` | [`session-telemetry`](../packages/session/session-telemetry) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | - | - | The seam captures, redacts, and hands session records to one backend; nothing else consumes the service — its output leaves the process. |
| `ctx.storage` | `seam` | [`storage`](../packages/storage/storage) | [`storage-json`](../packages/storage/storage-json), [`storage-sqlite`](../packages/storage/storage-sqlite) | [`storage-domain`](../packages/storage/storage-domain) | - | Backends register side by side under names; data forms (domain first) mount on the hub and translate typed operations into opaque KV-unit primitives. |
-| `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace) | - | Waits for every configured backend, then publishes the domain form as one lifecycle-bound service for typed durable state. |
+| `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace), [`message-feedback`](../packages/feedback/message-feedback) | - | Waits for every configured backend, then publishes the domain form as one lifecycle-bound service for typed durable state. |
+| `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | Owns local per-assistant-message feedback, lifecycle and target validation, per-item compare-and-set, and the Host unary Remote contract without entering Session history or telemetry. |
| `ctx.workspace` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | Owns WorkspaceId-branded records over the domain facility; stable sessionIds accounts drive Host RPC and GUI projections. |
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/session-query/tool-session-query) | - | The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations, while the model consumer owns workspace authority and cursor-free rendering. |
| `ctx.sessionReferences` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax. |
| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-message-llm`](../packages/session/session-title-first-message-llm), [`session-title-all-messages-llm`](../packages/session/session-title-all-messages-llm) | - | - | Owns the deterministic fallback, latest-title fold, and sole optional asynchronous provider registration. |
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-pty`](../packages/pty/tool-pty), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. |
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/interaction/tool-ask-user), [`tool-bash`](../packages/bash/tool-bash), [`tool-cordis`](../packages/self-modification/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-pty`](../packages/pty/tool-pty), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. |
-| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/interaction/user-interaction) | - | [`tool-ask-user`](../packages/interaction/tool-ask-user) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
+| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/interaction/user-interaction) | - | [`tool-ask-user`](../packages/interaction/tool-ask-user) | - | UI front ends provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
| `ctx.planMode` | `core` | [`plan-mode`](../packages/plan/plan-mode) | - | - | - | Folds logged plan/mode state, flushes user selections at turn boundaries, renders deployment-owned guidance, registers /plan, and keeps the plan-exit schema stable across transitions. |
+| `ctx.agentPresets` | `core` | [`agent-presets`](../packages/preset/agent-presets) | - | - | - | Discovers preset directories over trusted and user-authored roots and mounts one preset cordis.yml under an agent scope during creation, rejecting a row that never activates or that publishes into the root service realm. |
| `ctx.commands` | `core` | [`commands`](../packages/interaction/commands) | - | - | - | Plugins register direct human commands without sending invocations to the model. |
| `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session/session-projection) | - | [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session/session-title), [`host-apiproxy`](../packages/host/apiproxy) | - | Domains register state-driven fold units; the eager drive keeps per-session watermark states and api-proxy serves baselines and pushes changed values. |
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs. |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge), [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
-| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`headless`](../packages/bundle/headless), [`host-apiproxy`](../packages/host/apiproxy) | - | Layers the default ModelSelection through settings so direct and Host-backed Agent front doors share one state owner. |
+| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`headless`](../packages/bundle/headless), [`host-apiproxy`](../packages/host/apiproxy) | - | Layers the default ModelSelection through settings so direct and Host-backed Agent entry points share one state owner. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. |
| `ctx.e2b` | `core` | [`e2b`](../packages/e2b/e2b) | - | [`fs-e2b`](../packages/e2b/fs-e2b), [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | - | Owns one shared E2B SDK handle, remote working directory, and final sandbox disposition so both fundamental E2B providers inhabit the same Linux runtime. |
| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local), [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`pty-local`](../packages/pty/pty-local), [`lsp-local`](../packages/lsp/lsp-local), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | The bash executors, the PTY shell backend, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn through ctx.subprocess; the service owns process coordinates, tree/session lifetime, stdio dispositions, terminal mechanics, and kill escalation. |
| `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`pwsh-local`](../packages/bash/pwsh-local) | [`tool-bash`](../packages/bash/tool-bash), [`tool-pwsh`](../packages/bash/tool-pwsh), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them. |
| `ctx.bashEnv` | `core` | [`bash-env`](../packages/bash/bash-env) | - | [`tool-bash`](../packages/bash/tool-bash), [`tool-pwsh`](../packages/bash/tool-pwsh) | - | Plugins declare effect-scoped DSH_* facts; each shell tool collects one trusted snapshot per execution and its executor rebuilds the namespace. |
-| `ctx.pty` | `seam` | [`pty`](../packages/pty/pty) | [`pty-local`](../packages/pty/pty-local) | [`tool-pty`](../packages/pty/tool-pty) | - | The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-pty exposes the owner-scoped model surface. |
+| `ctx.pty` | `seam` | [`pty`](../packages/pty/pty) | [`pty-local`](../packages/pty/pty-local) | [`tool-pty`](../packages/pty/tool-pty) | - | The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-pty exposes the owner-scoped model tools. |
| `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local) | [`bash-sandbox`](../packages/bash/bash-sandbox), [`pty-local`](../packages/pty/pty-local) | - | Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement. |
| `ctx.sandboxPolicy` | `core` | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | - | [`bash-sandbox`](../packages/bash/bash-sandbox), [`fs-sandbox`](../packages/fs/fs-sandbox), [`pty-local`](../packages/pty/pty-local) | - | The one home for the deployment default mode + workspace root; only the sandboxed executor and provider read the service (the tool layers use the pure `sandbox/mode` fold it also exports). Both enforcing families read it so bash and fs cannot confine to different roots. |
| `ctx.approval` | `seam` | `approval` | [`acp`](../packages/acp/acp) | [`tools`](../packages/core/tools), [`tool-bash`](../packages/bash/tool-bash) | - | One-shot permission decisions dispatched over the `approval/request` waterfall; answerers are listeners (the ACP bridge for its own agents), absence fails closed to `unavailable`. |
@@ -419,12 +439,12 @@ flowchart LR
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-e2b`](../packages/e2b/fs-e2b) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-policy contributes observed-state checks through the fs/* event gate. |
| `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; there is no model-facing compact tool. |
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. |
-| `ctx.tasks` | `seam` | [`tasks`](../packages/tasks/tasks) | [`tasks-local`](../packages/tasks/tasks-local) | [`tool-bash`](../packages/bash/tool-bash), [`tool-pty`](../packages/pty/tool-pty), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-tasks`](../packages/tasks/tool-tasks) | - | Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it; tasks-local is the process-local registry. |
+| `ctx.tasks` | `seam` | [`tasks`](../packages/tasks/tasks) | [`tasks-local`](../packages/tasks/tasks-local) | [`tool-bash`](../packages/bash/tool-bash), [`tool-pty`](../packages/pty/tool-pty), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-tasks`](../packages/tasks/tool-tasks) | - | Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing controller that reads, lists, and kills it; tasks-local is the process-local registry. |
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-local`](../packages/web/web-fetch-local) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. |
| `ctx.directoryPicker` | `seam` | `directory-picker` | `directory-picker-native`, `directory-picker-browse` | `apiproxy` | - | Discriminated interaction capability: the native backend opens one OS chooser on the host display, the browse backend serves listing/creation primitives for the in-app browser; dual-face backends fill ui-workspace directory-flow slots from their browser halves (no wire advertisement). |
| `ctx.httpServer` | `core` | `webserver` | - | `connection`, `modules`, `hmr` | - | Plain node:http carrier: named-route registry, index transform taps, and the static dist fallback; web-transport plugins register their own routes. |
-| `ctx.clientModuleHost` | `core` | `modules` | - | `hmr` | - | Composes the __DSH_BOOT__ entry graph from an incremental dshClient scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers. |
-| `ctx.workflows` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | [`tool-workflow`](../packages/workflow/tool-workflow), [`tool-ralph`](../packages/workflow/tool-ralph) | - | One engine per context (bash shape, no named-provider registry); the general workflow and fixed Ralph consumers start runs whose agent() calls fan out through ctx.subagents. |
+| `ctx.clientModuleHost` | `core` | `modules` | - | `hmr` | - | Composes the __DSH_BOOT__ entry graph from an incremental dsh.client scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers. |
+| `ctx.workflows` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | [`tool-workflow`](../packages/workflow/tool-workflow), [`tool-ralph`](../packages/workflow/tool-ralph) | - | One engine per context, as in bash, with no named-provider registry; the general workflow and fixed Ralph consumers start runs whose agent() calls fan out through ctx.subagents. |
Maintenance mode: hybrid: services are discovered from Cordis declarations; interface/implementation/consumer roles are classified in `scripts/gen-doc-graphs.ts` with a completeness guard.
diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md
index 7dff963646..8d2cf846a6 100644
--- a/docs/capability-seams.zh.md
+++ b/docs/capability-seams.zh.md
@@ -9,10 +9,14 @@
```mermaid
flowchart LR
+ pkg_attachment["attachment"]
+ svc_attachments["ctx.attachments
Durable binary attachment storage"]
+ pkg_attachment_local["attachment-local"]
+ pkg_host_runtime["host-runtime"]
+ pkg_llm_pi_ai["llm-pi-ai"]
pkg_llm["llm"]
svc_llm["ctx.llm
LLM adapter registry"]
pkg_llm_deepseek["llm-deepseek"]
- pkg_llm_pi_ai["llm-pi-ai"]
pkg_llm_replay["llm-replay"]
pkg_agent_loop["agent-loop"]
pkg_compact_basic["compact-basic"]
@@ -28,6 +32,7 @@ flowchart LR
pkg_session_query_sqlite["session-query-sqlite"]
pkg_subagent_inprocess["subagent-inprocess"]
pkg_invariants["invariants"]
+ pkg_message_feedback["message-feedback"]
svc_invariants["ctx.invariants
Package-owned invariant registry"]
pkg_scope["scope"]
pkg_typert_registry["typert-registry"]
@@ -58,6 +63,7 @@ flowchart LR
pkg_storage_domain["storage-domain"]
svc_storageDomain["ctx.storageDomain
Domain data facility"]
pkg_workspace["workspace"]
+ svc_messageFeedback["ctx.messageFeedback
Lifecycle-bound message feedback"]
svc_workspace["ctx.workspace
Workspace entity registry"]
svc_sessionQuery["ctx.sessionQuery
Session reads, traces, filters, and search"]
pkg_session_reference["session-reference"]
@@ -83,6 +89,8 @@ flowchart LR
svc_userInteraction["ctx.userInteraction
Human question/answer seam"]
pkg_plan_mode["plan-mode"]
svc_planMode["ctx.planMode
Plan collaboration state"]
+ pkg_agent_presets["agent-presets"]
+ svc_agentPresets["ctx.agentPresets
Per-session agent composition"]
pkg_commands["commands"]
svc_commands["ctx.commands
Human command registry"]
pkg_session_projection["session-projection"]
@@ -183,8 +191,11 @@ flowchart LR
pkg_agent --> svc_agents
pkg_agent_default_model --> svc_agentDefaultModel
pkg_agent_loop --> svc_agentLoop
+ pkg_agent_presets --> svc_agentPresets
pkg_api_gateway --> svc_typertGateway
pkg_approval --> svc_approval
+ pkg_attachment --> svc_attachments
+ pkg_attachment_local --> svc_attachments
pkg_bash --> svc_bash
pkg_bash_env --> svc_bashEnv
pkg_bash_local --> svc_bash
@@ -211,6 +222,7 @@ flowchart LR
pkg_llm_deepseek --> svc_llm
pkg_llm_pi_ai --> svc_llm
pkg_llm_replay --> svc_llm
+ pkg_message_feedback --> svc_messageFeedback
pkg_modules --> svc_clientModuleHost
pkg_permission --> svc_permission
pkg_plan_mode --> svc_planMode
@@ -279,6 +291,8 @@ flowchart LR
svc_agents --> pkg_subagent_inprocess
svc_approval --> pkg_tool_bash
svc_approval --> pkg_tools
+ svc_attachments --> pkg_host_runtime
+ svc_attachments --> pkg_llm_pi_ai
svc_bash --> pkg_hooks_claude
svc_bash --> pkg_hooks_codex
svc_bash --> pkg_tool_bash
@@ -313,6 +327,7 @@ flowchart LR
svc_sessionPersistence --> pkg_agent_loop
svc_sessionPersistence --> pkg_hooks_claude
svc_sessionPersistence --> pkg_hooks_codex
+ svc_sessionPersistence --> pkg_message_feedback
svc_sessionPersistence --> pkg_session_query
svc_sessionPersistence --> pkg_session_query_sqlite
svc_sessionPersistence --> pkg_tool_bash
@@ -325,6 +340,7 @@ flowchart LR
svc_sessions --> pkg_agent
svc_sessions --> pkg_agent_loop
svc_sessions --> pkg_invariants
+ svc_sessions --> pkg_message_feedback
svc_sessions --> pkg_session_persistence
svc_sessions --> pkg_session_query
svc_sessions --> pkg_session_query_sqlite
@@ -335,6 +351,7 @@ flowchart LR
svc_skills --> pkg_tool_skill
svc_spillStore --> pkg_spill_policy
svc_storage --> pkg_storage_domain
+ svc_storageDomain --> pkg_message_feedback
svc_storageDomain --> pkg_workspace
svc_subagents --> pkg_tool_ralph
svc_subagents --> pkg_tool_subagent
@@ -379,33 +396,36 @@ flowchart LR
| ctx 键 | 角色 | 所属包 | 实现 | 直接消费方 | 配套插件 | 说明 |
| --- | --- | --- | --- | --- | --- | --- |
+| `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | `host-runtime`、[`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | 宿主会在会话事件之前提交已接受的图片;提供方适配器将已授权的持久引用解析为提供方原生内容。 |
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek)、[`llm-pi-ai`](../packages/llm/llm-pi-ai)、[`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop)、[`compact-basic`](../packages/compact/compact-basic) | - | 适配器注册提供方实现;agent loop(智能体循环)与压缩功能调用提供方无关的流服务。 |
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | 拥有按会话隔离的回放折叠区;压力消费方共享不可变且带修订版本的测量结果。 |
| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 |
-| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop)、[`agent`](../packages/core/agent)、[`session-persistence`](../packages/session/session-persistence)、[`session-query`](../packages/session-query/session-query)、[`session-query-sqlite`](../packages/session-query/session-query-sqlite)、[`subagent-inprocess`](../packages/subagent/subagent-inprocess)、[`invariants`](../packages/support/invariants) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 |
+| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop)、[`agent`](../packages/core/agent)、[`session-persistence`](../packages/session/session-persistence)、[`session-query`](../packages/session-query/session-query)、[`session-query-sqlite`](../packages/session-query/session-query-sqlite)、[`subagent-inprocess`](../packages/subagent/subagent-inprocess)、[`invariants`](../packages/support/invariants)、[`message-feedback`](../packages/feedback/message-feedback) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 |
| `ctx.invariants` | `core` | [`invariants`](../packages/support/invariants) | - | [`session`](../packages/core/session)、[`agent`](../packages/core/agent)、[`scope`](../packages/core/scope)、[`agent-loop`](../packages/core/agent-loop) | - | 配套子路径注册所属包本地的检查;该服务负责选择、唯一性、子 fiber,以及标明所属包的失败。 |
| `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader)、[`api-gateway`](../packages/api/gateway) | - | 插件直接或通过 dsh-typert-loader 注册实时 zod 贡献;API 网关消费调用描述符和提供方,其他运行时消费方则在各自边界查询 schema 与反射元数据。 |
| `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | 将生成的 Remote 描述符与实时 Cordis 服务关联,解析已注册的身份,并通过共享的 Connection RPC 载体提供一元调用。 |
-| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl)、[`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop)、[`tool-bash`](../packages/bash/tool-bash)、[`hooks-claude`](../packages/hooks/hooks-claude)、[`hooks-codex`](../packages/hooks/hooks-codex)、[`session-query`](../packages/session-query/session-query)、[`session-query-sqlite`](../packages/session-query/session-query-sqlite) | - | 各后端持久化同一套 SessionEvent 词汇;应用在组合时选择后端。 |
+| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl)、[`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop)、[`tool-bash`](../packages/bash/tool-bash)、[`hooks-claude`](../packages/hooks/hooks-claude)、[`hooks-codex`](../packages/hooks/hooks-codex)、[`session-query`](../packages/session-query/session-query)、[`session-query-sqlite`](../packages/session-query/session-query-sqlite)、[`message-feedback`](../packages/feedback/message-feedback) | - | 各后端持久化同一套 SessionEvent 词汇;应用在组合时选择后端。 |
| `ctx.settings` | `seam` | [`settings`](../packages/settings/settings) | [`settings-local`](../packages/settings/settings-local) | [`llm-deepseek`](../packages/llm/llm-deepseek)、[`llm-pi-ai`](../packages/llm/llm-pi-ai)、`apiproxy` | - | 插件注册命名空间 schema 并解析分层值;提供方存储原始文档。LLM(大语言模型)适配器在用户分区下将其入口配置注册为组合基础;Web 网关提供经过脱敏的分层描述符,并写入用户层。 |
| `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`llm-deepseek`](../packages/llm/llm-deepseek)、[`llm-pi-ai`](../packages/llm/llm-pi-ai)、`apiproxy` | - | 配置携带对机密信息的引用;提供方拥有实际值。消费方按操作解析,因此轮换后的凭据会在紧接着的下一次请求中生效;Web 网关提供不含实际值的视图和只写存储。 |
| `ctx.telemetry` | `seam` | [`session-telemetry`](../packages/session/session-telemetry) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | - | - | 该 seam 捕获会话记录、进行脱敏并交给一个后端;没有其他组件消费该服务,其输出会离开当前进程。 |
| `ctx.storage` | `seam` | [`storage`](../packages/storage/storage) | [`storage-json`](../packages/storage/storage-json)、[`storage-sqlite`](../packages/storage/storage-sqlite) | [`storage-domain`](../packages/storage/storage-domain) | - | 各后端以不同名称并列注册;数据形态(领域优先)挂载到枢纽上,并将类型化操作转换为不透明的 KV 单元原语。 |
-| `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace) | - | 等待所有已配置后端就绪,然后将领域形态发布为一个受生命周期约束的服务,用于类型化持久状态。 |
+| `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace)、[`message-feedback`](../packages/feedback/message-feedback) | - | 等待所有已配置后端就绪,然后将领域形态发布为一个受生命周期约束的服务,用于类型化持久状态。 |
+| `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | 拥有本地逐 assistant 消息反馈、生命周期与目标校验、逐条目 compare-and-set 及 Host 一元 Remote 契约,且不进入 Session 历史或遥测。 |
| `ctx.workspace` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | 通过领域设施拥有带 WorkspaceId 品牌类型的记录;稳定的 sessionIds 账户驱动 Host RPC 与 GUI 投影。 |
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference)、[`tool-session-query`](../packages/session-query/tool-session-query) | - | 该接口提供精确读取、过滤和追踪;具体后端还提供全文协调、排序、摘要片段和游标世代,而模型消费方负责工作区权限与不含游标的渲染。 |
| `ctx.sessionReferences` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | 将当前表层中有界的对话快照投影为持久但不可信的消息上下文;Host 适配器负责提及语法。 |
| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-message-llm`](../packages/session/session-title-first-message-llm)、[`session-title-all-messages-llm`](../packages/session/session-title-all-messages-llm) | - | - | 负责确定性回退、最新标题折叠区,以及唯一的可选异步提供方注册。 |
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop)、[`tools`](../packages/core/tools)、[`tool-fs`](../packages/fs/tool-fs)、[`tool-pty`](../packages/pty/tool-pty)、[`tool-web`](../packages/web/tool-web) | - | 为每个步骤收集提示词各部分和面向模型的工具 schema。 |
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop)、[`tool-ask-user`](../packages/interaction/tool-ask-user)、[`tool-bash`](../packages/bash/tool-bash)、[`tool-cordis`](../packages/self-modification/tool-cordis)、[`tool-fs`](../packages/fs/tool-fs)、[`tool-pty`](../packages/pty/tool-pty)、[`tool-skill`](../packages/skill/tool-skill)、[`tool-subagent`](../packages/subagent/tool-subagent)、[`tool-todo`](../packages/todo/tool-todo)、[`tool-web`](../packages/web/tool-web) | - | 注册能力,负责 Code Mode 传输,并让调用依次经过策略前处理、单调守卫、环绕分派、策略后处理和最终结果观测。 |
-| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/interaction/user-interaction) | - | [`tool-ask-user`](../packages/interaction/tool-ask-user) | - | UI 入口提供当前生效的人工回答提供方;tool-ask-user 在提供方无关的 ask() promise 上暂停工具调用。 |
+| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/interaction/user-interaction) | - | [`tool-ask-user`](../packages/interaction/tool-ask-user) | - | UI 前端提供当前生效的人工回答提供方;tool-ask-user 在提供方无关的 ask() promise 上暂停工具调用。 |
| `ctx.planMode` | `core` | [`plan-mode`](../packages/plan/plan-mode) | - | - | - | 折叠已记录的计划/模式状态,在轮次边界刷新用户选择,渲染由部署方拥有的指导信息,注册 /plan,并在状态转换期间保持计划退出 schema 稳定。 |
+| `ctx.agentPresets` | `core` | [`agent-presets`](../packages/preset/agent-presets) | - | - | - | 在受信任根目录与用户创作根目录上发现 preset 目录,并在创建期把一份 preset cordis.yml 挂载到 agent 作用域之下,拒绝始终未激活或向根服务 realm 发布服务的行。 |
| `ctx.commands` | `core` | [`commands`](../packages/interaction/commands) | - | - | - | 插件注册直接面向人的命令,而不会把调用发送给模型。 |
| `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session/session-projection) | - | [`tool-todo`](../packages/todo/tool-todo)、[`session-title`](../packages/session/session-title)、[`host-apiproxy`](../packages/host/apiproxy) | - | 各领域注册由状态驱动的折叠单元;主动驱动过程维护每个会话的水位状态,api-proxy 提供基线并推送发生变化的值。 |
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | 按会话持久保存投影单元状态的检查点(节流检查点,以及轮次/结束/分离时的必选检查点),并提供冷读取阶梯:缓存行加持久化尾部回放,因此列表读取永远不需要加载完整日志。 |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge)、[`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | 合并提供方的 skill(技能)目录;tool-skill 渲染会话前缀目录,并加载完整的 skill 正文。 |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop)、[`acp`](../packages/acp/acp)、[`subagent-inprocess`](../packages/subagent/subagent-inprocess) | - | 拥有实时 Agent 句柄、创建/恢复工厂 seam,以及进程本地的发起方传播。 |
-| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`headless`](../packages/bundle/headless)、[`host-apiproxy`](../packages/host/apiproxy) | - | 通过 settings 分层默认 `ModelSelection`,让直接前门与 Host 支撑的 Agent 前门共享同一个状态所有者。 |
+| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`headless`](../packages/bundle/headless)、[`host-apiproxy`](../packages/host/apiproxy) | - | 通过 settings 分层默认 `ModelSelection`,让直接入口与 Host 支撑的 Agent 入口共享同一个状态所有者。 |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | 唯一的具体循环插件;扩展包依赖 dsh-agent 的事件和服务,而不依赖此包。 |
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | 从会话日志折叠带修订版本的目标状态,并将实时延续激活保留在进程本地。 |
| `ctx.e2b` | `core` | [`e2b`](../packages/e2b/e2b) | - | [`fs-e2b`](../packages/e2b/fs-e2b)、[`subprocess-e2b`](../packages/e2b/subprocess-e2b) | - | 拥有一个共享的 E2B SDK 句柄、远程工作目录和最终沙箱处置,使两个基础 E2B 提供方处于同一个 Linux 运行时中。 |
@@ -421,12 +441,12 @@ flowchart LR
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local)、[`fs-sandbox`](../packages/fs/fs-sandbox)、[`fs-e2b`](../packages/e2b/fs-e2b) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs 通过 ctx.fs 执行读取/写入/编辑;fs-sandbox 按共享沙箱模式限制变更;fs-policy 通过 fs/* 事件门禁贡献基于观测状态的检查。 |
| `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | 基础后端消费步骤后的压力事件和请求错误恢复事件;不存在面向模型的压缩工具。 |
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn)、[`subagent-fork`](../packages/subagent/subagent-fork)、[`subagent-acp`](../packages/subagent/subagent-acp)、[`subagent-codex`](../packages/subagent/subagent-codex)、[`subagent-claude-code`](../packages/subagent/subagent-claude-code)、[`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent)、[`tool-subagent-control`](../packages/subagent/tool-subagent-control)、[`tool-ralph`](../packages/workflow/tool-ralph) | - | 提供方实现传输;该服务还负责可选的、基于 Activation 的延续编排,tool-subagent 选择一次性或可延续委派,tool-subagent-control 传递后续消息,而 tool-ralph 要求一条全新的结构化输出路由。 |
-| `ctx.tasks` | `seam` | [`tasks`](../packages/tasks/tasks) | [`tasks-local`](../packages/tasks/tasks-local) | [`tool-bash`](../packages/bash/tool-bash)、[`tool-pty`](../packages/pty/tool-pty)、[`tool-subagent`](../packages/subagent/tool-subagent)、[`tool-tasks`](../packages/tasks/tool-tasks) | - | 生产方(后台 bash、PTY 发送和 subagent 委派)登记正在运行的工作;tool-tasks 是面向模型的控制接口,用于读取、列出和终止这些工作;tasks-local 是进程本地注册表。 |
+| `ctx.tasks` | `seam` | [`tasks`](../packages/tasks/tasks) | [`tasks-local`](../packages/tasks/tasks-local) | [`tool-bash`](../packages/bash/tool-bash)、[`tool-pty`](../packages/pty/tool-pty)、[`tool-subagent`](../packages/subagent/tool-subagent)、[`tool-tasks`](../packages/tasks/tool-tasks) | - | 生产方(后台 bash、PTY 发送和 subagent 委派)登记正在运行的工作;tool-tasks 是面向模型的控制器,用于读取、列出和终止这些工作;tasks-local 是进程本地注册表。 |
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa)、[`web-search-perplexity`](../packages/web/web-search-perplexity)、[`web-search-deepseek`](../packages/web/web-search-deepseek)、[`web-fetch-local`](../packages/web/web-fetch-local) | [`tool-web`](../packages/web/tool-web) | - | 搜索和抓取提供方注册到同一个 ctx.web seam;tool-web 负责稳定的面向模型名称。 |
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | 后端保存过大的工具文本,并返回面向模型的定位信息和取回提示;spill-policy 是 tools/post-execute 消费方,负责决定何时 spill。 |
| `ctx.directoryPicker` | `seam` | `directory-picker` | `directory-picker-native`、`directory-picker-browse` | `apiproxy` | - | 带判别标记的交互能力:原生后端在 Host 显示设备上打开一个操作系统选择器,浏览后端为应用内浏览器提供列表与创建原语;双端后端通过其浏览器侧填充 ui-workspace 目录流程的 slot(不通过协议发布)。 |
| `ctx.httpServer` | `core` | `webserver` | - | `connection`、`modules`、`hmr` | - | 普通的 node:http 载体:具名路由注册表、索引转换 tap,以及静态 dist 回退;Web 传输插件注册自己的路由。 |
-| `ctx.clientModuleHost` | `core` | `modules` | - | `hmr` | - | 通过增量 dshClient 扫描组合 __DSH_BOOT__ 入口图,提供插件组合包,并通知重建/图变更订阅方。 |
-| `ctx.workflows` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | [`tool-workflow`](../packages/workflow/tool-workflow)、[`tool-ralph`](../packages/workflow/tool-ralph) | - | 每个上下文使用一个引擎(bash 形态,无具名提供方注册表);通用工作流与固定 Ralph 消费方启动运行,其中的 agent() 调用通过 ctx.subagents 扇出。 |
+| `ctx.clientModuleHost` | `core` | `modules` | - | `hmr` | - | 通过增量 `dsh.client` 扫描组合 __DSH_BOOT__ 入口图,提供插件组合包,并通知重建/图变更订阅方。 |
+| `ctx.workflows` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | [`tool-workflow`](../packages/workflow/tool-workflow)、[`tool-ralph`](../packages/workflow/tool-ralph) | - | 每个上下文使用一个引擎,与 bash 相同,且没有具名提供方注册表;通用工作流与固定 Ralph 消费方启动运行,其中的 agent() 调用通过 ctx.subagents 扇出。 |
维护模式:混合模式。服务从 Cordis 声明中发现;接口、实现和消费方角色在 `scripts/gen-doc-graphs.ts` 中分类,并设有完整性守卫。
diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index 7b40f69749..915b9f7e43 100644
--- a/docs/config-catalog.i18n.yaml
+++ b/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: d97792027eb771895f6993ec83d24763364d88a7
-config-catalog.zh.md: 850d6d40549806273ae209dad40ad9ba45e4de4c
+config-catalog.md: b979e250facc7975dd4e4a2e0c1d2563e5a7ff4e
+config-catalog.zh.md: e42fd503f31ca48ebd1f043e42066741158d2bee
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index d97792027e..b979e250fa 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -3,7 +3,7 @@
# Plugin Config Catalog
-Every `config:` block a `cordis.yml` entry can set: for each loadable harness package, the verbatim config declaration (JSDoc included) its `apply` function or service constructor receives, with every referenced type pasted alongside (package-local types) or linked (everything else). The paste is the plugin's full declared config type — a field the runtime schema deliberately excludes is a runtime-only seam (its own JSDoc says so) and is not settable from `cordis.yml`. This is the **deployment**-axis reference — the wiring a plugin author works against is the generated `cordis-surface` region on each [subsystem page](subsystems/core.md), the model-facing tool schemas are the [tool catalog](tool-catalog.md), and [subsystems/](subsystems/core.md) documents the types these declarations reference.
+Every `config:` block a `cordis.yml` entry can set: for each loadable harness package, the verbatim config declaration (JSDoc included) its `apply` function or service constructor receives, with every referenced type pasted alongside (package-local types) or linked (everything else). The paste is the plugin's full declared config type — a field the runtime schema deliberately excludes is a runtime-only seam (its own JSDoc says so) and is not settable from `cordis.yml`. This is the **deployment**-axis reference — the wiring a plugin author works against is the generated Cordis API region on each [subsystem page](subsystems/core.md), the model-facing tool schemas are the [tool catalog](tool-catalog.md), and [subsystems/](subsystems/core.md) documents the types these declarations reference.
This file is GENERATED from source (`scripts/gen-config-catalog.ts`) and verified fresh by `pnpm run verify-config-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks use a `ts config-catalog` fence (skipped by doc-typecheck, since a lone declaration referencing imports is not standalone-compilable). The generator also cross-checks the runtime schemastery schema against the pasted declaration — every schema-validated key, nested keys included, must be locatable on the declared config type — so the paste cannot hide a loader-accepted field.
@@ -69,7 +69,7 @@ export interface Config {
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-core. */
toolBash?: NonNullable
- /** Generic background-task controls forwarded through agent-core; set false to omit their tool surface. */
+ /** Generic background-task controls forwarded through agent-core; set false to omit their tools. */
toolTasks?: NonNullable
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and tools. */
goals?: agentCore.GoalConfig | false
@@ -124,6 +124,37 @@ Depends on: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/cor
Source: [`packages/core/agent-loop/src/index.ts:236`](../packages/core/agent-loop/src/index.ts)
+## `@deepseek-ai/dsh-agent-presets`
+
+Requires: `loader`
+
+```ts config-catalog
+/** Plugin config: which preset is the default, and where presets live. */
+export interface Config {
+ /** Preset id mounted when a caller names none. Missing at mount time fails loud. */
+ default: string
+ /** Scanned roots in precedence order; an earlier root wins a duplicate id. */
+ roots: PresetRoot[]
+}
+
+/** One directory scanned for preset subdirectories. */
+export interface PresetRoot {
+ /** Directory holding one subdirectory per preset; a leading `~` expands. */
+ path: string
+ /** Trust recorded on every preset discovered under this root. */
+ trust: PresetTrust
+}
+
+/**
+ * Where a preset's composition came from. A `system` preset ships with the
+ * deployment; a `user` preset was authored locally, by a person or by an
+ * agent, and therefore carries the same trust as shell access.
+ */
+export type PresetTrust = 'system' | 'user'
+```
+
+Source: [`packages/preset/agent-presets/src/types.ts:52`](../packages/preset/agent-presets/src/types.ts)
+
## `@deepseek-ai/dsh-agent-spine-demo`
```ts config-catalog
@@ -208,6 +239,48 @@ Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfi
Source: [`packages/examples/agent-spine-demo/src/index.ts:90`](../packages/examples/agent-spine-demo/src/index.ts)
+## `@deepseek-ai/dsh-agent-tool-mode`
+
+Requires: `tools`
+
+```ts config-catalog
+/** Plugin config. */
+export interface Config {
+ /**
+ * The form this agent's model sees. `native` sends every visible schema,
+ * `code` sends only `run_code` plus a generated SDK, `both` sends both.
+ * Required rather than defaulted: the deployment default is what a preset
+ * without this row already gets, so an omitted value would mean the row was
+ * composed for nothing.
+ */
+ mode: ToolPresentationMode
+}
+```
+
+Depends on: [`ToolPresentationMode`](subsystems/tools.md)
+
+Source: [`packages/core/agent-tool-mode/src/index.ts:38`](../packages/core/agent-tool-mode/src/index.ts)
+
+## `@deepseek-ai/dsh-attachment-local`
+
+```ts config-catalog
+/** Local attachment backend configuration. */
+export interface Config {
+ /** Explicit harness home; omitted follows `DSH_HOME`, then `~/.dsh`. */
+ dshHome?: string
+ /** Maximum encoded bytes accepted for one image. */
+ maxImageBytes?: number
+ /** Maximum image count accepted in one submitted message. */
+ maxImagesPerMessage?: number
+ /** Maximum aggregate encoded image bytes accepted in one submitted message. */
+ maxMessageImageBytes?: number
+ /** Maximum intrinsic width multiplied by height accepted for one image. */
+ maxImagePixels?: number
+}
+```
+
+Source: [`packages/attachment/attachment-local/src/index.ts:24`](../packages/attachment/attachment-local/src/index.ts)
+
## `@deepseek-ai/dsh-bash-env`
```ts config-catalog
@@ -279,10 +352,12 @@ export interface ConnectionConfig {
* that is not a bare, canonical authority fails the plugin load.
*/
trustedHosts?: string[]
+ /** Maximum buffered JSON body for every `/api` request. */
+ maxRequestBodyBytes?: number
}
```
-Source: [`packages/client/connection/src/index.ts:32`](../packages/client/connection/src/index.ts)
+Source: [`packages/client/connection/src/index.ts:52`](../packages/client/connection/src/index.ts)
## `@deepseek-ai/dsh-client-hmr`
@@ -296,7 +371,7 @@ export interface Config {
}
```
-Source: [`packages/client/hmr/src/index.ts:29`](../packages/client/hmr/src/index.ts)
+Source: [`packages/client/hmr/src/index.ts:31`](../packages/client/hmr/src/index.ts)
## `@deepseek-ai/dsh-code-runtime-worker`
@@ -450,10 +525,15 @@ Source: [`packages/host/frontend-static/src/index.ts:28`](../packages/host/front
export interface Config {
/** Base directory for relative paths. Defaults to `process.cwd()`. */
cwd?: string
+ /**
+ * Exclusive UTF-8 byte limit on each overwrite-diff side, capped by the
+ * runtime's safe allocation/decode maximum. Defaults to 10 MiB.
+ */
+ diffBasisMaxBytes?: number
}
```
-Source: [`packages/fs/fs-local/src/index.ts:39`](../packages/fs/fs-local/src/index.ts)
+Source: [`packages/fs/fs-local/src/index.ts:41`](../packages/fs/fs-local/src/index.ts)
## `@deepseek-ai/dsh-fs-sandbox`
@@ -461,10 +541,10 @@ Requires: `sandboxPolicy`
```ts config-catalog
/**
- * Plugin config: the local backend's knobs, verbatim (only `cwd`, the resolve
- * base for relative paths). The sandbox default (mode + `workspace-write`
- * fallback root) is NOT here — `ctx.sandboxPolicy` resolves each calling
- * session for every enforcing capability.
+ * Plugin config: the local backend's knobs verbatim (`cwd` resolution default
+ * and `diffBasisMaxBytes` overwrite-presentation bound). The sandbox default
+ * (mode + `workspace-write` fallback root) is NOT here — `ctx.sandboxPolicy`
+ * resolves each calling session for every enforcing capability.
*/
export type Config = LocalConfig
```
@@ -492,14 +572,14 @@ Source: [`packages/goal/goal/src/index.ts:116`](../packages/goal/goal/src/index.
Requires: `agentDefaultModel` · `agents` · `sessions`
```ts config-catalog
-/** Plugin config: the task, patched in by the launcher. */
+/** Plugin config: the task resolved from this app's injected provider service. */
export interface Config {
/** The prompt text for the single run. */
task: string
}
```
-Source: [`packages/bundle/headless/src/index.ts:29`](../packages/bundle/headless/src/index.ts)
+Source: [`packages/bundle/headless/src/index.ts:31`](../packages/bundle/headless/src/index.ts)
## `@deepseek-ai/dsh-hooks-claude`
@@ -564,17 +644,29 @@ Source: [`packages/hooks/hooks-codex/src/index.ts:44`](../packages/hooks/hooks-c
## `@deepseek-ai/dsh-host-apiproxy`
-Requires: `agentDefaultModel` · `agents` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `tools` · `userInteraction` · `workspace`
+Requires: `agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `tools` · `userInteraction` · `workspace`
```ts config-catalog
-/** Gateway plugin config: the Host-only Workspace creation root. */
+/** Gateway plugin configuration. */
export interface Config {
- /** Parent directory for name-created Workspaces; defaults to the Host cwd. */
- workspaceRoot?: string
+ /**
+ * Whether this deployment can hand paths to a native desktop opener —
+ * the `hasDocument` capability the agent-preset roster reports. Absent,
+ * the platform is asked (macOS/Windows/WSL yes; Linux only with a display
+ * server); set it explicitly where detection misleads, e.g. `false` in a
+ * container whose DISPLAY points nowhere a user can see.
+ */
+ nativeOpen?: boolean
+ /**
+ * DEFLATE level for every session-log ZIP entry: `0` stores without
+ * compression, `1` favors CPU/latency, and `9` favors archive size.
+ * @default 6
+ */
+ sessionExportCompressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
}
```
-Source: [`packages/host/apiproxy/src/index.ts:38`](../packages/host/apiproxy/src/index.ts)
+Source: [`packages/host/apiproxy/src/index.ts:41`](../packages/host/apiproxy/src/index.ts)
## `@deepseek-ai/dsh-host-directory-picker-browse`
@@ -638,7 +730,7 @@ export interface JsonRpcConfig {
Depends on: `Readable` (`node:stream`) · `Writable` (`node:stream`)
-Source: [`packages/scaffold/server/src/index.ts:29`](../packages/scaffold/server/src/index.ts)
+Source: [`packages/sdk/server/src/index.ts:29`](../packages/sdk/server/src/index.ts)
## `@deepseek-ai/dsh-llm-deepseek`
@@ -817,12 +909,12 @@ export type PiAiModelOverride = Omit
* default) or per model (winning over the route). Only the switches pi-ai's
* reasoning dispatch reads are offered; the rest of pi-ai's compat surface
* keeps its baseURL-derived auto-detection. pi-ai types both fields only on
- * `OpenAICompletionsCompat` — the other wire protocols carry their reasoning
- * shape in the protocol itself — so resolution rejects a model-level switch
+ * `OpenAICompletionsCompat` — the other wire protocols define their reasoning
+ * fields in the protocol itself — so resolution rejects a model-level switch
* anywhere else, while a route-level default skips past models it cannot fit.
*/
export interface PiAiCompatProfile {
- /** Reasoning parameter shape the endpoint expects; absent keeps the catalog entry's, then pi-ai's baseURL-derived guess. */
+ /** Reasoning parameter format the endpoint expects; absent keeps the catalog entry's, then pi-ai's baseURL-derived guess. */
thinkingFormat?: PiAiThinkingFormat
/** Whether the endpoint accepts `reasoning_effort`; absent keeps the catalog entry's, then pi-ai's baseURL-derived guess. */
supportsReasoningEffort?: boolean
@@ -900,12 +992,26 @@ export interface ReplayModelConfig {
description?: string
/** Optional positive integer context capacity published by the replay adapter. */
contextWindow?: number
+ /** Optional declared input modalities, so a scenario can exercise capability gates (e.g. image-capable `read_image`). */
+ inputModalities?: readonly ModelModality[]
+ /**
+ * Optional per-request output cap the replay route materializes when callers
+ * omit one, so replay reconstructs the request header a live catalog produced.
+ */
+ defaultMaxTokens?: number
+ /** Optional reasoning-effort ids the replay route accepts, in display order. */
+ reasoningEfforts?: string[]
+ /**
+ * Optional effort materialized when callers omit one; must appear in
+ * {@link reasoningEfforts} or call resolution rejects the route.
+ */
+ defaultReasoningEffort?: string
}
```
-Depends on: [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
+Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
-Source: [`packages/support/llm-replay/src/index.ts:744`](../packages/support/llm-replay/src/index.ts)
+Source: [`packages/support/llm-replay/src/index.ts:776`](../packages/support/llm-replay/src/index.ts)
## `@deepseek-ai/dsh-llm-retry`
@@ -988,6 +1094,8 @@ export interface StdioConfig {
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
+ /** Automatic reconnect policy after a lost connection; omission uses the defaults. */
+ reconnect?: ReconnectConfig
}
/** Config for connecting to an MCP server over Streamable HTTP (SSE). */
@@ -1008,10 +1116,38 @@ export interface StreamableHttpConfig {
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
+ /** Automatic reconnect policy after a lost connection; omission uses the defaults. */
+ reconnect?: ReconnectConfig
+}
+
+/** Automatic reconnect policy for one MCP server connection. */
+export interface ReconnectConfig {
+ /** Reconnect automatically after a lost connection (default true). */
+ enabled?: boolean
+ /** First reconnect delay in milliseconds; doubles per consecutive failed attempt (default 500). */
+ initialDelayMs?: number
+ /** Backoff ceiling in milliseconds; also the uptime after which the attempt budget resets (default 30000). */
+ maxDelayMs?: number
+ /** Consecutive failed attempts per outage before giving up for good (default 10). */
+ maxAttempts?: number
}
```
-Source: [`packages/mcp/mcp-client/src/index.ts:100`](../packages/mcp/mcp-client/src/index.ts)
+Source: [`packages/mcp/mcp-client/src/index.ts:98`](../packages/mcp/mcp-client/src/index.ts)
+
+## `@deepseek-ai/dsh-message-feedback`
+
+Requires: `storageDomain` · `sessionPersistence` · `sessions`
+
+```ts config-catalog
+/** Required deployment policy for optional notes. */
+export interface Config {
+ /** Maximum UTF-8 byte length accepted for one note. */
+ readonly maxNoteBytes: number
+}
+```
+
+Source: [`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts)
## `@deepseek-ai/dsh-permission`
@@ -1050,6 +1186,26 @@ Depends on: [`ApprovalPolicy`](subsystems/approval.md) · [`SandboxMode`](subsys
Source: [`packages/interaction/permission/src/index.ts:140`](../packages/interaction/permission/src/index.ts)
+## `@deepseek-ai/dsh-persona`
+
+Requires: `systemPrompt`
+
+```ts config-catalog
+/** Plugin config: the persona text this composition contributes. */
+export interface Config {
+ /**
+ * Persona prose rendered as the `deployment:persona` section. A template:
+ * complete `{{…}}` groups interpolate strictly against registered prompt
+ * variables. Empty text drops the section at render, matching the registry.
+ */
+ text: string
+ /** Make this persona the complete system prompt, suppressing every other section. */
+ complete?: boolean
+}
+```
+
+Source: [`packages/preset/persona/src/index.ts:34`](../packages/preset/persona/src/index.ts)
+
## `@deepseek-ai/dsh-plan-mode`
Requires: `tools` · `systemPrompt`
@@ -1062,7 +1218,7 @@ export interface PlanModeConfig {
}
```
-Source: [`packages/plan/plan-mode/src/index.ts:69`](../packages/plan/plan-mode/src/index.ts)
+Source: [`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts)
## `@deepseek-ai/dsh-pty-local`
@@ -1138,6 +1294,26 @@ export interface Config {
Source: [`packages/bash/pwsh-local/src/index.ts:54`](../packages/bash/pwsh-local/src/index.ts)
+## `@deepseek-ai/dsh-pwsh-sandbox`
+
+Requires: `subprocess` · `sandbox` · `sandboxPolicy`
+
+```ts config-catalog
+/**
+ * Plugin config: the local executor's knobs, verbatim. The sandbox policy —
+ * the default mode and fallback `workspace-write` root — is NOT here: it lives
+ * on `ctx.sandboxPolicy` (`@deepseek-ai/dsh-sandbox-policy`), which resolves
+ * each calling session's mode and cwd for every enforcing capability. The
+ * runner choice is likewise the `ctx.sandbox` provider's config, not this
+ * executor's.
+ */
+export type Config = LocalConfig
+```
+
+Depends on: [`LocalConfig`](#deepseek-aidsh-pwsh-local)
+
+Source: [`packages/bash/pwsh-sandbox/src/index.ts:40`](../packages/bash/pwsh-sandbox/src/index.ts)
+
## `@deepseek-ai/dsh-repeat-tool-guard`
```ts config-catalog
@@ -1170,29 +1346,13 @@ export interface Config {
Source: [`packages/guard/repeat-tool-guard/src/index.ts:28`](../packages/guard/repeat-tool-guard/src/index.ts)
-## `@deepseek-ai/dsh-repository-plugin`
-
-Requires: `loader`
-
-```ts config-catalog
-/** Repository Plugin runtime and source-list configuration. */
-export interface Config {
- /** GitHub repository sources with explicit refs and optional `.dsh-plugin` subpaths. */
- repositories?: string[]
- /** Persistent generation cache; defaults to `$DSH_HOME/cache/repository-plugins`. */
- cacheDir?: string
-}
-```
-
-Source: [`packages/self-modification/repository-plugin/src/index.ts:44`](../packages/self-modification/repository-plugin/src/index.ts)
-
## `@deepseek-ai/dsh-sandbox-local`
```ts config-catalog
/** Plugin config. All optional — `static Config` supplies the defaults. */
export interface Config {
/**
- * Override the runner argv; bwrap-shaped profile arguments are appended. A
+ * Override the runner argv; bwrap-compatible profile arguments are appended. A
* non-empty override asserts full enforcement and skips built-in selection and
* probing. A runner that starts but refuses its profile must be identifiable by
* {@link runnerFailureSignatures}. Consumers classify a spawn rejection only after
@@ -1214,7 +1374,7 @@ export interface Config {
}
```
-Source: [`packages/sandbox/sandbox-local/src/index.ts:24`](../packages/sandbox/sandbox-local/src/index.ts)
+Source: [`packages/sandbox/sandbox-local/src/index.ts:44`](../packages/sandbox/sandbox-local/src/index.ts)
## `@deepseek-ai/dsh-sandbox-policy`
@@ -1276,7 +1436,7 @@ export interface Config {
export type JsonlCompression = 'zstd' | 'none'
```
-Source: [`packages/session/session-persistence-jsonl/src/index.ts:59`](../packages/session/session-persistence-jsonl/src/index.ts)
+Source: [`packages/session/session-persistence-jsonl/src/index.ts:60`](../packages/session/session-persistence-jsonl/src/index.ts)
## `@deepseek-ai/dsh-session-persistence-sqlite`
@@ -1319,7 +1479,7 @@ export interface Config {
export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
```
-Source: [`packages/session/session-persistence-sqlite/src/index.ts:67`](../packages/session/session-persistence-sqlite/src/index.ts)
+Source: [`packages/session/session-persistence-sqlite/src/index.ts:70`](../packages/session/session-persistence-sqlite/src/index.ts)
## `@deepseek-ai/dsh-session-projection-cache`
@@ -1404,7 +1564,7 @@ Requires: `sessions`
```ts config-catalog
/**
- * Plugin configuration: one sharing policy, two verbatim SDK option shapes,
+ * Plugin configuration: one sharing policy, two verbatim SDK option objects,
* and one DSH-owned shutdown bound. Uploading modes validate their endpoint
* and shutdown deadline at plugin load; `DISABLED` reads neither.
*/
@@ -1440,7 +1600,7 @@ export enum TelemetryMode {
Depends on: `BatchLogRecordProcessorOptions` (`@opentelemetry/sdk-logs`) · `OTLPExporterNodeConfigBase` (`@opentelemetry/otlp-exporter-base`)
-Source: [`packages/session/session-telemetry-otel/src/index.ts:80`](../packages/session/session-telemetry-otel/src/index.ts)
+Source: [`packages/session/session-telemetry-otel/src/index.ts:91`](../packages/session/session-telemetry-otel/src/index.ts)
## `@deepseek-ai/dsh-session-title`
@@ -1514,7 +1674,7 @@ export interface Config {
}
```
-Source: [`packages/skill/skill/src/index.ts:266`](../packages/skill/skill/src/index.ts)
+Source: [`packages/skill/skill/src/index.ts:279`](../packages/skill/skill/src/index.ts)
## `@deepseek-ai/dsh-skill-local`
@@ -1688,7 +1848,7 @@ export interface Config {
/**
* How to auto-answer the child's `session/request_permission` prompts:
* `reject` (default — decline every prompt) or `allow` (approve via the first
- * allow-shaped option). No prompt is surfaced to a human.
+ * `allow_once` or `allow_always` option). No prompt is surfaced to a human.
*/
permission: PermissionPolicy
/**
@@ -1860,14 +2020,14 @@ export interface Config {
persona?: string
/**
* Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once.
- * Shape errors fail at load and unknown names fail at assembly; known names
+ * Invalid fields fail at load and unknown names fail at assembly; known names
* hidden in one scope may be absent there. Omitted means lexicographic order.
*/
toolOrder?: string[]
}
```
-Source: [`packages/core/system-prompt/src/index.ts:166`](../packages/core/system-prompt/src/index.ts)
+Source: [`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
## `@deepseek-ai/dsh-time-context`
@@ -1978,7 +2138,7 @@ export interface Config {
}
```
-Source: [`packages/fs/tool-fs/src/index.ts:24`](../packages/fs/tool-fs/src/index.ts)
+Source: [`packages/fs/tool-fs/src/index.ts:25`](../packages/fs/tool-fs/src/index.ts)
## `@deepseek-ai/dsh-tool-fs-search`
@@ -2070,7 +2230,7 @@ export interface Config {
}
```
-Source: [`packages/bash/tool-pwsh/src/index.ts:43`](../packages/bash/tool-pwsh/src/index.ts)
+Source: [`packages/bash/tool-pwsh/src/index.ts:52`](../packages/bash/tool-pwsh/src/index.ts)
## `@deepseek-ai/dsh-tool-ralph`
@@ -2202,14 +2362,15 @@ Source: [`packages/subagent/tool-subagent/src/index.ts:25`](../packages/subagent
## `@deepseek-ai/dsh-tool-subagent-report`
-Requires: `subagents` · `tools`
+Requires: `subagents` · `tools` · `systemPrompt`
```ts config-catalog
/** Config: how accepted reports are scheduled on the parent. */
export interface Config {
/**
- * Parent scheduling (default `quiet`). `quiet` adds context without waking;
- * `wakeup` creates one ordinary later parent turn.
+ * Parent scheduling (default `wakeup`). `wakeup` creates one ordinary later
+ * parent turn; `quiet` adds context without waking, so a parked parent learns
+ * of the report only when something else wakes it.
*/
reportDelivery?: SubagentReportDelivery
}
@@ -2217,7 +2378,7 @@ export interface Config {
Depends on: [`SubagentReportDelivery`](subsystems/subagent.md)
-Source: [`packages/subagent/tool-subagent-report/src/index.ts:22`](../packages/subagent/tool-subagent-report/src/index.ts)
+Source: [`packages/subagent/tool-subagent-report/src/index.ts:27`](../packages/subagent/tool-subagent-report/src/index.ts)
## `@deepseek-ai/dsh-tool-tasks`
@@ -2293,7 +2454,7 @@ export interface Config {
}
```
-Source: [`packages/workflow/tool-workflow/src/index.ts:27`](../packages/workflow/tool-workflow/src/index.ts)
+Source: [`packages/workflow/tool-workflow/src/index.ts:33`](../packages/workflow/tool-workflow/src/index.ts)
## `@deepseek-ai/dsh-tools`
@@ -2303,11 +2464,16 @@ Requires: `systemPrompt`
/** Plugin config: how the registered tools are presented to the model. */
export interface Config {
/**
- * Model presentation. `native` (default) sends every visible schema; `code`
- * sends only `run_code` plus a generated SDK prompt; `both` sends both forms.
- * Code modes require a `ctx.codeRuntime` whose `language` has a registered
- * SDK renderer (TypeScript or Python) and fail prompt assembly when it is
- * absent or has no renderer. Under `code`, native names in `toolOrder` are invalid.
+ * Model presentation for agents that declare none of their own. `native`
+ * (default) sends every visible schema; `code` sends only `run_code` plus a
+ * generated SDK prompt; `both` sends both forms. Code modes require a
+ * `ctx.codeRuntime` whose `language` has a registered SDK renderer
+ * (TypeScript or Python) and fail prompt assembly when it is absent or has
+ * no renderer. Under `code`, native names in `toolOrder` are invalid.
+ *
+ * One agent overrides this for itself with {@link ToolRegistry.presentAs},
+ * which is how an agent preset composes a Code Mode agent beside native
+ * ones in the same process.
*/
mode?: ToolPresentationMode
/**
@@ -2324,7 +2490,7 @@ export interface Config {
export type ToolPresentationMode = 'native' | 'code' | 'both'
```
-Source: [`packages/core/tools/src/index.ts:624`](../packages/core/tools/src/index.ts)
+Source: [`packages/core/tools/src/index.ts:625`](../packages/core/tools/src/index.ts)
## `@deepseek-ai/dsh-typert-loader`
@@ -2393,33 +2559,23 @@ Source: [`packages/web/web/src/index.ts:55`](../packages/web/web/src/index.ts)
Requires: `httpServer`
```ts config-catalog
-/** Plugin config: the surface facts the launcher patches over this bundle's defaults. */
+/** Plugin config: composed deployment settings plus per-invocation command-line values. */
export interface Config {
- /** Whether this process mounted the client-plugin HMR receiver (`dsh web --dev`). */
- mode: WebMode
- /** Print the URL line on activation; a headless layer over this bundle turns it off. */
+ /** Print the URL line on activation; a non-interactive layer can turn it off. */
printUrl: boolean
/**
* Register the model-visible surface context (the `app:web-surface` prompt
- * section and the `DSH_WEB_URL`/`DSH_WEB_MODE` bash variables). A one-shot
- * layer turns it off: its user is not interacting through the GUI, so the
+ * section and the `DSH_WEB_URL` bash variable). A one-shot non-interactive
+ * layer can turn it off when its user is not in the GUI, so the
* orientation text would be false.
*/
surfaceContext: boolean
- /**
- * LAN IPv4 addresses sampled once by the launcher when the effective bind
- * is all-interfaces — the exact snapshot the /api trust fence was
- * configured with, so the printed LAN URL can never name an address the
- * fence rejects. Empty on a loopback bind.
- */
- lanAddresses: string[]
+ /** Explicit `--trusted-host` authorities from this invocation. */
+ trustedHosts: string[]
}
-
-/** Web runtime mode: production, or development when the client-plugin HMR receiver is active. */
-export type WebMode = 'production' | 'development'
```
-Source: [`packages/bundle/web-app/src/index.ts:32`](../packages/bundle/web-app/src/index.ts)
+Source: [`packages/bundle/web-app/src/index.ts:38`](../packages/bundle/web-app/src/index.ts)
## `@deepseek-ai/dsh-web-fetch-local`
@@ -2573,7 +2729,7 @@ Source: [`packages/context/workspace-context/src/config.ts:18`](../packages/cont
## Loadable plugins with no config
-These load from a `cordis.yml` entry with no `config:` block; they declare no config surface.
+These load from a `cordis.yml` entry with no `config:` block; they declare no configuration API.
- `@deepseek-ai/dsh-agent` ([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts))
- `@deepseek-ai/dsh-api-gateway` — requires `typert` ([`packages/api/gateway/src/index.ts`](../packages/api/gateway/src/index.ts))
@@ -2581,6 +2737,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-client-locale` ([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts))
- `@deepseek-ai/dsh-client-modules` — requires `httpServer` · `loader` ([`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts))
- `@deepseek-ai/dsh-client-runtime` ([`packages/client/runtime/src/index.ts`](../packages/client/runtime/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-agent-preset` ([`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts))
- `@deepseek-ai/dsh-client-ui-command` ([`packages/client/ui-command/src/index.ts`](../packages/client/ui-command/src/index.ts))
- `@deepseek-ai/dsh-client-ui-conversation` ([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts))
- `@deepseek-ai/dsh-client-ui-deliverables` ([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts))
@@ -2590,16 +2747,18 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-client-ui-models` ([`packages/client/ui-models/src/index.ts`](../packages/client/ui-models/src/index.ts))
- `@deepseek-ai/dsh-client-ui-permission` ([`packages/client/ui-permission/src/index.ts`](../packages/client/ui-permission/src/index.ts))
- `@deepseek-ai/dsh-client-ui-plan` ([`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts))
-- `@deepseek-ai/dsh-client-ui-question` — requires `tools` · `userInteraction` ([`packages/client/ui-question/src/index.ts`](../packages/client/ui-question/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-question` ([`packages/client/ui-question/src/index.ts`](../packages/client/ui-question/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings` ([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings-general` ([`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts))
- `@deepseek-ai/dsh-client-ui-sidebar` ([`packages/client/ui-sidebar/src/index.ts`](../packages/client/ui-sidebar/src/index.ts))
- `@deepseek-ai/dsh-client-ui-skill` ([`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts))
- `@deepseek-ai/dsh-client-ui-slash` ([`packages/client/ui-slash/src/index.ts`](../packages/client/ui-slash/src/index.ts))
- `@deepseek-ai/dsh-client-ui-subagent` ([`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-task` ([`packages/client/ui-task/src/index.ts`](../packages/client/ui-task/src/index.ts))
- `@deepseek-ai/dsh-client-ui-theme` ([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts))
- `@deepseek-ai/dsh-client-ui-tool` ([`packages/client/ui-tool/src/index.ts`](../packages/client/ui-tool/src/index.ts))
- `@deepseek-ai/dsh-client-ui-trajectory` ([`packages/client/ui-trajectory/src/index.ts`](../packages/client/ui-trajectory/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-workflow-run` ([`packages/client/ui-workflow-run/src/index.ts`](../packages/client/ui-workflow-run/src/index.ts))
- `@deepseek-ai/dsh-client-ui-workspace` ([`packages/client/ui-workspace/src/index.ts`](../packages/client/ui-workspace/src/index.ts))
- `@deepseek-ai/dsh-command-compact` — requires `commands` · `compact` ([`packages/compact/command-compact/src/index.ts`](../packages/compact/command-compact/src/index.ts))
- `@deepseek-ai/dsh-command-feedback` — requires `commands` ([`packages/feedback/command-feedback/src/index.ts`](../packages/feedback/command-feedback/src/index.ts))
@@ -2632,6 +2791,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
Abstract service classes — a deployment loads a concrete implementation package instead ([capability seams](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)).
+- `@deepseek-ai/dsh-attachment` — abstract `AttachmentStore` ([`packages/attachment/attachment/src/index.ts`](../packages/attachment/attachment/src/index.ts))
- `@deepseek-ai/dsh-bash` — abstract `BashExecutor` ([`packages/bash/bash/src/index.ts`](../packages/bash/bash/src/index.ts))
- `@deepseek-ai/dsh-code-runtime` — abstract `CodeRuntime` ([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
- `@deepseek-ai/dsh-compact` — abstract `CompactService` ([`packages/compact/compact/src/index.ts`](../packages/compact/compact/src/index.ts))
@@ -2651,7 +2811,6 @@ Abstract service classes — a deployment loads a concrete implementation packag
Imported as libraries by other packages; a `cordis.yml` cannot load them.
-- `@deepseek-ai/create-sdk` ([`packages/scaffold/create-sdk/src/index.ts`](../packages/scaffold/create-sdk/src/index.ts))
- `@deepseek-ai/dsh-acp-snapshot` ([`packages/support/acp-snapshot/src/index.ts`](../packages/support/acp-snapshot/src/index.ts))
- `@deepseek-ai/dsh-agent-loop-testkit` ([`packages/support/agent-loop-testkit/src/index.ts`](../packages/support/agent-loop-testkit/src/index.ts))
- `@deepseek-ai/dsh-app-boot` ([`packages/boot/app-boot/src/index.ts`](../packages/boot/app-boot/src/index.ts))
@@ -2664,8 +2823,8 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/dsh-client-ui-slots` ([`packages/client/ui-slots/src/index.ts`](../packages/client/ui-slots/src/index.ts))
- `@deepseek-ai/dsh-client-web` ([`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts))
- `@deepseek-ai/dsh-client-web-react` ([`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts))
+- `@deepseek-ai/dsh-cmdline` ([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts))
- `@deepseek-ai/dsh-environment` ([`packages/util/environment/src/index.ts`](../packages/util/environment/src/index.ts))
-- `@deepseek-ai/dsh-helper` ([`packages/scaffold/helper/src/index.ts`](../packages/scaffold/helper/src/index.ts))
- `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
- `@deepseek-ai/dsh-jsonrpc-demo` ([`packages/examples/jsonrpc-demo/src/index.ts`](../packages/examples/jsonrpc-demo/src/index.ts))
- `@deepseek-ai/dsh-llm-mock-server` ([`packages/support/llm-mock-server/src/index.ts`](../packages/support/llm-mock-server/src/index.ts))
@@ -2673,15 +2832,15 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/dsh-native-command` ([`packages/util/native-command/src/index.ts`](../packages/util/native-command/src/index.ts))
- `@deepseek-ai/dsh-paths` ([`packages/util/paths/src/index.ts`](../packages/util/paths/src/index.ts))
- `@deepseek-ai/dsh-retention` ([`packages/util/retention/src/index.ts`](../packages/util/retention/src/index.ts))
+- `@deepseek-ai/dsh-sandbox-windows-acl` ([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts))
- `@deepseek-ai/dsh-scope` ([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts))
-- `@deepseek-ai/dsh-scripts` ([`packages/scaffold/scripts/src/index.ts`](../packages/scaffold/scripts/src/index.ts))
-- `@deepseek-ai/dsh-sdk-client` ([`packages/scaffold/client/src/index.ts`](../packages/scaffold/client/src/index.ts))
-- `@deepseek-ai/dsh-sdk-protocol` ([`packages/scaffold/protocol/src/index.ts`](../packages/scaffold/protocol/src/index.ts))
+- `@deepseek-ai/dsh-sdk-client` ([`packages/sdk/client/src/index.ts`](../packages/sdk/client/src/index.ts))
+- `@deepseek-ai/dsh-sdk-protocol` ([`packages/sdk/protocol/src/index.ts`](../packages/sdk/protocol/src/index.ts))
- `@deepseek-ai/dsh-session-telemetry` ([`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts))
- `@deepseek-ai/dsh-session-title-llm` ([`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts))
- `@deepseek-ai/dsh-subagent-inprocess` ([`packages/subagent/subagent-inprocess/src/index.ts`](../packages/subagent/subagent-inprocess/src/index.ts))
-- `@deepseek-ai/dsh-telemetry` ([`packages/scaffold/telemetry/src/index.ts`](../packages/scaffold/telemetry/src/index.ts))
- `@deepseek-ai/dsh-timeout` ([`packages/util/timeout/src/index.ts`](../packages/util/timeout/src/index.ts))
- `@deepseek-ai/dsh-type-meta` ([`packages/typert/type-meta/src/index.ts`](../packages/typert/type-meta/src/index.ts))
- `@deepseek-ai/dsh-typert-generator` ([`packages/typert/generator/src/index.ts`](../packages/typert/generator/src/index.ts))
- `@deepseek-ai/dsh-typert-registry` ([`packages/typert/registry/src/index.ts`](../packages/typert/registry/src/index.ts))
+- `@deepseek-ai/dsh-user-id` ([`packages/session/user-id/src/index.ts`](../packages/session/user-id/src/index.ts))
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index 850d6d4054..e42fd503f3 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -71,7 +71,7 @@ export interface Config {
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-core. */
toolBash?: NonNullable
- /** Generic background-task controls forwarded through agent-core; set false to omit their tool surface. */
+ /** Generic background-task controls forwarded through agent-core; set false to omit their tools. */
toolTasks?: NonNullable
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and tools. */
goals?: agentCore.GoalConfig | false
@@ -126,6 +126,37 @@ export interface Config {
来源:[`packages/core/agent-loop/src/index.ts:236`](../packages/core/agent-loop/src/index.ts)
+## `@deepseek-ai/dsh-agent-presets`
+
+需要:`loader`
+
+```ts config-catalog
+/** Plugin config: which preset is the default, and where presets live. */
+export interface Config {
+ /** Preset id mounted when a caller names none. Missing at mount time fails loud. */
+ default: string
+ /** Scanned roots in precedence order; an earlier root wins a duplicate id. */
+ roots: PresetRoot[]
+}
+
+/** One directory scanned for preset subdirectories. */
+export interface PresetRoot {
+ /** Directory holding one subdirectory per preset; a leading `~` expands. */
+ path: string
+ /** Trust recorded on every preset discovered under this root. */
+ trust: PresetTrust
+}
+
+/**
+ * Where a preset's composition came from. A `system` preset ships with the
+ * deployment; a `user` preset was authored locally, by a person or by an
+ * agent, and therefore carries the same trust as shell access.
+ */
+export type PresetTrust = 'system' | 'user'
+```
+
+来源:[`packages/preset/agent-presets/src/types.ts:52`](../packages/preset/agent-presets/src/types.ts)
+
## `@deepseek-ai/dsh-agent-spine-demo`
```ts config-catalog
@@ -210,6 +241,48 @@ export interface GoalConfig {
来源:[`packages/examples/agent-spine-demo/src/index.ts:90`](../packages/examples/agent-spine-demo/src/index.ts)
+## `@deepseek-ai/dsh-agent-tool-mode`
+
+需要:`tools`
+
+```ts config-catalog
+/** Plugin config. */
+export interface Config {
+ /**
+ * The form this agent's model sees. `native` sends every visible schema,
+ * `code` sends only `run_code` plus a generated SDK, `both` sends both.
+ * Required rather than defaulted: the deployment default is what a preset
+ * without this row already gets, so an omitted value would mean the row was
+ * composed for nothing.
+ */
+ mode: ToolPresentationMode
+}
+```
+
+依赖:[`ToolPresentationMode`](subsystems/tools.md)
+
+来源:[`packages/core/agent-tool-mode/src/index.ts:38`](../packages/core/agent-tool-mode/src/index.ts)
+
+## `@deepseek-ai/dsh-attachment-local`
+
+```ts config-catalog
+/** Local attachment backend configuration. */
+export interface Config {
+ /** Explicit harness home; omitted follows `DSH_HOME`, then `~/.dsh`. */
+ dshHome?: string
+ /** Maximum encoded bytes accepted for one image. */
+ maxImageBytes?: number
+ /** Maximum image count accepted in one submitted message. */
+ maxImagesPerMessage?: number
+ /** Maximum aggregate encoded image bytes accepted in one submitted message. */
+ maxMessageImageBytes?: number
+ /** Maximum intrinsic width multiplied by height accepted for one image. */
+ maxImagePixels?: number
+}
+```
+
+来源:[`packages/attachment/attachment-local/src/index.ts:24`](../packages/attachment/attachment-local/src/index.ts)
+
## `@deepseek-ai/dsh-bash-env`
```ts config-catalog
@@ -281,10 +354,12 @@ export interface ConnectionConfig {
* that is not a bare, canonical authority fails the plugin load.
*/
trustedHosts?: string[]
+ /** Maximum buffered JSON body for every `/api` request. */
+ maxRequestBodyBytes?: number
}
```
-来源:[`packages/client/connection/src/index.ts:32`](../packages/client/connection/src/index.ts)
+来源:[`packages/client/connection/src/index.ts:52`](../packages/client/connection/src/index.ts)
## `@deepseek-ai/dsh-client-hmr`
@@ -298,7 +373,7 @@ export interface Config {
}
```
-来源:[`packages/client/hmr/src/index.ts:29`](../packages/client/hmr/src/index.ts)
+来源:[`packages/client/hmr/src/index.ts:31`](../packages/client/hmr/src/index.ts)
## `@deepseek-ai/dsh-code-runtime-worker`
@@ -452,10 +527,15 @@ export interface Config {
export interface Config {
/** Base directory for relative paths. Defaults to `process.cwd()`. */
cwd?: string
+ /**
+ * Exclusive UTF-8 byte limit on each overwrite-diff side, capped by the
+ * runtime's safe allocation/decode maximum. Defaults to 10 MiB.
+ */
+ diffBasisMaxBytes?: number
}
```
-来源:[`packages/fs/fs-local/src/index.ts:39`](../packages/fs/fs-local/src/index.ts)
+来源:[`packages/fs/fs-local/src/index.ts:40`](../packages/fs/fs-local/src/index.ts)
## `@deepseek-ai/dsh-fs-sandbox`
@@ -463,10 +543,10 @@ export interface Config {
```ts config-catalog
/**
- * Plugin config: the local backend's knobs, verbatim (only `cwd`, the resolve
- * base for relative paths). The sandbox default (mode + `workspace-write`
- * fallback root) is NOT here — `ctx.sandboxPolicy` resolves each calling
- * session for every enforcing capability.
+ * Plugin config: the local backend's knobs verbatim (`cwd` resolution default
+ * and `diffBasisMaxBytes` overwrite-presentation bound). The sandbox default
+ * (mode + `workspace-write` fallback root) is NOT here — `ctx.sandboxPolicy`
+ * resolves each calling session for every enforcing capability.
*/
export type Config = LocalConfig
```
@@ -494,14 +574,14 @@ export interface Config {
需要:`agentDefaultModel` · `agents` · `sessions`
```ts config-catalog
-/** Plugin config: the task, patched in by the launcher. */
+/** Plugin config: the task resolved from this app's injected provider service. */
export interface Config {
/** The prompt text for the single run. */
task: string
}
```
-来源:[`packages/bundle/headless/src/index.ts:29`](../packages/bundle/headless/src/index.ts)
+来源:[`packages/bundle/headless/src/index.ts:31`](../packages/bundle/headless/src/index.ts)
## `@deepseek-ai/dsh-hooks-claude`
@@ -566,17 +646,29 @@ export interface Config {
## `@deepseek-ai/dsh-host-apiproxy`
-需要:`agentDefaultModel` · `agents` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `tools` · `userInteraction` · `workspace`
+需要:`agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `tools` · `userInteraction` · `workspace`
```ts config-catalog
-/** Gateway plugin config: the Host-only Workspace creation root. */
+/** Gateway plugin configuration. */
export interface Config {
- /** Parent directory for name-created Workspaces; defaults to the Host cwd. */
- workspaceRoot?: string
+ /**
+ * Whether this deployment can hand paths to a native desktop opener —
+ * the `hasDocument` capability the agent-preset roster reports. Absent,
+ * the platform is asked (macOS/Windows/WSL yes; Linux only with a display
+ * server); set it explicitly where detection misleads, e.g. `false` in a
+ * container whose DISPLAY points nowhere a user can see.
+ */
+ nativeOpen?: boolean
+ /**
+ * DEFLATE level for every session-log ZIP entry: `0` stores without
+ * compression, `1` favors CPU/latency, and `9` favors archive size.
+ * @default 6
+ */
+ sessionExportCompressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
}
```
-来源:[`packages/host/apiproxy/src/index.ts:38`](../packages/host/apiproxy/src/index.ts)
+来源:[`packages/host/apiproxy/src/index.ts:41`](../packages/host/apiproxy/src/index.ts)
## `@deepseek-ai/dsh-host-directory-picker-browse`
@@ -640,7 +732,7 @@ export interface JsonRpcConfig {
依赖:`Readable`(`node:stream`)· `Writable`(`node:stream`)
-来源:[`packages/scaffold/server/src/index.ts:29`](../packages/scaffold/server/src/index.ts)
+来源:[`packages/sdk/server/src/index.ts:29`](../packages/sdk/server/src/index.ts)
## `@deepseek-ai/dsh-llm-deepseek`
@@ -819,12 +911,12 @@ export type PiAiModelOverride = Omit
* default) or per model (winning over the route). Only the switches pi-ai's
* reasoning dispatch reads are offered; the rest of pi-ai's compat surface
* keeps its baseURL-derived auto-detection. pi-ai types both fields only on
- * `OpenAICompletionsCompat` — the other wire protocols carry their reasoning
- * shape in the protocol itself — so resolution rejects a model-level switch
+ * `OpenAICompletionsCompat` — the other wire protocols define their reasoning
+ * fields in the protocol itself — so resolution rejects a model-level switch
* anywhere else, while a route-level default skips past models it cannot fit.
*/
export interface PiAiCompatProfile {
- /** Reasoning parameter shape the endpoint expects; absent keeps the catalog entry's, then pi-ai's baseURL-derived guess. */
+ /** Reasoning parameter format the endpoint expects; absent keeps the catalog entry's, then pi-ai's baseURL-derived guess. */
thinkingFormat?: PiAiThinkingFormat
/** Whether the endpoint accepts `reasoning_effort`; absent keeps the catalog entry's, then pi-ai's baseURL-derived guess. */
supportsReasoningEffort?: boolean
@@ -902,12 +994,26 @@ export interface ReplayModelConfig {
description?: string
/** Optional positive integer context capacity published by the replay adapter. */
contextWindow?: number
+ /** Optional declared input modalities, so a scenario can exercise capability gates (e.g. image-capable `read_image`). */
+ inputModalities?: readonly ModelModality[]
+ /**
+ * Optional per-request output cap the replay route materializes when callers
+ * omit one, so replay reconstructs the request header a live catalog produced.
+ */
+ defaultMaxTokens?: number
+ /** Optional reasoning-effort ids the replay route accepts, in display order. */
+ reasoningEfforts?: string[]
+ /**
+ * Optional effort materialized when callers omit one; must appear in
+ * {@link reasoningEfforts} or call resolution rejects the route.
+ */
+ defaultReasoningEffort?: string
}
```
-依赖:[`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
+依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
-来源:[`packages/support/llm-replay/src/index.ts:744`](../packages/support/llm-replay/src/index.ts)
+来源:[`packages/support/llm-replay/src/index.ts:776`](../packages/support/llm-replay/src/index.ts)
## `@deepseek-ai/dsh-llm-retry`
@@ -990,6 +1096,8 @@ export interface StdioConfig {
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
+ /** Automatic reconnect policy after a lost connection; omission uses the defaults. */
+ reconnect?: ReconnectConfig
}
/** Config for connecting to an MCP server over Streamable HTTP (SSE). */
@@ -1010,10 +1118,38 @@ export interface StreamableHttpConfig {
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
+ /** Automatic reconnect policy after a lost connection; omission uses the defaults. */
+ reconnect?: ReconnectConfig
+}
+
+/** Automatic reconnect policy for one MCP server connection. */
+export interface ReconnectConfig {
+ /** Reconnect automatically after a lost connection (default true). */
+ enabled?: boolean
+ /** First reconnect delay in milliseconds; doubles per consecutive failed attempt (default 500). */
+ initialDelayMs?: number
+ /** Backoff ceiling in milliseconds; also the uptime after which the attempt budget resets (default 30000). */
+ maxDelayMs?: number
+ /** Consecutive failed attempts per outage before giving up for good (default 10). */
+ maxAttempts?: number
}
```
-来源:[`packages/mcp/mcp-client/src/index.ts:100`](../packages/mcp/mcp-client/src/index.ts)
+来源:[`packages/mcp/mcp-client/src/index.ts:94`](../packages/mcp/mcp-client/src/index.ts)
+
+## `@deepseek-ai/dsh-message-feedback`
+
+需要:`storageDomain` · `sessionPersistence` · `sessions`
+
+```ts config-catalog
+/** Required deployment policy for optional notes. */
+export interface Config {
+ /** Maximum UTF-8 byte length accepted for one note. */
+ readonly maxNoteBytes: number
+}
+```
+
+来源:[`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts)
## `@deepseek-ai/dsh-permission`
@@ -1052,6 +1188,26 @@ export interface PresetSpec {
来源:[`packages/interaction/permission/src/index.ts:140`](../packages/interaction/permission/src/index.ts)
+## `@deepseek-ai/dsh-persona`
+
+需要:`systemPrompt`
+
+```ts config-catalog
+/** Plugin config: the persona text this composition contributes. */
+export interface Config {
+ /**
+ * Persona prose rendered as the `deployment:persona` section. A template:
+ * complete `{{…}}` groups interpolate strictly against registered prompt
+ * variables. Empty text drops the section at render, matching the registry.
+ */
+ text: string
+ /** Make this persona the complete system prompt, suppressing every other section. */
+ complete?: boolean
+}
+```
+
+来源:[`packages/preset/persona/src/index.ts:34`](../packages/preset/persona/src/index.ts)
+
## `@deepseek-ai/dsh-plan-mode`
需要:`tools` · `systemPrompt`
@@ -1064,7 +1220,7 @@ export interface PlanModeConfig {
}
```
-来源:[`packages/plan/plan-mode/src/index.ts:69`](../packages/plan/plan-mode/src/index.ts)
+来源:[`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts)
## `@deepseek-ai/dsh-pty-local`
@@ -1140,6 +1296,26 @@ export interface Config {
来源:[`packages/bash/pwsh-local/src/index.ts:54`](../packages/bash/pwsh-local/src/index.ts)
+## `@deepseek-ai/dsh-pwsh-sandbox`
+
+需要:`subprocess` · `sandbox` · `sandboxPolicy`
+
+```ts config-catalog
+/**
+ * Plugin config: the local executor's knobs, verbatim. The sandbox policy —
+ * the default mode and fallback `workspace-write` root — is NOT here: it lives
+ * on `ctx.sandboxPolicy` (`@deepseek-ai/dsh-sandbox-policy`), which resolves
+ * each calling session's mode and cwd for every enforcing capability. The
+ * runner choice is likewise the `ctx.sandbox` provider's config, not this
+ * executor's.
+ */
+export type Config = LocalConfig
+```
+
+依赖:[`LocalConfig`](#deepseek-aidsh-pwsh-local)
+
+来源:[`packages/bash/pwsh-sandbox/src/index.ts:40`](../packages/bash/pwsh-sandbox/src/index.ts)
+
## `@deepseek-ai/dsh-repeat-tool-guard`
```ts config-catalog
@@ -1172,29 +1348,13 @@ export interface Config {
来源:[`packages/guard/repeat-tool-guard/src/index.ts:28`](../packages/guard/repeat-tool-guard/src/index.ts)
-## `@deepseek-ai/dsh-repository-plugin`
-
-需要:`loader`
-
-```ts config-catalog
-/** Repository Plugin runtime and source-list configuration. */
-export interface Config {
- /** GitHub repository sources with explicit refs and optional `.dsh-plugin` subpaths. */
- repositories?: string[]
- /** Persistent generation cache; defaults to `$DSH_HOME/cache/repository-plugins`. */
- cacheDir?: string
-}
-```
-
-来源:[`packages/self-modification/repository-plugin/src/index.ts:44`](../packages/self-modification/repository-plugin/src/index.ts)
-
## `@deepseek-ai/dsh-sandbox-local`
```ts config-catalog
/** Plugin config. All optional — `static Config` supplies the defaults. */
export interface Config {
/**
- * Override the runner argv; bwrap-shaped profile arguments are appended. A
+ * Override the runner argv; bwrap-compatible profile arguments are appended. A
* non-empty override asserts full enforcement and skips built-in selection and
* probing. A runner that starts but refuses its profile must be identifiable by
* {@link runnerFailureSignatures}. Consumers classify a spawn rejection only after
@@ -1216,7 +1376,7 @@ export interface Config {
}
```
-来源:[`packages/sandbox/sandbox-local/src/index.ts:24`](../packages/sandbox/sandbox-local/src/index.ts)
+来源:[`packages/sandbox/sandbox-local/src/index.ts:44`](../packages/sandbox/sandbox-local/src/index.ts)
## `@deepseek-ai/dsh-sandbox-policy`
@@ -1321,7 +1481,7 @@ export interface Config {
export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
```
-来源:[`packages/session/session-persistence-sqlite/src/index.ts:67`](../packages/session/session-persistence-sqlite/src/index.ts)
+来源:[`packages/session/session-persistence-sqlite/src/index.ts:70`](../packages/session/session-persistence-sqlite/src/index.ts)
## `@deepseek-ai/dsh-session-projection-cache`
@@ -1406,7 +1566,7 @@ export interface Config {
```ts config-catalog
/**
- * Plugin configuration: one sharing policy, two verbatim SDK option shapes,
+ * Plugin configuration: one sharing policy, two verbatim SDK option objects,
* and one DSH-owned shutdown bound. Uploading modes validate their endpoint
* and shutdown deadline at plugin load; `DISABLED` reads neither.
*/
@@ -1442,7 +1602,7 @@ export enum TelemetryMode {
依赖:`BatchLogRecordProcessorOptions`(`@opentelemetry/sdk-logs`)· `OTLPExporterNodeConfigBase`(`@opentelemetry/otlp-exporter-base`)
-来源:[`packages/session/session-telemetry-otel/src/index.ts:80`](../packages/session/session-telemetry-otel/src/index.ts)
+来源:[`packages/session/session-telemetry-otel/src/index.ts:79`](../packages/session/session-telemetry-otel/src/index.ts)
## `@deepseek-ai/dsh-session-title`
@@ -1516,7 +1676,7 @@ export interface Config {
}
```
-来源:[`packages/skill/skill/src/index.ts:266`](../packages/skill/skill/src/index.ts)
+来源:[`packages/skill/skill/src/index.ts:279`](../packages/skill/skill/src/index.ts)
## `@deepseek-ai/dsh-skill-local`
@@ -1690,7 +1850,7 @@ export interface Config {
/**
* How to auto-answer the child's `session/request_permission` prompts:
* `reject` (default — decline every prompt) or `allow` (approve via the first
- * allow-shaped option). No prompt is surfaced to a human.
+ * `allow_once` or `allow_always` option). No prompt is surfaced to a human.
*/
permission: PermissionPolicy
/**
@@ -1862,14 +2022,14 @@ export interface Config {
persona?: string
/**
* Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once.
- * Shape errors fail at load and unknown names fail at assembly; known names
+ * Invalid fields fail at load and unknown names fail at assembly; known names
* hidden in one scope may be absent there. Omitted means lexicographic order.
*/
toolOrder?: string[]
}
```
-来源:[`packages/core/system-prompt/src/index.ts:166`](../packages/core/system-prompt/src/index.ts)
+来源:[`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
## `@deepseek-ai/dsh-time-context`
@@ -2071,7 +2231,7 @@ export interface Config {
}
```
-来源:[`packages/bash/tool-pwsh/src/index.ts:43`](../packages/bash/tool-pwsh/src/index.ts)
+来源:[`packages/bash/tool-pwsh/src/index.ts:52`](../packages/bash/tool-pwsh/src/index.ts)
## `@deepseek-ai/dsh-tool-ralph`
@@ -2203,14 +2363,15 @@ export interface Config {
## `@deepseek-ai/dsh-tool-subagent-report`
-需要:`subagents` · `tools`
+需要:`subagents` · `tools` · `systemPrompt`
```ts config-catalog
/** Config: how accepted reports are scheduled on the parent. */
export interface Config {
/**
- * Parent scheduling (default `quiet`). `quiet` adds context without waking;
- * `wakeup` creates one ordinary later parent turn.
+ * Parent scheduling (default `wakeup`). `wakeup` creates one ordinary later
+ * parent turn; `quiet` adds context without waking, so a parked parent learns
+ * of the report only when something else wakes it.
*/
reportDelivery?: SubagentReportDelivery
}
@@ -2218,7 +2379,7 @@ export interface Config {
依赖:[`SubagentReportDelivery`](subsystems/subagent.md)
-来源:[`packages/subagent/tool-subagent-report/src/index.ts:22`](../packages/subagent/tool-subagent-report/src/index.ts)
+来源:[`packages/subagent/tool-subagent-report/src/index.ts:27`](../packages/subagent/tool-subagent-report/src/index.ts)
## `@deepseek-ai/dsh-tool-tasks`
@@ -2294,7 +2455,7 @@ export interface Config {
}
```
-来源:[`packages/workflow/tool-workflow/src/index.ts:27`](../packages/workflow/tool-workflow/src/index.ts)
+来源:[`packages/workflow/tool-workflow/src/index.ts:33`](../packages/workflow/tool-workflow/src/index.ts)
## `@deepseek-ai/dsh-tools`
@@ -2304,11 +2465,16 @@ export interface Config {
/** Plugin config: how the registered tools are presented to the model. */
export interface Config {
/**
- * Model presentation. `native` (default) sends every visible schema; `code`
- * sends only `run_code` plus a generated SDK prompt; `both` sends both forms.
- * Code modes require a `ctx.codeRuntime` whose `language` has a registered
- * SDK renderer (TypeScript or Python) and fail prompt assembly when it is
- * absent or has no renderer. Under `code`, native names in `toolOrder` are invalid.
+ * Model presentation for agents that declare none of their own. `native`
+ * (default) sends every visible schema; `code` sends only `run_code` plus a
+ * generated SDK prompt; `both` sends both forms. Code modes require a
+ * `ctx.codeRuntime` whose `language` has a registered SDK renderer
+ * (TypeScript or Python) and fail prompt assembly when it is absent or has
+ * no renderer. Under `code`, native names in `toolOrder` are invalid.
+ *
+ * One agent overrides this for itself with {@link ToolRegistry.presentAs},
+ * which is how an agent preset composes a Code Mode agent beside native
+ * ones in the same process.
*/
mode?: ToolPresentationMode
/**
@@ -2325,7 +2491,7 @@ export interface Config {
export type ToolPresentationMode = 'native' | 'code' | 'both'
```
-来源:[`packages/core/tools/src/index.ts:616`](../packages/core/tools/src/index.ts)
+来源:[`packages/core/tools/src/index.ts:617`](../packages/core/tools/src/index.ts)
## `@deepseek-ai/dsh-typert-loader`
@@ -2394,33 +2560,23 @@ export interface WebServiceConfig {
需要:`httpServer`
```ts config-catalog
-/** Plugin config: the surface facts the launcher patches over this bundle's defaults. */
+/** Plugin config: composed deployment settings plus per-invocation command-line values. */
export interface Config {
- /** Whether this process mounted the client-plugin HMR receiver (`dsh web --dev`). */
- mode: WebMode
- /** Print the URL line on activation; a headless layer over this bundle turns it off. */
+ /** Print the URL line on activation; a non-interactive layer can turn it off. */
printUrl: boolean
/**
* Register the model-visible surface context (the `app:web-surface` prompt
- * section and the `DSH_WEB_URL`/`DSH_WEB_MODE` bash variables). A one-shot
- * layer turns it off: its user is not interacting through the GUI, so the
+ * section and the `DSH_WEB_URL` bash variable). A one-shot non-interactive
+ * layer can turn it off when its user is not in the GUI, so the
* orientation text would be false.
*/
surfaceContext: boolean
- /**
- * LAN IPv4 addresses sampled once by the launcher when the effective bind
- * is all-interfaces — the exact snapshot the /api trust fence was
- * configured with, so the printed LAN URL can never name an address the
- * fence rejects. Empty on a loopback bind.
- */
- lanAddresses: string[]
+ /** Explicit `--trusted-host` authorities from this invocation. */
+ trustedHosts: string[]
}
-
-/** Web runtime mode: production, or development when the client-plugin HMR receiver is active. */
-export type WebMode = 'production' | 'development'
```
-来源:[`packages/bundle/web-app/src/index.ts:32`](../packages/bundle/web-app/src/index.ts)
+来源:[`packages/bundle/web-app/src/index.ts:38`](../packages/bundle/web-app/src/index.ts)
## `@deepseek-ai/dsh-web-fetch-local`
@@ -2582,6 +2738,7 @@ export interface Config {
- `@deepseek-ai/dsh-client-locale`([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts))
- `@deepseek-ai/dsh-client-modules` — 需要 `httpServer` · `loader`([`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts))
- `@deepseek-ai/dsh-client-runtime`([`packages/client/runtime/src/index.ts`](../packages/client/runtime/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-agent-preset`([`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts))
- `@deepseek-ai/dsh-client-ui-command`([`packages/client/ui-command/src/index.ts`](../packages/client/ui-command/src/index.ts))
- `@deepseek-ai/dsh-client-ui-conversation`([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts))
- `@deepseek-ai/dsh-client-ui-deliverables`([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts))
@@ -2591,16 +2748,18 @@ export interface Config {
- `@deepseek-ai/dsh-client-ui-models`([`packages/client/ui-models/src/index.ts`](../packages/client/ui-models/src/index.ts))
- `@deepseek-ai/dsh-client-ui-permission`([`packages/client/ui-permission/src/index.ts`](../packages/client/ui-permission/src/index.ts))
- `@deepseek-ai/dsh-client-ui-plan`([`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts))
-- `@deepseek-ai/dsh-client-ui-question` — 需要 `tools` · `userInteraction`([`packages/client/ui-question/src/index.ts`](../packages/client/ui-question/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-question`([`packages/client/ui-question/src/index.ts`](../packages/client/ui-question/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings`([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings-general`([`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts))
- `@deepseek-ai/dsh-client-ui-sidebar`([`packages/client/ui-sidebar/src/index.ts`](../packages/client/ui-sidebar/src/index.ts))
- `@deepseek-ai/dsh-client-ui-skill`([`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts))
- `@deepseek-ai/dsh-client-ui-slash`([`packages/client/ui-slash/src/index.ts`](../packages/client/ui-slash/src/index.ts))
- `@deepseek-ai/dsh-client-ui-subagent`([`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-task`([`packages/client/ui-task/src/index.ts`](../packages/client/ui-task/src/index.ts))
- `@deepseek-ai/dsh-client-ui-theme`([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts))
- `@deepseek-ai/dsh-client-ui-tool`([`packages/client/ui-tool/src/index.ts`](../packages/client/ui-tool/src/index.ts))
- `@deepseek-ai/dsh-client-ui-trajectory`([`packages/client/ui-trajectory/src/index.ts`](../packages/client/ui-trajectory/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-workflow-run`([`packages/client/ui-workflow-run/src/index.ts`](../packages/client/ui-workflow-run/src/index.ts))
- `@deepseek-ai/dsh-client-ui-workspace`([`packages/client/ui-workspace/src/index.ts`](../packages/client/ui-workspace/src/index.ts))
- `@deepseek-ai/dsh-command-compact` — 需要 `commands` · `compact`([`packages/compact/command-compact/src/index.ts`](../packages/compact/command-compact/src/index.ts))
- `@deepseek-ai/dsh-command-feedback` — 需要 `commands`([`packages/feedback/command-feedback/src/index.ts`](../packages/feedback/command-feedback/src/index.ts))
@@ -2633,6 +2792,7 @@ export interface Config {
抽象服务类——部署时应改为加载具体的实现包(参见[能力 seam](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md))。
+- `@deepseek-ai/dsh-attachment` — 抽象 `AttachmentStore`([`packages/attachment/attachment/src/index.ts`](../packages/attachment/attachment/src/index.ts))
- `@deepseek-ai/dsh-bash` — 抽象 `BashExecutor`([`packages/bash/bash/src/index.ts`](../packages/bash/bash/src/index.ts))
- `@deepseek-ai/dsh-code-runtime` — 抽象 `CodeRuntime`([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
- `@deepseek-ai/dsh-compact` — 抽象 `CompactService`([`packages/compact/compact/src/index.ts`](../packages/compact/compact/src/index.ts))
@@ -2651,7 +2811,6 @@ export interface Config {
由其他包作为库导入;`cordis.yml` 无法加载它们。
-- `@deepseek-ai/create-sdk`([`packages/scaffold/create-sdk/src/index.ts`](../packages/scaffold/create-sdk/src/index.ts))
- `@deepseek-ai/dsh-acp-snapshot`([`packages/support/acp-snapshot/src/index.ts`](../packages/support/acp-snapshot/src/index.ts))
- `@deepseek-ai/dsh-agent-loop-testkit`([`packages/support/agent-loop-testkit/src/index.ts`](../packages/support/agent-loop-testkit/src/index.ts))
- `@deepseek-ai/dsh-app-boot`([`packages/boot/app-boot/src/index.ts`](../packages/boot/app-boot/src/index.ts))
@@ -2664,8 +2823,8 @@ export interface Config {
- `@deepseek-ai/dsh-client-ui-slots`([`packages/client/ui-slots/src/index.ts`](../packages/client/ui-slots/src/index.ts))
- `@deepseek-ai/dsh-client-web`([`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts))
- `@deepseek-ai/dsh-client-web-react`([`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts))
+- `@deepseek-ai/dsh-cmdline`([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts))
- `@deepseek-ai/dsh-environment`([`packages/util/environment/src/index.ts`](../packages/util/environment/src/index.ts))
-- `@deepseek-ai/dsh-helper`([`packages/scaffold/helper/src/index.ts`](../packages/scaffold/helper/src/index.ts))
- `@deepseek-ai/dsh-hook-protocol`([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
- `@deepseek-ai/dsh-jsonrpc-demo`([`packages/examples/jsonrpc-demo/src/index.ts`](../packages/examples/jsonrpc-demo/src/index.ts))
- `@deepseek-ai/dsh-llm-mock-server`([`packages/support/llm-mock-server/src/index.ts`](../packages/support/llm-mock-server/src/index.ts))
@@ -2673,15 +2832,15 @@ export interface Config {
- `@deepseek-ai/dsh-native-command`([`packages/util/native-command/src/index.ts`](../packages/util/native-command/src/index.ts))
- `@deepseek-ai/dsh-paths`([`packages/util/paths/src/index.ts`](../packages/util/paths/src/index.ts))
- `@deepseek-ai/dsh-retention`([`packages/util/retention/src/index.ts`](../packages/util/retention/src/index.ts))
+- `@deepseek-ai/dsh-sandbox-windows-acl`([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts))
- `@deepseek-ai/dsh-scope`([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts))
-- `@deepseek-ai/dsh-scripts`([`packages/scaffold/scripts/src/index.ts`](../packages/scaffold/scripts/src/index.ts))
-- `@deepseek-ai/dsh-sdk-client`([`packages/scaffold/client/src/index.ts`](../packages/scaffold/client/src/index.ts))
-- `@deepseek-ai/dsh-sdk-protocol`([`packages/scaffold/protocol/src/index.ts`](../packages/scaffold/protocol/src/index.ts))
+- `@deepseek-ai/dsh-sdk-client`([`packages/sdk/client/src/index.ts`](../packages/sdk/client/src/index.ts))
+- `@deepseek-ai/dsh-sdk-protocol`([`packages/sdk/protocol/src/index.ts`](../packages/sdk/protocol/src/index.ts))
- `@deepseek-ai/dsh-session-telemetry`([`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts))
- `@deepseek-ai/dsh-session-title-llm`([`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts))
- `@deepseek-ai/dsh-subagent-inprocess`([`packages/subagent/subagent-inprocess/src/index.ts`](../packages/subagent/subagent-inprocess/src/index.ts))
-- `@deepseek-ai/dsh-telemetry`([`packages/scaffold/telemetry/src/index.ts`](../packages/scaffold/telemetry/src/index.ts))
- `@deepseek-ai/dsh-timeout`([`packages/util/timeout/src/index.ts`](../packages/util/timeout/src/index.ts))
- `@deepseek-ai/dsh-type-meta`([`packages/typert/type-meta/src/index.ts`](../packages/typert/type-meta/src/index.ts))
- `@deepseek-ai/dsh-typert-generator`([`packages/typert/generator/src/index.ts`](../packages/typert/generator/src/index.ts))
- `@deepseek-ai/dsh-typert-registry`([`packages/typert/registry/src/index.ts`](../packages/typert/registry/src/index.ts))
+- `@deepseek-ai/dsh-user-id`([`packages/session/user-id/src/index.ts`](../packages/session/user-id/src/index.ts))
diff --git a/docs/cookbook/adding-a-conversation-node.i18n.yaml b/docs/cookbook/adding-a-conversation-node.i18n.yaml
index aa268e9461..52234b5562 100644
--- a/docs/cookbook/adding-a-conversation-node.i18n.yaml
+++ b/docs/cookbook/adding-a-conversation-node.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/adding-a-conversation-node.md
-adding-a-conversation-node.md: ea4ec73eb109af6b0e4c7cf50fc8692942c75dd4
-adding-a-conversation-node.zh.md: 4b9a8049e2f1d060ec4bc3334036559b989ea562
+adding-a-conversation-node.md: c1965dc8a3081eebb8c1026ac53d2f7b8964edb7
+adding-a-conversation-node.zh.md: 92445e1432369a4e42cc372b5d5869c3cdeada4a
diff --git a/docs/cookbook/adding-a-conversation-node.md b/docs/cookbook/adding-a-conversation-node.md
index ea4ec73eb1..c1965dc8a3 100644
--- a/docs/cookbook/adding-a-conversation-node.md
+++ b/docs/cookbook/adding-a-conversation-node.md
@@ -120,6 +120,7 @@ function viewData(state: ReviewState): ReviewChatData {
const reviewDefinition: ConversationNodeDefinition = {
kind: 'review-job',
+ target: 'chat',
match: (event) => {
if (event.type === 'review/start') {
return { id: String(event.data.reviewId), role: 'start' }
@@ -161,8 +162,8 @@ const reviewDefinition: ConversationNodeDefinition = {
value: viewData(context.state),
}
},
- buildViewNode: (context, target) => {
- if (target !== 'chat' || context.state === undefined) return null
+ buildViewNode: (context) => {
+ if (context.state === undefined) return null
return {
key: context.key,
kind: 'review-job',
@@ -196,7 +197,7 @@ export function apply(ctx: ClientContext): void {
`buildLocationData(context, scope)` optionally publishes Definition-owned data onto an engine-owned Turn or Step. Use declaration merging to give each key a precise value type. Another Node in the same Location can consume that value through its constrained slot hook, such as `useTurnData(key)`, without receiving the Session or scanning `snapshot.chat.nodes`.
-`buildViewNode(context, target)` materializes the final target-specific Node. Preserve `context.key` as the React-facing identity, choose `anchorSeq` from durable ordering evidence, and return only renderer-ready data. Once a target Node has been published, keep returning the same key; use `visibility: 'hidden'` when it must temporarily leave the visible flow rather than withdrawing it with `null`.
+`target` and `buildViewNode(context)` declare one target-owned rendering contribution and must appear together. Preserve `context.key` as the React-facing identity, choose `anchorSeq` from durable ordering evidence, and return only renderer-ready data. Once a target Node has been published, keep returning the same key; use `visibility: 'hidden'` when it must temporarily leave the visible flow rather than withdrawing it with `null`.
## 3. Query an earlier business Context only at start
diff --git a/docs/cookbook/adding-a-conversation-node.zh.md b/docs/cookbook/adding-a-conversation-node.zh.md
index 4b9a8049e2..92445e1432 100644
--- a/docs/cookbook/adding-a-conversation-node.zh.md
+++ b/docs/cookbook/adding-a-conversation-node.zh.md
@@ -120,6 +120,7 @@ function viewData(state: ReviewState): ReviewChatData {
const reviewDefinition: ConversationNodeDefinition = {
kind: 'review-job',
+ target: 'chat',
match: (event) => {
if (event.type === 'review/start') {
return { id: String(event.data.reviewId), role: 'start' }
@@ -161,8 +162,8 @@ const reviewDefinition: ConversationNodeDefinition = {
value: viewData(context.state),
}
},
- buildViewNode: (context, target) => {
- if (target !== 'chat' || context.state === undefined) return null
+ buildViewNode: (context) => {
+ if (context.state === undefined) return null
return {
key: context.key,
kind: 'review-job',
@@ -196,7 +197,7 @@ export function apply(ctx: ClientContext): void {
`buildLocationData(context, scope)` 可以把 Definition 拥有的数据发布到引擎拥有的 Turn 或 Step 上。通过 declaration merging 为每个 key 指定精确 value 类型。同一 Location 内的另一个 Node 可以使用受限 slot hook(例如 `useTurnData(key)`)读取该值,无须取得 Session,也无须扫描 `snapshot.chat.nodes`。
-`buildViewNode(context, target)` 物化最终的目标专用 Node。把 `context.key` 保留为 React 侧身份,根据持久排序证据选择 `anchorSeq`,并且只返回 renderer 可以直接使用的数据。某个 target Node 一旦发布,就要继续返回同一个 key;需要暂时离开可见流时使用 `visibility: 'hidden'`,不要改为返回 `null` 撤回它。
+`target` 与 `buildViewNode(context)` 必须同时声明一项由 target 拥有的渲染贡献。把 `context.key` 保留为 React 侧身份,根据持久排序证据选择 `anchorSeq`,并且只返回 renderer 可以直接使用的数据。某个 target Node 一旦发布,就要继续返回同一个 key;需要暂时离开可见流时使用 `visibility: 'hidden'`,不要改为返回 `null` 撤回它。
## 3. 只在 start 时查询更早的业务 Context
diff --git a/docs/cookbook/adding-a-package.i18n.yaml b/docs/cookbook/adding-a-package.i18n.yaml
index aa582c2bb1..3418096c10 100644
--- a/docs/cookbook/adding-a-package.i18n.yaml
+++ b/docs/cookbook/adding-a-package.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/adding-a-package.md
-adding-a-package.md: 79e8604a47a4bdbb7e2912990a8b1c8be300b3b0
-adding-a-package.zh.md: 5fcdf03f52dfdfe375aeb2b6fd41e50ee406d043
+adding-a-package.md: 4320d9737da1a6144f77fc3f02e2801e723f62b4
+adding-a-package.zh.md: 5e2c9094fbd0fb3f03c115598afdb32ac42e1e01
diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md
index 79e8604a47..4320d9737d 100644
--- a/docs/cookbook/adding-a-package.md
+++ b/docs/cookbook/adding-a-package.md
@@ -22,7 +22,7 @@ packages///
Choose an existing group when one matches the package's role (`core`, `llm`, `bash`, `compact`, `subagent`, `todo`, `session-persistence`, `ui`, `util`, or `support`). A new group is allowed, but it is a pure container: no `package.json`, no source files, and packages still sit exactly one level below it.
-package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, a `version` matching the root `package.json`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list contains exactly `lib/index.js`, `lib/invariant.js`, `lib/types/**/*.d.ts`, and package-specific runtime artifacts recognized by the gate; a package whose runtime export points into the emitted tree also includes `lib/types/**/*.js`. Do not publish `src`, declaration maps, JS maps, or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`.
+package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, a `version` matching the root `package.json`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `@deepseek-ai/cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `@deepseek-ai/schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list contains exactly `lib/index.js`, `lib/invariant.js`, `lib/types/**/*.d.ts`, and package-specific runtime artifacts recognized by the gate; a package whose runtime export points into the emitted tree also includes `lib/types/**/*.js`. Do not publish `src`, declaration maps, JS maps, or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`.
In-package relative imports use explicit `.ts` specifiers in source (for example, `export * from './types.ts'`). The compiler rewrites those to `.js` in emitted JS and leaves explicit `.ts` specifiers in declarations, which standard NodeNext/Node16 TypeScript consumers resolve to the sibling `.d.ts` files.
@@ -34,7 +34,7 @@ In-package relative imports use explicit `.ts` specifiers in source (for example
| `tsconfig.host.json` (Host package) or `tsconfig.client.json` (Client package) | add `{ "path": "./packages//" }` to `references` — an ordinary package belongs to exactly one aggregate, never both. `api/remotes` uses a repository-specific split because the Host generates a contract that the Client consumes in a later phase; new packages must not copy it ([layout](../development.md#typescript-project-layout)) |
| `knip.json` | only if the package has entrypoints that repository discovery does not already cover |
-A `packages/client/*` package additionally extends `tsconfig.base.client.json` instead of `tsconfig.base.json`, and a client plugin package declares `dshClient` in package.json, exports `./client`, and calls the shared tsdown preset (`packages/client/tsdown.client.ts`) — see [packages/client/AGENTS.md](../../packages/client/AGENTS.md) for the client-side contract.
+A `packages/client/*` package additionally extends `tsconfig.base.client.json` instead of `tsconfig.base.json`, and a client plugin package declares `dsh.client` in package.json, exports `./client`, and calls the shared tsdown preset (`packages/client/tsdown.client.ts`) — see [packages/client/AGENTS.md](../../packages/client/AGENTS.md) for the client-side contract.
Covered automatically by globs or package-manifest discovery — no edits needed: root `package.json` workspaces, `scripts/publint-all.ts`, `tsdown.config.ts`, `.oxlintrc.json`, `scripts/check-workspace-constraints.ts`.
@@ -49,11 +49,11 @@ Keep package-specific service API, config, events, extension points, and design
````markdown
## Model Experience
-### Request surface and condition
+### Request context and condition
#### What the model sees
-An exact data-dependent shape, an anchored generated-catalog link, or an introduction to the verbatim literal below.
+The exact data-dependent fields, an anchored generated-catalog link, or an introduction to the verbatim literal below.
##### Verbatim text for this field, when needed
@@ -71,10 +71,10 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex
## Known Limitations and Deferred Work
-- **Consumer-visible gap** — exact boundary, consequence, or maintainer constraint.
+- **Consumer-visible gap** — exact missing operation or case, its consequence, and any maintainer constraint.
````
-Fill Model Experience from the implementation. Use one H3 per direct, conditional, capped, lifetime, or auxiliary-model surface, with the three ordered H4 fields shown above and one prose paragraph under each. Quote stable text owned by the package: system-prompt prose goes in a titled H5 plus `markdown` fence under the field that introduces it—normally `What the model sees`—other short literals stay inline with named placeholders, and other long literals use the same nested form. Summarize only data-dependent or provider-owned text. A tool-schema surface links its anchored section in the generated [tool catalog](../tool-catalog.md) and states only deltas absent there. Keep prompt and schema surfaces separate when scoping can hide one without the other. In `KV Cache effect`, distinguish append-only growth, a stable repeated prefix, replacement of earlier request tokens, and an independent model request, then name the package-owned changes that can invalidate reuse. “Does not invalidate” means the package preserves an already-reusable prefix; provider cache availability and eviction remain outside the package contract. The [prose standard](../../.agents/skills/dsh-prose-standard/SKILL.md) governs completeness and ownership; the verifier enforces the mechanical shape.
+Fill Model Experience from the implementation. Use one H3 per direct, conditional, capped, lifetime, or auxiliary model-context entry, with the three ordered H4 fields shown above and one prose paragraph under each. Quote stable text owned by the package: system-prompt prose goes in a titled H5 plus `markdown` fence under the field that introduces it—normally `What the model sees`—other short literals stay inline with named placeholders, and other long literals use the same nested form. Summarize only data-dependent or provider-owned text. A tool-schema entry links its anchored section in the generated [tool catalog](../tool-catalog.md) and states only deltas absent there. Keep prompt and schema entries separate when scoping can hide one without the other. In `KV Cache effect`, distinguish append-only growth, a stable repeated prefix, replacement of earlier request tokens, and an independent model request, then name the package-owned changes that can invalidate reuse. “Does not invalidate” means the package preserves an already-reusable prefix; provider cache availability and eviction remain outside the package contract. The [prose standard](../../.agents/skills/dsh-prose-standard/SKILL.md) governs completeness and ownership; the verifier enforces the required section structure.
A package with no context effect or one consumer-owned path uses the audited `None, as ` or `Indirectly, through ` sentence in [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts), followed by a `KV Cache effect` H4 and one non-empty paragraph; a model-agnostic generic package may instead join `NO_MODEL_EXPERIENCE_SECTION`. Do not expand either case into a description of another package's work. The limitations [allowlist](../../scripts/verify-package-readme-limitations.ts) is independent. The [Model Experience Agent Note](../../.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md) records the rationale.
diff --git a/docs/cookbook/adding-a-package.zh.md b/docs/cookbook/adding-a-package.zh.md
index 5fcdf03f52..5e2c9094fb 100644
--- a/docs/cookbook/adding-a-package.zh.md
+++ b/docs/cookbook/adding-a-package.zh.md
@@ -22,7 +22,7 @@ packages///
当已有分组与包的角色匹配时,选择该分组(`core`、`llm`、`bash`、`compact`、`subagent`、`todo`、`session-persistence`、`ui`、`util` 或 `support`)。允许新建分组,但分组只是纯容器:没有 `package.json`,没有源文件,包仍然恰好位于其下一层。
-package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-constraints.ts` 强制执行):`private: true`,`version` 与根 `package.json` 一致,`type: module`,`main: "lib/index.js"`,`types: "lib/types/index.d.ts"`,`exports["."].types: "./lib/types/index.d.ts"`,`exports["."].default: "./lib/index.js"`,`cordis` 同时出现在 peerDependencies 和 devDependencies 中(相同范围)。每个 dsh 对等依赖(peer dependency)都要在 devDependencies 中镜像。`schemastery` 放在 `dependencies` 中(它是运行时校验器),与 agent-loop 保持一致。`files` 列表精确包含 `lib/index.js`、`lib/invariant.js`、`lib/types/**/*.d.ts` 以及门禁认可的包专用运行时产物;如果包的运行时 export 指向输出树,还要包含 `lib/types/**/*.js`。不要发布 `src`、声明映射、JS map 或陈旧的根声明文件。带有 `bin` 的 CLI 应用包在 `files` 中将 `lib/bin.js` 紧跟在 `lib/index.js` 之后。
+package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-constraints.ts` 强制执行):`private: true`,`version` 与根 `package.json` 一致,`type: module`,`main: "lib/index.js"`,`types: "lib/types/index.d.ts"`,`exports["."].types: "./lib/types/index.d.ts"`,`exports["."].default: "./lib/index.js"`,`@deepseek-ai/cordis` 同时出现在 peerDependencies 和 devDependencies 中(相同范围)。每个 dsh 对等依赖(peer dependency)都要在 devDependencies 中镜像。`@deepseek-ai/schemastery` 放在 `dependencies` 中(它是运行时校验器),与 agent-loop 保持一致。`files` 列表精确包含 `lib/index.js`、`lib/invariant.js`、`lib/types/**/*.d.ts` 以及门禁认可的包专用运行时产物;如果包的运行时 export 指向输出树,还要包含 `lib/types/**/*.js`。不要发布 `src`、声明映射、JS map 或陈旧的根声明文件。带有 `bin` 的 CLI 应用包在 `files` 中将 `lib/bin.js` 紧跟在 `lib/index.js` 之后。
包内的相对导入在源码中使用显式 `.ts` 后缀(例如 `export * from './types.ts'`)。编译器在输出的 JS 中将其重写为 `.js`,在声明文件中保留显式 `.ts` 后缀;标准的 NodeNext/Node16 TypeScript 消费方会将其解析到同目录的 `.d.ts` 文件。
@@ -34,7 +34,7 @@ package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-c
| `tsconfig.host.json`(Host 包)或 `tsconfig.client.json`(Client 包) | 在 `references` 中添加 `{ "path": "./packages//" }`——普通包恰好属于一个 aggregate,绝不两个都加。`api/remotes` 因 Host 生成约定与 Client 消费约定之间存在顺序依赖而使用仓库专属拆分,新增包不得仿照([布局](../development.md#typescript-project-layout)) |
| `knip.json` | 仅当包有仓库发现机制尚未覆盖的入口时需要 |
-`packages/client/*` 包改为 extends `tsconfig.base.client.json`(而非 `tsconfig.base.json`);client 插件包还需在 package.json 声明 `dshClient`、导出 `./client`、调用共享 tsdown preset(`packages/client/tsdown.client.ts`)——client 侧见 [packages/client/AGENTS.md](../../packages/client/AGENTS.md)。
+`packages/client/*` 包改为 extends `tsconfig.base.client.json`(而非 `tsconfig.base.json`);client 插件包还需在 package.json 声明 `dsh.client`、导出 `./client`、调用共享 tsdown preset(`packages/client/tsdown.client.ts`)——client 侧见 [packages/client/AGENTS.md](../../packages/client/AGENTS.md)。
以下内容由 glob 或包 manifest 发现机制自动覆盖,无需手动编辑:根 `package.json` workspaces、`scripts/publint-all.ts`、`tsdown.config.ts`、`.oxlintrc.json`、`scripts/check-workspace-constraints.ts`。
@@ -49,11 +49,11 @@ package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-c
````markdown
## Model Experience
-### Request surface and condition
+### Request context and condition
#### What the model sees
-An exact data-dependent shape, an anchored generated-catalog link, or an introduction to the verbatim literal below.
+The exact data-dependent fields, an anchored generated-catalog link, or an introduction to the verbatim literal below.
##### Verbatim text for this field, when needed
@@ -71,10 +71,10 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex
## Known Limitations and Deferred Work
-- **Consumer-visible gap** — exact boundary, consequence, or maintainer constraint.
+- **Consumer-visible gap** — exact missing operation or case, its consequence, and any maintainer constraint.
````
-根据实现填写 Model Experience。每个直接、条件、上限、生命周期或辅助模型的 surface 使用一个 H3,包含上述三个有序 H4 字段,每个字段下有一个正文段落。引用包拥有的稳定文本:系统提示词放在引出它的字段下,用带标题的 H5 加 `markdown` 围栏表示,通常归入 `What the model sees`;其他短文本以命名占位符内联,其他长文本使用相同的嵌套形式。仅概述数据依赖或提供方拥有的文本。tool-schema surface 链接到生成的[工具目录](../tool-catalog.md)中对应的锚定章节,仅说明该处缺失的差异。当作用域可以隐藏 prompt 或 schema 其中之一而不影响另一个时,将二者分开。填写 `KV Cache effect` 时,应区分仅追加增长、稳定重复的前缀、替换既有请求 token 和独立模型请求,并列出会使缓存复用失效、且由本包拥有的变化。“不使缓存失效”仅表示本包保留了已有的可复用前缀;缓存是否可用以及何时淘汰不属于本包约定。[行文标准](../../.agents/skills/dsh-prose-standard/SKILL.md)约束完整性与归属;验证器强制执行机械形状。
+根据实现填写 Model Experience。每个直接、条件、上限、生命周期或辅助的模型上下文条目使用一个 H3,包含上述三个有序 H4 字段,每个字段下有一个正文段落。引用包拥有的稳定文本:系统提示词放在引出它的字段下,用带标题的 H5 加 `markdown` 围栏表示,通常归入 `What the model sees`;其他短文本以命名占位符内联,其他长文本使用相同的嵌套形式。仅概述数据依赖或提供方拥有的文本。工具 schema 条目链接到生成的[工具目录](../tool-catalog.md)中对应的锚定章节,仅说明该处缺失的差异。当作用域可以隐藏 prompt 或 schema 其中之一而不影响另一个时,将二者分开。填写 `KV Cache effect` 时,应区分仅追加增长、稳定重复的前缀、替换既有请求 token 和独立模型请求,并列出会使缓存复用失效、且由本包拥有的变化。“不使缓存失效”仅表示本包保留了已有的可复用前缀;缓存是否可用以及何时淘汰不属于本包约定。[行文标准](../../.agents/skills/dsh-prose-standard/SKILL.md)约束完整性与归属;验证器强制执行所需章节结构。
没有上下文效果或仅有消费方拥有路径的包使用 [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts) 中经过审计的 `None, as ` 或 `Indirectly, through ` 语句,随后添加 `KV Cache effect` H4 和一个非空正文段落;与模型无关的通用包可以改为加入 `NO_MODEL_EXPERIENCE_SECTION`。两种情况都不要展开为对另一个包工作的描述。limitations [allowlist](../../scripts/verify-package-readme-limitations.ts) 独立管理。[Model Experience Agent Note](../../.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md) 记录了设计动机。
diff --git a/docs/cookbook/adding-a-tool.i18n.yaml b/docs/cookbook/adding-a-tool.i18n.yaml
index 16439b1732..3e2be7f506 100644
--- a/docs/cookbook/adding-a-tool.i18n.yaml
+++ b/docs/cookbook/adding-a-tool.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/adding-a-tool.md
-adding-a-tool.md: cb418a9118901cc6572fb17125351bdda922434b
-adding-a-tool.zh.md: 22eccec67a3f941976608dc7fc1cc4120e4e1bd1
+adding-a-tool.md: 291e3a8b684a0d042102502279fe4c4393e34735
+adding-a-tool.zh.md: 69bd7ee150f30b1d3e6be08d87347e52952a360c
diff --git a/docs/cookbook/adding-a-tool.md b/docs/cookbook/adding-a-tool.md
index cb418a9118..291e3a8b68 100644
--- a/docs/cookbook/adding-a-tool.md
+++ b/docs/cookbook/adding-a-tool.md
@@ -8,7 +8,7 @@ Reference for the contracts a model-facing tool must satisfy. For an ordered fir
```ts
import { readFile } from 'node:fs/promises'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-tool'
@@ -50,13 +50,13 @@ Registration is effect-based: disposing the plugin fiber unregisters the tool. S
## Long-running work
-Gate `run_in_background` with producer config, then register through `ctx.tasks.start({ kind, label, owner: exec.agent, run })`. The registry rejects a pre-aborted invocation before the producer body; the runtime validates ownership and control-surface availability before `run()` starts work, then supplies the id, session fence, generic control tools, notices, and owner cleanup. A successful background branch returns a typed canonical handle such as `{ kind: 'background', taskId }`; its Native renderer may keep human prose such as `started background task bash-1`, but Code Mode must never parse that prose to recover the id.
+Gate `run_in_background` with producer config, then register through `ctx.tasks.start({ kind, label, owner: exec.agent, run })`. The registry rejects a pre-aborted invocation before the producer body; the runtime validates ownership and task-controller availability before `run()` starts work, then supplies the id, session fence, generic control tools, notices, and owner cleanup. A successful background branch returns a typed canonical handle such as `{ kind: 'background', taskId }`; its Native renderer may keep human prose such as `started background task bash-1`, but Code Mode must never parse that prose to recover the id.
The producer supplies synchronous `cancel`, non-rejecting `done` that settles after resource cleanup, and optional consuming `readOutput` with bounded-output formatting. A pre-aborted call is a failure because no task exists whose id could satisfy the successful output schema. Once `ctx.tasks.start()` publishes the id, use a task-owned cancellation signal rather than `exec.signal`: later outer-call cancellation stops waiting for the call but does not kill published work; `task_kill`, owner disposal, and service teardown own that lifetime. Foreground work remains coupled to `exec.signal`. See the [background task runtime Agent Note](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) and `dsh-tool-bash` for a stream producer.
## Execution policy and observation
-Prefer not to build deployment policy into the tool. Use `tools/pre-execute` for extensible allow/deny/ask policy (the [permission-gate example](extension-cookbook.md#a-hook-plugin-permission-gate-example)), `ctx.tools.guard()` for a final monotonic deny that later listeners cannot undo, `tools/execute` to wrap canonical dispatch with a deadline/retry/metrics scope, `tools/post-execute` to replace either presentation content or the canonical value, block, or attach model-facing context, and `tools/result` to observe the immutable normalized outcome. A content replacement leaves programmatic access to `value` intact; confidentiality policy blocks or replaces the value. A sandboxing implementation can also sit behind the tool's executor capability seam; the exact contracts are in the [`dsh-tools` README](../../packages/core/tools/README.md#extension-points).
+Prefer not to build deployment policy into the tool. Use `tools/pre-execute` for extensible allow/deny/ask policy (the [permission-gate example](extension-cookbook.md#a-hook-plugin-permission-gate-example)), `ctx.tools.guard()` for a final monotonic deny that later listeners cannot undo, `tools/execute` to wrap dispatch with a deadline, retry, or metrics collection, `tools/post-execute` to replace presentation content or the returned value, block the result, or attach model-facing context, and `tools/result` to observe the immutable normalized outcome. A content replacement leaves programmatic access to `value` intact; confidentiality policy blocks or replaces the value. A sandboxing implementation can also run inside the tool's executor implementation; the [`dsh-tools` README](../../packages/core/tools/README.md#extension-points) defines each extension point's inputs, order, return values, and failure behavior.
## Code Mode reaches your tool for free
@@ -85,7 +85,7 @@ Hard rules (they bite if broken):
- **Purity.** These run on live streaming AND on session-log REPLAY, so they must be pure functions of `args` (+ the result) — NO I/O, NO reading session state, NO clock/random. A diff is derived from the args (`write` uses `oldText: null` because a call-time presenter has no prior file content); the UI adapter, not the tool, supplies session context. If you find yourself wanting the file's old content or the working directory inside `presentCall`, stop — that belongs in durable result metadata or the adapter, not the presenter.
- **UI-only formatting stays out of the model result.** A fenced ` ```console ` block, a diff, a relativized path—none of these belongs in the canonical value or Native content merely to serve a UI. `output.render` owns model-facing prose; `presentationMeta` plus the card presenters own replayable UI state. A `terminal` result view carries raw output and the adapter adds any fallback framing.
-- **`defineTool` soft-validates the display path.** A malformed/older logged arg shape makes the wrapper return `undefined` (a generic fallback) rather than throw — display must never crash a replay.
+- **`defineTool` soft-validates the display path.** Malformed or older logged arguments make the wrapper return `undefined` (a generic fallback) rather than throw — display must never crash a replay.
The neutral vocabulary lives in `dsh-tools`; tools never import a UI or transport type. Host/client runtimes map each `card` into their own view. The design and the why are in [the render-intent-union Agent Note](../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md); `dsh-tool-fs` (generic/diff) and `dsh-tool-bash` (terminal) are the reference implementations.
diff --git a/docs/cookbook/adding-a-tool.zh.md b/docs/cookbook/adding-a-tool.zh.md
index 22eccec67a..69bd7ee150 100644
--- a/docs/cookbook/adding-a-tool.zh.md
+++ b/docs/cookbook/adding-a-tool.zh.md
@@ -8,7 +8,7 @@
```ts
import { readFile } from 'node:fs/promises'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-tool'
@@ -50,13 +50,13 @@ export function apply(ctx: Context) {
## 长时间运行的工作
-通过 producer 配置控制 `run_in_background`,然后使用 `ctx.tasks.start({ kind, label, owner: exec.agent, run })` 注册任务。注册表会在进入 producer 主体前将已预先中止的调用判为失败;运行时会在 `run()` 启动工作前校验 owner 和控制面是否可用,随后提供 id、会话围栏、通用控制工具、通知和 owner cleanup。成功的后台分支会返回类型化的规范句柄,如 `{ kind: 'background', taskId }`;其 Native 渲染器可以保留 `started background task bash-1` 这类供人阅读的自然语言,但 Code Mode 绝不能通过解析该文本取得 id。
+通过 producer 配置控制 `run_in_background`,然后使用 `ctx.tasks.start({ kind, label, owner: exec.agent, run })` 注册任务。注册表会在进入 producer 主体前将已预先中止的调用判为失败;运行时会在 `run()` 启动工作前校验 owner 和任务控制器是否可用,随后提供 id、会话围栏、通用控制工具、通知和 owner cleanup。成功的后台分支会返回类型化的规范句柄,如 `{ kind: 'background', taskId }`;其 Native 渲染器可以保留 `started background task bash-1` 这类供人阅读的自然语言,但 Code Mode 绝不能通过解析该文本取得 id。
producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的 `done`,以及可选的消费式 `readOutput`(负责有界输出的格式化)。预先中止的调用属于失败,因为此时没有任务,其 id 无法满足成功输出 schema。`ctx.tasks.start()` 发布 id 后,应使用任务自有的取消信号,而不是 `exec.signal`:之后取消外层调用只会停止等待本次调用,不会终止已经发布的工作;该生命周期归 `task_kill`、owner dispose 和服务 teardown 所有。前台工作仍与 `exec.signal` 耦合。流式 producer 的示例和完整约定见[后台 task 运行时 Agent Note](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)与 `dsh-tool-bash`。
## 执行策略与观测
-尽量不要把部署策略内建到工具中。使用 `tools/pre-execute` 实现可扩展的允许/拒绝/询问策略(见[权限门禁示例](extension-cookbook.md#a-hook-plugin-permission-gate-example));使用 `ctx.tools.guard()` 设置最终的单调拒绝,后续监听器无法撤销;使用 `tools/execute` 为规范分发包装截止时间/重试/指标作用域;使用 `tools/post-execute` 替换展示内容或规范值、阻止调用,或附加模型可见上下文;使用 `tools/result` 观测不可变的归一化结果而不改变它。替换内容不会阻止程序化访问 `value`;保密策略必须阻止调用或替换值。沙箱实现也可以位于工具执行器的能力 seam 之后;确切约定见 [`dsh-tools` README](../../packages/core/tools/README.md#extension-points)。
+尽量不要把部署策略内建到工具中。使用 `tools/pre-execute` 实现可扩展的允许/拒绝/询问策略(见[权限门禁示例](extension-cookbook.md#a-hook-plugin-permission-gate-example));使用 `ctx.tools.guard()` 设置最终的单调拒绝,后续监听器无法撤销;使用 `tools/execute` 为分发添加截止时间、重试或指标收集;使用 `tools/post-execute` 替换展示内容或返回值、阻止结果,或附加模型可见上下文;使用 `tools/result` 观测不可变的归一化结果而不改变它。替换内容不会阻止程序化访问 `value`;保密策略必须阻止调用或替换值。沙箱实现也可以在工具的执行器实现中运行;[`dsh-tools` README](../../packages/core/tools/README.md#extension-points) 定义每个扩展点的输入、顺序、返回值和失败行为。
## Code Mode 自动触达你的工具
@@ -85,7 +85,7 @@ producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的
- **纯函数。** 这些方法在实时流式输出和会话日志回放时都会运行,因此必须是 `args`(加 result)的纯函数——不做 I/O、不读会话状态、不用时钟/随机数。diff 从 args 派生(`write` 使用 `oldText: null`,因为调用时的展示器没有文件先前内容);会话上下文由 UI 适配器而非工具提供。如果你发现自己想在 `presentCall` 内获取文件旧内容或工作目录,请停下:那属于持久结果元数据或适配器,不属于展示器。
- **UI 格式不进入模型结果。** 围栏 ` ```console ` 块、diff、相对化路径均不应仅为服务 UI 而进入规范值或 Native 内容。`output.render` 负责模型可见的自然语言;`presentationMeta` 和卡片展示器负责可回放的 UI 状态。`terminal` 结果视图携带原始输出,由适配器按需添加回退格式。
-- **`defineTool` 对展示路径做软校验。** 格式错误或旧版日志中的 arg 形态会使包装器返回 `undefined`(通用回退)而非抛异常——展示绝不能导致回放崩溃。
+- **`defineTool` 对展示路径做软校验。** 格式错误或旧版日志中的参数会使包装器返回 `undefined`(通用回退)而非抛异常——展示绝不能导致回放崩溃。
中性词汇定义在 `dsh-tools` 中;工具绝不导入 UI 或传输类型。host/client 运行时将每个 `card` 映射到各自的视图。设计与原因见[渲染意图联合体 Agent Note](../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md);`dsh-tool-fs`(generic/diff)和 `dsh-tool-bash`(terminal)是参考实现。
diff --git a/docs/cookbook/adding-a-vendored-package.i18n.yaml b/docs/cookbook/adding-a-vendored-package.i18n.yaml
index b8416c384f..b17f3390cf 100644
--- a/docs/cookbook/adding-a-vendored-package.i18n.yaml
+++ b/docs/cookbook/adding-a-vendored-package.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/adding-a-vendored-package.md
-adding-a-vendored-package.md: b85d74a3a09b27254883b88cb8e6587e32ed811c
-adding-a-vendored-package.zh.md: 2927837a28d1e7b593090d581522f69f84504806
+adding-a-vendored-package.md: 239ac27565204332559038014fabae83fc2d1057
+adding-a-vendored-package.zh.md: 2bf5c7eeaffec594dbc4d13e0aa33b12a9eccf4f
diff --git a/docs/cookbook/adding-a-vendored-package.md b/docs/cookbook/adding-a-vendored-package.md
index b85d74a3a0..239ac27565 100644
--- a/docs/cookbook/adding-a-vendored-package.md
+++ b/docs/cookbook/adding-a-vendored-package.md
@@ -8,8 +8,8 @@ When the harness needs another upstream Cordis package (e.g. `@cordisjs/plugin-h
```
vendor//
- package.json # from upstream; set "private": true, keep name/exports/type
- tsconfig.json # extends ../../tsconfig.base.json (see shape below)
+ package.json # from upstream; set "private": true, rescope the name, keep exports/type
+ tsconfig.json # extends ../../tsconfig.base.json (see configuration below)
src/ # the upstream src/ verbatim
README.md LICENSE # if upstream ships them
```
@@ -29,9 +29,9 @@ vendor//
}
```
-`package.json` invariants: `"private": true` (vendored packages are never published), keep upstream's `name`/`version`/`exports`/`type`, point declaration metadata at `lib/types`, publish `.d.ts` and `.d.ts.map` declaration outputs, and list its cordis deps in `peerDependencies` (matching the upstream manifest). Transitive upstream deps must themselves be vendored or already present — vendoring one package often means vendoring its dependency tree (e.g. `@cordisjs/plugin-http` pulls `@cordisjs/fetch-file`).
+`package.json` invariants: `"private": true` (vendored packages are never published), rescope the `name` ([mapping](../rescope.md)) while keeping upstream's `version`/`exports`/`type`, point declaration metadata at `lib/types`, publish `.d.ts` and `.d.ts.map` declaration outputs, and list its cordis deps in `peerDependencies` (matching the upstream manifest). Transitive upstream deps must themselves be vendored or already present — vendoring one package often means vendoring its dependency tree (e.g. `@cordisjs/plugin-http` pulls `@cordisjs/fetch-file`).
-Local relative imports/exports in vendored TypeScript source use explicit `.ts` specifiers after copying. This is a repo-local build-shape divergence from upstream: `rewriteRelativeImportExtensions` emits `.js` runtime imports while declarations keep explicit `.ts` specifiers that NodeNext/Node16 TypeScript consumers can resolve.
+Local relative imports/exports in vendored TypeScript source use explicit `.ts` specifiers after copying. This is a repo-local build difference from upstream: `rewriteRelativeImportExtensions` emits `.js` runtime imports while declarations keep explicit `.ts` specifiers that NodeNext/Node16 TypeScript consumers can resolve.
## 2. Register it in the root configs
@@ -42,7 +42,7 @@ Local relative imports/exports in vendored TypeScript source use explicit `.ts`
| `vendor/README.md` | add a manifest table row (dir, npm name, version, upstream repo, commit SHA) and log any local modifications |
| `scripts/publint-all.ts` | only if the vendored package is itself published from here (vendored deps normally are not — skip) |
-Covered automatically by globs — no edits needed: root `package.json` workspaces (`vendor/*`), `tsdown.config.ts`, `vitest.config.ts`, `.oxlintrc.json`. A per-package `vendor//tsdown.config.ts` is needed ONLY if the build shape diverges from the root default (dual ESM/CJS or multiple entries — see `vendor/schemastery` and `vendor/logger-console`); its entry should read the JS emitted under `lib/types`.
+Covered automatically by globs — no edits needed: root `package.json` workspaces (`vendor/*`), `tsdown.config.ts`, `vitest.config.ts`, `.oxlintrc.json`. A per-package `vendor//tsdown.config.ts` is needed ONLY if the build configuration differs from the root default (dual ESM/CJS or multiple entries — see `vendor/schemastery` and `vendor/logger-console`); its entry should read the JS emitted under `lib/types`.
## 3. Mind the manifest guard
diff --git a/docs/cookbook/adding-a-vendored-package.zh.md b/docs/cookbook/adding-a-vendored-package.zh.md
index 2927837a28..2bf5c7eeaf 100644
--- a/docs/cookbook/adding-a-vendored-package.zh.md
+++ b/docs/cookbook/adding-a-vendored-package.zh.md
@@ -8,8 +8,8 @@
```
vendor//
- package.json # from upstream; set "private": true, keep name/exports/type
- tsconfig.json # extends ../../tsconfig.base.json (see shape below)
+ package.json # from upstream; set "private": true, rescope the name, keep exports/type
+ tsconfig.json # extends ../../tsconfig.base.json (see configuration below)
src/ # the upstream src/ verbatim
README.md LICENSE # if upstream ships them
```
@@ -29,9 +29,9 @@ vendor//
}
```
-`package.json` 的不变式:`"private": true`(vendored 包永不发布);保留上游的 `name`/`version`/`exports`/`type`;声明元数据指向 `lib/types`;发布 `.d.ts` 与 `.d.ts.map` 声明输出;在 `peerDependencies` 中列出其 Cordis 依赖(与上游 manifest(元数据清单)一致)。传递性上游依赖本身也必须被 vendor 或已存在于仓库中——vendor 一个包往往意味着 vendor 其整条依赖树(如 `@cordisjs/plugin-http` 会拉入 `@cordisjs/fetch-file`)。
+`package.json` 的不变式:`"private": true`(vendored 包永不发布);改写 `name` 的 scope([映射](../rescope.md)),保留上游的 `version`/`exports`/`type`;声明元数据指向 `lib/types`;发布 `.d.ts` 与 `.d.ts.map` 声明输出;在 `peerDependencies` 中列出其 Cordis 依赖(与上游 manifest(元数据清单)一致)。传递性上游依赖本身也必须被 vendor 或已存在于仓库中——vendor 一个包往往意味着 vendor 其整条依赖树(如 `@cordisjs/plugin-http` 会拉入 `@cordisjs/fetch-file`)。
-vendored TypeScript 源码中的本地相对导入/导出在复制后使用显式 `.ts` 后缀。这是仓库本地的构建形态与上游的差异:`rewriteRelativeImportExtensions` 输出 `.js` 运行时导入,而声明文件保留显式 `.ts` 后缀,使 NodeNext/Node16 的 TypeScript 消费方能够解析。
+vendored TypeScript 源码中的本地相对导入/导出在复制后使用显式 `.ts` 后缀。这是仓库本地构建与上游的差异:`rewriteRelativeImportExtensions` 输出 `.js` 运行时导入,而声明文件保留显式 `.ts` 后缀,使 NodeNext/Node16 的 TypeScript 消费方能够解析。
## 2. 在根配置中注册
@@ -42,7 +42,7 @@ vendored TypeScript 源码中的本地相对导入/导出在复制后使用显
| `vendor/README.md` | 添加一行 manifest 表格行(dir、npm name、version、upstream repo、commit SHA)并记录所有本地修改 |
| `scripts/publint-all.ts` | 仅当该 vendored 包本身从此仓库发布时才需要(vendored 依赖通常不发布——跳过) |
-以下由 glob 自动覆盖,无需手动编辑:根 `package.json` 的 workspaces(`vendor/*`)、`tsdown.config.ts`、`vitest.config.ts`、`.oxlintrc.json`。只有当构建形态偏离根默认值时(双 ESM/CJS 或多入口——参见 `vendor/schemastery` 和 `vendor/logger-console`),才需要单独的 `vendor//tsdown.config.ts`;其入口应读取 `lib/types` 下输出的 JS。
+以下由 glob 自动覆盖,无需手动编辑:根 `package.json` 的 workspaces(`vendor/*`)、`tsdown.config.ts`、`vitest.config.ts`、`.oxlintrc.json`。只有当构建配置与根默认值不同时(双 ESM/CJS 或多入口——参见 `vendor/schemastery` 和 `vendor/logger-console`),才需要单独的 `vendor//tsdown.config.ts`;其入口应读取 `lib/types` 下输出的 JS。
## 3. 注意 manifest 守卫
diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml
index 6e83450ec3..3a98437564 100644
--- a/docs/cookbook/extension-cookbook.i18n.yaml
+++ b/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: aba220ec6dc3cf3d0f99edd0e47ffa6069499b47
-extension-cookbook.zh.md: 34e8fc0fa3d2d57136616d66cfb4f3f6de20605e
+extension-cookbook.md: f292075dfdad5016d81521318b38594e3d7ee8b4
+extension-cookbook.zh.md: 0623c49d9d7075b3823c1fd340b36a5fba21f31e
diff --git a/docs/cookbook/extension-cookbook.md b/docs/cookbook/extension-cookbook.md
index aba220ec6d..f292075dfd 100644
--- a/docs/cookbook/extension-cookbook.md
+++ b/docs/cookbook/extension-cookbook.md
@@ -2,18 +2,18 @@
English | [中文](extension-cookbook.zh.md)
-Reference shapes for the harness extension surface. The snippets omit imports and helper implementations and are not copy-paste-complete. For concrete authoring paths, see the [package checklist](adding-a-package.md), [first-tool tutorial](../user/develop/basic/tool.md), [tool reference](adding-a-tool.md), and [LLM adapter guide](adding-an-llm-adapter.md); the [architecture](../architecture.md) owns the system and extension-point map.
+Reference patterns for harness extensions. The snippets omit imports and helper implementations and are not copy-paste-complete. For concrete authoring paths, see the [package checklist](adding-a-package.md), [first-tool tutorial](../user/develop/basic/tool.md), [tool reference](adding-a-tool.md), and [LLM adapter guide](adding-an-llm-adapter.md); the [architecture](../architecture.md) owns the system and extension-point map.
## A tool plugin
-A tool registers on `ctx.tools`. The annotated `defineTool` example (typed `execute` args, result shaping, the `run_in_background` pattern) lives in [adding-a-tool.md](adding-a-tool.md) — that guide is the source of truth for the tool shape. Raw JSON-Schema `ToolDefinition`s are also accepted by `ctx.tools.register()` directly (that is how MCP-sourced tools arrive); `defineTool` is the typed sugar for first-party tools.
+A tool registers on `ctx.tools`. The annotated `defineTool` example (typed `execute` arguments, result construction, the `run_in_background` pattern) lives in [adding-a-tool.md](adding-a-tool.md) — that guide is the source of truth for tool definitions. Raw JSON-Schema `ToolDefinition`s are also accepted by `ctx.tools.register()` directly (that is how MCP-sourced tools arrive); `defineTool` is the typed helper for first-party tools.
## A hook plugin (permission-gate example)
This permission gate is one example of a hook plugin. It returns a typed decision from the `tools/pre-execute` gate to allow or deny a call; sandbox, permission, and plan-mode plugins can use this extension point. Hook plugins can intercept other extension points and are not inherently permission gates. A "native hook" is an ordinary Cordis plugin on an interception point; it needs no external protocol.
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
declare function isAllowed(exec: ToolExecution): Promise
@@ -37,7 +37,7 @@ This waterfall is the reorderable policy layer. Use `ctx.tools.guard()` when an
A UI plugin renders from the `session/event` feed (the assistant token stream as `assistant/chunk`, plus turn/step boundaries and tool activity), and drives input back in via `agent.followup()` / `agent.steer()`. A browser plugin contributing a business row to the built-in Web Client instead registers a `ConversationNodeDefinition` and keyed Chat renderer; follow the [Conversation Node guide](adding-a-conversation-node.md).
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
@@ -64,10 +64,10 @@ export function apply(ctx: Context) {
A *protocol driver* adapts a wire peer to `ctx.agents`; it may serve a UI or an automation client. A stdio driver owns stdout, creates or resumes agents through the factory, and maps protocol requests to `followup()` or `cancel()`. A low-level prompt request returns its durable enqueue receipt; it does not acquire a result by correlating `MessageId` with `turn/end`. Publish whole-agent status separately. An automation method may wait from its receipt through the next idle and summarize that explicitly owned interval, while a UI normally keeps observing the open-ended event stream. Tear agents down with `AgentHandle.dispose()` so disposal reaches quiescence.
-[`packages/acp/acp`](../../packages/acp/acp) is the automation-only worked example: it exposes fresh text sessions over Agent Client Protocol JSON-RPC stdio, emits committed assistant text, and registers a one-shot machine permission answerer for agents it owns. Its [README](../../packages/acp/acp/README.md) owns the exact method and lifecycle contract.
+[`packages/acp/acp`](../../packages/acp/acp) is the automation-only worked example: it exposes fresh text sessions over Agent Client Protocol JSON-RPC stdio, emits committed assistant text, and registers a one-shot machine permission answerer for agents it owns. Its [README](../../packages/acp/acp/README.md) defines the exact methods, event order, and lifecycle contract.
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-protocol-bridge'
export const inject = ['agents', 'sessions', 'sessionPersistence']
diff --git a/docs/cookbook/extension-cookbook.zh.md b/docs/cookbook/extension-cookbook.zh.md
index 34e8fc0fa3..0623c49d9d 100644
--- a/docs/cookbook/extension-cookbook.zh.md
+++ b/docs/cookbook/extension-cookbook.zh.md
@@ -2,18 +2,18 @@
[English](extension-cookbook.md) | 中文
-harness 扩展表面的参考形态。代码片段省略了 import 和辅助实现,无法直接复制运行。具体编写路径见[包检查清单](adding-a-package.md)、[第一个工具教程](../user/develop/basic/tool.md)、[工具参考](adding-a-tool.md)和 [LLM(大语言模型)适配器指南](adding-an-llm-adapter.md);系统与扩展点映射由[架构文档](../architecture.md)负责。
+harness 扩展的参考模式。代码片段省略了 import 和辅助实现,无法直接复制运行。具体编写路径见[包检查清单](adding-a-package.md)、[第一个工具教程](../user/develop/basic/tool.md)、[工具参考](adding-a-tool.md)和 [LLM(大语言模型)适配器指南](adding-an-llm-adapter.md);系统与扩展点映射由[架构文档](../architecture.md)负责。
## 工具插件
-工具在 `ctx.tools` 上注册。带注解的 `defineTool` 示例(类型化的 `execute` 参数、结果塑形、`run_in_background` 模式)见 [adding-a-tool.md](adding-a-tool.md)——该指南是工具形态的真源。`ctx.tools.register()` 也直接接受原始 JSON Schema `ToolDefinition`(MCP 来源的工具就是这样到达的);`defineTool` 是为第一方工具提供的类型化语法糖。
+工具在 `ctx.tools` 上注册。带注解的 `defineTool` 示例(类型化的 `execute` 参数、结果构造、`run_in_background` 模式)见 [adding-a-tool.md](adding-a-tool.md)——该指南是工具定义的真源。`ctx.tools.register()` 也直接接受原始 JSON Schema `ToolDefinition`(MCP 来源的工具就是这样到达的);`defineTool` 是第一方工具使用的类型化辅助函数。
## 钩子插件(以权限门禁为例)
这个权限门禁是钩子插件的一个示例。它从 `tools/pre-execute` 门禁返回一个类型化的决策,用于允许或拒绝一次调用;沙箱、权限和 plan-mode 插件都可以使用该扩展点。钩子插件也可以拦截其他扩展点,本身并不等同于权限门禁。「原生钩子」是在拦截点上运行的普通 Cordis 插件,不需要外部协议。
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
declare function isAllowed(exec: ToolExecution): Promise
@@ -37,7 +37,7 @@ export function apply(ctx: Context) {
UI 插件从 `session/event` 事件流渲染(助手 token 流以 `assistant/chunk` 形式到达,加上轮次/步骤边界与工具活动),并通过 `agent.followup()` / `agent.steer()` 将输入驱动回去。如果浏览器插件要向内建 Web Client 贡献业务行,则应注册 `ConversationNodeDefinition` 与 keyed Chat renderer;具体步骤见 [Conversation Node 指南](adding-a-conversation-node.md)。
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
@@ -64,10 +64,10 @@ export function apply(ctx: Context) {
*协议驱动*将协议对端接入 `ctx.agents`;它可以服务于 UI 或自动化客户端。stdio 驱动拥有 stdout,通过工厂创建或恢复 agent(智能体),并将协议请求映射为 `followup()` 或 `cancel()`。底层提示词请求返回其持久入队回执;它不会通过关联 `MessageId` 与 `turn/end` 获得结果。整个 agent 的状态应单独发布。自动化方法可以从回执等待到下一次 idle,并概括这一显式拥有的区间;UI 通常则会持续观察开放式事件流。通过 `AgentHandle.dispose()` 拆除 agent,以使 dispose(资源释放)达到完全停稳。
-[`packages/acp/acp`](../../packages/acp/acp) 是仅面向自动化的完整示例:它通过 ACP(Agent Client Protocol)JSON-RPC stdio 提供全新文本会话,发出已提交的助手文本,并为其拥有的 agent 注册一次性机器权限应答器。其 [README](../../packages/acp/acp/README.md) 拥有精确的方法和生命周期约定。
+[`packages/acp/acp`](../../packages/acp/acp) 是仅面向自动化的完整示例:它通过 ACP(Agent Client Protocol)JSON-RPC stdio 提供全新文本会话,发出已提交的助手文本,并为其拥有的 agent 注册一次性机器权限应答器。其 [README](../../packages/acp/acp/README.md) 定义确切的方法、事件顺序和生命周期约定。
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-protocol-bridge'
export const inject = ['agents', 'sessions', 'sessionPersistence']
diff --git a/docs/cookbook/maintaining-dsh-code-review.i18n.yaml b/docs/cookbook/maintaining-dsh-code-review.i18n.yaml
index 331373fb1e..8ceb56b26b 100644
--- a/docs/cookbook/maintaining-dsh-code-review.i18n.yaml
+++ b/docs/cookbook/maintaining-dsh-code-review.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/maintaining-dsh-code-review.md
-maintaining-dsh-code-review.md: c8517054434f4b090c67455cda0a992c2c3173ee
-maintaining-dsh-code-review.zh.md: 3d26d540a41661aa45b41d6738118f36d904ccf3
+maintaining-dsh-code-review.md: a8c2a66c065aaec5c03f0ab6965377d1d1eb14bd
+maintaining-dsh-code-review.zh.md: c72323c4ab0c31fa30aa3ef0e8ea41b129ad0c1d
diff --git a/docs/cookbook/maintaining-dsh-code-review.md b/docs/cookbook/maintaining-dsh-code-review.md
index c851705443..a8c2a66c06 100644
--- a/docs/cookbook/maintaining-dsh-code-review.md
+++ b/docs/cookbook/maintaining-dsh-code-review.md
@@ -20,7 +20,7 @@ Each run stores its artifacts on the operator's machine. The saved diff, candida
When a run produces a candidate, a macOS notification arrives with a `dsh-code-review-promote ` hint.
-1. **Read the diff on its own merits.** Do not defer to "the reviewers approved" — the maintainer contract is that the operator is the final judgment. Look for checklist bloat, historical prose, unsupported extrapolation from a single incident, and duplicated coverage with existing skill or authoritative-doc content.
+1. **Read the diff on its own merits.** Do not defer to "the reviewers approved"; the maintainer contract is that the operator makes the final decision. Look for checklist bloat, historical prose, unsupported extrapolation from a single incident, and duplicated coverage with existing skill or authoritative-doc content.
```sh
ls ~/dsh-code-review-outputs/ # every candidate ever produced
diff --git a/docs/cookbook/maintaining-dsh-code-review.zh.md b/docs/cookbook/maintaining-dsh-code-review.zh.md
index 3d26d540a4..c72323c4ab 100644
--- a/docs/cookbook/maintaining-dsh-code-review.zh.md
+++ b/docs/cookbook/maintaining-dsh-code-review.zh.md
@@ -20,7 +20,7 @@
某次运行产出候选版本时,macOS 会发出一条带 `dsh-code-review-promote ` 提示的通知。
-1. **根据 diff 本身作出判断。** 不要因为「评审者已经批准」就直接接受:维护者约定规定最终判断由操作员作出。检查清单是否膨胀、是否有历史叙述、是否根据单次事件作出无依据的外推,以及是否与现有 skill 或权威文档重复。
+1. **根据 diff 本身作出判断。** 不要因为「评审者已经批准」就直接接受;维护者约定规定由操作员作出最终决定。检查清单是否膨胀、是否有历史叙述、是否根据单次事件作出无依据的外推,以及是否与现有 skill 或权威文档重复。
```sh
ls ~/dsh-code-review-outputs/ # every candidate ever produced
diff --git a/docs/cordis-api/fiber.i18n.yaml b/docs/cordis-api/fiber.i18n.yaml
index 6c01366dc1..537be01dbc 100644
--- a/docs/cordis-api/fiber.i18n.yaml
+++ b/docs/cordis-api/fiber.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/cordis-api/fiber.md
-fiber.md: 36d2861ac6a53e8186a92d86c65ba228d4b59ee5
-fiber.zh.md: fafa559ca911677c43893862190009d82c39c56b
+fiber.md: 182b77390b29b8a90504437d0ccc2dfeba23921a
+fiber.zh.md: 9ed3e52618586dc3815b9d913439d11a227fb64b
diff --git a/docs/cordis-api/fiber.md b/docs/cordis-api/fiber.md
index 36d2861ac6..182b77390b 100644
--- a/docs/cordis-api/fiber.md
+++ b/docs/cordis-api/fiber.md
@@ -34,7 +34,7 @@ Register a cleanup-aware effect on this fiber.
**Returns** a disposer that tears the effect down and settles once done.
-[Source](../../vendor/cordis/src/fiber.ts#L420)
+[Source](../../vendor/cordis/src/fiber.ts#L415)
### ctx.fiber
@@ -97,7 +97,7 @@ public state
Current lifecycle state; transitions emit `internal/status`.
-[Source](../../vendor/cordis/src/fiber.ts#L192)
+[Source](../../vendor/cordis/src/fiber.ts#L194)
### fiber.dispose
@@ -108,7 +108,7 @@ public readonly dispose: () => Promise
Dispose this fiber: unload the plugin, then settle once cleanup finished.
-[Source](../../vendor/cordis/src/fiber.ts#L194)
+[Source](../../vendor/cordis/src/fiber.ts#L196)
### fiber.store
@@ -119,7 +119,7 @@ public store: Dict | undefined
Snapshot of required service implementations while loaded; `undefined` otherwise.
-[Source](../../vendor/cordis/src/fiber.ts#L196)
+[Source](../../vendor/cordis/src/fiber.ts#L198)
### fiber.inertia
@@ -130,7 +130,7 @@ public inertia: Promise | undefined
The in-flight load/unload transition, if one is currently running.
-[Source](../../vendor/cordis/src/fiber.ts#L198)
+[Source](../../vendor/cordis/src/fiber.ts#L200)
### fiber.name
@@ -141,7 +141,7 @@ get name()
The plugin's display name, inherited from the nearest named ancestor, else `'root'`.
-[Source](../../vendor/cordis/src/fiber.ts#L341)
+[Source](../../vendor/cordis/src/fiber.ts#L336)
### fiber.assertActive()
@@ -159,7 +159,7 @@ Throw if the fiber has already been disposed.
**Returns** nothing when the fiber is still active.
-[Source](../../vendor/cordis/src/fiber.ts#L356)
+[Source](../../vendor/cordis/src/fiber.ts#L351)
### fiber.effect(execute, label?)
@@ -190,7 +190,7 @@ Register a cleanup-aware effect on this fiber.
**Returns** a disposer that tears the effect down and settles once done.
-[Source](../../vendor/cordis/src/fiber.ts#L420)
+[Source](../../vendor/cordis/src/fiber.ts#L415)
### fiber.getEffects()
@@ -207,7 +207,7 @@ Return metadata for currently registered effects.
**Returns** one `EffectMeta` tree per labeled live effect.
-[Source](../../vendor/cordis/src/fiber.ts#L573)
+[Source](../../vendor/cordis/src/fiber.ts#L568)
### fiber.await()
@@ -225,7 +225,7 @@ Wait for current lifecycle work and rethrow startup errors.
**Returns** this fiber, once it has settled into a stable state.
-[Source](../../vendor/cordis/src/fiber.ts#L702)
+[Source](../../vendor/cordis/src/fiber.ts#L704)
### fiber.restart()
@@ -243,7 +243,7 @@ Dispose and immediately reload this plugin with its current config.
**Returns** a promise resolving once the reload settled.
-[Source](../../vendor/cordis/src/fiber.ts#L716)
+[Source](../../vendor/cordis/src/fiber.ts#L718)
### fiber.update(config, noSave?)
@@ -271,7 +271,7 @@ Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto o
**Returns** the update waterfall result; the default restart returns a promise.
-[Source](../../vendor/cordis/src/fiber.ts#L734)
+[Source](../../vendor/cordis/src/fiber.ts#L736)
## Effect
diff --git a/docs/cordis-api/fiber.zh.md b/docs/cordis-api/fiber.zh.md
index fafa559ca9..9ed3e52618 100644
--- a/docs/cordis-api/fiber.zh.md
+++ b/docs/cordis-api/fiber.zh.md
@@ -36,7 +36,7 @@ effect(execute: () => Effect, label?: string): AsyncDisposable>
**返回**一个用于撤销该作用的清理函数,并在清理完成后结算。
-[源码](../../vendor/cordis/src/fiber.ts#L420)
+[源码](../../vendor/cordis/src/fiber.ts#L415)
### ctx.fiber
@@ -99,7 +99,7 @@ public state
当前生命周期状态;状态转换会发出 `internal/status`。
-[源码](../../vendor/cordis/src/fiber.ts#L192)
+[源码](../../vendor/cordis/src/fiber.ts#L194)
### fiber.dispose
@@ -110,7 +110,7 @@ public readonly dispose: () => Promise
dispose 此 fiber:卸载插件,并在清理完成后结算。
-[源码](../../vendor/cordis/src/fiber.ts#L194)
+[源码](../../vendor/cordis/src/fiber.ts#L196)
### fiber.store
@@ -121,7 +121,7 @@ public store: Dict | undefined
加载期间所需服务实现的快照;其他情况下为 `undefined`。
-[源码](../../vendor/cordis/src/fiber.ts#L196)
+[源码](../../vendor/cordis/src/fiber.ts#L198)
### fiber.inertia
@@ -132,7 +132,7 @@ public inertia: Promise | undefined
当前正在进行的加载或卸载转换;如果没有此类转换,则为 undefined。
-[源码](../../vendor/cordis/src/fiber.ts#L198)
+[源码](../../vendor/cordis/src/fiber.ts#L200)
### fiber.name
@@ -143,7 +143,7 @@ get name()
插件的显示名称,继承自最近的具名祖先;如果不存在,则为 `'root'`。
-[源码](../../vendor/cordis/src/fiber.ts#L341)
+[源码](../../vendor/cordis/src/fiber.ts#L336)
### fiber.assertActive()
@@ -161,7 +161,7 @@ assertActive()
**返回**:fiber 仍处于活动状态时不返回任何内容。
-[源码](../../vendor/cordis/src/fiber.ts#L356)
+[源码](../../vendor/cordis/src/fiber.ts#L351)
### fiber.effect(execute, label?)
@@ -192,7 +192,7 @@ effect(execute: () => Effect, label?: string): AsyncDisposable>
**返回**一个用于撤销该作用的清理函数,并在清理完成后结算。
-[源码](../../vendor/cordis/src/fiber.ts#L420)
+[源码](../../vendor/cordis/src/fiber.ts#L415)
### fiber.getEffects()
@@ -209,7 +209,7 @@ getEffects()
**返回**:每个带标签的活动作用对应一棵 `EffectMeta` 树。
-[源码](../../vendor/cordis/src/fiber.ts#L573)
+[源码](../../vendor/cordis/src/fiber.ts#L568)
### fiber.await()
@@ -227,7 +227,7 @@ async await()
**返回**:进入稳定状态后的此 fiber。
-[源码](../../vendor/cordis/src/fiber.ts#L702)
+[源码](../../vendor/cordis/src/fiber.ts#L704)
### fiber.restart()
@@ -245,7 +245,7 @@ dispose 此插件,并立即使用其当前配置重新加载。
**返回**一个在重新加载完成后兑现的 promise。
-[源码](../../vendor/cordis/src/fiber.ts#L716)
+[源码](../../vendor/cordis/src/fiber.ts#L718)
### fiber.update(config, noSave?)
@@ -273,7 +273,7 @@ update(config: any, noSave = false)
**返回**更新 waterfall 的结果;默认的重新启动操作返回一个 promise。
-[源码](../../vendor/cordis/src/fiber.ts#L734)
+[源码](../../vendor/cordis/src/fiber.ts#L736)
## Effect
diff --git a/docs/cordis-api/inherited.md b/docs/cordis-api/inherited.md
index 8bf23274e5..2f83a9cd66 100644
--- a/docs/cordis-api/inherited.md
+++ b/docs/cordis-api/inherited.md
@@ -1,7 +1,7 @@
-# Inherited Cordis Surface
+# Inherited Cordis API
The framework `ctx` members and events every plugin sees beyond the harness tier — pinned vendor source ([vendoring policy](../../vendor/README.md)), summarized tersely so the harness pages stay focused on repository-owned vocabulary. Detailed Context, Fiber, Registry, and Service APIs are generated in [context.md](context.md), [fiber.md](fiber.md), [registry.md](registry.md), and [service.md](service.md); the event-dispatch methods in [events.md](events.md).
diff --git a/docs/cordis-primer.i18n.yaml b/docs/cordis-primer.i18n.yaml
index 9e1143231e..180ba85c01 100644
--- a/docs/cordis-primer.i18n.yaml
+++ b/docs/cordis-primer.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/cordis-primer.md
-cordis-primer.md: 4bcb2c9979994ca70f92031cbdc5dd22df9c1977
-cordis-primer.zh.md: a18b8b37af19a610b71babbe5e67f96bb09e81b1
+cordis-primer.md: d1e7c5fd8eaaa89fe448d238359389d945cd6346
+cordis-primer.zh.md: d6ce0f2024f65b006c9505daffaa06a08bb56875
diff --git a/docs/cordis-primer.md b/docs/cordis-primer.md
index 4bcb2c9979..d1e7c5fd8e 100644
--- a/docs/cordis-primer.md
+++ b/docs/cordis-primer.md
@@ -23,7 +23,7 @@ Every event can have one of the following dispatch mode and can only be dispatch
| `parallel` | Yes | all listeners observe the event in parallel | No |
| `serial` | Yes | listeners observe in registration order | Yes |
-The mode is part of the event's public contract. New harness events document it with an `@mode` tag so the generated catalog can check declarations against dispatch sites.
+The dispatch mode is part of the event's public contract. New harness events document it with an `@mode` tag so the generated catalog can check declarations against dispatch sites.
## Cordis Waterfall Semantics
@@ -35,7 +35,7 @@ For single-decision events, short-circuiting is the design. A policy listener ca
## Loader Configuration
-`@cordisjs/plugin-include` parses `!!js` into expression nodes, but the Loader interpolates only an entry's `config` before mounting the plugin. Entry metadata (`id`, `name`, `group`, `disabled`, `inject`, `intercept`, and `isolate`) remains literal; `disabled: !!js ...` is therefore a truthy object that always disables the entry. Use explicit config overlays when environment selection changes which plugins are mounted.
+`@deepseek-ai/cordis-plugin-include` parses `!!js` into expression nodes. Loader interpolates only an entry's `config`, after declared injections activate, against that plugin context (`ctx.serviceName`); Include preserves nested row expressions until target activation. Entry metadata (`id`, `name`, `group`, `disabled`, `inject`, `intercept`, `isolate`) stays literal, so `disabled: !!js ...` always disables the entry. Use overlays when the environment selects plugins.
## Practical Rules
diff --git a/docs/cordis-primer.zh.md b/docs/cordis-primer.zh.md
index a18b8b37af..d6ce0f2024 100644
--- a/docs/cordis-primer.zh.md
+++ b/docs/cordis-primer.zh.md
@@ -39,7 +39,7 @@ Cordis 是 DeepSeek Harness SDK 底层以 vendor 方式引入的插件框架。
## Loader 配置
-`@cordisjs/plugin-include` 将 `!!js` 解析为表达式节点,但 Loader 仅在挂载插件前对条目的 `config` 做插值。条目元数据(`id`、`name`、`group`、`disabled`、`inject`、`intercept` 和 `isolate`)保持字面值;因此 `disabled: !!js ...` 是一个 truthy 对象,会始终禁用该条目。需要根据环境选择挂载哪些插件时,请使用显式的配置覆盖层。
+`@deepseek-ai/cordis-plugin-include` 将 `!!js` 解析为表达式节点。Loader 只在声明的注入激活后,基于该插件上下文(`ctx.serviceName`)插值条目的 `config`;Include 会保留嵌套行表达式,直到目标行激活。条目元数据(`id`、`name`、`group`、`disabled`、`inject`、`intercept`、`isolate`)保持字面值,因此 `disabled: !!js ...` 始终禁用该条目。由环境选择插件时,请使用 overlay。
## 实践规则
diff --git a/docs/cordis-tutorial/01-first-plugin.i18n.yaml b/docs/cordis-tutorial/01-first-plugin.i18n.yaml
index 1fe3585ba1..cbb4aee438 100644
--- a/docs/cordis-tutorial/01-first-plugin.i18n.yaml
+++ b/docs/cordis-tutorial/01-first-plugin.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/cordis-tutorial/01-first-plugin.md
-01-first-plugin.md: 4359dfe4883f12e9cb242cf3009827fd7864768c
-01-first-plugin.zh.md: 9965f4ddb75fa338ba7fd9d564bd4a32fced7b93
+01-first-plugin.md: 79df2f42df1f34a7ef32cc81607e1b926a1854ae
+01-first-plugin.zh.md: 87cd7a96843d69d68fb95662e59bbfb9ebfc2a37
diff --git a/docs/cordis-tutorial/01-first-plugin.md b/docs/cordis-tutorial/01-first-plugin.md
index 4359dfe488..79df2f42df 100644
--- a/docs/cordis-tutorial/01-first-plugin.md
+++ b/docs/cordis-tutorial/01-first-plugin.md
@@ -9,7 +9,7 @@ In the loader configuration used here, a Cordis plugin module named-exports an `
In your `tmp/cordis-tutorial` directory (see [setup](index.md#setup)), create `hello.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello'
@@ -52,10 +52,10 @@ There is no framework bootstrap code in your file: a plugin describes what it co
## The two other plugin shapes
-A function is the most common shape, but Cordis accepts three:
+A function is the most common form, but Cordis accepts three:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
// 1. Function plugin (what you just wrote).
export function apply(ctx: Context) {}
@@ -92,4 +92,4 @@ One caveat worth knowing early: a config entry whose module cannot be **resolved
Next: [Lifecycle and effects](02-lifecycle-and-effects.md) — what happens when a plugin unloads.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/01-first-plugin.zh.md b/docs/cordis-tutorial/01-first-plugin.zh.md
index 9965f4ddb7..87cd7a9684 100644
--- a/docs/cordis-tutorial/01-first-plugin.zh.md
+++ b/docs/cordis-tutorial/01-first-plugin.zh.md
@@ -9,7 +9,7 @@
在 `tmp/cordis-tutorial` 目录中(参见[环境设置](index.md#setup))创建 `hello.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello'
@@ -52,10 +52,10 @@ hello from my first plugin
## 其他两种插件形态
-函数是最常见的形态,但 Cordis 接受三种形态:
+函数是最常见的形式,但 Cordis 接受三种形式:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
// 1. Function plugin (what you just wrote).
export function apply(ctx: Context) {}
@@ -92,4 +92,4 @@ export function apply(ctx: Context) {
下一章:[生命周期与 effect](02-lifecycle-and-effects.md):插件卸载时会发生什么。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/02-lifecycle-and-effects.i18n.yaml b/docs/cordis-tutorial/02-lifecycle-and-effects.i18n.yaml
index 12793267e2..26ffcac268 100644
--- a/docs/cordis-tutorial/02-lifecycle-and-effects.i18n.yaml
+++ b/docs/cordis-tutorial/02-lifecycle-and-effects.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/cordis-tutorial/02-lifecycle-and-effects.md
-02-lifecycle-and-effects.md: 7b195b63a1e8730f27b9dd9af8af6a68a588cee9
-02-lifecycle-and-effects.zh.md: 4a3f83dedd5c95c7fcb5c1aebbbb8cb2e849b9cf
+02-lifecycle-and-effects.md: 8e75708eb0cba1aceb7fa3dae5ae334995eb113f
+02-lifecycle-and-effects.zh.md: bc4489e702ab58b5d1efd8a72fc693ee48af2624
diff --git a/docs/cordis-tutorial/02-lifecycle-and-effects.md b/docs/cordis-tutorial/02-lifecycle-and-effects.md
index 7b195b63a1..8e75708eb0 100644
--- a/docs/cordis-tutorial/02-lifecycle-and-effects.md
+++ b/docs/cordis-tutorial/02-lifecycle-and-effects.md
@@ -11,7 +11,7 @@ For a resource Cordis does not already manage — a timer, a connection, a watch
Create `lifecycle.ts` in `tmp/cordis-tutorial`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'lifecycle-demo'
@@ -95,4 +95,4 @@ One ordering caveat: disposers start in reverse registration order, but multiple
Next: [Services](03-services.md) — how plugins share capabilities.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md b/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md
index 4a3f83dedd..bc4489e702 100644
--- a/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md
+++ b/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md
@@ -11,7 +11,7 @@ Cordis 插件可能因修改配置、热重载、显式资源释放或所需服
创建 `lifecycle.ts`,将它放在 `tmp/cordis-tutorial` 中:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'lifecycle-demo'
@@ -95,4 +95,4 @@ PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
下一章:[服务](03-services.md):插件如何共享功能。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/03-services.i18n.yaml b/docs/cordis-tutorial/03-services.i18n.yaml
index bdb7e19387..372e028a47 100644
--- a/docs/cordis-tutorial/03-services.i18n.yaml
+++ b/docs/cordis-tutorial/03-services.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/cordis-tutorial/03-services.md
-03-services.md: 82b08b7b8a2ec8a6b340dd1fdc7fa3de98cedff9
-03-services.zh.md: ba4152454eb79a21b183b867c0ba2ef32cd43923
+03-services.md: 32007284be99ef46b4621089c9b3a80317e77189
+03-services.zh.md: d82be29aa69686b8dc10cc6a45a658683c017cbd
diff --git a/docs/cordis-tutorial/03-services.md b/docs/cordis-tutorial/03-services.md
index 82b08b7b8a..32007284be 100644
--- a/docs/cordis-tutorial/03-services.md
+++ b/docs/cordis-tutorial/03-services.md
@@ -9,9 +9,9 @@ A **service** is a named capability one plugin provides and other plugins consum
Create `greeter.ts` in `tmp/cordis-tutorial`:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
greeter: GreeterService
}
@@ -37,7 +37,7 @@ export function apply(ctx: Context) {
Two pieces work together:
- **Runtime**: `super(ctx, 'greeter')` registers the instance under the name `greeter`. From then on, any plugin can reach it as `ctx.greeter`. The registration is an effect — unloading the provider removes the service.
-- **Compile time**: the `declare module 'cordis'` block is TypeScript declaration merging. It adds `greeter` to the `Context` interface so `ctx.greeter` typechecks everywhere. It generates no code; without it the service still works at runtime, but consumers lose type safety.
+- **Compile time**: the `declare module '@deepseek-ai/cordis'` block is TypeScript declaration merging. It adds `greeter` to the `Context` interface so `ctx.greeter` typechecks everywhere. It generates no code; without it the service still works at runtime, but consumers lose type safety.
A `Service` subclass is itself a plugin (the class form from chapter 1), so `ctx.plugin(GreeterService)` mounts it like any other.
@@ -46,7 +46,7 @@ A `Service` subclass is itself a plugin (the class form from chapter 1), so `ctx
Create `consumer.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'consumer'
export const inject = ['greeter']
@@ -95,4 +95,4 @@ Service names live in one flat namespace per application. Prefix or namespace yo
Next: [Events](04-events.md) — communication without a shared service.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/03-services.zh.md b/docs/cordis-tutorial/03-services.zh.md
index ba4152454e..d82be29aa6 100644
--- a/docs/cordis-tutorial/03-services.zh.md
+++ b/docs/cordis-tutorial/03-services.zh.md
@@ -9,9 +9,9 @@
创建 `greeter.ts`,将它放在 `tmp/cordis-tutorial` 中:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
greeter: GreeterService
}
@@ -37,7 +37,7 @@ export function apply(ctx: Context) {
两部分协同工作:
- **运行时**:`super(ctx, 'greeter')` 以名称 `greeter` 注册该实例。此后,任何插件都可以通过 `ctx.greeter` 访问它。注册属于 effect,卸载提供方时会移除该服务。
-- **编译时**:`declare module 'cordis'` 块使用 TypeScript 声明合并,把 `greeter` 加入 `Context` 接口,使 `ctx.greeter` 在各处都能通过类型检查。它不会生成代码;没有该声明时,服务在运行时仍能工作,但消费方会失去类型安全。
+- **编译时**:`declare module '@deepseek-ai/cordis'` 块使用 TypeScript 声明合并,把 `greeter` 加入 `Context` 接口,使 `ctx.greeter` 在各处都能通过类型检查。它不会生成代码;没有该声明时,服务在运行时仍能工作,但消费方会失去类型安全。
`Service` 子类本身就是插件(第 1 章介绍的类形态),因此 `ctx.plugin(GreeterService)` 会像挂载其他插件一样挂载它。
@@ -46,7 +46,7 @@ export function apply(ctx: Context) {
创建 `consumer.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'consumer'
export const inject = ['greeter']
@@ -95,4 +95,4 @@ export function apply(ctx: Context) {
下一章:[事件](04-events.md):无需共享服务即可通信。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/04-events.i18n.yaml b/docs/cordis-tutorial/04-events.i18n.yaml
index b453ffb1f6..4ce3fb9a65 100644
--- a/docs/cordis-tutorial/04-events.i18n.yaml
+++ b/docs/cordis-tutorial/04-events.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/cordis-tutorial/04-events.md
-04-events.md: e77641dffcb82fcb50a24ca2e3e764d152218094
-04-events.zh.md: 00cce854e9a54fddb594ffa8e306f60a725ac012
+04-events.md: db911b9a06d7304d73030a2020de3d465fde2f9c
+04-events.zh.md: e0357b2c04785d52a54f839d1b8ff3ecccb027ed
diff --git a/docs/cordis-tutorial/04-events.md b/docs/cordis-tutorial/04-events.md
index e77641dffc..db911b9a06 100644
--- a/docs/cordis-tutorial/04-events.md
+++ b/docs/cordis-tutorial/04-events.md
@@ -9,9 +9,9 @@ Services support direct calls; **events** let a plugin announce something withou
Create `stats.ts` in `tmp/cordis-tutorial` — a service that counts things and announces each change:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
stats: StatsService
}
@@ -46,7 +46,7 @@ The `interface Events` merge is the event-system twin of the `interface Context`
Create `reporter.ts`:
```ts ignore-check
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from './stats.ts'
export const name = 'reporter'
@@ -96,9 +96,9 @@ Every harness event documents its mode in the generated reference on its owning
Waterfall is the mode that powers interception. Each listener receives the arguments plus a `next()` continuation; it can transform what `next()` returns, or return without calling `next()` and short-circuit the rest of the chain — what the Cordis docs call the veto. Create `waterfall-demo.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Events {
'demo/transform'(input: string, next: () => Promise): Promise
}
@@ -141,4 +141,4 @@ The harness uses waterfalls for decisions that cooperating plugins may wrap or a
Next: [Configuration](05-config.md) — plugin options from `cordis.yml`.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/04-events.zh.md b/docs/cordis-tutorial/04-events.zh.md
index 00cce854e9..e0357b2c04 100644
--- a/docs/cordis-tutorial/04-events.zh.md
+++ b/docs/cordis-tutorial/04-events.zh.md
@@ -9,9 +9,9 @@
创建 `stats.ts`,将它放在 `tmp/cordis-tutorial` 中。它是一项负责计数并在每次变化时发出通知的服务:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
stats: StatsService
}
@@ -46,7 +46,7 @@ export function apply(ctx: Context) {
创建 `reporter.ts`:
```ts ignore-check
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from './stats.ts'
export const name = 'reporter'
@@ -96,9 +96,9 @@ export function apply(ctx: Context) {
waterfall 是实现拦截的模式。每个监听器都会收到参数和一个 `next()` continuation;它可以转换 `next()` 的返回值,也可以不调用 `next()` 就直接返回,从而短路链条的其余部分。Cordis 文档把后一种行为称为否决。创建 `waterfall-demo.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Events {
'demo/transform'(input: string, next: () => Promise): Promise
}
@@ -141,4 +141,4 @@ harness 使用 waterfall 处理协作插件可以包装或回答的决策:[`ag
下一章:[配置](05-config.md):来自 `cordis.yml` 的插件选项。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/05-config.i18n.yaml b/docs/cordis-tutorial/05-config.i18n.yaml
index 7db45165c4..4e953918dd 100644
--- a/docs/cordis-tutorial/05-config.i18n.yaml
+++ b/docs/cordis-tutorial/05-config.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/cordis-tutorial/05-config.md
-05-config.md: 834bb140cc1ff976acc8f21c8f54a7fb02636eac
-05-config.zh.md: f5cc6ac1ca4fa02eba6a1b015b9f6ae3b1a925fc
+05-config.md: 2357f663135d6fc78a65f9d0952e0bc3f5eefae4
+05-config.zh.md: fbd94d179494ad0b6f73baff2ca525c786cc9e33
diff --git a/docs/cordis-tutorial/05-config.md b/docs/cordis-tutorial/05-config.md
index 834bb140cc..2357f66313 100644
--- a/docs/cordis-tutorial/05-config.md
+++ b/docs/cordis-tutorial/05-config.md
@@ -9,8 +9,8 @@ Each `cordis.yml` entry can carry a `config` block, and the plugin declares a sc
Create `config-demo.ts` in `tmp/cordis-tutorial`:
```ts
-import type { Context } from 'cordis'
-import Schema from 'schemastery'
+import type { Context } from '@deepseek-ai/cordis'
+import Schema from '@deepseek-ai/schemastery'
export const name = 'config-demo'
@@ -81,4 +81,4 @@ The loader used in this repo supports a `!!js` tag for config values that must b
Next: [Composition and HMR](06-composition-and-hmr.md) — treating `cordis.yml` as the application.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/05-config.zh.md b/docs/cordis-tutorial/05-config.zh.md
index f5cc6ac1ca..fbd94d1794 100644
--- a/docs/cordis-tutorial/05-config.zh.md
+++ b/docs/cordis-tutorial/05-config.zh.md
@@ -9,8 +9,8 @@
创建 `config-demo.ts`,并将其放在 `tmp/cordis-tutorial` 中:
```ts
-import type { Context } from 'cordis'
-import Schema from 'schemastery'
+import type { Context } from '@deepseek-ai/cordis'
+import Schema from '@deepseek-ai/schemastery'
export const name = 'config-demo'
@@ -81,4 +81,4 @@ ValidationError: invalid config:
下一章:[组合与 HMR(热模块替换)](06-composition-and-hmr.md):将 `cordis.yml` 视为应用。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml b/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml
index 3732651e58..6a79850803 100644
--- a/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml
+++ b/docs/cordis-tutorial/06-composition-and-hmr.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/cordis-tutorial/06-composition-and-hmr.md
-06-composition-and-hmr.md: a169d7a164be63c939e352e4e5b0bf9bce43da29
-06-composition-and-hmr.zh.md: 07ae46555c390d625a4397933e2ec5ac059bd270
+06-composition-and-hmr.md: 87ea26014657ae8c8199e1ebb486556c827d96ca
+06-composition-and-hmr.zh.md: 830f55de7c1be351fe701cb068197543602619a7
diff --git a/docs/cordis-tutorial/06-composition-and-hmr.md b/docs/cordis-tutorial/06-composition-and-hmr.md
index a169d7a164..87ea260146 100644
--- a/docs/cordis-tutorial/06-composition-and-hmr.md
+++ b/docs/cordis-tutorial/06-composition-and-hmr.md
@@ -22,24 +22,24 @@ Groups nest a sub-list of entries that load and unload as one unit, and `isolate
## Hot module replacement
-Because unloading releases effects ([chapter 2](02-lifecycle-and-effects.md)) and loading follows dependencies ([chapter 3](03-services.md)), HMR can replace a running plugin by unloading and loading it. The `@cordisjs/plugin-hmr` plugin watches your files and does exactly that on save.
+Because unloading releases effects ([chapter 2](02-lifecycle-and-effects.md)) and loading follows dependencies ([chapter 3](03-services.md)), HMR can replace a running plugin by unloading and loading it. The `@deepseek-ai/cordis-plugin-hmr` plugin watches your files and does exactly that on save.
In `tmp/cordis-tutorial`, write `cordis.yml`:
```yaml
- id: logger
- name: '@cordisjs/plugin-logger-console'
+ name: '@deepseek-ai/cordis-plugin-logger-console'
- id: timer
- name: '@cordisjs/plugin-timer'
+ name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
- name: '@cordisjs/plugin-hmr'
+ name: '@deepseek-ai/cordis-plugin-hmr'
config:
root: ['.']
- id: hello
name: './hello.ts'
```
-Two support plugins joined the list: HMR logs through the Cordis logger service, so without a console exporter you would not see its messages, and it `inject`s the `timer` service for debouncing — without `@cordisjs/plugin-timer` it sits in PENDING forever, silently. That silence is the subject of the next section.
+Two support plugins joined the list: HMR logs through the Cordis logger service, so without a console exporter you would not see its messages, and it `inject`s the `timer` service for debouncing — without `@deepseek-ai/cordis-plugin-timer` it sits in PENDING forever, silently. That silence is the subject of the next section.
HMR reads Node's loader internals through the Loader's native helper. Run Cordis under tsx:
@@ -65,7 +65,7 @@ The flip side of dependency-driven loading: a plugin whose `inject` names a serv
You can see the states directly. Every context can enumerate the plugin registry; create `diagnose.ts`:
```ts
-import { FiberState, type Context } from 'cordis'
+import { FiberState, type Context } from '@deepseek-ai/cordis'
export const name = 'diagnose'
@@ -85,7 +85,7 @@ export function apply(ctx: Context) {
And a plugin with an unsatisfiable dependency, `needs-timer.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'needs-timer'
export const inject = ['timer']
@@ -106,8 +106,8 @@ Run it (plain `node --import tsx ../../vendor/cordis/bin.js`; stop with Ctrl-C):
needs-timer is PENDING — a required service is missing
```
-`inject: ['timer']` has no provider. Add `- name: '@cordisjs/plugin-timer'` to the list and the plugin loads. When a plugin does nothing and reports nothing, inspect its fiber state. Iterating without the PENDING filter also shows the loader's own plugins (Loader, Include) as ACTIVE fibers because plugins mount the config file itself.
+`inject: ['timer']` has no provider. Add `- name: '@deepseek-ai/cordis-plugin-timer'` to the list and the plugin loads. When a plugin does nothing and reports nothing, inspect its fiber state. Iterating without the PENDING filter also shows the loader's own plugins (Loader, Include) as ACTIVE fibers because plugins mount the config file itself.
Next: [Into the harness](07-into-the-harness.md) — the same patterns against real harness services.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/06-composition-and-hmr.zh.md b/docs/cordis-tutorial/06-composition-and-hmr.zh.md
index 07ae46555c..830f55de7c 100644
--- a/docs/cordis-tutorial/06-composition-and-hmr.zh.md
+++ b/docs/cordis-tutorial/06-composition-and-hmr.zh.md
@@ -22,24 +22,24 @@ Cordis 配置项除了 `name` 和 `config`,还接受其他元数据:
## 热模块替换
-卸载会释放 effect([第 2 章](02-lifecycle-and-effects.md)),加载则遵循依赖关系([第 3 章](03-services.md)),因此 HMR 可以先卸载、再加载,以替换正在运行的插件。`@cordisjs/plugin-hmr` 插件会监视文件,并在保存时执行这一过程。
+卸载会释放 effect([第 2 章](02-lifecycle-and-effects.md)),加载则遵循依赖关系([第 3 章](03-services.md)),因此 HMR 可以先卸载、再加载,以替换正在运行的插件。`@deepseek-ai/cordis-plugin-hmr` 插件会监视文件,并在保存时执行这一过程。
在 `tmp/cordis-tutorial` 中编写 `cordis.yml`:
```yaml
- id: logger
- name: '@cordisjs/plugin-logger-console'
+ name: '@deepseek-ai/cordis-plugin-logger-console'
- id: timer
- name: '@cordisjs/plugin-timer'
+ name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
- name: '@cordisjs/plugin-hmr'
+ name: '@deepseek-ai/cordis-plugin-hmr'
config:
root: ['.']
- id: hello
name: './hello.ts'
```
-列表中增加了两个辅助插件:HMR 通过 Cordis logger 服务记录日志,因此没有控制台导出器时看不到其消息;它还会 `inject` `timer` 服务来实现去抖,如果没有 `@cordisjs/plugin-timer`,它就会永远停在 PENDING,而且不发出任何提示。下一节就讨论这种静默状态。
+列表中增加了两个辅助插件:HMR 通过 Cordis logger 服务记录日志,因此没有控制台导出器时看不到其消息;它还会 `inject` `timer` 服务来实现去抖,如果没有 `@deepseek-ai/cordis-plugin-timer`,它就会永远停在 PENDING,而且不发出任何提示。下一节就讨论这种静默状态。
HMR 通过 Loader 的原生辅助工具读取 Node 的 loader 内部结构。请在 tsx 下运行 Cordis:
@@ -65,7 +65,7 @@ hello from my EDITED plugin
你可以直接查看这些状态。每个上下文都能枚举插件注册表;创建 `diagnose.ts`:
```ts
-import { FiberState, type Context } from 'cordis'
+import { FiberState, type Context } from '@deepseek-ai/cordis'
export const name = 'diagnose'
@@ -85,7 +85,7 @@ export function apply(ctx: Context) {
再创建一个依赖无法满足的插件 `needs-timer.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'needs-timer'
export const inject = ['timer']
@@ -106,8 +106,8 @@ export function apply(ctx: Context) {
needs-timer is PENDING — a required service is missing
```
-`inject: ['timer']` 没有提供方。向列表添加 `- name: '@cordisjs/plugin-timer'` 后,插件就会加载。如果插件既不执行任何操作,也不报告任何内容,请检查其 fiber 状态。不加 PENDING 过滤条件进行迭代时,还会看到 loader 自身的插件(Loader、Include)处于 ACTIVE,因为配置文件本身也是通过插件挂载的。
+`inject: ['timer']` 没有提供方。向列表添加 `- name: '@deepseek-ai/cordis-plugin-timer'` 后,插件就会加载。如果插件既不执行任何操作,也不报告任何内容,请检查其 fiber 状态。不加 PENDING 过滤条件进行迭代时,还会看到 loader 自身的插件(Loader、Include)处于 ACTIVE,因为配置文件本身也是通过插件挂载的。
下一章:[进入 harness](07-into-the-harness.md):把相同模式用于真实的 harness 服务。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/07-into-the-harness.i18n.yaml b/docs/cordis-tutorial/07-into-the-harness.i18n.yaml
index fd29baab75..fbc6ef1cde 100644
--- a/docs/cordis-tutorial/07-into-the-harness.i18n.yaml
+++ b/docs/cordis-tutorial/07-into-the-harness.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/cordis-tutorial/07-into-the-harness.md
-07-into-the-harness.md: 38483b5c4993a44562970782dca5f676e4cb84f6
-07-into-the-harness.zh.md: 59ce716bdace894682bc1c8e6e00cf174008c26c
+07-into-the-harness.md: 2d3c23f9f7f7fc6bd6cabd4e7e68ebfc46e20665
+07-into-the-harness.zh.md: 45dc0ee6f07ab3b0275499cd4ab0c436eebdddef
diff --git a/docs/cordis-tutorial/07-into-the-harness.md b/docs/cordis-tutorial/07-into-the-harness.md
index 38483b5c49..2d3c23f9f7 100644
--- a/docs/cordis-tutorial/07-into-the-harness.md
+++ b/docs/cordis-tutorial/07-into-the-harness.md
@@ -9,7 +9,7 @@ This chapter registers a model-callable tool with the harness's `tools` service,
Create `greet-tool.ts` in `tmp/cordis-tutorial`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { CallId } from '@deepseek-ai/dsh-llm'
@@ -53,7 +53,7 @@ Every pattern here is from the earlier chapters: `inject: ['tools']` ([chapter 3
Create `tool-logger.ts` — a separate plugin that watches every tool call in the app through the harness's `tools/result` event:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
@@ -95,7 +95,7 @@ The logger fired first: `tools/result` is emitted as part of result materializat
## From here to a full agent
-A real agent is this composition plus more plugins: an LLM adapter, the agent loop, persistence, a front end. Compare [examples/headless-agent/cordis.yml](../../examples/headless-agent/cordis.yml) — you can read every entry in it now. Add your `greet-tool.ts` to a copy of that file.
+A real agent is this composition plus more plugins: an LLM adapter, the agent loop, persistence, an entry point. Compare [examples/headless-agent/cordis.yml](../../examples/headless-agent/cordis.yml) — you can read every entry in it now. Add your `greet-tool.ts` to a copy of that file.
Where to go next:
@@ -104,4 +104,4 @@ Where to go next:
- The generated `cordis-surface` regions on the [subsystem pages](../subsystems/core.md) — everything you can inject and listen to, each on its owning page.
- [Architecture](../architecture.md) — the system map these plugins live in.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/07-into-the-harness.zh.md b/docs/cordis-tutorial/07-into-the-harness.zh.md
index 59ce716bda..45dc0ee6f0 100644
--- a/docs/cordis-tutorial/07-into-the-harness.zh.md
+++ b/docs/cordis-tutorial/07-into-the-harness.zh.md
@@ -9,7 +9,7 @@
创建 `greet-tool.ts`,将它放在 `tmp/cordis-tutorial` 中:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { CallId } from '@deepseek-ai/dsh-llm'
@@ -53,7 +53,7 @@ export function apply(ctx: Context) {
创建 `tool-logger.ts`。这是一个独立插件,通过 harness 的 `tools/result` 事件观察应用中的每次工具调用:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
@@ -95,7 +95,7 @@ logger 会先触发:`tools/result` 在结果物化过程中发出,发生在
## 从这里走向完整 agent(智能体)
-真实 agent 就是这套组合再加上更多插件:LLM(大语言模型)适配器、agent loop(智能体循环)、持久化和前端。对照 [examples/headless-agent/cordis.yml](../../examples/headless-agent/cordis.yml),你现在已经可以读懂其中每个配置项。将 `greet-tool.ts` 加入该文件的副本即可。
+真实 agent 就是这套组合再加上更多插件:LLM(大语言模型)适配器、agent loop(智能体循环)、持久化和运行入口。对照 [examples/headless-agent/cordis.yml](../../examples/headless-agent/cordis.yml),你现在已经可以读懂其中每个配置项。将 `greet-tool.ts` 加入该文件的副本即可。
后续可以阅读:
@@ -104,4 +104,4 @@ logger 会先触发:`tools/result` 在结果物化过程中发出,发生在
- [子系统页面](../subsystems/core.md)上生成的 `cordis-surface` 区块:可以注入和监听的所有内容,各在其所属页面上。
- [架构](../architecture.md):这些插件所处的系统地图。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/index.i18n.yaml b/docs/cordis-tutorial/index.i18n.yaml
index fa810d635f..06857ab177 100644
--- a/docs/cordis-tutorial/index.i18n.yaml
+++ b/docs/cordis-tutorial/index.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/cordis-tutorial/index.md
-index.md: 7a0bb6f8c736bf31d655a7763cfb7039c343d1a2
-index.zh.md: e6f6dc0cccef3f44273655b98b695bdc4632e95a
+index.md: fb700344e6d07d3864655009d2edac15ee9eede8
+index.zh.md: a68e931d81e745164d8f9a5dc7ec9aec4cd0e590
diff --git a/docs/cordis-tutorial/index.md b/docs/cordis-tutorial/index.md
index 7a0bb6f8c7..fb700344e6 100644
--- a/docs/cordis-tutorial/index.md
+++ b/docs/cordis-tutorial/index.md
@@ -13,7 +13,7 @@ If you want the condensed concept reference instead of a walkthrough, read the [
You need a clone of this repository with dependencies installed — the [quick start](../user/guide/quickstart.md) covers prerequisites. No API key is needed for this tutorial; every example runs keylessly.
```sh
-git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
+git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
```
@@ -50,9 +50,9 @@ That one-file launcher (see [vendor/cordis/bin.js](../../vendor/cordis/bin.js))
The examples use three TypeScript features beyond ordinary modern JavaScript:
- **Type annotations** describe values without changing runtime behavior: `ctx: Context` says that `ctx` has the Cordis context API, `who: string` accepts text, and `string[]` means an array of strings.
-- **`import type { Context } from 'cordis'`** imports only type information. It vanishes at runtime, so a plugin file that needs `Context` solely for annotations adds no runtime dependency.
-- **Declaration merging** (`declare module 'cordis' { ... }`) adds your entries to interfaces that Cordis already declares — for example the type of a new `ctx.greeter` property or event name. It generates no runtime wiring; the plugin separately provides the service or emits the event. Chapter 3 shows the pattern in full.
+- **`import type { Context } from '@deepseek-ai/cordis'`** imports only type information. It vanishes at runtime, so a plugin file that needs `Context` solely for annotations adds no runtime dependency.
+- **Declaration merging** (`declare module '@deepseek-ai/cordis' { ... }`) adds your entries to interfaces that Cordis already declares — for example the type of a new `ctx.greeter` property or event name. It generates no runtime wiring; the plugin separately provides the service or emits the event. Chapter 3 shows the pattern in full.
-Chapter 5 also uses an `interface` to describe a configuration object's fields and a generic type such as `Schema` to say which object shape a schema validates. You can copy those declarations as shown; the surrounding text explains what each one connects.
+Chapter 5 also uses an `interface` to describe a configuration object's fields and a generic type such as `Schema` to say which object fields a schema validates. You can copy those declarations as shown; the surrounding text explains what each one connects.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/index.zh.md b/docs/cordis-tutorial/index.zh.md
index e6f6dc0ccc..a68e931d81 100644
--- a/docs/cordis-tutorial/index.zh.md
+++ b/docs/cordis-tutorial/index.zh.md
@@ -13,7 +13,7 @@ Cordis 是 DeepSeek Harness SDK 底层的插件框架:它是一个小型运行
你需要克隆本仓库并安装依赖,具体前置条件见[快速入门](../user/guide/quickstart.md)。本教程不需要 API 密钥;所有示例均可在无密钥环境中运行。
```sh
-git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
+git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
```
@@ -50,9 +50,9 @@ node --import tsx ../../vendor/cordis/bin.js
这些示例使用了普通现代 JavaScript 之外的三项 TypeScript 功能:
- **类型注解** 描述值,但不会改变运行时行为:`ctx: Context` 表示 `ctx` 具备 Cordis 上下文 API,`who: string` 接受文本,而 `string[]` 表示字符串数组。
-- **`import type { Context } from 'cordis'`** 只导入类型信息。它在运行时会消失,因此仅为类型注解使用 `Context` 的插件文件不会增加运行时依赖。
-- **声明合并**(`declare module 'cordis' { ... }`)会为 Cordis 已经声明的接口添加你的条目,例如新 `ctx.greeter` 属性的类型或事件名称。它不会生成任何运行时接线;插件必须另行提供服务或发出事件。第 3 章会完整展示该模式。
+- **`import type { Context } from '@deepseek-ai/cordis'`** 只导入类型信息。它在运行时会消失,因此仅为类型注解使用 `Context` 的插件文件不会增加运行时依赖。
+- **声明合并**(`declare module '@deepseek-ai/cordis' { ... }`)会为 Cordis 已经声明的接口添加你的条目,例如新 `ctx.greeter` 属性的类型或事件名称。它不会生成任何运行时接线;插件必须另行提供服务或发出事件。第 3 章会完整展示该模式。
-第 5 章还会使用 `interface` 描述配置对象的字段,并使用 `Schema` 这类泛型表示 schema 所校验的对象形状。你可以直接照写这些声明;周围的正文会解释每项声明连接了什么。
+第 5 章还会使用 `interface` 描述配置对象的字段,并使用 `Schema` 这类泛型表示 schema 校验哪些对象字段。你可以直接照写这些声明;周围的正文会解释每项声明连接了什么。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/defensive-patterns.i18n.yaml b/docs/defensive-patterns.i18n.yaml
index 68f62583c2..18b28ca58c 100644
--- a/docs/defensive-patterns.i18n.yaml
+++ b/docs/defensive-patterns.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/defensive-patterns.md
-defensive-patterns.md: afb462e120892eafe676d8b7feef273c2d0e42df
-defensive-patterns.zh.md: ab5f689d47f89b6930b749824c69325490bd4586
+defensive-patterns.md: 368c9876f1a4e7042b003f6acfb30af3b2daf402
+defensive-patterns.zh.md: c7d4c1bf37ef17947913ac4011624d04ffd8c1a3
diff --git a/docs/defensive-patterns.md b/docs/defensive-patterns.md
index afb462e120..368c9876f1 100644
--- a/docs/defensive-patterns.md
+++ b/docs/defensive-patterns.md
@@ -10,7 +10,7 @@ A result can be several things at once — a process can time out AND exit 0 bec
## Honor public contracts on BOTH sides
-When an implementation boundary receives several representations of one outcome, normalize them before crossing the public contract. `LlmAdapter.stream()` implementations may throw or emit `finish {kind:'error'|'aborted'}`, but `LlmService.stream()` exposes model-request failures only as terminal finish chunks; middleware and consumer defects remain thrown. This keeps consumers from guessing whether a caught exception came from the provider, a wrapper, chunk logging, or their own assembly. Document the normalized contract where the type is defined; exercise every source form through the real consumer.
+When an implementation receives several representations of one outcome, normalize them before returning through the public API. `LlmAdapter.stream()` implementations may throw or emit `finish {kind:'error'|'aborted'}`, but `LlmService.stream()` exposes model-request failures only as terminal finish chunks; middleware and consumer defects remain thrown. This keeps consumers from guessing whether a caught exception came from the provider, a wrapper, chunk logging, or their own assembly. Document the normalized contract where the type is defined; exercise every source form through the real consumer.
## Async state is not synchronous state
@@ -20,7 +20,7 @@ When an implementation boundary receives several representations of one outcome,
A teardown that issues kills/aborts but returns before the work stops leaves orphans. Make cleanup async and await the children's exit (kill → await `done`), and close listener/notification registries BEFORE killing so late completions stay silent.
-## Contain callback exceptions at the boundary
+## Contain callback exceptions in the dispatcher
A user-supplied listener that throws must not reject the promise it runs inside or starve the listeners after it. Wrap the dispatch loop in try/catch and log; one bad subscriber never breaks core lifecycle.
diff --git a/docs/defensive-patterns.zh.md b/docs/defensive-patterns.zh.md
index ab5f689d47..c7d4c1bf37 100644
--- a/docs/defensive-patterns.zh.md
+++ b/docs/defensive-patterns.zh.md
@@ -10,7 +10,7 @@
## 公共约定两侧都要遵守
-当一个实现边界接收到同一结果的多种表示时,应在跨越公共约定前将其规范化。`LlmAdapter.stream()` 的实现可以抛出异常或发出 `finish {kind:'error'|'aborted'}`,但 `LlmService.stream()` 只会通过终止 finish chunk 暴露模型请求失败;middleware 与消费方缺陷仍会抛出。这使消费方不必猜测捕获的异常究竟来自提供方、包装层、chunk 日志记录还是自身组装逻辑。请在类型定义处记录规范化约定;通过真实消费方覆盖每种来源形式。
+当一个实现收到同一结果的多种表示时,应在通过公共 API 返回前将其规范化。`LlmAdapter.stream()` 的实现可以抛出异常或发出 `finish {kind:'error'|'aborted'}`,但 `LlmService.stream()` 只会通过终止型 finish 分片暴露模型请求失败;middleware 缺陷与消费方缺陷仍会以异常形式抛出。这使消费方不必猜测捕获的异常究竟来自提供方、包装层、chunk 日志记录还是自身组装逻辑。请在类型定义处记录规范化后的约定;通过真实消费方覆盖每种来源形式。
## 异步状态不是同步状态
@@ -20,7 +20,7 @@
如果清理流程只发出终止或中止信号便返回,而不等待工作真正停止,就会留下孤儿进程。清理逻辑应采用异步流程,并等待子进程退出(发出终止信号后等待 `done`);还应在终止进程前关闭监听器和通知注册表,使迟到的完成事件保持静默。
-## 在边界处隔离回调异常
+## 在分发器中隔离回调异常
用户提供的监听器如果抛出异常,不得导致它所在的 promise 被 reject,也不得饿死排在它后面的监听器。请用 try/catch 包裹分发循环并记录日志;一个行为不当的订阅者绝不能破坏核心生命周期。
diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml
index 84cff8fa96..9d8d8fba47 100644
--- a/docs/development.i18n.yaml
+++ b/docs/development.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/development.md
-development.md: 8b6c148d87fdd6288695b0029b9dfcc0139d2144
-development.zh.md: 55ca013cc0dd901500e340a7447057de27665f61
+development.md: 7d6ca74df1311560b0d61444c92e74927639ca31
+development.zh.md: 1461717cf496b1863f2f0c47ad4b42990dc789e9
diff --git a/docs/development.md b/docs/development.md
index 8b6c148d87..7d6ca74df1 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -2,7 +2,7 @@
English | [中文](development.zh.md)
-The setup tutorial takes a new contributor from prerequisites to a checked checkout. The contributor reference that follows covers repository layout, daily workflow, and CI shape. Design rationale and implementation details belong to the linked Agent Notes and scripts.
+The setup tutorial takes a new contributor from prerequisites to a checked checkout. The contributor reference that follows covers repository layout, daily workflow, and CI organization. Design rationale and implementation details belong to the linked Agent Notes and scripts.
## Setup tutorial
@@ -51,7 +51,7 @@ The repository uses isolated Host and Client aggregates. An ordinary package is
| `tsconfig.host.json` | Host aggregate: Host packages, examples, tests, scripts, website, and the exceptional Host project of `api/remotes`. | Yes |
| `tsconfig.client.json` | Client aggregate: `packages/client/*` packages and their tests, `apps/web`, and the exceptional Client project of `api/remotes`. | Yes |
| `tsconfig.base.json` | Shared compilerOptions and the source `paths` map. Also the resolution facade the vitest configs point vite-tsconfig-paths at: it has no `include`, so its `paths` apply to every importer. | No |
-| `tsconfig.base.client.json` | Browser compiler shape (`jsx`, DOM libs, `types: []`) extended by the Client aggregate and every `packages/client/*` package. | No |
+| `tsconfig.base.client.json` | Browser compiler settings (`jsx`, DOM libs, `types: []`) extended by the Client aggregate and every `packages/client/*` package. | No |
Host and Client stay two aggregate programs because both sides declaration-merge the cordis `Context` interface under the same keys with different services; one program seeing both merges reports a collision. The collision exists only inside a `ts.Program` — module resolution never triggers it — which is why the solution may reference both aggregates and one paths facade may span both sides. Three disciplines follow:
@@ -59,7 +59,7 @@ Host and Client stay two aggregate programs because both sides declaration-merge
- A script that builds a repo-wide `ts.Program` seeds `tsconfig.host.json` or `tsconfig.client.json` explicitly — never the root solution, because flattening both aggregates into one program collides the `Context` merges.
- A new package is registered in exactly one aggregate. Having both a Node loader entry and a browser entry is not a reason to split a package; an ordinary Client plugin produces both runtime artifacts during the Client build phase.
-`api/remotes` is the repository's only package with split Host and Client tsconfigs. Its Host entry must participate in the Host TypeRT graph, while its Client entry imports `/remote` declarations that Host tsdown must generate first. The package-root `tsconfig.json` is therefore only a solution, and the two aggregates and direct consumers reference `tsconfig.host.json` or `tsconfig.client.json` respectively. The workspace `constraints` gate walks the reachable Project Reference graph and checks each referencing project's own compiler face: a single-config target remains valid from either face, while a split target must name the matching leaf rather than its solution root or opposite leaf. Do not copy this structure to other packages; see the [`api-remotes` README](../packages/api/remotes/README.md) for the complete boundary.
+`api/remotes` is the repository's only package with split Host and Client tsconfigs. Its Host entry must participate in the Host TypeRT graph, while its Client entry imports `/remote` declarations that Host tsdown must generate first. The package-root `tsconfig.json` is therefore only a solution, and the two aggregates and direct consumers reference `tsconfig.host.json` or `tsconfig.client.json` respectively. The workspace `constraints` gate walks the reachable Project Reference graph and checks each referencing project's own compiler face: a single-config target remains valid from either face, while a split target must name the matching leaf rather than its solution root or opposite leaf. Do not copy this structure to other packages; the [`api-remotes` README](../packages/api/remotes/README.md) explains the Host/Client split and build order.
The root build follows the generated dependency order:
@@ -75,7 +75,7 @@ Both tsdown passes use the same complete workspace match. They neither scan buil
TypeRT runs only during Host tsdown, seeded by `tsconfig.host.json`. It analyzes Host types and generates both Host reflection artifacts and the Host-for-Client Remote projection; Client tsdown does not start TypeRT. Consequently, `pnpm run typecheck` runs the complete Host lib phase before Client tsc, while `pnpm run build` continues through Client tsdown and the Web build. The [API Remotes generated-contract build note](../.agents/notes/implemented/process/2026-08-08-api-remotes-generated-contract-build.md) records this ordering decision.
-Static analysis and tests resolve workspace imports through the base `paths` map to `src` and must pass on a clean tree; gates that consume built `lib/` output declare that dependency explicitly. Generated Host-for-Client Remote declarations are the deliberate exception: the public `typecheck`, `lint`, and `doc-typecheck` commands generate them first, while internal `*:contracts-ready` scripts assume that an invoking public command or scheduler gate already owns an explicit dependency on the TypeRT contract pass or the complete build. See the [solution-root note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md) for the two-aggregate topology, the [ts-build-config note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md) for tsc-first emit ownership, and the [TypeRT Remote note](../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md) for the gate-preparation contract.
+Static analysis and tests resolve workspace imports through the base `paths` map to `src` and must pass on a clean tree; gates that consume built `lib/` output declare that dependency explicitly. Generated Host-for-Client Remote declarations are the deliberate exception: the public `typecheck`, `lint`, and `doc-typecheck` commands generate them first, while internal `*:contracts-ready` scripts assume that an invoking public command or scheduler gate already depends on the TypeRT contract-generation pass or the complete build. See the [solution-root note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md) for the two-aggregate setup, the [ts-build-config note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md) for tsc-first emit ownership, and the [TypeRT Remote note](../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md) for the gate-preparation contract.
Business services declare callable methods on the Host with `@Remote` or `@RemoteScope`; the Host build generates Host-for-Client types and runtime contributions, and the Client's `api-remotes` composition loads those contributions under `ctx.remote` and scoped `agentCtx.remote` namespaces. See [API Gateway](api-gateway.md) for the generated artifacts on both sides, their assembly relationships, the SRC development fallback, and the Web build order.
@@ -100,7 +100,7 @@ DEEPSEEK_BASE_URL=https://... # optional
### Git integrations
-The pairing merge driver derives a conflicted `.i18n.yaml` record from the confirmed ancestor, current, and other owner blobs when both language files use Git's default text strategy and merge cleanly. It fails closed on owner conflicts, non-text merge configuration, or invalid records; after an already-stopped merge, run `pnpm run resolve-translation-pairing-conflicts`, which stages every safe pairing record and exits unsuccessfully if other pairing conflicts still need manual work. See the [bilingual documentation contract](i18n/README.md#the-pairing-contract) for the exact boundary.
+The pairing merge driver derives a conflicted `.i18n.yaml` record from the confirmed ancestor, current, and other owner blobs when both language files use Git's default text strategy and merge cleanly. It fails closed on owner conflicts, non-text merge configuration, or invalid records; after an already-stopped merge, run `pnpm run resolve-translation-pairing-conflicts`, which stages every safe pairing record and exits unsuccessfully if other pairing conflicts still need manual work. See the [bilingual documentation contract](i18n/README.md#the-pairing-contract) for the exact files and states the driver accepts.
The installer probes the exact Node/tsx driver entrypoint before publishing its worktree configuration. If that runtime later becomes unavailable, the Node-independent launcher writes Git's ordinary text result, leaves the sidecar unresolved, and prints the recovery path; restore dependencies and run `pnpm run resolve-translation-pairing-conflicts`, or run `git merge --abort`. If `pre-merge-commit` rejects an otherwise clean merge, Git leaves the complete result staged without a commit; repair the failure and run `git commit`, or abort. The [automatic pairing merges Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md#failure-contract) owns the exact index and `MERGE_HEAD` states.
@@ -129,7 +129,7 @@ The root [contributor instructions](../AGENTS.md#commands) summarize common comm
The one-shot Headless coding agent needs `DEEPSEEK_API_KEY` in the environment or repo-root `.env`:
```sh
-pnpm run demo:headless "summarize this workspace"
+pnpm dsh --profile headless "summarize this workspace"
```
The self-referential cordis demo can inspect and modify its live plugin runtime and needs the same credentials (`web` by default, or `acp`):
@@ -156,10 +156,10 @@ Pick the tag that matches the urgency so anyone scanning the code can tell a rel
### Documenting types verbatim (`ts type-equiv`)
-The [subsystems](subsystems/README.md) pages paste source-equivalent declarations together with their original JSDoc so a reader sees the exact shape and source contract. To keep a paste from drifting when source changes, fence it as ` ```ts type-equiv ` (instead of ` ```ts `) and register it in `scripts/type-equiv.manifest.json` with the source file and symbol it mirrors:
+The [subsystems](subsystems/README.md) pages paste source-equivalent declarations together with their original JSDoc so a reader sees the exact type definition and source contract. To keep a paste from drifting when source changes, fence it as ` ```ts type-equiv ` (instead of ` ```ts `) and register it in `scripts/type-equiv.manifest.json` with the source file and symbol it mirrors:
```json
{ "doc": "docs/subsystems/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" }
```
-`pnpm run verify-type-equiv` (part of `doc-sync`) then extracts that symbol's declaration and attached JSDoc from source via the TypeScript parser and asserts the block matches both. For a class whose implementation bodies do not belong in the catalog, use ` ```ts public-api ` and set `"projection": "public-api"`; the checked projection retains the public fields, constructor, accessors, methods, and original class/member JSDoc while omitting bodies and private or protected members. Comparison ignores whitespace and non-JSDoc comments but requires every original JSDoc comment, including member documentation, so readers see the source contract beside the exact shape. The gate enforces a 1:1 correspondence by document, symbol, and projection between primary blocks and manifest entries; a paired `.zh.md` block reuses its unsuffixed sibling's entry only when the whole tracked fence sequence is byte-identical and ordered identically. `doc-typecheck` applies the same derivative rule to compilable fences, while skipping both source-equivalence fence kinds from compilation and its opt-out ratio. When you change a documented declaration or its JSDoc, the gate fails until you update the paste; when you add or remove a primary block, update the manifest in the same change.
+`pnpm run verify-type-equiv` (part of `doc-sync`) then extracts that symbol's declaration and attached JSDoc from source via the TypeScript parser and asserts the block matches both. For a class whose implementation bodies do not belong in the catalog, use ` ```ts public-api ` and set `"projection": "public-api"`; the checked projection retains the public fields, constructor, accessors, methods, and original class/member JSDoc while omitting bodies and private or protected members. Comparison ignores whitespace and non-JSDoc comments but requires every original JSDoc comment, including member documentation, so readers see the source contract beside the exact type definition. The gate enforces a 1:1 correspondence by document, symbol, and projection between primary blocks and manifest entries; a paired `.zh.md` block reuses its unsuffixed sibling's entry only when the whole tracked fence sequence is byte-identical and ordered identically. `doc-typecheck` applies the same derivative rule to compilable fences, while skipping both source-equivalence fence kinds from compilation and its opt-out ratio. When you change a documented declaration or its JSDoc, the gate fails until you update the paste; when you add or remove a primary block, update the manifest in the same change.
diff --git a/docs/development.zh.md b/docs/development.zh.md
index 55ca013cc0..1461717cf4 100644
--- a/docs/development.zh.md
+++ b/docs/development.zh.md
@@ -2,7 +2,7 @@
[English](development.md) | 中文
-搭建教程引导新贡献者从准备前置条件开始,直到检出通过检查。后面的贡献者参考介绍仓库布局、日常工作流和 CI 形态。设计依据与实现细节属于链接的 Agent Note 和脚本。
+搭建教程引导新贡献者从准备前置条件开始,直到检出通过检查。后面的贡献者参考介绍仓库布局、日常工作流和 CI 组织方式。设计依据与实现细节属于链接的 Agent Note 和脚本。
## 搭建教程
@@ -51,7 +51,7 @@ pnpm run typecheck
| `tsconfig.host.json` | Host aggregate:Host package、示例、测试、脚本和 website,以及 `api/remotes` 的 Host 特例 project。 | 是 |
| `tsconfig.client.json` | Client aggregate:`packages/client/*` package 及其测试、`apps/web`,以及 `api/remotes` 的 Client 特例 project。 | 是 |
| `tsconfig.base.json` | 共享 compilerOptions 与源码 `paths` 映射。同时是各 vitest 配置让 vite-tsconfig-paths 指向的解析门面:它没有 `include`,因此其 `paths` 适用于任何 importer。 | 否 |
-| `tsconfig.base.client.json` | 浏览器编译形状(`jsx`、DOM lib、`types: []`),由 Client aggregate 和每个 `packages/client/*` package extends。 | 否 |
+| `tsconfig.base.client.json` | 浏览器编译设置(`jsx`、DOM lib、`types: []`),由 Client aggregate 和每个 `packages/client/*` package extends。 | 否 |
Host 与 Client 保持两个 aggregate program,是因为两侧在相同键下以不同服务对 cordis `Context` 接口做声明合并;单一 program 同时看到两份合并会报冲突。这种冲突只存在于 `ts.Program` 内部——模块解析永远不会触发它——所以 solution 可以同时引用两个 aggregate,一个 paths 门面也可以横跨两侧。由此推出三条纪律:
@@ -59,7 +59,7 @@ Host 与 Client 保持两个 aggregate program,是因为两侧在相同键下
- 构造全仓 `ts.Program` 的脚本显式种子 `tsconfig.host.json` 或 `tsconfig.client.json`——永不种子根 solution,因为把两个 aggregate 展平进一个 program 会撞上 `Context` 合并冲突。
- 新 package 只登记进一个 aggregate。包同时具有 Node loader 入口和 browser 入口并不构成拆分理由;普通 Client plugin 的两份运行时产物都在 Client 构建阶段生成。
-`api/remotes` 是唯一拆分 Host/Client tsconfig 的仓库特例。它的 Host 入口必须进入 Host TypeRT 图,而 Client 入口导入 Host tsdown 才会生成的 `/remote` 声明,因此本包根 `tsconfig.json` 只作为 solution,两个 aggregate 和直接消费方分别引用 `tsconfig.host.json` 或 `tsconfig.client.json`。workspace `constraints` 门禁遍历可达的 Project Reference 图,并按各引用 project 自身的 compiler face 检查:只有单一配置的目标可由任一 face 引用,拆分配置的目标则必须引用匹配的 leaf,不得引用 solution 根或另一侧 leaf。不要把该结构推广到其他包;完整边界见 [`api-remotes` README](../packages/api/remotes/README.md)。
+`api/remotes` 是唯一拆分 Host/Client tsconfig 的仓库特例。它的 Host 入口必须进入 Host TypeRT 图,而 Client 入口导入 Host tsdown 才会生成的 `/remote` 声明,因此本包根 `tsconfig.json` 只作为 solution,两个 aggregate 和直接消费方分别引用 `tsconfig.host.json` 或 `tsconfig.client.json`。workspace `constraints` 门禁遍历可达的 Project Reference 图,并按各引用 project 自身的 compiler face 检查:只有单一配置的目标可由任一 face 引用,拆分配置的目标则必须引用匹配的 leaf,不得引用 solution 根或另一侧 leaf。不要把该结构推广到其他包;[`api-remotes` README](../packages/api/remotes/README.md) 说明 Host/Client 拆分与构建顺序。
根构建按生成依赖排序:
@@ -75,7 +75,7 @@ pnpm run build:web
TypeRT 只在 Host tsdown 中以 `tsconfig.host.json` 为种子运行。它分析 Host 类型并生成 Host 反射产物及 Host-for-Client Remote 投影;Client tsdown 不启动 TypeRT。`pnpm run typecheck` 因此先执行完整 Host lib 阶段,再运行 Client tsc;`pnpm run build` 继续执行 Client tsdown 和 Web 构建。该顺序的决策记录见 [API Remotes 生成约定构建 Note](../.agents/notes/implemented/process/2026-08-08-api-remotes-generated-contract-build.md)。
-静态分析和测试通过 base 的 `paths` 映射把工作区 import 解析到 `src`,且必须在干净树上通过;消费构建产物 `lib/` 的门禁显式声明该依赖。生成的 Host-for-Client Remote 声明是有意设置的例外:公共 `typecheck`、`lint` 和 `doc-typecheck` 命令会先生成这些声明,而内部 `*:contracts-ready` 脚本以调用它的公共命令或调度器门禁已经显式依赖 TypeRT 约定 pass 或完整构建为前提。双 aggregate 拓扑见 [solution-root Note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md),tsc-first 发射职责见 [ts-build-config Note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md),门禁准备约定见 [TypeRT Remote Agent Note](../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md)。
+静态分析和测试通过 base 的 `paths` 映射把工作区 import 解析到 `src`,且必须在干净树上通过;消费构建产物 `lib/` 的门禁显式声明该依赖。生成的 Host-for-Client Remote 声明是有意设置的例外:公共 `typecheck`、`lint` 和 `doc-typecheck` 命令会先生成这些声明,而内部 `*:contracts-ready` 脚本假定调用它的公共命令或调度器门禁已经依赖 TypeRT 约定生成阶段或完整构建。两个 aggregate 的设置见 [solution-root Note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md),tsc-first 发射职责见 [ts-build-config Note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md),门禁准备约定见 [TypeRT Remote Agent Note](../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md)。
业务 Service 在 Host 使用 `@Remote` 或 `@RemoteScope` 声明可调用方法;Host 构建生成 Host-for-Client 类型与运行时贡献,Client 的 `api-remotes` 组合加载这些贡献并挂到 `ctx.remote` 与作用域 `agentCtx.remote` namespace。两侧的生成产物、装配关系、SRC 开发回退和 Web 构建顺序见 [API Gateway](api-gateway.md)。
@@ -100,7 +100,7 @@ DEEPSEEK_BASE_URL=https://... # optional
### Git 集成
-当两种语言的文件都使用 Git 默认文本策略且能干净合并时,配对合并驱动会根据已确认的祖先、当前和另一侧的配对文档 blob,推导出发生冲突的 `.i18n.yaml` 记录。配对文档发生冲突、存在非文本合并配置或记录无效时,它会拒绝处理并保留冲突;如果合并已经因冲突而停止,请运行 `pnpm run resolve-translation-pairing-conflicts`,该命令会暂存每份可安全生成的配对记录;如果其他配对冲突仍需手工处理,则以非零状态退出。确切边界见[双语文档约定](i18n/README.md#the-pairing-contract)。
+当两种语言的文件都使用 Git 默认文本策略且能干净合并时,配对合并驱动会根据已确认的祖先、当前和另一侧的配对文档 blob,推导出发生冲突的 `.i18n.yaml` 记录。配对文档发生冲突、存在非文本合并配置或记录无效时,它会拒绝处理并保留冲突;如果合并已经因冲突而停止,请运行 `pnpm run resolve-translation-pairing-conflicts`,该命令会暂存每份可安全生成的配对记录;如果其他配对冲突仍需手工处理,则以非零状态退出。[双语文档约定](i18n/README.md#the-pairing-contract)列出该驱动接受的确切文件和状态。
安装脚本在发布 worktree 配置前,会探测确切的 Node/tsx 驱动入口点。如果该运行时之后变得不可用,不依赖 Node 的启动器会写入 Git 的普通文本合并结果、让伴随文件保持未解决状态,并打印恢复路径;请恢复依赖后运行 `pnpm run resolve-translation-pairing-conflicts`,或运行 `git merge --abort`。如果 `pre-merge-commit` 拒绝原本能干净完成的合并,Git 会把完整结果留在暂存区但不创建提交;请修复失败后运行 `git commit`,或中止合并。确切的索引与 `MERGE_HEAD` 状态由[自动配对合并 Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md#failure-contract)负责记录。
@@ -129,7 +129,7 @@ keyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若
单次运行的 Headless coding agent 需要环境变量或仓库根目录 `.env` 中的 `DEEPSEEK_API_KEY`:
```sh
-pnpm run demo:headless "summarize this workspace"
+pnpm dsh --profile headless "summarize this workspace"
```
自指的 cordis 演示可以检查并修改其实时插件运行时,并需要相同的凭证(默认 `web`,也可用 `acp`):
@@ -156,10 +156,10 @@ pnpm run demo:acp
### 逐字记录类型(`ts type-equiv`)
-[子系统](subsystems/README.md)页面会把与源码等价的声明及其原始 JSDoc 一并粘贴,让读者看到确切形状和源码约定。为防止粘贴内容在源码变化时漂移,请将其围栏为 ` ```ts type-equiv `(而不是 ` ```ts `),并在 `scripts/type-equiv.manifest.json` 中登记它镜像的源文件和符号:
+[子系统](subsystems/README.md)页面会把与源码等价的声明及其原始 JSDoc 一并粘贴,让读者看到确切类型定义和源码约定。为防止粘贴内容在源码变化时漂移,请将其围栏为 ` ```ts type-equiv `(而不是 ` ```ts `),并在 `scripts/type-equiv.manifest.json` 中登记它镜像的源文件和符号:
```json
{ "doc": "docs/subsystems/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" }
```
-`pnpm run verify-type-equiv`(`doc-sync` 的一环)随后通过 TypeScript 解析器从源码提取该符号的声明及其附带的 JSDoc,并断言代码块同时匹配两者。对于不应把实现体写进目录的类,请使用 ` ```ts public-api ` 并设置 `"projection": "public-api"`;门禁检查的投影会保留公共字段、构造函数、访问器、方法以及类和成员的原始 JSDoc,同时省略实现体和私有或受保护成员。比对会忽略空白和非 JSDoc 注释,但要求保留每条原始 JSDoc(包括成员文档),让读者同时看到源码约定和确切形状。该门禁按文档、符号和投影,在主块与 manifest 条目之间强制 1:1 对应;只有当配对 `.zh.md` 块的完整受跟踪围栏序列与其无后缀兄弟文件按字节一致且顺序相同时,才会复用后者的条目。`doc-typecheck` 对可编译围栏应用同一派生规则,同时跳过两种源码等价围栏的编译,并将其排除在 opt-out 比例之外。当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败直到你更新粘贴内容;当你增删一个主块时,请在同一个变更里更新 manifest。
+`pnpm run verify-type-equiv`(`doc-sync` 的一环)随后通过 TypeScript 解析器从源码提取该符号的声明及其附带的 JSDoc,并断言代码块同时匹配两者。对于不应把实现体写进目录的类,请使用 ` ```ts public-api ` 并设置 `"projection": "public-api"`;门禁检查的投影会保留公共字段、构造函数、访问器、方法以及类和成员的原始 JSDoc,同时省略实现体和私有或受保护成员。比对会忽略空白和非 JSDoc 注释,但要求保留每条原始 JSDoc(包括成员文档),让读者同时看到源码约定和确切类型定义。该门禁按文档、符号和投影,在主块与 manifest 条目之间强制 1:1 对应;只有当配对 `.zh.md` 块的完整受跟踪围栏序列与其无后缀兄弟文件按字节一致且顺序相同时,才会复用后者的条目。`doc-typecheck` 对可编译围栏应用同一派生规则,同时跳过两种源码等价围栏的编译,并将其排除在 opt-out 比例之外。当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败直到你更新粘贴内容;当你增删一个主块时,请在同一个变更里更新 manifest。
diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml
index 69e4e07b49..d7cae1d98a 100644
--- a/docs/event-producer-consumer.i18n.yaml
+++ b/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: 12d2e3d96004803c4f40644860f2c00c6d14bc3a
-event-producer-consumer.zh.md: 0650b6600e151abc9a346c87eeb9caf8865f645c
+event-producer-consumer.md: 5007f7e86a477ba8d5120e204b033d2224838d7b
+event-producer-consumer.zh.md: 5efd0553e63bd7752a85f4315c714e7ebad177a7
diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md
index 12d2e3d960..5007f7e86a 100644
--- a/docs/event-producer-consumer.md
+++ b/docs/event-producer-consumer.md
@@ -8,7 +8,7 @@ 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:182`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`tool-schedule`](../packages/schedule/tool-schedule) |
+| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-session`](../packages/goal/goal-session), [`tool-schedule`](../packages/schedule/tool-schedule) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) |
@@ -28,49 +28,50 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-policy`](../packages/fs/fs-policy), [`skill-local`](../packages/skill/skill-local) |
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
-| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:73`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
-| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:62`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
-| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-schedule`](../packages/schedule/tool-schedule), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
-| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
-| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
-| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
+| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
+| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
+| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-schedule`](../packages/schedule/tool-schedule), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
+| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
+| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
-| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:284`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
-| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:162`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
-| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:136`](../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:142`](../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:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
-| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:29`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) |
-| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:35`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
+| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
+| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
+| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:140`](../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:146`](../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:157`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`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), [`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`) | - |
| `telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
-| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:192`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
-| `tools/code-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:174`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
+| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:193`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
+| `tools/code-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:175`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:149`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`timeout-policy`](../packages/guard/timeout-policy) |
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:161`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:138`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-tasks`](../packages/tasks/tool-tasks) |
-| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:182`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`workspace-context`](../packages/context/workspace-context) |
-| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:81`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
-| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:70`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
-| `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:91`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
-| `workflow/log` | `emit` | [`packages/workflow/workflow/src/index.ts:60`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
-| `workflow/phase` | `emit` | [`packages/workflow/workflow/src/index.ts:53`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
-| `workflow/start` | `emit` | [`packages/workflow/workflow/src/index.ts:45`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
+| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:183`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`workspace-context`](../packages/context/workspace-context) |
+| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
+| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
+| `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:89`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
+| `workflow/log` | `emit` | [`packages/workflow/workflow/src/index.ts:58`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
+| `workflow/phase` | `emit` | [`packages/workflow/workflow/src/index.ts:51`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
+| `workflow/start` | `emit` | [`packages/workflow/workflow/src/index.ts:43`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
## Non-harness or undeclared event strings seen in package source
| Event string | Dispatchers | Listeners |
| --- | --- | --- |
| `commands/changed` | `runtime` (`emit`) | `ui-command` |
-| `connection/reset` | `runtime` (`emit`) | `ui-command`, `ui-models`, `ui-permission`, `ui-settings-general` |
+| `connection/reset` | `runtime` (`emit`) | `runtime`, `ui-command`, `ui-models`, `ui-permission`, `ui-settings-general` |
| `credentials/changed` | `runtime` (`emit`) | `ui-models` |
-| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), [`pty-local`](../packages/pty/pty-local), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-schedule`](../packages/schedule/tool-schedule), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
+| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), [`pty-local`](../packages/pty/pty-local), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-schedule`](../packages/schedule/tool-schedule), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
| `internal/plugin` | - | `hmr`, `loader`, [`lsp-local`](../packages/lsp/lsp-local), `modules`, `webserver` |
-| `internal/service` | - | `gateway` |
+| `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` |
| `internal/status` | - | [`agent`](../packages/core/agent) |
| `locale/change` | `locale` (`emit`) | `locale` |
| `models/changed` | `runtime` (`emit`) | `ui-models` |
-| `settings/changed` | `runtime` (`emit`) | `ui-models`, `ui-permission`, `ui-settings-general` |
+| `session/preset-changed` | `runtime` (`emit`) | `ui-command` |
+| `settings/changed` | `runtime` (`emit`) | `runtime`, `ui-models`, `ui-permission`, `ui-settings-general` |
| `slash/input-begin-command` | - | `ui-conversation` |
| `slash/input-consume-token` | - | `ui-conversation` |
| `slash/input-insert-reference` | - | `ui-conversation` |
diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md
index 0650b6600e..5efd0553e6 100644
--- a/docs/event-producer-consumer.zh.md
+++ b/docs/event-producer-consumer.zh.md
@@ -10,7 +10,7 @@
| 事件 | 模式 | 声明位置 | 派发方 | 监听方 |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:182`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`tool-schedule`](../packages/schedule/tool-schedule) |
+| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-session`](../packages/goal/goal-session), [`tool-schedule`](../packages/schedule/tool-schedule) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) |
@@ -30,49 +30,50 @@
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-policy`](../packages/fs/fs-policy), [`skill-local`](../packages/skill/skill-local) |
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
-| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:73`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
-| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:62`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
-| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-schedule`](../packages/schedule/tool-schedule), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
-| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
-| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
-| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
+| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
+| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
+| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-schedule`](../packages/schedule/tool-schedule), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
+| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
+| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
-| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:284`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
-| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:162`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
-| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:136`](../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:142`](../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:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
-| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:29`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) |
-| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:35`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
+| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
+| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
+| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:140`](../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:146`](../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:157`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`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), [`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`) | - |
| `telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
-| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:192`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
-| `tools/code-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:174`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
+| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:193`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
+| `tools/code-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:175`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:149`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`timeout-policy`](../packages/guard/timeout-policy) |
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:161`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:138`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-tasks`](../packages/tasks/tool-tasks) |
-| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:182`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`workspace-context`](../packages/context/workspace-context) |
-| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:81`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
-| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:70`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
-| `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:91`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
-| `workflow/log` | `emit` | [`packages/workflow/workflow/src/index.ts:60`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
-| `workflow/phase` | `emit` | [`packages/workflow/workflow/src/index.ts:53`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
-| `workflow/start` | `emit` | [`packages/workflow/workflow/src/index.ts:45`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
+| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:183`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`workspace-context`](../packages/context/workspace-context) |
+| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
+| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
+| `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:89`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
+| `workflow/log` | `emit` | [`packages/workflow/workflow/src/index.ts:58`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
+| `workflow/phase` | `emit` | [`packages/workflow/workflow/src/index.ts:51`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
+| `workflow/start` | `emit` | [`packages/workflow/workflow/src/index.ts:43`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
## Non-harness or undeclared event strings seen in package source
| Event string | Dispatchers | Listeners |
| --- | --- | --- |
| `commands/changed` | `runtime` (`emit`) | `ui-command` |
-| `connection/reset` | `runtime` (`emit`) | `ui-command`, `ui-models`, `ui-permission`, `ui-settings-general` |
+| `connection/reset` | `runtime` (`emit`) | `runtime`, `ui-command`, `ui-models`, `ui-permission`, `ui-settings-general` |
| `credentials/changed` | `runtime` (`emit`) | `ui-models` |
-| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), [`pty-local`](../packages/pty/pty-local), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-schedule`](../packages/schedule/tool-schedule), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
+| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), [`pty-local`](../packages/pty/pty-local), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-schedule`](../packages/schedule/tool-schedule), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
| `internal/plugin` | - | `hmr`, `loader`, [`lsp-local`](../packages/lsp/lsp-local), `modules`, `webserver` |
-| `internal/service` | - | `gateway` |
+| `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` |
| `internal/status` | - | [`agent`](../packages/core/agent) |
| `locale/change` | `locale` (`emit`) | `locale` |
| `models/changed` | `runtime` (`emit`) | `ui-models` |
-| `settings/changed` | `runtime` (`emit`) | `ui-models`, `ui-permission`, `ui-settings-general` |
+| `session/preset-changed` | `runtime` (`emit`) | `ui-command` |
+| `settings/changed` | `runtime` (`emit`) | `runtime`, `ui-models`, `ui-permission`, `ui-settings-general` |
| `slash/input-begin-command` | - | `ui-conversation` |
| `slash/input-consume-token` | - | `ui-conversation` |
| `slash/input-insert-reference` | - | `ui-conversation` |
diff --git a/docs/glossary.i18n.yaml b/docs/glossary.i18n.yaml
index 1dfb89b07f..5c60b984da 100644
--- a/docs/glossary.i18n.yaml
+++ b/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: d556e760dd2f07ab640aafca06b2757b47935406
+glossary.md: 77c6c729417968c683d0128699804c6ca3a7fee7
glossary.zh.md: d6a8e423ff2a6324ce0cdd65dd3edde573fac5b1
diff --git a/docs/glossary.md b/docs/glossary.md
index d556e760dd..77c6c72941 100644
--- a/docs/glossary.md
+++ b/docs/glossary.md
@@ -16,7 +16,7 @@ Domain vocabulary for the DeepSeek Harness SDK uses one canonical term per conce
- **scope carrier** — the `thisArg` a scope-filtered dispatch carries (built by `scopeTarget`); its filter admits untagged listeners plus the subject's own. A *subject-less* carrier (no key) admits untagged listeners only.
- **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 surface 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.
+- **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.
- **lineage** — parent/child facts carried as data (`parentSession`, durable `delegationDepth`, runtime `subagentDepth`); never affects visibility.
diff --git a/docs/graph-atlas.i18n.yaml b/docs/graph-atlas.i18n.yaml
index 0cd73d4f39..c697f8e1c1 100644
--- a/docs/graph-atlas.i18n.yaml
+++ b/docs/graph-atlas.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/graph-atlas.md
-graph-atlas.md: 5b831520fb4e1c5d49d83738ba979ce04ce4f69f
-graph-atlas.zh.md: 1f9a30124284549248bbb21f926772d5cdc2598a
+graph-atlas.md: 1da995e8ff6b47e0a83f0308342c5f45ccbd3835
+graph-atlas.zh.md: 780e5295f74f10ee4fe762e280e5c1c820c481c4
diff --git a/docs/graph-atlas.md b/docs/graph-atlas.md
index 5b831520fb..1da995e8ff 100644
--- a/docs/graph-atlas.md
+++ b/docs/graph-atlas.md
@@ -3,7 +3,7 @@
# Documentation Graph Index
-These diagrams are the relationship layer above the generated catalogs. Use them to navigate package topology, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type shapes still live in the [subsystem pages](subsystems/core.md) (types + the generated `cordis-surface` regions) and [tool-catalog.md](tool-catalog.md).
+These diagrams show relationships that the generated catalogs do not. Use them to find package relationships, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type definitions still live in the [subsystem pages](subsystems/core.md) (types + the generated Cordis API regions) and [tool-catalog.md](tool-catalog.md).
The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.md).
diff --git a/docs/graph-atlas.zh.md b/docs/graph-atlas.zh.md
index 1f9a301242..780e5295f7 100644
--- a/docs/graph-atlas.zh.md
+++ b/docs/graph-atlas.zh.md
@@ -5,7 +5,7 @@
[English](graph-atlas.md) | 中文
-这些图构成生成目录之上的关系层。你可以借助它们了解包拓扑、能力 seam、事件流、面向模型的工具、应用组合以及运行时生命周期路径。精确签名和类型结构仍以[子系统页面](subsystems/core.md)(类型和生成的 `cordis-surface` 区域)及[工具目录](tool-catalog.md)为准。
+这些图展示生成目录未包含的关系。可以用它们查找包之间的关系、能力 seam、事件流、面向模型的工具、应用组合和运行时生命周期路径。精确签名和类型定义仍以[子系统页面](subsystems/core.md)(类型和生成的 `cordis-surface` 区域)及[工具目录](tool-catalog.md)为准。
本索引背后的流程决策记录在[文档图 Agent Note](../.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.md)中。
diff --git a/docs/i18n/README.i18n.yaml b/docs/i18n/README.i18n.yaml
index d84f04b02a..334be9e2cf 100644
--- a/docs/i18n/README.i18n.yaml
+++ b/docs/i18n/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/i18n/README.md
-README.md: 042fe71e796340c5c653e1f671c53a95a8496a38
-README.zh.md: d2d3b84e98cf4b761ae0174459b91236a71fb187
+README.md: 23400801426f77dae5136406cd747dbe4b06a4c5
+README.zh.md: fe3cc7b5a5403fc9cf0c9ce536178d4fa7581e3c
diff --git a/docs/i18n/README.md b/docs/i18n/README.md
index 042fe71e79..2340080142 100644
--- a/docs/i18n/README.md
+++ b/docs/i18n/README.md
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
-This repo's documentation is read by people and agents both inside and outside the company, so every document in scope is maintained in English and Simplified Chinese. This page defines the pairing contract, enforcement gate, scope, and exclusions; [translation-rules.md](translation-rules.md) defines how to translate; [terminology.md](terminology.md) is the terminology source of truth. The committed agent workflow lives in [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md).
+This repo's documentation is read by people and agents both inside and outside the company, so every document in scope is maintained in English and Simplified Chinese. This page defines the pairing contract, checks, scope, and exclusions; [translation-rules.md](translation-rules.md) defines how to translate; [terminology.md](terminology.md) is the terminology source of truth. Routine agent work follows the lightweight path in [docs/AGENTS.md](../AGENTS.md); the extended [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) workflow is available only through explicit user invocation.
## The pairing contract
@@ -15,9 +15,9 @@ This repo's documentation is read by people and agents both inside and outside t
foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b
```
- Blob hashes, not commit hashes, so the record is computable for files edited in the same PR (`git hash-object foo.md`) and consistency is a pure content comparison. `--write` stores those snapshots in the local Git object database before recording them, including uncommitted working-tree contents, and pins every distinct stored blob under a content-addressed `refs/dsh/translation-pairing/snapshots/` ref so garbage collection cannot invalidate a recorded recovery pointer. The recorded hashes therefore recover the exact last-confirmed text of either side, so an out-of-sync pair is updated by patching the counterpart minimally against the edited side's diff — never by re-translating whole files. `pnpm run gen-translation-brief ` assembles that update's working set mechanically at the narrowest safely aligned granularity — changed Markdown units, then heading sections, then whole document — with the edited side's diff since last confirmation, each changed span's three-way text, the terminology rows the change touches, and the binding update rules; a change confined to the pair's byte-identical code fences is computed outright, and `--apply` splices it into the counterpart after structural validation ([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.md)). After bringing the pair back in line, `pnpm run verify-translation-pairing --write ` re-records both hashes; that yaml diff is the reviewable act of confirming consistency, which is why `--write` requires naming the pairs you confirmed (`--write --all` is the explicit corpus-wide form).
+ Blob hashes, not commit hashes, so the record is computable for files edited in the same PR (`git hash-object foo.md`) and consistency is a pure content comparison. `--write` stores those snapshots in the local Git object database before recording them, including uncommitted working-tree contents, and pins every distinct stored blob under a content-addressed `refs/dsh/translation-pairing/snapshots/` ref so garbage collection cannot invalidate a recorded recovery pointer. The recorded hashes therefore recover the exact last-confirmed text of either side, so an out-of-sync pair is updated by patching the counterpart minimally against the edited side's diff — never by re-translating whole files. Routine work makes that patch directly; when the user explicitly invokes the extended workflow, `pnpm run gen-translation-brief ` can instead assemble the update at the narrowest safely aligned granularity and `--apply` can splice a code-fence-only change after structural validation ([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.md)). After bringing the pair back in line, `pnpm run verify-translation-pairing --write ` re-records both hashes; that yaml diff is the reviewable act of confirming consistency, which is why `--write` requires naming the pairs you confirmed (`--write --all` is the explicit corpus-wide form).
- When two branches contain valid confirmations of the same pair, the installed `dsh-translation-pairing` Git merge driver composes a new record only if Git's default text merge succeeds for both recorded owner-blob triplets and the merged pair retains its required switchers and structural signature. The Chinese file must retain its English backlink; an authored English source must retain its Chinese link, while a listed generated English source is exempt. Any uncertain shape remains an ordinary conflict; `pnpm run resolve-translation-pairing-conflicts` applies the same fail-closed operation to a merge that has already stopped, stages every safe pairing record, and exits unsuccessfully when other pairing conflicts remain. The [automatic pairing merges Agent Note](../../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) owns the mechanism and alternatives.
+ When two branches contain valid confirmations of the same pair, the installed `dsh-translation-pairing` Git merge driver composes a new record only if Git's default text merge succeeds for both recorded owner-blob triplets and the merged pair retains its required switchers and structural signature. The Chinese file must retain its English backlink; an authored English source must retain its Chinese link, while a listed generated English source is exempt. Any structure the driver cannot verify remains an ordinary conflict; `pnpm run resolve-translation-pairing-conflicts` applies the same fail-closed operation to a merge that has already stopped, stages every safe pairing record, and exits unsuccessfully when other pairing conflicts remain. The [automatic pairing merges Agent Note](../../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) owns the mechanism and alternatives.
- **Language switcher.** The Chinese file always links back immediately after its H1 heading with `[English](foo.md) | 中文`. An authored English file reciprocates there with `English | [中文](foo.zh.md)`; a listed generated English source omits that line so it remains byte-identical to generator output.
- **Structure mirrors the counterpart.** Heading depths and order, list kinds, ordered-list starts, list item counts, table row and column counts, link targets, and verbatim code blocks match one to one across the pair — see [translation-rules.md](translation-rules.md) for the full preservation rules. Existing Markdown gates apply to `.zh.md` files unchanged (`verify-md-wrap`, `verify-md-links`).
@@ -35,13 +35,13 @@ Source-oriented code gates consume an exact `.zh.md` fence sequence as a derivat
`pnpm run verify-translation-pairing ` checks just the named pairs — any of a pair's three files (or its bare stem) names it — so an update loop verifies its own pair in seconds instead of re-scanning the corpus. The no-argument corpus-wide form is what `doc-sync` and CI run; a scoped green never substitutes for it at PR level.
-The practical rule this gate creates: **when a PR edits either side of a paired document, the same PR updates the counterpart and re-records the pair** (run the [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) skill, then `--write `), exactly like the repo's existing doc-sync rule for code and READMEs. A PR that leaves a pair out of sync goes red in CI.
+The practical rule this gate creates: **when a PR edits either side of a paired document, the same PR updates the counterpart directly in one terminology-guided pass and re-records the pair with `--write `**, exactly like the repo's existing doc-sync rule for code and READMEs. A PR that leaves a pair out of sync goes red in CI.
-The gate's limit, stated plainly: **a green gate means the pair was confirmed consistent at these exact contents, not that the confirmation was sound.** It checks hashes and shape; it cannot judge whether the two sides actually say the same thing, or whether the wording is accurate, well-termed, and natural — that is the reviewer's half of the contract, per [translation-rules.md](translation-rules.md). A re-recorded pair with a sloppy counterpart passes the gate; it must not pass review.
+The gate's limit, stated plainly: **a green gate means the pair was confirmed consistent at these exact contents, not that the confirmation was sound.** It checks hashes and Markdown structure; it cannot judge whether the two sides actually say the same thing, or whether the wording is accurate, well-termed, and natural — that is the reviewer's half of the contract, per [translation-rules.md](translation-rules.md). A re-recorded pair with a sloppy counterpart passes the gate; it must not pass review.
## Scope and exclusions
-**Scope**: every non-vendor README, plus every active document under `.agents/notes/**`, `docs/**`, and `python/**`. README matching is case-insensitive on the basename and covers future directories without another manifest edit. Dependency and ignored build-output trees and the frozen `.agents/notes/archived/` tree are discovery exclusions, not evolving translation source.
+**Scope**: the root CONTRIBUTING document, every non-vendor README, and every active document under `.agents/notes/**`, `docs/**`, and `python/**`. README matching is case-insensitive on the basename and covers future directories without another manifest edit. Dependency and ignored build-output trees and the frozen `.agents/notes/archived/` tree are discovery exclusions, not evolving translation source.
Generated English references and graphs participate in pairing when a reviewed Chinese counterpart is available. Their generators remain the English source of truth, and freshness and pairing gates enforce their respective invariants independently; regeneration that changes English leaves the pair out of sync until the reviewed Chinese counterpart is updated and re-recorded. Generated English sources omit the language switcher that ordinary authored sources carry, because adding it would make the generator stale; their Chinese counterparts still link back to the English source. A generated page's Chinese counterpart may rewrite only self-referential generation and maintenance statements that would otherwise be false for the reviewed translation; all technical content remains subject to the ordinary faithfulness rules.
@@ -57,4 +57,4 @@ Generated English references and graphs participate in pairing when a reviewed C
## Division of labor
-Counterparts here are produced by an agent running [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) and reviewed by a human — inference is cheap here, review attention is the scarce resource. The gate checks pair completeness, recorded hashes, the Chinese backlink and authored-source switcher (with the documented generated-source exception), and its documented structural signature. Review still owns translation quality, terminology, and structural requirements that the signature does not encode. The prompt contract is executable: [scripts/translation-prompt.ts](../../scripts/translation-prompt.ts) renders the committed template (terminology injected; the template carries its own calibrated rules) into either direction and parses the three-section response, while `verify-translation-prompt` exercises both render directions and the checked-in example in `doc-sync`.
+Routine counterparts are updated directly by the working agent in one shot and one pass after it loads [terminology.md](terminology.md); it does not invoke a translation skill, generate a briefing, run a separate translation-review pass, or delegate to a subagent. The extended [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) workflow retains those heavier mechanisms for explicit user invocation. The gate checks pair completeness, recorded hashes, the Chinese backlink and authored-source switcher (with the documented generated-source exception), and its documented structural signature. Review still owns translation quality, terminology, and structural requirements that the signature does not encode. The prompt contract is executable: [scripts/translation-prompt.ts](../../scripts/translation-prompt.ts) renders the committed template (terminology injected; the template carries its own calibrated rules) into either direction and parses the three-section response, while `verify-translation-prompt` exercises both render directions and the checked-in example in `doc-sync`.
diff --git a/docs/i18n/README.zh.md b/docs/i18n/README.zh.md
index d2d3b84e98..fe3cc7b5a5 100644
--- a/docs/i18n/README.zh.md
+++ b/docs/i18n/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-本仓库的文档会被公司内外的人和 agent(智能体)阅读,因此范围内的每篇文档都以英文和简体中文维护。本页定义配对约定、强制门禁、范围与排除规则;[translation-rules.md](translation-rules.md) 定义如何翻译;[terminology.md](terminology.md) 是术语真源。仓库内置的 agent 工作流见 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md)。
+本仓库的文档会被公司内外的人和 agent(智能体)阅读,因此范围内的每篇文档都以英文和简体中文维护。本页定义配对约定、检查、范围与排除规则;[translation-rules.md](translation-rules.md) 定义如何翻译;[terminology.md](terminology.md) 是术语真源。agent 的日常工作遵循 [docs/AGENTS.md](../AGENTS.md) 中的轻量路径;扩展版 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) 工作流仅在用户显式调用时可用。
## 配对约定
@@ -15,9 +15,9 @@
foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b
```
- 用 blob hash 而不是 commit hash,这样同一个 PR 里改动的文件也能算出记录(`git hash-object foo.md`),一致性是纯内容比较。`--write` 会先把这些快照存入本地 Git 对象库再写下记录,未提交的工作树内容也不例外;它还会在内容寻址的 `refs/dsh/translation-pairing/snapshots/` ref 下固定每个不同的已存 blob,使垃圾回收无法让已记录的恢复指针失效。因此记录的 hash 能还原任一侧上次确认时的确切文本,所以失去同步的配对是「按被改一侧的 diff 最小化地修补另一侧」,从不整篇重译。`pnpm run gen-translation-brief ` 会以能安全对齐的最窄粒度——先是有改动的 Markdown 单元,再是标题小节,最后是整篇文档——机械地汇集这次更新的工作集:被改一侧自上次确认以来的 diff、每个改动块的三方文本、改动触及的术语表行,以及有约束力的更新规则;仅落在配对中逐字节一致的围栏代码块内的改动可以直接算出,`--apply` 则经结构签名校验后把它拼接进对侧文件([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.md))。两侧对齐后,`pnpm run verify-translation-pairing --write ` 重新记录两个 hash;那份 yaml diff 就是「确认一致」这个动作本身,可以被评审,也正因如此,`--write` 要求点名你确认过的配对(`--write --all` 是显式的全语料形式)。
+ 用 blob hash 而不是 commit hash,这样同一个 PR 里改动的文件也能算出记录(`git hash-object foo.md`),一致性是纯内容比较。`--write` 会先把这些快照存入本地 Git 对象库再写下记录,未提交的工作树内容也不例外;它还会在内容寻址的 `refs/dsh/translation-pairing/snapshots/` ref 下固定每个不同的已存 blob,使垃圾回收无法让已记录的恢复指针失效。因此记录的 hash 能还原任一侧上次确认时的确切文本,所以失去同步的配对是「按被改一侧的 diff 最小化地修补另一侧」,从不整篇重译。日常工作会直接完成这份修补;用户显式调用扩展工作流时,可改由 `pnpm run gen-translation-brief ` 以能安全对齐的最窄粒度汇集这次更新,并由 `--apply` 在结构校验后拼接仅涉及围栏代码块的改动([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.md))。两侧对齐后,`pnpm run verify-translation-pairing --write ` 重新记录两个 hash;那份 yaml diff 就是「确认一致」这个动作本身,可以被评审,也正因如此,`--write` 要求点名你确认过的配对(`--write --all` 是显式的全语料形式)。
- 当两个分支都包含同一配对的有效确认时,已安装的 `dsh-translation-pairing` Git 合并驱动只会在 Git 默认文本合并能分别干净合并记录所指向的英文三方 blob 与中文三方 blob,且合并后的配对仍保留必需的语言切换行和结构签名时,组合出一份新记录。中文文件必须保留指向英文的反向链接;普通撰写的英文源必须保留指向中文的链接,而清单内的生成英文源不作此要求。任何无法确定的情形都保留为普通冲突;`pnpm run resolve-translation-pairing-conflicts` 会对已经停止的合并执行同一套遇错即保留冲突的操作,暂存每份可安全生成的配对记录,并在还有其他配对冲突时以非零状态退出。[自动配对合并 Agent Note](../../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) 负责记录该机制与备选方案。
+ 当两个分支都包含同一配对的有效确认时,已安装的 `dsh-translation-pairing` Git 合并驱动只会在 Git 默认文本合并能分别干净合并记录所指向的英文三方 blob 与中文三方 blob,且合并后的配对仍保留必需的语言切换行和结构签名时,组合出一份新记录。中文文件必须保留指向英文的反向链接;普通撰写的英文源必须保留指向中文的链接,而清单内的生成英文源不作此要求。任何合并驱动无法验证的结构都保留为普通冲突;`pnpm run resolve-translation-pairing-conflicts` 会对已经停止的合并执行同一套遇错即保留冲突的操作,暂存每份可安全生成的配对记录,并在还有其他配对冲突时以非零状态退出。[自动配对合并 Agent Note](../../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) 负责记录该机制与备选方案。
- **语言切换行。** 中文文件一律在 H1 标题后立即以 `[English](foo.md) | 中文` 链回英文。普通撰写的英文文件在同一位置以 `English | [中文](foo.zh.md)` 互链;清单内的生成英文源省略此行,以便与生成器输出逐字节一致。
- **结构与另一侧一一对应。** 标题深度与顺序、列表类型、有序列表起始编号、列表项数量、表格行列数、链接目标与逐字节一致的代码块在配对两侧一一对应;完整保持规则见 [translation-rules.md](translation-rules.md)。既有 Markdown 门禁对 `.zh.md` 文件原样生效(`verify-md-wrap`、`verify-md-links`)。
@@ -35,13 +35,13 @@
`pnpm run verify-translation-pairing ` 只检查被点名的配对——配对的三个文件中的任意一个(或其裸词干)都能点名它——因此更新循环几秒内就能验证自己的配对,而不必重新扫描全语料。`doc-sync` 与 CI 运行的是无参数的全语料形式;限定范围的绿灯在 PR 层面永远不能替代它。
-这个门禁带来的实际规则是:**当一个 PR 修改了已配对文档的任一侧时,同一个 PR 更新另一侧并重新记录配对**(运行 [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) skill(技能),再 `--write `),与本仓库既有的代码与 README 的 doc-sync 规则完全一致。留下失去同步的配对的 PR 会在 CI 变红。
+这个门禁带来的实际规则是:**当一个 PR 修改了已配对文档的任一侧时,同一个 PR 在术语指导下直接一次完成对侧文件的更新,并用 `--write ` 重新记录配对**,与本仓库既有的代码与 README 的 doc-sync 规则完全一致。留下失去同步的配对的 PR 会在 CI 变红。
-把门禁的边界说白:**门禁通过意味着这组文档在当前内容上的一致性得到了确认,不代表确认本身正确可靠。** 它检查记录的 hash 与结构签名;它无法判断两侧是否真的在说同样的话,也无法判断措辞是否准确、术语是否得当、行文是否自然;这部分约定由评审者把关,见 [translation-rules.md](translation-rules.md)。重新记录了 hash 但另一侧翻得潦草的配对能通过门禁;它不得通过评审。
+门禁的限制很明确:**门禁通过意味着这组文档在当前内容上的一致性得到了确认,不代表确认本身正确可靠。** 它检查记录的 hash 与 Markdown 结构;它无法判断两侧是否真的在说同样的话,也无法判断措辞是否准确、术语是否得当、行文是否自然;这部分约定由评审者把关,见 [translation-rules.md](translation-rules.md)。重新记录了 hash 但另一侧翻得潦草的配对能通过门禁;它不得通过评审。
## 范围与排除
-**范围**:除 vendor 源码外的全部 README,以及 `.agents/notes/**`、`docs/**` 与 `python/**` 下的全部活跃文档。匹配 README 时只看文件名且不区分大小写,因此今后新增的目录无需再修改 manifest。依赖目录、被忽略的构建产物目录以及冻结的 `.agents/notes/archived/` 目录树只在发现阶段排除,不属于持续演进的翻译源文档。
+**范围**:根目录 CONTRIBUTING 文档、除 vendor 源码外的全部 README,以及 `.agents/notes/**`、`docs/**` 与 `python/**` 下的全部活跃文档。匹配 README 时只看文件名且不区分大小写,因此今后新增的目录无需再修改 manifest。依赖目录、被忽略的构建产物目录以及冻结的 `.agents/notes/archived/` 目录树只在发现阶段排除,不属于持续演进的翻译源文档。
有经评审中文对侧的生成英文参考文档和图文档遵循配对规则。生成器仍是英文真源,新鲜度门禁与配对门禁各自独立强制其约束;重新生成导致英文变化后,配对会保持失去同步状态,直至经评审的中文对侧完成更新并重新记录。生成的英文源文件不含普通撰写文档所带的语言切换行,因为添加该行会使生成器新鲜度检查失败;中文对侧仍链接回英文源。生成页的中文对侧只能改写若直译便不再符合经评审译文事实的自指生成与维护说明;所有技术内容仍受普通忠实性规则约束。
@@ -57,4 +57,4 @@
## 分工
-这里的对侧文件由运行 [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) 的 agent 生成,再由人评审:在这里推理(inference)很便宜,评审注意力才是稀缺资源。门禁负责检查配对是否完整、记录的 hash、中文反向链接和普通撰写源的切换行(生成源按本文规则例外),以及本文列出的结构签名;翻译质量、术语和签名未涵盖的结构要求仍由评审把关。提示词约定也有可执行实现:[scripts/translation-prompt.ts](../../scripts/translation-prompt.ts) 会把仓库内置的模板(注入术语表;模板自带经人工校准的规则)渲染为英译中或中译英两个方向的提示词,并解析三段式响应;`doc-sync` 中的 `verify-translation-prompt` 会检查两个渲染方向与仓库内示例。
+日常更新对侧文件时,负责处理的 agent 会先加载 [terminology.md](terminology.md),再直接一次性更新且只处理一遍;它不会调用翻译 skill(技能)、生成简报、执行单独的翻译评审轮次,也不会委派给 subagent。扩展版 [dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) 工作流保留这些较重的机制,仅供用户显式调用。门禁负责检查配对是否完整、记录的 hash、中文反向链接和普通撰写源的切换行(生成源按本文规则例外),以及本文列出的结构签名;翻译质量、术语和签名未涵盖的结构要求仍由评审把关。提示词约定也有可执行实现:[scripts/translation-prompt.ts](../../scripts/translation-prompt.ts) 会把仓库内置的模板(注入术语表;模板自带经人工校准的规则)渲染为英译中或中译英两个方向的提示词,并解析三段式响应;`doc-sync` 中的 `verify-translation-prompt` 会检查两个渲染方向与仓库内示例。
diff --git a/docs/i18n/style-samples.md b/docs/i18n/style-samples.md
index 1b8a0e3f1a..61ef77065d 100644
--- a/docs/i18n/style-samples.md
+++ b/docs/i18n/style-samples.md
@@ -6,7 +6,7 @@
## ① 架构叙述
-> This document describes the architecture of the DeepSeek Harness — the foundation of **DeepSeek Code**. The governing principle, from the microkernel design discussion: **everything is a plugin**. The core is deliberately tiny — a handful of abstract services plus one concrete loop plugin (`dsh-agent-loop`) — and every product feature is a plugin against the extension surface described here, without modifying the loop.
+> This document describes the architecture of the DeepSeek Harness — the foundation of **DeepSeek Code**. The governing principle, from the microkernel design discussion: **everything is a plugin**. The core is deliberately tiny — a handful of abstract services plus one concrete loop plugin (`dsh-agent-loop`) — and every product feature is a plugin against the extension API described here, without modifying the loop.
本文介绍 DeepSeek Harness 整体架构,它是 **DeepSeek Code** 的底层基座。微内核设计讨论中确立了核心设计准则:**一切皆插件**。内核刻意做得极精简,仅包含少量抽象服务,外加一个实体循环插件 `dsh-agent-loop`。所有产品功能均基于本文定义的扩展接口开发为独立插件,无需改动主循环逻辑。
@@ -14,9 +14,9 @@
依赖约束规范:各类扩展插件仅依赖抽象接口,严禁直接依赖 `dsh-agent-loop`(该主循环支持替换实现);唯一允许的特例是组合包 `dsh-agent-spine-demo`,它的职责是组装整套实体主干。
-> This document covers **behavior**; type shapes live in [subsystems/](../subsystems/core.md), the per-event/service reference in the generated regions of [subsystems/](../subsystems/core.md), per-package contracts in the package READMEs ([map](../../packages/README.md)).
+> This document covers **behavior**; type definitions live in [subsystems/](../subsystems/core.md), the per-event/service reference lives in the generated regions of [subsystems/](../subsystems/core.md), and package contracts in the package READMEs state each package's required configuration and behavior ([map](../../packages/README.md)).
-本文档描述整体行为逻辑;类型定义存放于 [subsystems/](../subsystems/core.md);各类事件、服务的详细参考见 [subsystems/](../subsystems/core.md) 中的生成区块;各包(package)的对外约定写在相应的 README 中([索引](../../packages/README.md))。
+本文档描述整体行为逻辑;类型定义存放于 [subsystems/](../subsystems/core.md);各类事件、服务的详细参考见 [subsystems/](../subsystems/core.md) 中的生成区块;相应的 README 说明每个包(package)要求的配置和行为([索引](../../packages/README.md))。
## ② 防御模式规则
@@ -46,9 +46,9 @@
自带自动跳过逻辑,仅用于保障无密钥 CI 环境、无权限贡献者不会被流程拦截,不代表可以以此为由削减真实接口测试投入。
-> **Prefer the real implementation over a mock** — Mock only the genuinely expensive or non-deterministic boundary (the LLM adapter, the network, the clock); keep everything downstream real. A hand-rolled stand-in proves the bridge moves bytes, not that the shipping tool behaves as asserted — the two drift while the test stays green.
+> **Prefer the real implementation over a mock** — Mock only genuinely expensive or non-deterministic dependencies (the LLM adapter, the network, the clock); keep everything downstream real. A hand-rolled stand-in proves the bridge moves bytes, not that the shipping tool behaves as asserted — the two drift while the test stays green.
-**优先使用真实实现,而非 mock 替身**——仅对开销极大、结果不确定的边界模块做 mock(LLM(大语言模型)适配器、网络、时钟),其余下游组件全部使用真实实现。手写的 mock 替身只能验证数据通路能传输字节,无法保证线上工具符合预期逻辑;长期下来业务逻辑与 mock 实现会出现偏差,但测试仍会显示通过。
+**优先使用真实实现,而非 mock 替身**——仅对开销极大、结果不确定的依赖做 mock(LLM(大语言模型)适配器、网络、时钟),其余下游组件全部使用真实实现。手写的 mock 替身只能验证数据通路能传输字节,无法保证线上工具符合预期逻辑;长期下来业务逻辑与 mock 实现会出现偏差,但测试仍会显示通过。
## ④ 机制描述
@@ -58,9 +58,9 @@
## ⑤ 政策声明
-> The gate's limit, stated plainly: a green gate means the pair was confirmed consistent at these exact contents, not that the confirmation was sound. It checks hashes and shape; it cannot judge whether the two sides actually say the same thing — that is the reviewer's half of the contract. A re-recorded pair with a sloppy counterpart passes the gate; it must not pass review.
+> The gate's limit, stated plainly: a green gate means the pair was confirmed consistent at these exact contents, not that the confirmation was sound. It checks hashes and Markdown structure; it cannot judge whether the two sides actually say the same thing — that is the reviewer's half of the contract. A re-recorded pair with a sloppy counterpart passes the gate; it must not pass review.
-门禁的边界很明确:通过门禁只说明两侧文件当前的 blob hash 与伴随记录吻合,并且结构签名一致,也就是说,这组内容曾被确认一致;它不代表这次确认可靠。门禁无法判断两种语言是否真正表达了相同的意思;这部分约定要由评审人把关。即使译文粗糙、表意有误,重新记录配对后仍能通过门禁,但绝不能通过人工评审。
+门禁的限制很明确:通过门禁只说明两侧文件当前的 blob hash 与伴随记录吻合,并且 Markdown 结构签名一致,也就是说,这组内容曾被确认一致;它不代表这次确认可靠。评审人必须检查两种语言是否真正表达了相同的意思。即使译文粗糙、表意有误,重新记录配对后仍能通过门禁,但绝不能通过人工评审。
## ⑥ Agent Note 论证
@@ -84,4 +84,4 @@
- 长段按语义单元拆段,一段一件事;名词短语展开为动词句。
- 母语重写不等于删减:原文每个语义成分都要落地。
- 样例与 [terminology.md](terminology.md) 冲突时,以术语表为准:收录样例前按表修正术语(例如 agent、mock、LLM 保留英文,cancellation 译「取消」)。
-- 代码体标识符(事件名 `agent/status`、状态值 `running`、包名 `dsh-bash-local` 等)在译文中保留 code span 原文,不得口语化改写——这是行文规则的硬边界,Pass 2 逐句核验的重点。
+- 代码体标识符(事件名 `agent/status`、状态值 `running`、包名 `dsh-bash-local` 等)在译文中保留 code span 原文,不得口语化改写;Pass 2 必须逐句核验。
diff --git a/docs/i18n/terminology.md b/docs/i18n/terminology.md
index 51ec3673a6..a4d9ce43fa 100644
--- a/docs/i18n/terminology.md
+++ b/docs/i18n/terminology.md
@@ -167,7 +167,7 @@
| same-world subprocess | 与宿主共享文件系统和内核的子进程 | | 同世界子进程 | |
| sandbox | 沙箱 | | | |
| service | 服务 | | | |
-| serving surface | 对外服务接口 | | | |
+| serving interface | 对外服务接口 | | | |
| session | 会话 | | | |
| session event | 会话事件 | | | |
| setup card | 设置卡片 | | | 首次运行时代替行卡直接展开的配置卡 |
diff --git a/docs/i18n/translation-prompt.md b/docs/i18n/translation-prompt.md
index d8c6d95f73..b4ade80981 100644
--- a/docs/i18n/translation-prompt.md
+++ b/docs/i18n/translation-prompt.md
@@ -70,7 +70,7 @@ A lower-priority rule may refine but never override a higher-priority requiremen
- The translation must read as if originally written in the target language by a native technical author. If an expression sounds like a word-for-word rendering from the source language, rephrase it.
- Write in a professional, formal tone appropriate for developer documentation. Never use colloquial or casual expressions.
- Name an actor when the target language would otherwise obscure an actor that the source states or unambiguously implies. Never invent responsibility merely to avoid a passive construction.
-- Prefer established target-language engineering idiom over literal renderings, and localize metaphors instead of transplanting them.
+- Prefer established target-language engineering terms over literal renderings. Replace metaphors with direct descriptions that preserve the source meaning.
- Use polite imperative forms where the text instructs the reader to do something. In Chinese, address the reader as `你`, not `您`.
- Keep the author's register: concise stays concise, detailed stays detailed.
@@ -127,7 +127,7 @@ A terminology table is provided below. Follow it strictly:
## Output Format
-Return exactly three raw XML sections in the order shown below. Do not wrap the response in a Markdown code fence and do not add analysis or text before, between, or after the sections. The fence below only displays the required shape; do not reproduce the fence.
+Return exactly three raw XML sections in the order shown below. Do not wrap the response in a Markdown code fence and do not add analysis or text before, between, or after the sections. The fence below only displays the required format; do not reproduce the fence.
The outer section tags are framing. If Markdown inside any section body contains a line consisting only of ``, ``, ``, ``, ``, or ``, prefix that line with `\`. If the original line already has one or more backslashes immediately before the tag, add one more. The parser removes exactly one framing escape; tags mentioned inline need no escaping.
@@ -152,10 +152,10 @@ The outer section tags are framing. If Markdown inside any section body contains
## Self-Review Instructions
-After writing ``, verify it in two directions. First re-read it in the target language only, without looking at the source; awkward phrasing is easier to notice without source-language anchoring. Then compare it against the source clause by clause for completeness and exact meaning. Resolve doubts before writing ``; do not include reasoning transcripts, checks that passed, tentative suggestions, retractions, or no-op corrections.
+After writing ``, verify it in two directions. First re-read it in the target language only without comparing it with the source; this makes awkward phrasing easier to notice. Then compare it against the source clause by clause for completeness and exact meaning. Resolve doubts before writing ``; do not include reasoning transcripts, checks that passed, tentative suggestions, retractions, or no-op corrections.
**Structure**
-- Is the heading hierarchy and order, list shape and count, ordered-list start, table shape, and code block content identical to the source?
+- Are the heading hierarchy and order, list kind and item count, ordered-list start, table dimensions, and code block content identical to the source?
- Are ALL comments and info strings inside code blocks left untranslated and byte-identical to the source?
- Are inline code spans and machine-readable tokens verbatim?
- Is an existing language switcher correctly flipped, and is no switcher or filename invented when the source lacks one?
@@ -169,7 +169,7 @@ After writing ``, verify it in two directions. First re-read it in
**Tone & Style**
- Does every sentence read as if originally written by a native technical author?
-- Is there any colloquial, casual, overly informal, promotional, or transplanted metaphorical phrasing?
+- Is there any colloquial, casual, overly informal, promotional, or metaphorical phrasing?
- Are actors explicit where the target language needs them, without inventing responsibility?
**Sentence Structure**
@@ -228,9 +228,9 @@ Below are representative examples of common problems and their corrections. Foll
- Good: `FIXME:应当阻塞新版本发布的问题。除非评审者明确同意该更改可以合并,否则发布版本不应包含未解决的 FIXME。`
### Overly literal → Meaningful rendering
-- Source: `awkward phrasing is easier to hear without the source anchoring you`
-- Bad: `没有源文锚着,别扭的表述更容易被听出来`
-- Good: `不对照原文时,更容易察觉别扭的表达`
+- Source: `awkward phrasing is easier to notice when you read the translation without comparing it with the source`
+- Bad: `不把译文和原文比较时,尴尬的措辞更容易被注意`
+- Good: `不对照原文阅读译文时,更容易察觉别扭的表达`
### Terminology — do not translate what should be kept in English
- Source: `typed service seams, and explicit extension points`
diff --git a/docs/i18n/translation-rules.i18n.yaml b/docs/i18n/translation-rules.i18n.yaml
index e8e4d8f801..34b03c956f 100644
--- a/docs/i18n/translation-rules.i18n.yaml
+++ b/docs/i18n/translation-rules.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/i18n/translation-rules.md
-translation-rules.md: fb6aa9ac05bebe68ff9213af99f64457bdb1ad6f
-translation-rules.zh.md: 04dd0a704e19502c676ea0966437870c5af0624f
+translation-rules.md: ce20ed9a9673b0782ef07c9a4a21ff1c98ace960
+translation-rules.zh.md: daea57ab1d3a1abbad442982c8bb1c189478b8a8
diff --git a/docs/i18n/translation-rules.md b/docs/i18n/translation-rules.md
index fb6aa9ac05..ce20ed9a96 100644
--- a/docs/i18n/translation-rules.md
+++ b/docs/i18n/translation-rules.md
@@ -2,7 +2,7 @@
English | [中文](translation-rules.zh.md)
-How to translate between the two sides of a documentation pair in this repo. Both languages carry equal authority ([README.md](README.md)): a change is authored in either language, and that side is the source for that update — these rules govern producing or updating the counterpart. They bind humans and agents equally; the committed agent workflow that applies them is [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md). Rule levels follow RFC 2119 usage: **MUST** / **MUST NOT** are gate- or review-blocking; **SHOULD** needs a stated reason to deviate; **MAY** is discretionary.
+How to translate between the two sides of a documentation pair in this repo. Both languages carry equal authority ([README.md](README.md)): a change is authored in either language, and that side is the source for that update — these rules govern producing or updating the counterpart. They bind humans and agents equally. Routine agent work translates the changed content directly in one terminology-guided pass; the extended [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) workflow runs only when the user explicitly invokes it. Rule levels follow RFC 2119 usage: **MUST** / **MUST NOT** are gate- or review-blocking; **SHOULD** needs a stated reason to deviate; **MAY** is discretionary.
## Faithfulness
@@ -13,7 +13,7 @@ How to translate between the two sides of a documentation pair in this repo. Bot
## Voice
- The register is calibrated by [style-samples.md](style-samples.md) — human-approved gold pairs, one per document genre. The counterpart MUST match the target-language side of the nearest sample; where its voice and a prose voice rule disagree, the sample wins. Chinese targets use institutional technical Chinese; English targets use concise professional developer prose.
-- Write as a native technical author restating the content, not as a translator transposing sentences. Then verify against the source clause by clause: nothing added, nothing dropped — fluency never justifies losing a clause.
+- Write as a native technical author restating the content, not as a translator transposing sentences, while preserving every source clause: nothing added, nothing dropped — fluency never justifies losing a clause.
- Give sentences an explicit actor when the target language would otherwise obscure it; for Chinese, replace vague passives or abstract subjects with the actual actor (系统、门禁、评审人).
- Prefer established target-language engineering idiom over calques (误报/漏检 for false positive/negative, 执行红线 for enforcement frontier); localize metaphors instead of transplanting them, and unpack noun chains where the target language requires it.
- Split long paragraphs by semantic unit — one idea per paragraph. Paragraph boundaries MAY differ from the source; the structural signature does not count paragraphs.
@@ -54,8 +54,7 @@ These rules govern the Chinese side; the English side follows the repo's normal
## Quality bar
- A pair is done when a bilingual engineer reading either file alone gets everything a reader of the other gets — same facts, same caveats, same tone — and nothing extra.
-- Before handing off, self-check the result against this file and re-read the counterpart ALONE, without the source side by side; awkward phrasing is easier to hear without the source anchoring you.
-- Run `pnpm run verify-translation-pairing` and the rest of `doc-sync` for records, switchers, heading depths, code blocks, table row and column counts, list kinds, ordered-list starts, list item counts, links, and repository Markdown rules. Manually verify list and table order, noncanonical list numbering, inline code, emphasis, meaning, terminology, and tone.
+- Run `pnpm run verify-translation-pairing` and the rest of `doc-sync` for records, switchers, heading depths, code blocks, table row and column counts, list kinds, ordered-list starts, list item counts, links, and repository Markdown rules. Human review owns list and table order, noncanonical list numbering, inline code, emphasis, meaning, terminology, and tone.
## References
diff --git a/docs/i18n/translation-rules.zh.md b/docs/i18n/translation-rules.zh.md
index 04dd0a704e..daea57ab1d 100644
--- a/docs/i18n/translation-rules.zh.md
+++ b/docs/i18n/translation-rules.zh.md
@@ -2,7 +2,7 @@
[English](translation-rules.md) | 中文
-本文规定:如何在本仓库文档配对的中英文两种语言之间进行翻译。两种语言同权(见 [README.md](README.md)):每次变更可以用任一语言撰写,被编辑的一侧即为本次更新的源;本文的规则约束如何产出或更新对侧文件。这些规则对人类和 agent(智能体)同等生效;应用这些规则的仓库内置 agent 工作流是 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md)。规则级别沿用 RFC 2119 的用法:**必须(MUST)** / **禁止(MUST NOT)** 会卡门禁或评审;**应当(SHOULD)** 偏离时要说明理由;**可以(MAY)** 自行裁量。
+本文规定:如何在本仓库文档配对的中英文两种语言之间进行翻译。两种语言同权(见 [README.md](README.md)):每次变更可以用任一语言撰写,被编辑的一侧即为本次更新的源;本文的规则约束如何产出或更新对侧文件。这些规则对人类和 agent(智能体)同等生效。日常工作中,agent 会在术语指导下直接一次完成有改动内容的翻译;扩展版 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) 工作流仅在用户显式调用时运行。规则级别沿用 RFC 2119 的用法:**必须(MUST)** / **禁止(MUST NOT)** 会卡门禁或评审;**应当(SHOULD)** 偏离时要说明理由;**可以(MAY)** 自行裁量。
## 忠实性
@@ -13,7 +13,7 @@
## 行文
- 语体以 [style-samples.md](style-samples.md) 为校准锚点。人工定稿的金标样例按文体各一组,译文必须参照文体最接近的样例,采用其中目标语言一侧的语体;如果样例与本文的行文规则冲突,以样例为准。译成中文时,采用规范的技术制度文;译成英文时,采用简洁、专业的开发者文档语体。
-- 以母语技术作者的身份重述内容,而不是逐句转写的译者。写完后逐句对照原文核验:不添加、不遗漏——流畅永远不是丢掉语义成分的理由。
+- 以母语技术作者的身份重述内容,而不是以译者身份逐句转写,同时保留原文的每个语义成分:不添加、不遗漏——流畅永远不是丢掉语义成分的理由。
- 如果直译会让执行主体含糊,请明确写出实际执行者;译成中文时,应由「系统、门禁、评审人」等实际执行者作主语,避免含糊的被动句或抽象主语。
- 优先采用目标语言中通行的工程表达,避免生硬直译(false positive/negative→误报/漏检、enforcement frontier→执行红线);隐喻应自然改写,名词链则按目标语言的习惯拆开。
- 长段按语义单元拆分,一段一件事。段落边界可以与原文不同;结构签名不比对段落数。
@@ -54,8 +54,7 @@
## 质量标准
- 一对文档的完成标准:一位双语工程师只读其中任一文件,能获得与另一文件读者完全相同的信息(相同的事实、相同的告诫、相同的语气),并且没有任何多余的内容。
-- 交付前,请对照本文自查一遍,并**单独通读对侧文件**,不与源侧对照;不对照原文时,更容易察觉别扭的表达。
-- 请运行 `pnpm run verify-translation-pairing` 与 `doc-sync` 的其余门禁。这些门禁会检查一致性记录、切换行、标题深度、代码块、表格行列数、列表类型、有序列表起始编号、列表项数量、链接及仓库 Markdown 规则;列表与表格的顺序、非常规列表编号、行内代码、强调标记、语义、术语和语体仍需人工核对。
+- 请运行 `pnpm run verify-translation-pairing` 与 `doc-sync` 的其余门禁。这些门禁会检查一致性记录、切换行、标题深度、代码块、表格行列数、列表类型、有序列表起始编号、列表项数量、链接及仓库 Markdown 规则;列表与表格的顺序、非常规列表编号、行内代码、强调标记、语义、术语和语体则由人工评审负责。
## 参考资料
diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml
index a7338b24a0..1c8a4b31cc 100644
--- a/docs/module-graph.i18n.yaml
+++ b/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: b2c76f11d2ed57383f953b26327edffdbade1ee4
-module-graph.zh.md: cd882722c2f628c7c9e142ce1ba833cf4f557949
+module-graph.md: 609e0d971c385c01ca7582ea81e3167427ba72d6
+module-graph.zh.md: 3cc7c9228ce08731d0550426e6a38c239a5a5e74
diff --git a/docs/module-graph.md b/docs/module-graph.md
index b2c76f11d2..609e0d971c 100644
--- a/docs/module-graph.md
+++ b/docs/module-graph.md
@@ -27,6 +27,7 @@ flowchart TD
pkg_agent["agent"]
pkg_agent_default_model["agent-default-model"]
pkg_agent_loop["agent-loop"]
+ pkg_agent_tool_mode["agent-tool-mode"]
pkg_scope["scope"]
pkg_session["session"]
pkg_system_prompt["system-prompt"]
@@ -44,6 +45,7 @@ flowchart TD
pkg_bash_local["bash-local"]
pkg_bash_sandbox["bash-sandbox"]
pkg_pwsh_local["pwsh-local"]
+ pkg_pwsh_sandbox["pwsh-sandbox"]
pkg_tool_bash["tool-bash"]
pkg_tool_pwsh["tool-pwsh"]
end
@@ -125,8 +127,13 @@ flowchart TD
pkg_api_gateway["api-gateway"]
pkg_api_remotes["api-remotes"]
end
+ subgraph group_attachment["packages/attachment"]
+ pkg_attachment["attachment"]
+ pkg_attachment_local["attachment-local"]
+ end
subgraph group_boot["packages/boot"]
pkg_app_boot["app-boot"]
+ pkg_cmdline["cmdline"]
end
subgraph group_bundle["packages/bundle"]
pkg_base["base"]
@@ -141,6 +148,7 @@ flowchart TD
pkg_client_runtime["client-runtime"]
pkg_client_schema_form["client-schema-form"]
pkg_client_test_runtime["client-test-runtime"]
+ pkg_client_ui_agent_preset["client-ui-agent-preset"]
pkg_client_ui_command["client-ui-command"]
pkg_client_ui_conversation["client-ui-conversation"]
pkg_client_ui_deliverables["client-ui-deliverables"]
@@ -159,9 +167,11 @@ flowchart TD
pkg_client_ui_slash["client-ui-slash"]
pkg_client_ui_slots["client-ui-slots"]
pkg_client_ui_subagent["client-ui-subagent"]
+ pkg_client_ui_task["client-ui-task"]
pkg_client_ui_theme["client-ui-theme"]
pkg_client_ui_tool["client-ui-tool"]
pkg_client_ui_trajectory["client-ui-trajectory"]
+ pkg_client_ui_workflow_run["client-ui-workflow-run"]
pkg_client_ui_workspace["client-ui-workspace"]
pkg_client_web["client-web"]
pkg_client_web_react["client-web-react"]
@@ -192,6 +202,7 @@ flowchart TD
end
subgraph group_feedback["packages/feedback"]
pkg_command_feedback["command-feedback"]
+ pkg_message_feedback["message-feedback"]
end
subgraph group_guard["packages/guard"]
pkg_repeat_tool_guard["repeat-tool-guard"]
@@ -221,6 +232,10 @@ flowchart TD
subgraph group_mcp["packages/mcp"]
pkg_mcp_client["mcp-client"]
end
+ subgraph group_preset["packages/preset"]
+ pkg_agent_presets["agent-presets"]
+ pkg_persona["persona"]
+ end
subgraph group_pty["packages/pty"]
pkg_pty["pty"]
pkg_pty_local["pty-local"]
@@ -231,20 +246,17 @@ flowchart TD
pkg_sandbox["sandbox"]
pkg_sandbox_local["sandbox-local"]
pkg_sandbox_policy["sandbox-policy"]
- end
- subgraph group_scaffold["packages/scaffold"]
- pkg_helper["helper"]
- pkg_jsonrpc["jsonrpc"]
- pkg_scripts["scripts"]
- pkg_sdk_client["sdk-client"]
- pkg_sdk_protocol["sdk-protocol"]
- pkg_telemetry["telemetry"]
+ pkg_sandbox_windows_acl["sandbox-windows-acl"]
end
subgraph group_schedule["packages/schedule"]
pkg_tool_schedule["tool-schedule"]
end
+ subgraph group_sdk["packages/sdk"]
+ pkg_jsonrpc["jsonrpc"]
+ pkg_sdk_client["sdk-client"]
+ pkg_sdk_protocol["sdk-protocol"]
+ end
subgraph group_self_modification["packages/self-modification"]
- pkg_repository_plugin["repository-plugin"]
pkg_tool_cordis["tool-cordis"]
end
subgraph group_session["packages/session"]
@@ -260,6 +272,7 @@ flowchart TD
pkg_session_title_all_messages_llm["session-title-all-messages-llm"]
pkg_session_title_first_message_llm["session-title-first-message-llm"]
pkg_session_title_llm["session-title-llm"]
+ pkg_user_id["user-id"]
end
subgraph group_settings["packages/settings"]
pkg_settings["settings"]
@@ -304,6 +317,7 @@ flowchart TD
pkg_timeout --> pkg_invariants
pkg_scope --> pkg_invariants
pkg_llm_mock_server --> pkg_invariants
+ pkg_cmdline --> pkg_invariants
pkg_base --> pkg_invariants
pkg_client_modules --> pkg_invariants
pkg_client_schema_form --> pkg_invariants
@@ -314,17 +328,16 @@ flowchart TD
pkg_code_runtime --> pkg_invariants
pkg_e2b --> pkg_invariants
pkg_jsonrpc_demo --> pkg_invariants
- pkg_host_apiproxy --> pkg_invariants
pkg_host_directory_picker --> pkg_invariants
pkg_host_webserver --> pkg_invariants
+ pkg_sandbox_windows_acl --> pkg_invariants
pkg_storage --> pkg_invariants
pkg_subprocess --> pkg_invariants
pkg_type_meta --> pkg_invariants
pkg_typert_generator --> pkg_invariants
pkg_typert_registry --> pkg_invariants
- pkg_llm --> pkg_brand
- pkg_llm --> pkg_invariants
- pkg_llm --> pkg_timeout
+ pkg_attachment --> pkg_brand
+ pkg_attachment --> pkg_invariants
pkg_client_connection --> pkg_host_webserver
pkg_client_connection --> pkg_invariants
pkg_client_hmr --> pkg_client_modules
@@ -341,12 +354,9 @@ flowchart TD
pkg_subprocess_e2b --> pkg_timeout
pkg_frontend_static --> pkg_host_webserver
pkg_frontend_static --> pkg_invariants
- pkg_helper --> pkg_brand
- pkg_helper --> pkg_invariants
- pkg_helper --> pkg_subprocess
- pkg_telemetry --> pkg_brand
- pkg_telemetry --> pkg_invariants
- pkg_telemetry --> pkg_paths
+ pkg_user_id --> pkg_brand
+ pkg_user_id --> pkg_invariants
+ pkg_user_id --> pkg_paths
pkg_settings --> pkg_brand
pkg_settings --> pkg_invariants
pkg_storage_domain --> pkg_invariants
@@ -360,12 +370,48 @@ flowchart TD
pkg_subprocess_local --> pkg_timeout
pkg_typert_loader --> pkg_invariants
pkg_typert_loader --> pkg_typert_registry
+ pkg_llm --> pkg_attachment
+ pkg_llm --> pkg_brand
+ pkg_llm --> pkg_invariants
+ pkg_llm --> pkg_timeout
+ pkg_api_gateway --> pkg_client_connection
+ pkg_api_gateway --> pkg_invariants
+ pkg_api_gateway --> pkg_typert_registry
+ pkg_attachment_local --> pkg_attachment
+ pkg_attachment_local --> pkg_invariants
+ pkg_attachment_local --> pkg_paths
+ pkg_client_locale --> pkg_client_connection
+ pkg_client_locale --> pkg_client_runtime
+ pkg_client_locale --> pkg_client_ui_primitives
+ pkg_client_locale --> pkg_client_ui_slots
+ pkg_client_locale --> pkg_invariants
+ pkg_client_ui_models --> pkg_client_connection
+ pkg_client_ui_models --> pkg_client_runtime
+ pkg_client_ui_models --> pkg_client_schema_form
+ pkg_client_ui_models --> pkg_client_ui_primitives
+ pkg_client_ui_models --> pkg_client_ui_slots
+ pkg_client_ui_models --> pkg_client_web_react
+ pkg_client_ui_models --> pkg_invariants
+ pkg_client_ui_settings --> pkg_client_runtime
+ pkg_client_ui_settings --> pkg_client_ui_primitives
+ pkg_client_ui_settings --> pkg_client_ui_slots
+ pkg_client_ui_settings --> pkg_invariants
+ pkg_credentials_local --> pkg_atomic_write
+ pkg_credentials_local --> pkg_credentials
+ pkg_credentials_local --> pkg_environment
+ pkg_credentials_local --> pkg_invariants
+ pkg_credentials_local --> pkg_paths
+ pkg_settings_local --> pkg_atomic_write
+ pkg_settings_local --> pkg_invariants
+ pkg_settings_local --> pkg_paths
+ pkg_settings_local --> pkg_settings
pkg_llm_deepseek --> pkg_credentials
pkg_llm_deepseek --> pkg_environment
pkg_llm_deepseek --> pkg_invariants
pkg_llm_deepseek --> pkg_llm
pkg_llm_deepseek --> pkg_settings
pkg_llm_deepseek --> pkg_timeout
+ pkg_llm_pi_ai --> pkg_attachment
pkg_llm_pi_ai --> pkg_credentials
pkg_llm_pi_ai --> pkg_environment
pkg_llm_pi_ai --> pkg_invariants
@@ -382,61 +428,50 @@ flowchart TD
pkg_system_prompt --> pkg_scope
pkg_skill --> pkg_invariants
pkg_skill --> pkg_llm
+ pkg_skill --> pkg_scope
pkg_web --> pkg_invariants
pkg_web --> pkg_llm
- pkg_api_gateway --> pkg_client_connection
- pkg_api_gateway --> pkg_invariants
- pkg_api_gateway --> pkg_typert_registry
- pkg_client_locale --> pkg_client_runtime
- pkg_client_locale --> pkg_client_ui_primitives
- pkg_client_locale --> pkg_client_ui_slots
- pkg_client_locale --> pkg_invariants
- pkg_client_test_runtime --> pkg_client_runtime
- pkg_client_test_runtime --> pkg_client_ui_slots
- pkg_client_test_runtime --> pkg_client_web_react
- pkg_client_test_runtime --> pkg_host_apiproxy
- pkg_client_test_runtime --> pkg_invariants
- pkg_client_ui_models --> pkg_client_connection
- pkg_client_ui_models --> pkg_client_runtime
- pkg_client_ui_models --> pkg_client_schema_form
- pkg_client_ui_models --> pkg_client_ui_primitives
- pkg_client_ui_models --> pkg_client_ui_slots
- pkg_client_ui_models --> pkg_client_web_react
- pkg_client_ui_models --> pkg_invariants
- pkg_client_ui_settings --> pkg_client_runtime
- pkg_client_ui_settings --> pkg_client_ui_primitives
- pkg_client_ui_settings --> pkg_client_ui_slots
- pkg_client_ui_settings --> pkg_invariants
- pkg_client_ui_trajectory --> pkg_client_runtime
- pkg_client_ui_trajectory --> pkg_client_ui_primitives
- pkg_client_ui_trajectory --> pkg_invariants
- pkg_credentials_local --> pkg_atomic_write
- pkg_credentials_local --> pkg_credentials
- pkg_credentials_local --> pkg_environment
- pkg_credentials_local --> pkg_invariants
- pkg_credentials_local --> pkg_paths
+ pkg_client_ui_question --> pkg_client_locale
+ pkg_client_ui_question --> pkg_invariants
+ pkg_client_ui_settings_general --> pkg_client_connection
+ pkg_client_ui_settings_general --> pkg_client_locale
+ pkg_client_ui_settings_general --> pkg_client_runtime
+ pkg_client_ui_settings_general --> pkg_client_ui_primitives
+ pkg_client_ui_settings_general --> pkg_client_ui_settings
+ pkg_client_ui_settings_general --> pkg_client_ui_slots
+ pkg_client_ui_settings_general --> pkg_client_web_react
+ pkg_client_ui_settings_general --> pkg_invariants
+ pkg_client_ui_sidebar --> pkg_client_locale
+ pkg_client_ui_sidebar --> pkg_client_runtime
+ pkg_client_ui_sidebar --> pkg_client_ui_primitives
+ pkg_client_ui_sidebar --> pkg_client_ui_slots
+ pkg_client_ui_sidebar --> pkg_invariants
+ pkg_client_ui_slash --> pkg_client_locale
+ pkg_client_ui_slash --> pkg_client_runtime
+ pkg_client_ui_slash --> pkg_client_ui_primitives
+ pkg_client_ui_slash --> pkg_client_ui_slots
+ pkg_client_ui_slash --> pkg_invariants
+ pkg_client_ui_theme --> pkg_client_connection
+ pkg_client_ui_theme --> pkg_client_locale
+ pkg_client_ui_theme --> pkg_client_runtime
+ pkg_client_ui_theme --> pkg_client_ui_primitives
+ pkg_client_ui_theme --> pkg_client_ui_slots
+ pkg_client_ui_theme --> pkg_host_webserver
+ pkg_client_ui_theme --> pkg_invariants
+ pkg_client_ui_workspace --> pkg_client_locale
+ pkg_client_ui_workspace --> pkg_client_runtime
+ pkg_client_ui_workspace --> pkg_client_ui_primitives
+ pkg_client_ui_workspace --> pkg_client_ui_slots
+ pkg_client_ui_workspace --> pkg_invariants
pkg_lsp --> pkg_brand
pkg_lsp --> pkg_invariants
pkg_lsp --> pkg_llm
- pkg_sandbox --> pkg_invariants
- pkg_sandbox --> pkg_llm
- pkg_settings_local --> pkg_atomic_write
- pkg_settings_local --> pkg_invariants
- pkg_settings_local --> pkg_paths
- pkg_settings_local --> pkg_settings
pkg_agent --> pkg_invariants
pkg_agent --> pkg_llm
pkg_agent --> pkg_scope
pkg_agent --> pkg_session
pkg_agent --> pkg_system_prompt
pkg_agent --> pkg_type_meta
- pkg_bash --> pkg_invariants
- pkg_bash --> pkg_sandbox
- pkg_bash --> pkg_subprocess
- pkg_fs --> pkg_brand
- pkg_fs --> pkg_invariants
- pkg_fs --> pkg_llm
- pkg_fs --> pkg_sandbox
pkg_skill_badge --> pkg_invariants
pkg_skill_badge --> pkg_skill
pkg_web_fetch_local --> pkg_invariants
@@ -458,43 +493,29 @@ flowchart TD
pkg_app_boot --> pkg_invariants
pkg_app_boot --> pkg_paths
pkg_app_boot --> pkg_system_prompt
- pkg_client_ui_question --> pkg_client_locale
- pkg_client_ui_question --> pkg_invariants
- pkg_client_ui_settings_general --> pkg_client_connection
- pkg_client_ui_settings_general --> pkg_client_locale
- pkg_client_ui_settings_general --> pkg_client_runtime
- pkg_client_ui_settings_general --> pkg_client_ui_primitives
- pkg_client_ui_settings_general --> pkg_client_ui_settings
- pkg_client_ui_settings_general --> pkg_client_ui_slots
- pkg_client_ui_settings_general --> pkg_client_web_react
- pkg_client_ui_settings_general --> pkg_invariants
- pkg_client_ui_sidebar --> pkg_client_locale
- pkg_client_ui_sidebar --> pkg_client_runtime
- pkg_client_ui_sidebar --> pkg_client_ui_primitives
- pkg_client_ui_sidebar --> pkg_client_ui_slots
- pkg_client_ui_sidebar --> pkg_invariants
- pkg_client_ui_slash --> pkg_client_locale
- pkg_client_ui_slash --> pkg_client_runtime
- pkg_client_ui_slash --> pkg_client_ui_primitives
- pkg_client_ui_slash --> pkg_client_ui_slots
- pkg_client_ui_slash --> pkg_invariants
- pkg_client_ui_theme --> pkg_client_locale
- pkg_client_ui_theme --> pkg_client_runtime
- pkg_client_ui_theme --> pkg_client_ui_primitives
- pkg_client_ui_theme --> pkg_client_ui_slots
- pkg_client_ui_theme --> pkg_invariants
- pkg_client_ui_workspace --> pkg_client_locale
- pkg_client_ui_workspace --> pkg_client_runtime
- pkg_client_ui_workspace --> pkg_client_ui_primitives
- pkg_client_ui_workspace --> pkg_client_ui_slots
- pkg_client_ui_workspace --> pkg_invariants
+ pkg_client_ui_layout --> pkg_client_runtime
+ pkg_client_ui_layout --> pkg_client_ui_slots
+ pkg_client_ui_layout --> pkg_client_ui_theme
+ pkg_client_ui_layout --> pkg_invariants
pkg_code_runtime_worker --> pkg_code_runtime
pkg_code_runtime_worker --> pkg_invariants
pkg_code_runtime_worker --> pkg_session
pkg_code_runtime_worker --> pkg_timeout
- pkg_sandbox_local --> pkg_invariants
- pkg_sandbox_local --> pkg_llm
- pkg_sandbox_local --> pkg_sandbox
+ pkg_host_directory_picker_browse --> pkg_client_locale
+ pkg_host_directory_picker_browse --> pkg_client_runtime
+ pkg_host_directory_picker_browse --> pkg_client_ui_primitives
+ pkg_host_directory_picker_browse --> pkg_client_ui_slots
+ pkg_host_directory_picker_browse --> pkg_client_ui_workspace
+ pkg_host_directory_picker_browse --> pkg_invariants
+ pkg_host_directory_picker_native --> pkg_client_runtime
+ pkg_host_directory_picker_native --> pkg_client_ui_slots
+ pkg_host_directory_picker_native --> pkg_client_ui_workspace
+ pkg_host_directory_picker_native --> pkg_invariants
+ pkg_persona --> pkg_invariants
+ pkg_persona --> pkg_system_prompt
+ pkg_sandbox --> pkg_invariants
+ pkg_sandbox --> pkg_llm
+ pkg_sandbox --> pkg_session
pkg_session_persistence --> pkg_brand
pkg_session_persistence --> pkg_invariants
pkg_session_persistence --> pkg_session
@@ -519,22 +540,13 @@ flowchart TD
pkg_goal --> pkg_session
pkg_goal --> pkg_session_projection
pkg_goal --> pkg_type_meta
- pkg_bash_local --> pkg_bash
- pkg_bash_local --> pkg_invariants
- pkg_bash_local --> pkg_subprocess
- pkg_bash_local --> pkg_timeout
- pkg_pwsh_local --> pkg_bash
- pkg_pwsh_local --> pkg_invariants
- pkg_pwsh_local --> pkg_subprocess
- pkg_pwsh_local --> pkg_timeout
- pkg_fs_local --> pkg_fs
- pkg_fs_local --> pkg_invariants
- pkg_fs_policy --> pkg_fs
- pkg_fs_policy --> pkg_invariants
- pkg_skill_local --> pkg_fs
- pkg_skill_local --> pkg_invariants
- pkg_skill_local --> pkg_paths
- pkg_skill_local --> pkg_skill
+ pkg_bash --> pkg_invariants
+ pkg_bash --> pkg_sandbox
+ pkg_bash --> pkg_subprocess
+ pkg_fs --> pkg_brand
+ pkg_fs --> pkg_invariants
+ pkg_fs --> pkg_llm
+ pkg_fs --> pkg_sandbox
pkg_web_search_deepseek --> pkg_agent
pkg_web_search_deepseek --> pkg_credentials
pkg_web_search_deepseek --> pkg_environment
@@ -543,37 +555,24 @@ flowchart TD
pkg_web_search_deepseek --> pkg_web
pkg_spill_local --> pkg_invariants
pkg_spill_local --> pkg_spill
- pkg_hook_protocol --> pkg_bash
- pkg_hook_protocol --> pkg_invariants
- pkg_hook_protocol --> pkg_session
pkg_loader_smoke --> pkg_agent
pkg_loader_smoke --> pkg_invariants
pkg_loader_smoke --> pkg_llm
pkg_loader_smoke --> pkg_session
- pkg_client_ui_layout --> pkg_client_runtime
- pkg_client_ui_layout --> pkg_client_ui_slots
- pkg_client_ui_layout --> pkg_client_ui_theme
- pkg_client_ui_layout --> pkg_invariants
pkg_time_context --> pkg_agent
pkg_time_context --> pkg_invariants
pkg_time_context --> pkg_session
- pkg_tmux_context --> pkg_agent
- pkg_tmux_context --> pkg_bash
- pkg_tmux_context --> pkg_invariants
- pkg_tmux_context --> pkg_session
- pkg_fs_e2b --> pkg_e2b
- pkg_fs_e2b --> pkg_fs
- pkg_fs_e2b --> pkg_invariants
- pkg_host_directory_picker_browse --> pkg_client_locale
- pkg_host_directory_picker_browse --> pkg_client_runtime
- pkg_host_directory_picker_browse --> pkg_client_ui_primitives
- pkg_host_directory_picker_browse --> pkg_client_ui_slots
- pkg_host_directory_picker_browse --> pkg_client_ui_workspace
- pkg_host_directory_picker_browse --> pkg_invariants
- pkg_host_directory_picker_native --> pkg_client_runtime
- pkg_host_directory_picker_native --> pkg_client_ui_slots
- pkg_host_directory_picker_native --> pkg_client_ui_workspace
- pkg_host_directory_picker_native --> pkg_invariants
+ pkg_message_feedback --> pkg_brand
+ pkg_message_feedback --> pkg_invariants
+ pkg_message_feedback --> pkg_llm
+ pkg_message_feedback --> pkg_session
+ pkg_message_feedback --> pkg_session_persistence
+ pkg_message_feedback --> pkg_storage_domain
+ pkg_message_feedback --> pkg_type_meta
+ pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
+ pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
+ pkg_host_directory_picker_auto --> pkg_host_webserver
+ pkg_host_directory_picker_auto --> pkg_invariants
pkg_commands --> pkg_agent
pkg_commands --> pkg_brand
pkg_commands --> pkg_invariants
@@ -589,23 +588,26 @@ flowchart TD
pkg_user_interaction --> pkg_agent
pkg_user_interaction --> pkg_invariants
pkg_user_interaction --> pkg_llm
- pkg_lsp_local --> pkg_brand
- pkg_lsp_local --> pkg_fs
- pkg_lsp_local --> pkg_invariants
- pkg_lsp_local --> pkg_llm
- pkg_lsp_local --> pkg_lsp
- pkg_lsp_local --> pkg_subprocess
- pkg_lsp_local --> pkg_timeout
+ pkg_agent_presets --> pkg_agent
+ pkg_agent_presets --> pkg_atomic_write
+ pkg_agent_presets --> pkg_invariants
+ pkg_agent_presets --> pkg_paths
+ pkg_agent_presets --> pkg_scope
+ pkg_agent_presets --> pkg_session
+ pkg_agent_presets --> pkg_settings
+ pkg_agent_presets --> pkg_system_prompt
pkg_pty --> pkg_agent
pkg_pty --> pkg_brand
pkg_pty --> pkg_invariants
+ pkg_sandbox_local --> pkg_invariants
+ pkg_sandbox_local --> pkg_llm
+ pkg_sandbox_local --> pkg_sandbox
+ pkg_sandbox_local --> pkg_session
pkg_sandbox_policy --> pkg_agent
pkg_sandbox_policy --> pkg_invariants
pkg_sandbox_policy --> pkg_sandbox
pkg_sandbox_policy --> pkg_session
pkg_sandbox_policy --> pkg_system_prompt
- pkg_scripts --> pkg_app_boot
- pkg_scripts --> pkg_invariants
pkg_session_persistence_jsonl --> pkg_invariants
pkg_session_persistence_jsonl --> pkg_session
pkg_session_persistence_jsonl --> pkg_session_persistence
@@ -656,21 +658,30 @@ flowchart TD
pkg_goal_session --> pkg_invariants
pkg_goal_session --> pkg_llm
pkg_goal_session --> pkg_session
- pkg_bash_sandbox --> pkg_bash
- pkg_bash_sandbox --> pkg_bash_local
- pkg_bash_sandbox --> pkg_invariants
- pkg_bash_sandbox --> pkg_sandbox
- pkg_bash_sandbox --> pkg_sandbox_policy
- pkg_fs_sandbox --> pkg_fs
- pkg_fs_sandbox --> pkg_fs_local
- pkg_fs_sandbox --> pkg_invariants
- pkg_fs_sandbox --> pkg_sandbox
- pkg_fs_sandbox --> pkg_sandbox_policy
+ pkg_bash_local --> pkg_bash
+ pkg_bash_local --> pkg_invariants
+ pkg_bash_local --> pkg_subprocess
+ pkg_bash_local --> pkg_timeout
+ pkg_pwsh_local --> pkg_bash
+ pkg_pwsh_local --> pkg_invariants
+ pkg_pwsh_local --> pkg_subprocess
+ pkg_pwsh_local --> pkg_timeout
+ pkg_fs_local --> pkg_fs
+ pkg_fs_local --> pkg_invariants
+ pkg_fs_policy --> pkg_fs
+ pkg_fs_policy --> pkg_invariants
+ pkg_skill_local --> pkg_fs
+ pkg_skill_local --> pkg_invariants
+ pkg_skill_local --> pkg_paths
+ pkg_skill_local --> pkg_skill
pkg_compact --> pkg_brand
pkg_compact --> pkg_commands
pkg_compact --> pkg_invariants
pkg_compact --> pkg_llm
pkg_compact --> pkg_session
+ pkg_hook_protocol --> pkg_bash
+ pkg_hook_protocol --> pkg_invariants
+ pkg_hook_protocol --> pkg_session
pkg_session_query --> pkg_brand
pkg_session_query --> pkg_invariants
pkg_session_query --> pkg_llm
@@ -692,13 +703,20 @@ flowchart TD
pkg_headless --> pkg_invariants
pkg_headless --> pkg_llm
pkg_headless --> pkg_session
+ pkg_tmux_context --> pkg_agent
+ pkg_tmux_context --> pkg_bash
+ pkg_tmux_context --> pkg_invariants
+ pkg_tmux_context --> pkg_session
+ pkg_fs_e2b --> pkg_e2b
+ pkg_fs_e2b --> pkg_fs
+ pkg_fs_e2b --> pkg_invariants
pkg_command_feedback --> pkg_commands
pkg_command_feedback --> pkg_invariants
pkg_command_feedback --> pkg_session
- pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
- pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
- pkg_host_directory_picker_auto --> pkg_host_webserver
- pkg_host_directory_picker_auto --> pkg_invariants
+ pkg_command_feedback --> pkg_session_telemetry
+ pkg_command_feedback --> pkg_user_id
+ pkg_host_apiproxy --> pkg_agent_presets
+ pkg_host_apiproxy --> pkg_invariants
pkg_permission --> pkg_bash
pkg_permission --> pkg_commands
pkg_permission --> pkg_invariants
@@ -708,6 +726,13 @@ flowchart TD
pkg_permission --> pkg_session_projection
pkg_permission --> pkg_settings
pkg_permission --> pkg_user_approval
+ pkg_lsp_local --> pkg_brand
+ pkg_lsp_local --> pkg_fs
+ pkg_lsp_local --> pkg_invariants
+ pkg_lsp_local --> pkg_llm
+ pkg_lsp_local --> pkg_lsp
+ pkg_lsp_local --> pkg_subprocess
+ pkg_lsp_local --> pkg_timeout
pkg_pty_local --> pkg_agent
pkg_pty_local --> pkg_invariants
pkg_pty_local --> pkg_pty
@@ -722,6 +747,7 @@ flowchart TD
pkg_session_title_llm --> pkg_timeout
pkg_tasks_local --> pkg_agent
pkg_tasks_local --> pkg_invariants
+ pkg_tasks_local --> pkg_scope
pkg_tasks_local --> pkg_tasks
pkg_tasks_local --> pkg_timeout
pkg_token_meter --> pkg_compact
@@ -737,6 +763,8 @@ flowchart TD
pkg_agent_loop --> pkg_session_persistence
pkg_agent_loop --> pkg_system_prompt
pkg_agent_loop --> pkg_tools
+ pkg_agent_tool_mode --> pkg_invariants
+ pkg_agent_tool_mode --> pkg_tools
pkg_tool_goal --> pkg_agent
pkg_tool_goal --> pkg_goal
pkg_tool_goal --> pkg_invariants
@@ -749,6 +777,22 @@ flowchart TD
pkg_bash_env --> pkg_paths
pkg_bash_env --> pkg_session_persistence
pkg_bash_env --> pkg_tools
+ pkg_bash_sandbox --> pkg_bash
+ pkg_bash_sandbox --> pkg_bash_local
+ pkg_bash_sandbox --> pkg_invariants
+ pkg_bash_sandbox --> pkg_sandbox
+ pkg_bash_sandbox --> pkg_sandbox_policy
+ pkg_pwsh_sandbox --> pkg_bash
+ pkg_pwsh_sandbox --> pkg_invariants
+ pkg_pwsh_sandbox --> pkg_pwsh_local
+ pkg_pwsh_sandbox --> pkg_sandbox
+ pkg_pwsh_sandbox --> pkg_sandbox_policy
+ pkg_fs_sandbox --> pkg_fs
+ pkg_fs_sandbox --> pkg_fs_local
+ pkg_fs_sandbox --> pkg_invariants
+ pkg_fs_sandbox --> pkg_sandbox
+ pkg_fs_sandbox --> pkg_sandbox_policy
+ pkg_tool_fs --> pkg_attachment
pkg_tool_fs --> pkg_fs
pkg_tool_fs --> pkg_invariants
pkg_tool_fs --> pkg_llm
@@ -781,9 +825,12 @@ flowchart TD
pkg_command_compact --> pkg_compact
pkg_command_compact --> pkg_invariants
pkg_subagent --> pkg_agent
+ pkg_subagent --> pkg_agent_presets
pkg_subagent --> pkg_brand
pkg_subagent --> pkg_invariants
pkg_subagent --> pkg_llm
+ pkg_subagent --> pkg_sandbox
+ pkg_subagent --> pkg_sandbox_policy
pkg_subagent --> pkg_scope
pkg_subagent --> pkg_session
pkg_subagent --> pkg_session_persistence
@@ -791,6 +838,7 @@ flowchart TD
pkg_subagent --> pkg_session_projection_cache
pkg_subagent --> pkg_tasks
pkg_subagent --> pkg_tools
+ pkg_subagent --> pkg_user_approval
pkg_tool_web --> pkg_invariants
pkg_tool_web --> pkg_llm
pkg_tool_web --> pkg_system_prompt
@@ -844,6 +892,18 @@ flowchart TD
pkg_llm_replay --> pkg_invariants
pkg_llm_replay --> pkg_llm
pkg_llm_replay --> pkg_session
+ pkg_client_test_runtime --> pkg_client_runtime
+ pkg_client_test_runtime --> pkg_client_ui_slots
+ pkg_client_test_runtime --> pkg_client_web_react
+ pkg_client_test_runtime --> pkg_host_apiproxy
+ pkg_client_test_runtime --> pkg_invariants
+ pkg_client_ui_trajectory --> pkg_agent
+ pkg_client_ui_trajectory --> pkg_client_locale
+ pkg_client_ui_trajectory --> pkg_client_runtime
+ pkg_client_ui_trajectory --> pkg_client_ui_primitives
+ pkg_client_ui_trajectory --> pkg_compact
+ pkg_client_ui_trajectory --> pkg_invariants
+ pkg_client_ui_trajectory --> pkg_tools
pkg_session_reference --> pkg_agent
pkg_session_reference --> pkg_compact
pkg_session_reference --> pkg_invariants
@@ -878,6 +938,7 @@ flowchart TD
pkg_mcp_client --> pkg_invariants
pkg_mcp_client --> pkg_llm
pkg_mcp_client --> pkg_subprocess
+ pkg_mcp_client --> pkg_timeout
pkg_mcp_client --> pkg_tools
pkg_tool_bash_persistent --> pkg_agent
pkg_tool_bash_persistent --> pkg_invariants
@@ -908,13 +969,12 @@ flowchart TD
pkg_session_checkpoint_policy --> pkg_session
pkg_session_checkpoint_policy --> pkg_session_persistence
pkg_session_checkpoint_policy --> pkg_tools
- pkg_session_telemetry_otel --> pkg_brand
pkg_session_telemetry_otel --> pkg_command_feedback
pkg_session_telemetry_otel --> pkg_invariants
pkg_session_telemetry_otel --> pkg_llm
- pkg_session_telemetry_otel --> pkg_paths
pkg_session_telemetry_otel --> pkg_session
pkg_session_telemetry_otel --> pkg_session_telemetry
+ pkg_session_telemetry_otel --> pkg_user_id
pkg_session_title_all_messages_llm --> pkg_invariants
pkg_session_title_all_messages_llm --> pkg_llm
pkg_session_title_all_messages_llm --> pkg_session
@@ -935,6 +995,7 @@ flowchart TD
pkg_tool_workflow --> pkg_agent
pkg_tool_workflow --> pkg_invariants
pkg_tool_workflow --> pkg_llm
+ pkg_tool_workflow --> pkg_session
pkg_tool_workflow --> pkg_system_prompt
pkg_tool_workflow --> pkg_tools
pkg_tool_workflow --> pkg_workflow
@@ -954,9 +1015,12 @@ flowchart TD
pkg_tool_pwsh --> pkg_bash_env
pkg_tool_pwsh --> pkg_invariants
pkg_tool_pwsh --> pkg_llm
+ pkg_tool_pwsh --> pkg_sandbox
+ pkg_tool_pwsh --> pkg_sandbox_policy
pkg_tool_pwsh --> pkg_system_prompt
pkg_tool_pwsh --> pkg_tasks
pkg_tool_pwsh --> pkg_tools
+ pkg_tool_pwsh --> pkg_user_approval
pkg_compact_tool_result_prune --> pkg_compact
pkg_compact_tool_result_prune --> pkg_invariants
pkg_compact_tool_result_prune --> pkg_llm
@@ -978,12 +1042,10 @@ flowchart TD
pkg_subagent_inprocess --> pkg_agent
pkg_subagent_inprocess --> pkg_invariants
pkg_subagent_inprocess --> pkg_llm
- pkg_subagent_inprocess --> pkg_sandbox_policy
pkg_subagent_inprocess --> pkg_session
pkg_subagent_inprocess --> pkg_subagent
pkg_subagent_inprocess --> pkg_system_prompt
pkg_subagent_inprocess --> pkg_tools
- pkg_subagent_inprocess --> pkg_user_approval
pkg_tool_subagent --> pkg_agent
pkg_tool_subagent --> pkg_invariants
pkg_tool_subagent --> pkg_llm
@@ -998,6 +1060,7 @@ flowchart TD
pkg_tool_subagent_report --> pkg_invariants
pkg_tool_subagent_report --> pkg_llm
pkg_tool_subagent_report --> pkg_subagent
+ pkg_tool_subagent_report --> pkg_system_prompt
pkg_tool_subagent_report --> pkg_tools
pkg_hooks_claude --> pkg_agent
pkg_hooks_claude --> pkg_hook_protocol
@@ -1011,6 +1074,9 @@ flowchart TD
pkg_web_app --> pkg_invariants
pkg_web_app --> pkg_system_prompt
pkg_client_ui_conversation --> pkg_agent
+ pkg_client_ui_conversation --> pkg_attachment
+ pkg_client_ui_conversation --> pkg_brand
+ pkg_client_ui_conversation --> pkg_client_connection
pkg_client_ui_conversation --> pkg_client_locale
pkg_client_ui_conversation --> pkg_client_runtime
pkg_client_ui_conversation --> pkg_client_ui_primitives
@@ -1026,10 +1092,6 @@ flowchart TD
pkg_sdk_protocol --> pkg_llm
pkg_sdk_protocol --> pkg_session
pkg_sdk_protocol --> pkg_subagent
- pkg_repository_plugin --> pkg_invariants
- pkg_repository_plugin --> pkg_mcp_client
- pkg_repository_plugin --> pkg_paths
- pkg_repository_plugin --> pkg_skill_local
pkg_tool_ralph --> pkg_agent
pkg_tool_ralph --> pkg_invariants
pkg_tool_ralph --> pkg_llm
@@ -1068,6 +1130,15 @@ flowchart TD
pkg_subagent_spawn --> pkg_invariants
pkg_subagent_spawn --> pkg_subagent
pkg_subagent_spawn --> pkg_subagent_inprocess
+ pkg_client_ui_agent_preset --> pkg_client_connection
+ pkg_client_ui_agent_preset --> pkg_client_locale
+ pkg_client_ui_agent_preset --> pkg_client_runtime
+ pkg_client_ui_agent_preset --> pkg_client_ui_conversation
+ pkg_client_ui_agent_preset --> pkg_client_ui_primitives
+ pkg_client_ui_agent_preset --> pkg_client_ui_settings
+ pkg_client_ui_agent_preset --> pkg_client_ui_slots
+ pkg_client_ui_agent_preset --> pkg_client_web_react
+ pkg_client_ui_agent_preset --> pkg_invariants
pkg_client_ui_command --> pkg_client_connection
pkg_client_ui_command --> pkg_client_locale
pkg_client_ui_command --> pkg_client_runtime
@@ -1087,6 +1158,7 @@ flowchart TD
pkg_client_ui_goal --> pkg_client_ui_conversation
pkg_client_ui_goal --> pkg_client_ui_primitives
pkg_client_ui_goal --> pkg_client_ui_slots
+ pkg_client_ui_goal --> pkg_commands
pkg_client_ui_goal --> pkg_goal
pkg_client_ui_goal --> pkg_invariants
pkg_client_ui_plan --> pkg_client_connection
@@ -1106,12 +1178,27 @@ flowchart TD
pkg_client_ui_subagent --> pkg_invariants
pkg_client_ui_subagent --> pkg_subagent
pkg_client_ui_subagent --> pkg_token_meter
+ pkg_client_ui_task --> pkg_client_locale
+ pkg_client_ui_task --> pkg_client_runtime
+ pkg_client_ui_task --> pkg_client_ui_conversation
+ pkg_client_ui_task --> pkg_client_ui_primitives
+ pkg_client_ui_task --> pkg_client_ui_slots
+ pkg_client_ui_task --> pkg_invariants
pkg_client_ui_tool --> pkg_client_locale
pkg_client_ui_tool --> pkg_client_runtime
pkg_client_ui_tool --> pkg_client_ui_conversation
pkg_client_ui_tool --> pkg_client_ui_primitives
pkg_client_ui_tool --> pkg_client_ui_slots
pkg_client_ui_tool --> pkg_invariants
+ pkg_client_ui_workflow_run --> pkg_client_locale
+ pkg_client_ui_workflow_run --> pkg_client_runtime
+ pkg_client_ui_workflow_run --> pkg_client_ui_conversation
+ pkg_client_ui_workflow_run --> pkg_client_ui_primitives
+ pkg_client_ui_workflow_run --> pkg_client_ui_slots
+ pkg_client_ui_workflow_run --> pkg_invariants
+ pkg_client_ui_workflow_run --> pkg_session
+ pkg_client_ui_workflow_run --> pkg_tool_workflow
+ pkg_client_ui_workflow_run --> pkg_workflow
pkg_agent_spine_demo --> pkg_agent
pkg_agent_spine_demo --> pkg_agent_loop
pkg_agent_spine_demo --> pkg_bash_env
@@ -1204,6 +1291,7 @@ flowchart TD
| [`timeout`](../packages/util/timeout) | `util` | [`invariants`](../packages/support/invariants) |
| [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/support/invariants) |
| [`llm-mock-server`](../packages/support/llm-mock-server) | `support` | [`invariants`](../packages/support/invariants) |
+| [`cmdline`](../packages/boot/cmdline) | `boot` | [`invariants`](../packages/support/invariants) |
| [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/support/invariants) |
| [`client-modules`](../packages/client/modules) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-schema-form`](../packages/client/schema-form) | `client` | [`invariants`](../packages/support/invariants) |
@@ -1214,48 +1302,50 @@ flowchart TD
| [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | [`invariants`](../packages/support/invariants) |
| [`e2b`](../packages/e2b/e2b) | `e2b` | [`invariants`](../packages/support/invariants) |
| [`jsonrpc-demo`](../packages/examples/jsonrpc-demo) | `examples` | [`invariants`](../packages/support/invariants) |
-| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`invariants`](../packages/support/invariants) |
| [`host-directory-picker`](../packages/host/directory-picker) | `host` | [`invariants`](../packages/support/invariants) |
| [`host-webserver`](../packages/host/webserver) | `host` | [`invariants`](../packages/support/invariants) |
+| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`invariants`](../packages/support/invariants) |
| [`storage`](../packages/storage/storage) | `storage` | [`invariants`](../packages/support/invariants) |
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`invariants`](../packages/support/invariants) |
| [`type-meta`](../packages/typert/type-meta) | `typert` | [`invariants`](../packages/support/invariants) |
| [`typert-generator`](../packages/typert/generator) | `typert` | [`invariants`](../packages/support/invariants) |
| [`typert-registry`](../packages/typert/registry) | `typert` | [`invariants`](../packages/support/invariants) |
-| [`llm`](../packages/llm/llm) | `llm` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout) |
+| [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`client-connection`](../packages/client/connection) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`client-runtime`](../packages/client/runtime) | `client` | [`invariants`](../packages/support/invariants), [`type-meta`](../packages/typert/type-meta), [`typert-registry`](../packages/typert/registry) |
| [`credentials`](../packages/credentials/credentials) | `credentials` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
-| [`helper`](../packages/scaffold/helper) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess) |
-| [`telemetry`](../packages/scaffold/telemetry) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
+| [`user-id`](../packages/session/user-id) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
| [`storage-json`](../packages/storage/storage-json) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`typert-loader`](../packages/typert/loader) | `typert` | [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) |
-| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
-| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
-| [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`type-meta`](../packages/typert/type-meta) |
-| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
-| [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
-| [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
+| [`llm`](../packages/llm/llm) | `llm` | [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout) |
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) |
-| [`client-locale`](../packages/client/locale) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) |
+| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
+| [`client-locale`](../packages/client/locale) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`invariants`](../packages/support/invariants) |
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
-| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
-| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
| [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) |
+| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
+| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
+| [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`type-meta`](../packages/typert/type-meta) |
+| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
+| [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
+| [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
+| [`client-ui-question`](../packages/client/ui-question) | `client` | [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) |
+| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
+| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
+| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`type-meta`](../packages/typert/type-meta) |
-| [`bash`](../packages/bash/bash) | `bash` | [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`subprocess`](../packages/subprocess/subprocess) |
-| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`invariants`](../packages/support/invariants), [`skill`](../packages/skill/skill) |
| [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
| [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`web`](../packages/web/web) |
@@ -1263,41 +1353,32 @@ flowchart TD
| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) |
-| [`client-ui-question`](../packages/client/ui-question) | `client` | [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) |
-| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
-| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) |
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
-| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
+| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
+| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
+| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
+| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings) |
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`type-meta`](../packages/typert/type-meta) |
-| [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`pwsh-local`](../packages/bash/pwsh-local) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
-| [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
-| [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`skill`](../packages/skill/skill) |
+| [`bash`](../packages/bash/bash) | `bash` | [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`subprocess`](../packages/subprocess/subprocess) |
+| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`web`](../packages/web/web) |
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/support/invariants), [`spill`](../packages/spill/spill) |
-| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
-| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
-| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
-| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
-| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
+| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`type-meta`](../packages/typert/type-meta) |
+| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`user-interaction`](../packages/interaction/user-interaction) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
-| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt) |
| [`pty`](../packages/pty/pty) | `pty` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
+| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
-| [`scripts`](../packages/scaffold/scripts) | `scaffold` | [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/support/invariants) |
| [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
| [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) |
@@ -1309,29 +1390,40 @@ flowchart TD
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) |
| [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) |
| [`goal-session`](../packages/goal/goal-session) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
-| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
+| [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`pwsh-local`](../packages/bash/pwsh-local) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
+| [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
+| [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`skill`](../packages/skill/skill) |
| [`compact`](../packages/compact/compact) | `compact` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
+| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title) |
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/interaction/user-approval) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-registry`](../packages/typert/registry) |
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
-| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
+| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
+| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
+| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry), [`user-id`](../packages/session/user-id) |
+| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
| [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) |
+| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) |
| [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) |
-| [`tasks-local`](../packages/tasks/tasks-local) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tasks`](../packages/tasks/tasks), [`timeout`](../packages/util/timeout) |
+| [`tasks-local`](../packages/tasks/tasks-local) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tasks`](../packages/tasks/tasks), [`timeout`](../packages/util/timeout) |
| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`agent-tool-mode`](../packages/core/agent-tool-mode) | `core` | [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`bash-env`](../packages/bash/bash-env) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
-| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
+| [`pwsh-sandbox`](../packages/bash/pwsh-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`pwsh-local`](../packages/bash/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
+| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
+| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`command-compact`](../packages/compact/command-compact) | `compact` | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants) |
-| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
+| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
@@ -1341,53 +1433,57 @@ flowchart TD
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`llm-replay`](../packages/support/llm-replay) | `support` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
+| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) |
+| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
| [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-ask-user`](../packages/interaction/tool-ask-user) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools), [`user-interaction`](../packages/interaction/user-interaction) |
| [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) |
+| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-bash-persistent`](../packages/pty/tool-bash-persistent) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-pty`](../packages/pty/tool-pty) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`pty`](../packages/pty/pty), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-schedule`](../packages/schedule/tool-schedule) | `schedule` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
| [`tool-cordis`](../packages/self-modification/tool-cordis) | `self-modification` | [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) |
| [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
-| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`brand`](../packages/util/brand), [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
+| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry), [`user-id`](../packages/session/user-id) |
| [`session-title-all-messages-llm`](../packages/session/session-title-all-messages-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
| [`session-title-first-message-llm`](../packages/session/session-title-first-message-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
| [`tool-tasks`](../packages/tasks/tool-tasks) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
-| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
+| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
-| [`tool-pwsh`](../packages/bash/tool-pwsh) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
+| [`tool-pwsh`](../packages/bash/tool-pwsh) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
-| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
+| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
-| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
-| [`sdk-protocol`](../packages/scaffold/protocol) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
-| [`repository-plugin`](../packages/self-modification/repository-plugin) | `self-modification` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) |
+| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
+| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
-| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
| [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
+| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-command`](../packages/client/ui-command) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) |
+| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) |
| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`plan-mode`](../packages/plan/plan-mode) |
| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) |
+| [`client-ui-task`](../packages/client/ui-task) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+| [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`bash-env`](../packages/bash/bash-env), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks-local`](../packages/tasks/tasks-local), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) |
-| [`jsonrpc`](../packages/scaffold/server) | `scaffold` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
-| [`sdk-client`](../packages/scaffold/client) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session) |
-| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/scaffold/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
+| [`jsonrpc`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
+| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
+| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`client-ui-model`](../packages/client/ui-model) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-command`](../packages/client/ui-command), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-permission`](../packages/client/ui-permission) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-command`](../packages/client/ui-command), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`permission`](../packages/interaction/permission) |
| [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-tool`](../packages/client/ui-tool), [`invariants`](../packages/support/invariants) |
diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md
index cd882722c2..3cc7c9228c 100644
--- a/docs/module-graph.zh.md
+++ b/docs/module-graph.zh.md
@@ -29,6 +29,7 @@ flowchart TD
pkg_agent["agent"]
pkg_agent_default_model["agent-default-model"]
pkg_agent_loop["agent-loop"]
+ pkg_agent_tool_mode["agent-tool-mode"]
pkg_scope["scope"]
pkg_session["session"]
pkg_system_prompt["system-prompt"]
@@ -46,6 +47,7 @@ flowchart TD
pkg_bash_local["bash-local"]
pkg_bash_sandbox["bash-sandbox"]
pkg_pwsh_local["pwsh-local"]
+ pkg_pwsh_sandbox["pwsh-sandbox"]
pkg_tool_bash["tool-bash"]
pkg_tool_pwsh["tool-pwsh"]
end
@@ -127,8 +129,13 @@ flowchart TD
pkg_api_gateway["api-gateway"]
pkg_api_remotes["api-remotes"]
end
+ subgraph group_attachment["packages/attachment"]
+ pkg_attachment["attachment"]
+ pkg_attachment_local["attachment-local"]
+ end
subgraph group_boot["packages/boot"]
pkg_app_boot["app-boot"]
+ pkg_cmdline["cmdline"]
end
subgraph group_bundle["packages/bundle"]
pkg_base["base"]
@@ -143,6 +150,7 @@ flowchart TD
pkg_client_runtime["client-runtime"]
pkg_client_schema_form["client-schema-form"]
pkg_client_test_runtime["client-test-runtime"]
+ pkg_client_ui_agent_preset["client-ui-agent-preset"]
pkg_client_ui_command["client-ui-command"]
pkg_client_ui_conversation["client-ui-conversation"]
pkg_client_ui_deliverables["client-ui-deliverables"]
@@ -161,9 +169,11 @@ flowchart TD
pkg_client_ui_slash["client-ui-slash"]
pkg_client_ui_slots["client-ui-slots"]
pkg_client_ui_subagent["client-ui-subagent"]
+ pkg_client_ui_task["client-ui-task"]
pkg_client_ui_theme["client-ui-theme"]
pkg_client_ui_tool["client-ui-tool"]
pkg_client_ui_trajectory["client-ui-trajectory"]
+ pkg_client_ui_workflow_run["client-ui-workflow-run"]
pkg_client_ui_workspace["client-ui-workspace"]
pkg_client_web["client-web"]
pkg_client_web_react["client-web-react"]
@@ -194,6 +204,7 @@ flowchart TD
end
subgraph group_feedback["packages/feedback"]
pkg_command_feedback["command-feedback"]
+ pkg_message_feedback["message-feedback"]
end
subgraph group_guard["packages/guard"]
pkg_repeat_tool_guard["repeat-tool-guard"]
@@ -223,6 +234,10 @@ flowchart TD
subgraph group_mcp["packages/mcp"]
pkg_mcp_client["mcp-client"]
end
+ subgraph group_preset["packages/preset"]
+ pkg_agent_presets["agent-presets"]
+ pkg_persona["persona"]
+ end
subgraph group_pty["packages/pty"]
pkg_pty["pty"]
pkg_pty_local["pty-local"]
@@ -233,20 +248,17 @@ flowchart TD
pkg_sandbox["sandbox"]
pkg_sandbox_local["sandbox-local"]
pkg_sandbox_policy["sandbox-policy"]
- end
- subgraph group_scaffold["packages/scaffold"]
- pkg_helper["helper"]
- pkg_jsonrpc["jsonrpc"]
- pkg_scripts["scripts"]
- pkg_sdk_client["sdk-client"]
- pkg_sdk_protocol["sdk-protocol"]
- pkg_telemetry["telemetry"]
+ pkg_sandbox_windows_acl["sandbox-windows-acl"]
end
subgraph group_schedule["packages/schedule"]
pkg_tool_schedule["tool-schedule"]
end
+ subgraph group_sdk["packages/sdk"]
+ pkg_jsonrpc["jsonrpc"]
+ pkg_sdk_client["sdk-client"]
+ pkg_sdk_protocol["sdk-protocol"]
+ end
subgraph group_self_modification["packages/self-modification"]
- pkg_repository_plugin["repository-plugin"]
pkg_tool_cordis["tool-cordis"]
end
subgraph group_session["packages/session"]
@@ -262,6 +274,7 @@ flowchart TD
pkg_session_title_all_messages_llm["session-title-all-messages-llm"]
pkg_session_title_first_message_llm["session-title-first-message-llm"]
pkg_session_title_llm["session-title-llm"]
+ pkg_user_id["user-id"]
end
subgraph group_settings["packages/settings"]
pkg_settings["settings"]
@@ -306,6 +319,7 @@ flowchart TD
pkg_timeout --> pkg_invariants
pkg_scope --> pkg_invariants
pkg_llm_mock_server --> pkg_invariants
+ pkg_cmdline --> pkg_invariants
pkg_base --> pkg_invariants
pkg_client_modules --> pkg_invariants
pkg_client_schema_form --> pkg_invariants
@@ -316,17 +330,16 @@ flowchart TD
pkg_code_runtime --> pkg_invariants
pkg_e2b --> pkg_invariants
pkg_jsonrpc_demo --> pkg_invariants
- pkg_host_apiproxy --> pkg_invariants
pkg_host_directory_picker --> pkg_invariants
pkg_host_webserver --> pkg_invariants
+ pkg_sandbox_windows_acl --> pkg_invariants
pkg_storage --> pkg_invariants
pkg_subprocess --> pkg_invariants
pkg_type_meta --> pkg_invariants
pkg_typert_generator --> pkg_invariants
pkg_typert_registry --> pkg_invariants
- pkg_llm --> pkg_brand
- pkg_llm --> pkg_invariants
- pkg_llm --> pkg_timeout
+ pkg_attachment --> pkg_brand
+ pkg_attachment --> pkg_invariants
pkg_client_connection --> pkg_host_webserver
pkg_client_connection --> pkg_invariants
pkg_client_hmr --> pkg_client_modules
@@ -343,12 +356,9 @@ flowchart TD
pkg_subprocess_e2b --> pkg_timeout
pkg_frontend_static --> pkg_host_webserver
pkg_frontend_static --> pkg_invariants
- pkg_helper --> pkg_brand
- pkg_helper --> pkg_invariants
- pkg_helper --> pkg_subprocess
- pkg_telemetry --> pkg_brand
- pkg_telemetry --> pkg_invariants
- pkg_telemetry --> pkg_paths
+ pkg_user_id --> pkg_brand
+ pkg_user_id --> pkg_invariants
+ pkg_user_id --> pkg_paths
pkg_settings --> pkg_brand
pkg_settings --> pkg_invariants
pkg_storage_domain --> pkg_invariants
@@ -362,12 +372,48 @@ flowchart TD
pkg_subprocess_local --> pkg_timeout
pkg_typert_loader --> pkg_invariants
pkg_typert_loader --> pkg_typert_registry
+ pkg_llm --> pkg_attachment
+ pkg_llm --> pkg_brand
+ pkg_llm --> pkg_invariants
+ pkg_llm --> pkg_timeout
+ pkg_api_gateway --> pkg_client_connection
+ pkg_api_gateway --> pkg_invariants
+ pkg_api_gateway --> pkg_typert_registry
+ pkg_attachment_local --> pkg_attachment
+ pkg_attachment_local --> pkg_invariants
+ pkg_attachment_local --> pkg_paths
+ pkg_client_locale --> pkg_client_connection
+ pkg_client_locale --> pkg_client_runtime
+ pkg_client_locale --> pkg_client_ui_primitives
+ pkg_client_locale --> pkg_client_ui_slots
+ pkg_client_locale --> pkg_invariants
+ pkg_client_ui_models --> pkg_client_connection
+ pkg_client_ui_models --> pkg_client_runtime
+ pkg_client_ui_models --> pkg_client_schema_form
+ pkg_client_ui_models --> pkg_client_ui_primitives
+ pkg_client_ui_models --> pkg_client_ui_slots
+ pkg_client_ui_models --> pkg_client_web_react
+ pkg_client_ui_models --> pkg_invariants
+ pkg_client_ui_settings --> pkg_client_runtime
+ pkg_client_ui_settings --> pkg_client_ui_primitives
+ pkg_client_ui_settings --> pkg_client_ui_slots
+ pkg_client_ui_settings --> pkg_invariants
+ pkg_credentials_local --> pkg_atomic_write
+ pkg_credentials_local --> pkg_credentials
+ pkg_credentials_local --> pkg_environment
+ pkg_credentials_local --> pkg_invariants
+ pkg_credentials_local --> pkg_paths
+ pkg_settings_local --> pkg_atomic_write
+ pkg_settings_local --> pkg_invariants
+ pkg_settings_local --> pkg_paths
+ pkg_settings_local --> pkg_settings
pkg_llm_deepseek --> pkg_credentials
pkg_llm_deepseek --> pkg_environment
pkg_llm_deepseek --> pkg_invariants
pkg_llm_deepseek --> pkg_llm
pkg_llm_deepseek --> pkg_settings
pkg_llm_deepseek --> pkg_timeout
+ pkg_llm_pi_ai --> pkg_attachment
pkg_llm_pi_ai --> pkg_credentials
pkg_llm_pi_ai --> pkg_environment
pkg_llm_pi_ai --> pkg_invariants
@@ -384,61 +430,50 @@ flowchart TD
pkg_system_prompt --> pkg_scope
pkg_skill --> pkg_invariants
pkg_skill --> pkg_llm
+ pkg_skill --> pkg_scope
pkg_web --> pkg_invariants
pkg_web --> pkg_llm
- pkg_api_gateway --> pkg_client_connection
- pkg_api_gateway --> pkg_invariants
- pkg_api_gateway --> pkg_typert_registry
- pkg_client_locale --> pkg_client_runtime
- pkg_client_locale --> pkg_client_ui_primitives
- pkg_client_locale --> pkg_client_ui_slots
- pkg_client_locale --> pkg_invariants
- pkg_client_test_runtime --> pkg_client_runtime
- pkg_client_test_runtime --> pkg_client_ui_slots
- pkg_client_test_runtime --> pkg_client_web_react
- pkg_client_test_runtime --> pkg_host_apiproxy
- pkg_client_test_runtime --> pkg_invariants
- pkg_client_ui_models --> pkg_client_connection
- pkg_client_ui_models --> pkg_client_runtime
- pkg_client_ui_models --> pkg_client_schema_form
- pkg_client_ui_models --> pkg_client_ui_primitives
- pkg_client_ui_models --> pkg_client_ui_slots
- pkg_client_ui_models --> pkg_client_web_react
- pkg_client_ui_models --> pkg_invariants
- pkg_client_ui_settings --> pkg_client_runtime
- pkg_client_ui_settings --> pkg_client_ui_primitives
- pkg_client_ui_settings --> pkg_client_ui_slots
- pkg_client_ui_settings --> pkg_invariants
- pkg_client_ui_trajectory --> pkg_client_runtime
- pkg_client_ui_trajectory --> pkg_client_ui_primitives
- pkg_client_ui_trajectory --> pkg_invariants
- pkg_credentials_local --> pkg_atomic_write
- pkg_credentials_local --> pkg_credentials
- pkg_credentials_local --> pkg_environment
- pkg_credentials_local --> pkg_invariants
- pkg_credentials_local --> pkg_paths
+ pkg_client_ui_question --> pkg_client_locale
+ pkg_client_ui_question --> pkg_invariants
+ pkg_client_ui_settings_general --> pkg_client_connection
+ pkg_client_ui_settings_general --> pkg_client_locale
+ pkg_client_ui_settings_general --> pkg_client_runtime
+ pkg_client_ui_settings_general --> pkg_client_ui_primitives
+ pkg_client_ui_settings_general --> pkg_client_ui_settings
+ pkg_client_ui_settings_general --> pkg_client_ui_slots
+ pkg_client_ui_settings_general --> pkg_client_web_react
+ pkg_client_ui_settings_general --> pkg_invariants
+ pkg_client_ui_sidebar --> pkg_client_locale
+ pkg_client_ui_sidebar --> pkg_client_runtime
+ pkg_client_ui_sidebar --> pkg_client_ui_primitives
+ pkg_client_ui_sidebar --> pkg_client_ui_slots
+ pkg_client_ui_sidebar --> pkg_invariants
+ pkg_client_ui_slash --> pkg_client_locale
+ pkg_client_ui_slash --> pkg_client_runtime
+ pkg_client_ui_slash --> pkg_client_ui_primitives
+ pkg_client_ui_slash --> pkg_client_ui_slots
+ pkg_client_ui_slash --> pkg_invariants
+ pkg_client_ui_theme --> pkg_client_connection
+ pkg_client_ui_theme --> pkg_client_locale
+ pkg_client_ui_theme --> pkg_client_runtime
+ pkg_client_ui_theme --> pkg_client_ui_primitives
+ pkg_client_ui_theme --> pkg_client_ui_slots
+ pkg_client_ui_theme --> pkg_host_webserver
+ pkg_client_ui_theme --> pkg_invariants
+ pkg_client_ui_workspace --> pkg_client_locale
+ pkg_client_ui_workspace --> pkg_client_runtime
+ pkg_client_ui_workspace --> pkg_client_ui_primitives
+ pkg_client_ui_workspace --> pkg_client_ui_slots
+ pkg_client_ui_workspace --> pkg_invariants
pkg_lsp --> pkg_brand
pkg_lsp --> pkg_invariants
pkg_lsp --> pkg_llm
- pkg_sandbox --> pkg_invariants
- pkg_sandbox --> pkg_llm
- pkg_settings_local --> pkg_atomic_write
- pkg_settings_local --> pkg_invariants
- pkg_settings_local --> pkg_paths
- pkg_settings_local --> pkg_settings
pkg_agent --> pkg_invariants
pkg_agent --> pkg_llm
pkg_agent --> pkg_scope
pkg_agent --> pkg_session
pkg_agent --> pkg_system_prompt
pkg_agent --> pkg_type_meta
- pkg_bash --> pkg_invariants
- pkg_bash --> pkg_sandbox
- pkg_bash --> pkg_subprocess
- pkg_fs --> pkg_brand
- pkg_fs --> pkg_invariants
- pkg_fs --> pkg_llm
- pkg_fs --> pkg_sandbox
pkg_skill_badge --> pkg_invariants
pkg_skill_badge --> pkg_skill
pkg_web_fetch_local --> pkg_invariants
@@ -460,43 +495,29 @@ flowchart TD
pkg_app_boot --> pkg_invariants
pkg_app_boot --> pkg_paths
pkg_app_boot --> pkg_system_prompt
- pkg_client_ui_question --> pkg_client_locale
- pkg_client_ui_question --> pkg_invariants
- pkg_client_ui_settings_general --> pkg_client_connection
- pkg_client_ui_settings_general --> pkg_client_locale
- pkg_client_ui_settings_general --> pkg_client_runtime
- pkg_client_ui_settings_general --> pkg_client_ui_primitives
- pkg_client_ui_settings_general --> pkg_client_ui_settings
- pkg_client_ui_settings_general --> pkg_client_ui_slots
- pkg_client_ui_settings_general --> pkg_client_web_react
- pkg_client_ui_settings_general --> pkg_invariants
- pkg_client_ui_sidebar --> pkg_client_locale
- pkg_client_ui_sidebar --> pkg_client_runtime
- pkg_client_ui_sidebar --> pkg_client_ui_primitives
- pkg_client_ui_sidebar --> pkg_client_ui_slots
- pkg_client_ui_sidebar --> pkg_invariants
- pkg_client_ui_slash --> pkg_client_locale
- pkg_client_ui_slash --> pkg_client_runtime
- pkg_client_ui_slash --> pkg_client_ui_primitives
- pkg_client_ui_slash --> pkg_client_ui_slots
- pkg_client_ui_slash --> pkg_invariants
- pkg_client_ui_theme --> pkg_client_locale
- pkg_client_ui_theme --> pkg_client_runtime
- pkg_client_ui_theme --> pkg_client_ui_primitives
- pkg_client_ui_theme --> pkg_client_ui_slots
- pkg_client_ui_theme --> pkg_invariants
- pkg_client_ui_workspace --> pkg_client_locale
- pkg_client_ui_workspace --> pkg_client_runtime
- pkg_client_ui_workspace --> pkg_client_ui_primitives
- pkg_client_ui_workspace --> pkg_client_ui_slots
- pkg_client_ui_workspace --> pkg_invariants
+ pkg_client_ui_layout --> pkg_client_runtime
+ pkg_client_ui_layout --> pkg_client_ui_slots
+ pkg_client_ui_layout --> pkg_client_ui_theme
+ pkg_client_ui_layout --> pkg_invariants
pkg_code_runtime_worker --> pkg_code_runtime
pkg_code_runtime_worker --> pkg_invariants
pkg_code_runtime_worker --> pkg_session
pkg_code_runtime_worker --> pkg_timeout
- pkg_sandbox_local --> pkg_invariants
- pkg_sandbox_local --> pkg_llm
- pkg_sandbox_local --> pkg_sandbox
+ pkg_host_directory_picker_browse --> pkg_client_locale
+ pkg_host_directory_picker_browse --> pkg_client_runtime
+ pkg_host_directory_picker_browse --> pkg_client_ui_primitives
+ pkg_host_directory_picker_browse --> pkg_client_ui_slots
+ pkg_host_directory_picker_browse --> pkg_client_ui_workspace
+ pkg_host_directory_picker_browse --> pkg_invariants
+ pkg_host_directory_picker_native --> pkg_client_runtime
+ pkg_host_directory_picker_native --> pkg_client_ui_slots
+ pkg_host_directory_picker_native --> pkg_client_ui_workspace
+ pkg_host_directory_picker_native --> pkg_invariants
+ pkg_persona --> pkg_invariants
+ pkg_persona --> pkg_system_prompt
+ pkg_sandbox --> pkg_invariants
+ pkg_sandbox --> pkg_llm
+ pkg_sandbox --> pkg_session
pkg_session_persistence --> pkg_brand
pkg_session_persistence --> pkg_invariants
pkg_session_persistence --> pkg_session
@@ -521,22 +542,13 @@ flowchart TD
pkg_goal --> pkg_session
pkg_goal --> pkg_session_projection
pkg_goal --> pkg_type_meta
- pkg_bash_local --> pkg_bash
- pkg_bash_local --> pkg_invariants
- pkg_bash_local --> pkg_subprocess
- pkg_bash_local --> pkg_timeout
- pkg_pwsh_local --> pkg_bash
- pkg_pwsh_local --> pkg_invariants
- pkg_pwsh_local --> pkg_subprocess
- pkg_pwsh_local --> pkg_timeout
- pkg_fs_local --> pkg_fs
- pkg_fs_local --> pkg_invariants
- pkg_fs_policy --> pkg_fs
- pkg_fs_policy --> pkg_invariants
- pkg_skill_local --> pkg_fs
- pkg_skill_local --> pkg_invariants
- pkg_skill_local --> pkg_paths
- pkg_skill_local --> pkg_skill
+ pkg_bash --> pkg_invariants
+ pkg_bash --> pkg_sandbox
+ pkg_bash --> pkg_subprocess
+ pkg_fs --> pkg_brand
+ pkg_fs --> pkg_invariants
+ pkg_fs --> pkg_llm
+ pkg_fs --> pkg_sandbox
pkg_web_search_deepseek --> pkg_agent
pkg_web_search_deepseek --> pkg_credentials
pkg_web_search_deepseek --> pkg_environment
@@ -545,37 +557,24 @@ flowchart TD
pkg_web_search_deepseek --> pkg_web
pkg_spill_local --> pkg_invariants
pkg_spill_local --> pkg_spill
- pkg_hook_protocol --> pkg_bash
- pkg_hook_protocol --> pkg_invariants
- pkg_hook_protocol --> pkg_session
pkg_loader_smoke --> pkg_agent
pkg_loader_smoke --> pkg_invariants
pkg_loader_smoke --> pkg_llm
pkg_loader_smoke --> pkg_session
- pkg_client_ui_layout --> pkg_client_runtime
- pkg_client_ui_layout --> pkg_client_ui_slots
- pkg_client_ui_layout --> pkg_client_ui_theme
- pkg_client_ui_layout --> pkg_invariants
pkg_time_context --> pkg_agent
pkg_time_context --> pkg_invariants
pkg_time_context --> pkg_session
- pkg_tmux_context --> pkg_agent
- pkg_tmux_context --> pkg_bash
- pkg_tmux_context --> pkg_invariants
- pkg_tmux_context --> pkg_session
- pkg_fs_e2b --> pkg_e2b
- pkg_fs_e2b --> pkg_fs
- pkg_fs_e2b --> pkg_invariants
- pkg_host_directory_picker_browse --> pkg_client_locale
- pkg_host_directory_picker_browse --> pkg_client_runtime
- pkg_host_directory_picker_browse --> pkg_client_ui_primitives
- pkg_host_directory_picker_browse --> pkg_client_ui_slots
- pkg_host_directory_picker_browse --> pkg_client_ui_workspace
- pkg_host_directory_picker_browse --> pkg_invariants
- pkg_host_directory_picker_native --> pkg_client_runtime
- pkg_host_directory_picker_native --> pkg_client_ui_slots
- pkg_host_directory_picker_native --> pkg_client_ui_workspace
- pkg_host_directory_picker_native --> pkg_invariants
+ pkg_message_feedback --> pkg_brand
+ pkg_message_feedback --> pkg_invariants
+ pkg_message_feedback --> pkg_llm
+ pkg_message_feedback --> pkg_session
+ pkg_message_feedback --> pkg_session_persistence
+ pkg_message_feedback --> pkg_storage_domain
+ pkg_message_feedback --> pkg_type_meta
+ pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
+ pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
+ pkg_host_directory_picker_auto --> pkg_host_webserver
+ pkg_host_directory_picker_auto --> pkg_invariants
pkg_commands --> pkg_agent
pkg_commands --> pkg_brand
pkg_commands --> pkg_invariants
@@ -591,23 +590,26 @@ flowchart TD
pkg_user_interaction --> pkg_agent
pkg_user_interaction --> pkg_invariants
pkg_user_interaction --> pkg_llm
- pkg_lsp_local --> pkg_brand
- pkg_lsp_local --> pkg_fs
- pkg_lsp_local --> pkg_invariants
- pkg_lsp_local --> pkg_llm
- pkg_lsp_local --> pkg_lsp
- pkg_lsp_local --> pkg_subprocess
- pkg_lsp_local --> pkg_timeout
+ pkg_agent_presets --> pkg_agent
+ pkg_agent_presets --> pkg_atomic_write
+ pkg_agent_presets --> pkg_invariants
+ pkg_agent_presets --> pkg_paths
+ pkg_agent_presets --> pkg_scope
+ pkg_agent_presets --> pkg_session
+ pkg_agent_presets --> pkg_settings
+ pkg_agent_presets --> pkg_system_prompt
pkg_pty --> pkg_agent
pkg_pty --> pkg_brand
pkg_pty --> pkg_invariants
+ pkg_sandbox_local --> pkg_invariants
+ pkg_sandbox_local --> pkg_llm
+ pkg_sandbox_local --> pkg_sandbox
+ pkg_sandbox_local --> pkg_session
pkg_sandbox_policy --> pkg_agent
pkg_sandbox_policy --> pkg_invariants
pkg_sandbox_policy --> pkg_sandbox
pkg_sandbox_policy --> pkg_session
pkg_sandbox_policy --> pkg_system_prompt
- pkg_scripts --> pkg_app_boot
- pkg_scripts --> pkg_invariants
pkg_session_persistence_jsonl --> pkg_invariants
pkg_session_persistence_jsonl --> pkg_session
pkg_session_persistence_jsonl --> pkg_session_persistence
@@ -658,21 +660,30 @@ flowchart TD
pkg_goal_session --> pkg_invariants
pkg_goal_session --> pkg_llm
pkg_goal_session --> pkg_session
- pkg_bash_sandbox --> pkg_bash
- pkg_bash_sandbox --> pkg_bash_local
- pkg_bash_sandbox --> pkg_invariants
- pkg_bash_sandbox --> pkg_sandbox
- pkg_bash_sandbox --> pkg_sandbox_policy
- pkg_fs_sandbox --> pkg_fs
- pkg_fs_sandbox --> pkg_fs_local
- pkg_fs_sandbox --> pkg_invariants
- pkg_fs_sandbox --> pkg_sandbox
- pkg_fs_sandbox --> pkg_sandbox_policy
+ pkg_bash_local --> pkg_bash
+ pkg_bash_local --> pkg_invariants
+ pkg_bash_local --> pkg_subprocess
+ pkg_bash_local --> pkg_timeout
+ pkg_pwsh_local --> pkg_bash
+ pkg_pwsh_local --> pkg_invariants
+ pkg_pwsh_local --> pkg_subprocess
+ pkg_pwsh_local --> pkg_timeout
+ pkg_fs_local --> pkg_fs
+ pkg_fs_local --> pkg_invariants
+ pkg_fs_policy --> pkg_fs
+ pkg_fs_policy --> pkg_invariants
+ pkg_skill_local --> pkg_fs
+ pkg_skill_local --> pkg_invariants
+ pkg_skill_local --> pkg_paths
+ pkg_skill_local --> pkg_skill
pkg_compact --> pkg_brand
pkg_compact --> pkg_commands
pkg_compact --> pkg_invariants
pkg_compact --> pkg_llm
pkg_compact --> pkg_session
+ pkg_hook_protocol --> pkg_bash
+ pkg_hook_protocol --> pkg_invariants
+ pkg_hook_protocol --> pkg_session
pkg_session_query --> pkg_brand
pkg_session_query --> pkg_invariants
pkg_session_query --> pkg_llm
@@ -694,13 +705,20 @@ flowchart TD
pkg_headless --> pkg_invariants
pkg_headless --> pkg_llm
pkg_headless --> pkg_session
+ pkg_tmux_context --> pkg_agent
+ pkg_tmux_context --> pkg_bash
+ pkg_tmux_context --> pkg_invariants
+ pkg_tmux_context --> pkg_session
+ pkg_fs_e2b --> pkg_e2b
+ pkg_fs_e2b --> pkg_fs
+ pkg_fs_e2b --> pkg_invariants
pkg_command_feedback --> pkg_commands
pkg_command_feedback --> pkg_invariants
pkg_command_feedback --> pkg_session
- pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
- pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
- pkg_host_directory_picker_auto --> pkg_host_webserver
- pkg_host_directory_picker_auto --> pkg_invariants
+ pkg_command_feedback --> pkg_session_telemetry
+ pkg_command_feedback --> pkg_user_id
+ pkg_host_apiproxy --> pkg_agent_presets
+ pkg_host_apiproxy --> pkg_invariants
pkg_permission --> pkg_bash
pkg_permission --> pkg_commands
pkg_permission --> pkg_invariants
@@ -710,6 +728,13 @@ flowchart TD
pkg_permission --> pkg_session_projection
pkg_permission --> pkg_settings
pkg_permission --> pkg_user_approval
+ pkg_lsp_local --> pkg_brand
+ pkg_lsp_local --> pkg_fs
+ pkg_lsp_local --> pkg_invariants
+ pkg_lsp_local --> pkg_llm
+ pkg_lsp_local --> pkg_lsp
+ pkg_lsp_local --> pkg_subprocess
+ pkg_lsp_local --> pkg_timeout
pkg_pty_local --> pkg_agent
pkg_pty_local --> pkg_invariants
pkg_pty_local --> pkg_pty
@@ -724,6 +749,7 @@ flowchart TD
pkg_session_title_llm --> pkg_timeout
pkg_tasks_local --> pkg_agent
pkg_tasks_local --> pkg_invariants
+ pkg_tasks_local --> pkg_scope
pkg_tasks_local --> pkg_tasks
pkg_tasks_local --> pkg_timeout
pkg_token_meter --> pkg_compact
@@ -739,6 +765,8 @@ flowchart TD
pkg_agent_loop --> pkg_session_persistence
pkg_agent_loop --> pkg_system_prompt
pkg_agent_loop --> pkg_tools
+ pkg_agent_tool_mode --> pkg_invariants
+ pkg_agent_tool_mode --> pkg_tools
pkg_tool_goal --> pkg_agent
pkg_tool_goal --> pkg_goal
pkg_tool_goal --> pkg_invariants
@@ -751,6 +779,22 @@ flowchart TD
pkg_bash_env --> pkg_paths
pkg_bash_env --> pkg_session_persistence
pkg_bash_env --> pkg_tools
+ pkg_bash_sandbox --> pkg_bash
+ pkg_bash_sandbox --> pkg_bash_local
+ pkg_bash_sandbox --> pkg_invariants
+ pkg_bash_sandbox --> pkg_sandbox
+ pkg_bash_sandbox --> pkg_sandbox_policy
+ pkg_pwsh_sandbox --> pkg_bash
+ pkg_pwsh_sandbox --> pkg_invariants
+ pkg_pwsh_sandbox --> pkg_pwsh_local
+ pkg_pwsh_sandbox --> pkg_sandbox
+ pkg_pwsh_sandbox --> pkg_sandbox_policy
+ pkg_fs_sandbox --> pkg_fs
+ pkg_fs_sandbox --> pkg_fs_local
+ pkg_fs_sandbox --> pkg_invariants
+ pkg_fs_sandbox --> pkg_sandbox
+ pkg_fs_sandbox --> pkg_sandbox_policy
+ pkg_tool_fs --> pkg_attachment
pkg_tool_fs --> pkg_fs
pkg_tool_fs --> pkg_invariants
pkg_tool_fs --> pkg_llm
@@ -783,9 +827,12 @@ flowchart TD
pkg_command_compact --> pkg_compact
pkg_command_compact --> pkg_invariants
pkg_subagent --> pkg_agent
+ pkg_subagent --> pkg_agent_presets
pkg_subagent --> pkg_brand
pkg_subagent --> pkg_invariants
pkg_subagent --> pkg_llm
+ pkg_subagent --> pkg_sandbox
+ pkg_subagent --> pkg_sandbox_policy
pkg_subagent --> pkg_scope
pkg_subagent --> pkg_session
pkg_subagent --> pkg_session_persistence
@@ -793,6 +840,7 @@ flowchart TD
pkg_subagent --> pkg_session_projection_cache
pkg_subagent --> pkg_tasks
pkg_subagent --> pkg_tools
+ pkg_subagent --> pkg_user_approval
pkg_tool_web --> pkg_invariants
pkg_tool_web --> pkg_llm
pkg_tool_web --> pkg_system_prompt
@@ -846,6 +894,18 @@ flowchart TD
pkg_llm_replay --> pkg_invariants
pkg_llm_replay --> pkg_llm
pkg_llm_replay --> pkg_session
+ pkg_client_test_runtime --> pkg_client_runtime
+ pkg_client_test_runtime --> pkg_client_ui_slots
+ pkg_client_test_runtime --> pkg_client_web_react
+ pkg_client_test_runtime --> pkg_host_apiproxy
+ pkg_client_test_runtime --> pkg_invariants
+ pkg_client_ui_trajectory --> pkg_agent
+ pkg_client_ui_trajectory --> pkg_client_locale
+ pkg_client_ui_trajectory --> pkg_client_runtime
+ pkg_client_ui_trajectory --> pkg_client_ui_primitives
+ pkg_client_ui_trajectory --> pkg_compact
+ pkg_client_ui_trajectory --> pkg_invariants
+ pkg_client_ui_trajectory --> pkg_tools
pkg_session_reference --> pkg_agent
pkg_session_reference --> pkg_compact
pkg_session_reference --> pkg_invariants
@@ -880,6 +940,7 @@ flowchart TD
pkg_mcp_client --> pkg_invariants
pkg_mcp_client --> pkg_llm
pkg_mcp_client --> pkg_subprocess
+ pkg_mcp_client --> pkg_timeout
pkg_mcp_client --> pkg_tools
pkg_tool_bash_persistent --> pkg_agent
pkg_tool_bash_persistent --> pkg_invariants
@@ -910,13 +971,12 @@ flowchart TD
pkg_session_checkpoint_policy --> pkg_session
pkg_session_checkpoint_policy --> pkg_session_persistence
pkg_session_checkpoint_policy --> pkg_tools
- pkg_session_telemetry_otel --> pkg_brand
pkg_session_telemetry_otel --> pkg_command_feedback
pkg_session_telemetry_otel --> pkg_invariants
pkg_session_telemetry_otel --> pkg_llm
- pkg_session_telemetry_otel --> pkg_paths
pkg_session_telemetry_otel --> pkg_session
pkg_session_telemetry_otel --> pkg_session_telemetry
+ pkg_session_telemetry_otel --> pkg_user_id
pkg_session_title_all_messages_llm --> pkg_invariants
pkg_session_title_all_messages_llm --> pkg_llm
pkg_session_title_all_messages_llm --> pkg_session
@@ -937,6 +997,7 @@ flowchart TD
pkg_tool_workflow --> pkg_agent
pkg_tool_workflow --> pkg_invariants
pkg_tool_workflow --> pkg_llm
+ pkg_tool_workflow --> pkg_session
pkg_tool_workflow --> pkg_system_prompt
pkg_tool_workflow --> pkg_tools
pkg_tool_workflow --> pkg_workflow
@@ -956,9 +1017,12 @@ flowchart TD
pkg_tool_pwsh --> pkg_bash_env
pkg_tool_pwsh --> pkg_invariants
pkg_tool_pwsh --> pkg_llm
+ pkg_tool_pwsh --> pkg_sandbox
+ pkg_tool_pwsh --> pkg_sandbox_policy
pkg_tool_pwsh --> pkg_system_prompt
pkg_tool_pwsh --> pkg_tasks
pkg_tool_pwsh --> pkg_tools
+ pkg_tool_pwsh --> pkg_user_approval
pkg_compact_tool_result_prune --> pkg_compact
pkg_compact_tool_result_prune --> pkg_invariants
pkg_compact_tool_result_prune --> pkg_llm
@@ -980,12 +1044,10 @@ flowchart TD
pkg_subagent_inprocess --> pkg_agent
pkg_subagent_inprocess --> pkg_invariants
pkg_subagent_inprocess --> pkg_llm
- pkg_subagent_inprocess --> pkg_sandbox_policy
pkg_subagent_inprocess --> pkg_session
pkg_subagent_inprocess --> pkg_subagent
pkg_subagent_inprocess --> pkg_system_prompt
pkg_subagent_inprocess --> pkg_tools
- pkg_subagent_inprocess --> pkg_user_approval
pkg_tool_subagent --> pkg_agent
pkg_tool_subagent --> pkg_invariants
pkg_tool_subagent --> pkg_llm
@@ -1000,6 +1062,7 @@ flowchart TD
pkg_tool_subagent_report --> pkg_invariants
pkg_tool_subagent_report --> pkg_llm
pkg_tool_subagent_report --> pkg_subagent
+ pkg_tool_subagent_report --> pkg_system_prompt
pkg_tool_subagent_report --> pkg_tools
pkg_hooks_claude --> pkg_agent
pkg_hooks_claude --> pkg_hook_protocol
@@ -1013,6 +1076,9 @@ flowchart TD
pkg_web_app --> pkg_invariants
pkg_web_app --> pkg_system_prompt
pkg_client_ui_conversation --> pkg_agent
+ pkg_client_ui_conversation --> pkg_attachment
+ pkg_client_ui_conversation --> pkg_brand
+ pkg_client_ui_conversation --> pkg_client_connection
pkg_client_ui_conversation --> pkg_client_locale
pkg_client_ui_conversation --> pkg_client_runtime
pkg_client_ui_conversation --> pkg_client_ui_primitives
@@ -1028,10 +1094,6 @@ flowchart TD
pkg_sdk_protocol --> pkg_llm
pkg_sdk_protocol --> pkg_session
pkg_sdk_protocol --> pkg_subagent
- pkg_repository_plugin --> pkg_invariants
- pkg_repository_plugin --> pkg_mcp_client
- pkg_repository_plugin --> pkg_paths
- pkg_repository_plugin --> pkg_skill_local
pkg_tool_ralph --> pkg_agent
pkg_tool_ralph --> pkg_invariants
pkg_tool_ralph --> pkg_llm
@@ -1070,6 +1132,15 @@ flowchart TD
pkg_subagent_spawn --> pkg_invariants
pkg_subagent_spawn --> pkg_subagent
pkg_subagent_spawn --> pkg_subagent_inprocess
+ pkg_client_ui_agent_preset --> pkg_client_connection
+ pkg_client_ui_agent_preset --> pkg_client_locale
+ pkg_client_ui_agent_preset --> pkg_client_runtime
+ pkg_client_ui_agent_preset --> pkg_client_ui_conversation
+ pkg_client_ui_agent_preset --> pkg_client_ui_primitives
+ pkg_client_ui_agent_preset --> pkg_client_ui_settings
+ pkg_client_ui_agent_preset --> pkg_client_ui_slots
+ pkg_client_ui_agent_preset --> pkg_client_web_react
+ pkg_client_ui_agent_preset --> pkg_invariants
pkg_client_ui_command --> pkg_client_connection
pkg_client_ui_command --> pkg_client_locale
pkg_client_ui_command --> pkg_client_runtime
@@ -1089,6 +1160,7 @@ flowchart TD
pkg_client_ui_goal --> pkg_client_ui_conversation
pkg_client_ui_goal --> pkg_client_ui_primitives
pkg_client_ui_goal --> pkg_client_ui_slots
+ pkg_client_ui_goal --> pkg_commands
pkg_client_ui_goal --> pkg_goal
pkg_client_ui_goal --> pkg_invariants
pkg_client_ui_plan --> pkg_client_connection
@@ -1108,12 +1180,27 @@ flowchart TD
pkg_client_ui_subagent --> pkg_invariants
pkg_client_ui_subagent --> pkg_subagent
pkg_client_ui_subagent --> pkg_token_meter
+ pkg_client_ui_task --> pkg_client_locale
+ pkg_client_ui_task --> pkg_client_runtime
+ pkg_client_ui_task --> pkg_client_ui_conversation
+ pkg_client_ui_task --> pkg_client_ui_primitives
+ pkg_client_ui_task --> pkg_client_ui_slots
+ pkg_client_ui_task --> pkg_invariants
pkg_client_ui_tool --> pkg_client_locale
pkg_client_ui_tool --> pkg_client_runtime
pkg_client_ui_tool --> pkg_client_ui_conversation
pkg_client_ui_tool --> pkg_client_ui_primitives
pkg_client_ui_tool --> pkg_client_ui_slots
pkg_client_ui_tool --> pkg_invariants
+ pkg_client_ui_workflow_run --> pkg_client_locale
+ pkg_client_ui_workflow_run --> pkg_client_runtime
+ pkg_client_ui_workflow_run --> pkg_client_ui_conversation
+ pkg_client_ui_workflow_run --> pkg_client_ui_primitives
+ pkg_client_ui_workflow_run --> pkg_client_ui_slots
+ pkg_client_ui_workflow_run --> pkg_invariants
+ pkg_client_ui_workflow_run --> pkg_session
+ pkg_client_ui_workflow_run --> pkg_tool_workflow
+ pkg_client_ui_workflow_run --> pkg_workflow
pkg_agent_spine_demo --> pkg_agent
pkg_agent_spine_demo --> pkg_agent_loop
pkg_agent_spine_demo --> pkg_bash_env
@@ -1194,7 +1281,7 @@ flowchart TD
pkg_acp_demo --> pkg_workspace_context
```
-| 包 | 分组 | 依赖项 |
+| Package | Group | Depends on |
| --- | --- | --- |
| [`invariants`](../packages/support/invariants) | `support` | — |
| [`atomic-write`](../packages/util/atomic-write) | `util` | [`invariants`](../packages/support/invariants) |
@@ -1206,6 +1293,7 @@ flowchart TD
| [`timeout`](../packages/util/timeout) | `util` | [`invariants`](../packages/support/invariants) |
| [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/support/invariants) |
| [`llm-mock-server`](../packages/support/llm-mock-server) | `support` | [`invariants`](../packages/support/invariants) |
+| [`cmdline`](../packages/boot/cmdline) | `boot` | [`invariants`](../packages/support/invariants) |
| [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/support/invariants) |
| [`client-modules`](../packages/client/modules) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-schema-form`](../packages/client/schema-form) | `client` | [`invariants`](../packages/support/invariants) |
@@ -1216,48 +1304,50 @@ flowchart TD
| [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | [`invariants`](../packages/support/invariants) |
| [`e2b`](../packages/e2b/e2b) | `e2b` | [`invariants`](../packages/support/invariants) |
| [`jsonrpc-demo`](../packages/examples/jsonrpc-demo) | `examples` | [`invariants`](../packages/support/invariants) |
-| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`invariants`](../packages/support/invariants) |
| [`host-directory-picker`](../packages/host/directory-picker) | `host` | [`invariants`](../packages/support/invariants) |
| [`host-webserver`](../packages/host/webserver) | `host` | [`invariants`](../packages/support/invariants) |
+| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`invariants`](../packages/support/invariants) |
| [`storage`](../packages/storage/storage) | `storage` | [`invariants`](../packages/support/invariants) |
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`invariants`](../packages/support/invariants) |
| [`type-meta`](../packages/typert/type-meta) | `typert` | [`invariants`](../packages/support/invariants) |
| [`typert-generator`](../packages/typert/generator) | `typert` | [`invariants`](../packages/support/invariants) |
| [`typert-registry`](../packages/typert/registry) | `typert` | [`invariants`](../packages/support/invariants) |
-| [`llm`](../packages/llm/llm) | `llm` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout) |
+| [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`client-connection`](../packages/client/connection) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`client-runtime`](../packages/client/runtime) | `client` | [`invariants`](../packages/support/invariants), [`type-meta`](../packages/typert/type-meta), [`typert-registry`](../packages/typert/registry) |
| [`credentials`](../packages/credentials/credentials) | `credentials` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
-| [`helper`](../packages/scaffold/helper) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess) |
-| [`telemetry`](../packages/scaffold/telemetry) | `scaffold` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
+| [`user-id`](../packages/session/user-id) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
| [`storage-json`](../packages/storage/storage-json) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`typert-loader`](../packages/typert/loader) | `typert` | [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) |
-| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
-| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
-| [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`type-meta`](../packages/typert/type-meta) |
-| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
-| [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
-| [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
+| [`llm`](../packages/llm/llm) | `llm` | [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout) |
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) |
-| [`client-locale`](../packages/client/locale) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) |
+| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
+| [`client-locale`](../packages/client/locale) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`invariants`](../packages/support/invariants) |
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
-| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
-| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
| [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) |
+| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
+| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
+| [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`type-meta`](../packages/typert/type-meta) |
+| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
+| [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
+| [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
+| [`client-ui-question`](../packages/client/ui-question) | `client` | [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) |
+| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
+| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
+| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`type-meta`](../packages/typert/type-meta) |
-| [`bash`](../packages/bash/bash) | `bash` | [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`subprocess`](../packages/subprocess/subprocess) |
-| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`invariants`](../packages/support/invariants), [`skill`](../packages/skill/skill) |
| [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
| [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`web`](../packages/web/web) |
@@ -1265,41 +1355,32 @@ flowchart TD
| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) |
-| [`client-ui-question`](../packages/client/ui-question) | `client` | [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) |
-| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
-| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) |
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
-| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
+| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
+| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
+| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
+| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings) |
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`type-meta`](../packages/typert/type-meta) |
-| [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`pwsh-local`](../packages/bash/pwsh-local) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
-| [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
-| [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`skill`](../packages/skill/skill) |
+| [`bash`](../packages/bash/bash) | `bash` | [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`subprocess`](../packages/subprocess/subprocess) |
+| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`web`](../packages/web/web) |
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/support/invariants), [`spill`](../packages/spill/spill) |
-| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
-| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
-| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
-| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
-| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
+| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`type-meta`](../packages/typert/type-meta) |
+| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`user-interaction`](../packages/interaction/user-interaction) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
-| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt) |
| [`pty`](../packages/pty/pty) | `pty` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
+| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
-| [`scripts`](../packages/scaffold/scripts) | `scaffold` | [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/support/invariants) |
| [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
| [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) |
@@ -1311,29 +1392,40 @@ flowchart TD
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) |
| [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) |
| [`goal-session`](../packages/goal/goal-session) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
-| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
+| [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`pwsh-local`](../packages/bash/pwsh-local) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
+| [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
+| [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`skill`](../packages/skill/skill) |
| [`compact`](../packages/compact/compact) | `compact` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
+| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title) |
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/interaction/user-approval) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-registry`](../packages/typert/registry) |
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
-| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
+| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
+| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
+| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry), [`user-id`](../packages/session/user-id) |
+| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
| [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) |
+| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) |
| [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) |
-| [`tasks-local`](../packages/tasks/tasks-local) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tasks`](../packages/tasks/tasks), [`timeout`](../packages/util/timeout) |
+| [`tasks-local`](../packages/tasks/tasks-local) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tasks`](../packages/tasks/tasks), [`timeout`](../packages/util/timeout) |
| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`agent-tool-mode`](../packages/core/agent-tool-mode) | `core` | [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`bash-env`](../packages/bash/bash-env) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
-| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
+| [`pwsh-sandbox`](../packages/bash/pwsh-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`pwsh-local`](../packages/bash/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
+| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
+| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`command-compact`](../packages/compact/command-compact) | `compact` | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants) |
-| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
+| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
@@ -1343,53 +1435,57 @@ flowchart TD
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`llm-replay`](../packages/support/llm-replay) | `support` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
+| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) |
+| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
| [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-ask-user`](../packages/interaction/tool-ask-user) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools), [`user-interaction`](../packages/interaction/user-interaction) |
| [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) |
+| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-bash-persistent`](../packages/pty/tool-bash-persistent) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-pty`](../packages/pty/tool-pty) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`pty`](../packages/pty/pty), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-schedule`](../packages/schedule/tool-schedule) | `schedule` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
| [`tool-cordis`](../packages/self-modification/tool-cordis) | `self-modification` | [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) |
| [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
-| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`brand`](../packages/util/brand), [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
+| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`command-feedback`](../packages/feedback/command-feedback), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry), [`user-id`](../packages/session/user-id) |
| [`session-title-all-messages-llm`](../packages/session/session-title-all-messages-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
| [`session-title-first-message-llm`](../packages/session/session-title-first-message-llm) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
| [`tool-tasks`](../packages/tasks/tool-tasks) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
-| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
+| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
-| [`tool-pwsh`](../packages/bash/tool-pwsh) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
+| [`tool-pwsh`](../packages/bash/tool-pwsh) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
-| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
+| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
-| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
-| [`sdk-protocol`](../packages/scaffold/protocol) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
-| [`repository-plugin`](../packages/self-modification/repository-plugin) | `self-modification` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) |
+| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
+| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
-| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
| [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
+| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-command`](../packages/client/ui-command) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) |
+| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) |
| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`plan-mode`](../packages/plan/plan-mode) |
| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) |
+| [`client-ui-task`](../packages/client/ui-task) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+| [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`bash-env`](../packages/bash/bash-env), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks-local`](../packages/tasks/tasks-local), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) |
-| [`jsonrpc`](../packages/scaffold/server) | `scaffold` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
-| [`sdk-client`](../packages/scaffold/client) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session) |
-| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/scaffold/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
+| [`jsonrpc`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
+| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
+| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`client-ui-model`](../packages/client/ui-model) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-command`](../packages/client/ui-command), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-permission`](../packages/client/ui-permission) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-command`](../packages/client/ui-command), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`permission`](../packages/interaction/permission) |
| [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-tool`](../packages/client/ui-tool), [`invariants`](../packages/support/invariants) |
diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml
index bcaa79c8e2..c62da68ed7 100644
--- a/docs/persistence-catalog.i18n.yaml
+++ b/docs/persistence-catalog.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/persistence-catalog.md
-persistence-catalog.md: 00df908ccc65a75572ad64c5b1705ba3c1b38a1d
-persistence-catalog.zh.md: 23a3defb18edb2a642b6077c6ab6774d5461b508
+persistence-catalog.md: 93b4edb43820404b75f8306584e963e2d6707d2b
+persistence-catalog.zh.md: c3147f625a5e3f450abbed87e79d24a9ee7b3bfd
diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md
index 00df908ccc..93b4edb438 100644
--- a/docs/persistence-catalog.md
+++ b/docs/persistence-catalog.md
@@ -7,7 +7,7 @@ Every event type that can appear in a session's durable event log: the complete
This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md).
-The envelope declarations below compose each event's `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.
+The envelope declarations below compose each event's `type`, monotonic `seq`, epoch-ms `time`, `data`, the optional `ignorable` unknown-type skip marker, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.
## Event envelope
@@ -63,6 +63,17 @@ export type SessionEvent = {
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
+ /**
+ * Marks an event a reader may safely skip when it does not recognize
+ * `type`. Absent means required: a reader meeting an unrecognized type
+ * without this marker MUST refuse to reconstruct the session instead of
+ * silently dropping the event, because an unrecognized required event may
+ * change how the rest of the log is interpreted. A writer sets `true` only
+ * on purely informational records whose loss cannot affect reconstruction;
+ * defaulting to required means a forgotten marker over-refuses (an
+ * inconvenience) rather than silently resuming a gutted session.
+ */
+ ignorable?: true
} & (K extends SurfaceEventType ? {
/**
* Seq numbers of earlier events that this event cites as sources
@@ -79,7 +90,7 @@ export type SessionEvent = {
}[T]
```
-Sources: [`packages/core/session/src/types.ts:308`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:315`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:344`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:376`](../packages/core/session/src/types.ts)
+Sources: [`packages/core/session/src/types.ts:331`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:338`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:367`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:399`](../packages/core/session/src/types.ts)
## Events
@@ -104,6 +115,22 @@ Sources: [`packages/core/session/src/types.ts:308`](../packages/core/session/src
Source: [`packages/core/agent/src/types.ts:19`](../packages/core/agent/src/types.ts)
+### `agent-preset/*`
+
+#### `agent-preset/selected` — log-only
+
+```ts persistence-catalog
+/**
+ * The session's agent preset was chosen after creation, while the session
+ * was still blank. Log-only: it records the composition later turns ran
+ * under, so a resumed or forked session rebuilds the same one instead of
+ * the header's creation-time value.
+ */
+'agent-preset/selected': { agentPreset: string }
+```
+
+Source: [`packages/preset/agent-presets/src/session.ts:26`](../packages/preset/agent-presets/src/session.ts)
+
### `approval/*`
#### `approval/asked` — log-only
@@ -176,7 +203,7 @@ Source: [`packages/interaction/user-approval/src/index.ts:67`](../packages/inter
Types: [StreamChunk](subsystems/llm-streaming.md)
-Source: [`packages/core/session/src/types.ts:238`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:261`](../packages/core/session/src/types.ts)
#### `assistant/message` — surface
@@ -192,7 +219,7 @@ Source: [`packages/core/session/src/types.ts:238`](../packages/core/session/src/
Types: [TokenUsage](subsystems/llm-streaming.md)
-Source: [`packages/core/session/src/types.ts:245`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:268`](../packages/core/session/src/types.ts)
### `command/*`
@@ -343,12 +370,12 @@ Source: [`packages/compact/compact/src/types.ts:33`](../packages/compact/compact
```ts persistence-catalog
/**
* One recorded human remark about this session. Log-only and independent
- * of its trigger; it never enters the model surface or derived history.
+ * of its trigger; it never enters model context or derived history.
*/
'feedback/record': { text: string }
```
-Source: [`packages/feedback/command-feedback/src/index.ts:24`](../packages/feedback/command-feedback/src/index.ts)
+Source: [`packages/feedback/command-feedback/src/index.ts:62`](../packages/feedback/command-feedback/src/index.ts)
### `goal/*`
@@ -458,7 +485,7 @@ Source: [`packages/interaction/permission/src/index.ts:50`](../packages/interact
'plan/mode': { active: boolean }
```
-Source: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts)
+Source: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts)
### `request/*`
@@ -472,7 +499,7 @@ Source: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/s
'request/context': RequestContext
```
-Source: [`packages/core/session/src/types.ts:281`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:304`](../packages/core/session/src/types.ts)
#### `request/header` — log-only
@@ -484,7 +511,7 @@ Source: [`packages/core/session/src/types.ts:281`](../packages/core/session/src/
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
```
-Source: [`packages/core/session/src/types.ts:276`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts)
### `sandbox/*`
@@ -553,7 +580,7 @@ Source: [`packages/schedule/tool-schedule/src/types.ts:219`](../packages/schedul
'session/end-seed': Record
```
-Source: [`packages/core/session/src/types.ts:304`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:327`](../packages/core/session/src/types.ts)
#### `session/title` — log-only
@@ -589,7 +616,7 @@ Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/sessi
'step/end': { turn: number; step: number }
```
-Source: [`packages/core/session/src/types.ts:228`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/types.ts)
#### `step/start` — log-only
@@ -598,7 +625,7 @@ Source: [`packages/core/session/src/types.ts:228`](../packages/core/session/src/
'step/start': { turn: number; step: number }
```
-Source: [`packages/core/session/src/types.ts:226`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:249`](../packages/core/session/src/types.ts)
### `subagent/*`
@@ -628,7 +655,7 @@ Source: [`packages/subagent/subagent/src/descriptor.ts:37`](../packages/subagent
Types: [TodoItem](subsystems/session.md)
-Source: [`packages/core/session/src/types.ts:271`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:294`](../packages/core/session/src/types.ts)
### `tool/*`
@@ -645,7 +672,7 @@ Source: [`packages/core/session/src/types.ts:271`](../packages/core/session/src/
Types: [CallId](subsystems/core.md)
-Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:274`](../packages/core/session/src/types.ts)
#### `tool/code-dispatch` — log-only
@@ -714,7 +741,57 @@ Source: [`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types
}
```
-Source: [`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:286`](../packages/core/session/src/types.ts)
+
+### `tool-workflow/*`
+
+#### `tool-workflow/agent-end` — log-only
+
+```ts persistence-catalog
+/**
+ * Records one member settlement.
+ * @param data - run identity, paired member sequence, and outcome.
+ */
+'tool-workflow/agent-end': ToolWorkflowAgentEndData
+```
+
+Source: [`packages/workflow/tool-workflow/src/types.ts:57`](../packages/workflow/tool-workflow/src/types.ts)
+
+#### `tool-workflow/agent-start` — log-only
+
+```ts persistence-catalog
+/**
+ * Records one published workflow member.
+ * @param data - run identity, member sequence, display identity, and child Session.
+ */
+'tool-workflow/agent-start': ToolWorkflowAgentStartData
+```
+
+Source: [`packages/workflow/tool-workflow/src/types.ts:52`](../packages/workflow/tool-workflow/src/types.ts)
+
+#### `tool-workflow/run-end` — log-only
+
+```ts persistence-catalog
+/**
+ * Closes one workflow record after cleanup.
+ * @param data - stable run identity and terminal reason.
+ */
+'tool-workflow/run-end': ToolWorkflowRunEndData
+```
+
+Source: [`packages/workflow/tool-workflow/src/types.ts:62`](../packages/workflow/tool-workflow/src/types.ts)
+
+#### `tool-workflow/run-start` — log-only
+
+```ts persistence-catalog
+/**
+ * Opens one top-level workflow record.
+ * @param data - stable run identity and display name.
+ */
+'tool-workflow/run-start': ToolWorkflowRunStartData
+```
+
+Source: [`packages/workflow/tool-workflow/src/types.ts:47`](../packages/workflow/tool-workflow/src/types.ts)
### `turn/*`
@@ -734,7 +811,7 @@ Source: [`packages/core/session/src/types.ts:263`](../packages/core/session/src/
Types: [TurnEndReason](subsystems/session.md)
-Source: [`packages/core/session/src/types.ts:224`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:247`](../packages/core/session/src/types.ts)
#### `turn/start` — log-only
@@ -748,7 +825,7 @@ Source: [`packages/core/session/src/types.ts:224`](../packages/core/session/src/
'turn/start': { turn: number }
```
-Source: [`packages/core/session/src/types.ts:215`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:238`](../packages/core/session/src/types.ts)
### `user/*`
@@ -765,7 +842,7 @@ Source: [`packages/core/session/src/types.ts:215`](../packages/core/session/src/
'user/message': UserMessage
```
-Source: [`packages/core/session/src/types.ts:236`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:259`](../packages/core/session/src/types.ts)
### `web/*`
diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md
index 23a3defb18..c3147f625a 100644
--- a/docs/persistence-catalog.zh.md
+++ b/docs/persistence-catalog.zh.md
@@ -9,7 +9,7 @@
英文源文件根据源码生成(`scripts/gen-persistence-catalog.ts`),并由 `pnpm run verify-persistence-catalog`(`doc-sync`(文档同步门禁)的一部分)验证新鲜度;本中文文件作为经评审对侧通过双语配对维护。声明块保留源码声明和嵌套属性的 JSDoc,只移除其所在接口/模块带来的缩进,并使用 `ts persistence-catalog` 围栏(doc-typecheck 会跳过这些围栏,因为声明引用了其所属模块中的类型)。payload 中的类型名称会链接到记录该类型的页面。参见 [persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md)。
-以下信封声明组合了每个事件的 `type`、单调递增的 `seq`、以 epoch 毫秒表示的 `time`、`data`,以及条件字段 `surfaceOp`/`sourceEventSeqs`。**surface** 表示 `SurfaceEventType` 成员:它会生成一条 LLM(大语言模型)消息,并声明该事件如何加入 surface 列表。**log-only** 表示其他所有事件:这类记录可持久化、可回放,但不参与派生历史。每个 payload 均可进行 JSON 序列化(在 `Session.append` 处强制执行),整个格式固定为 `SESSION_FORMAT_VERSION = 0`:这是预发布格式,不暗示任何兼容性(参见[版本立场](subsystems/persistence.md))。范围仅限本仓库中的包;下游插件可以继续合并其他事件类型,而这些类型按设计不属于本目录。
+以下信封声明组合了每个事件的 `type`、单调递增的 `seq`、以 epoch 毫秒表示的 `time`、`data`、可选的未知类型跳过标记 `ignorable`,以及条件字段 `surfaceOp`/`sourceEventSeqs`。**surface** 表示 `SurfaceEventType` 成员:它会生成一条 LLM(大语言模型)消息,并声明该事件如何加入 surface 列表。**log-only** 表示其他所有事件:这类记录可持久化、可回放,但不参与派生历史。每个 payload 均可进行 JSON 序列化(在 `Session.append` 处强制执行),整个格式固定为 `SESSION_FORMAT_VERSION = 0`:这是预发布格式,不暗示任何兼容性(参见[版本立场](subsystems/persistence.md))。范围仅限本仓库中的包;下游插件可以继续合并其他事件类型,而这些类型按设计不属于本目录。
## 事件信封
@@ -65,6 +65,17 @@ export type SessionEvent = {
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
+ /**
+ * Marks an event a reader may safely skip when it does not recognize
+ * `type`. Absent means required: a reader meeting an unrecognized type
+ * without this marker MUST refuse to reconstruct the session instead of
+ * silently dropping the event, because an unrecognized required event may
+ * change how the rest of the log is interpreted. A writer sets `true` only
+ * on purely informational records whose loss cannot affect reconstruction;
+ * defaulting to required means a forgotten marker over-refuses (an
+ * inconvenience) rather than silently resuming a gutted session.
+ */
+ ignorable?: true
} & (K extends SurfaceEventType ? {
/**
* Seq numbers of earlier events that this event cites as sources
@@ -81,7 +92,7 @@ export type SessionEvent = {
}[T]
```
-来源:[`packages/core/session/src/types.ts:308`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:315`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:344`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:376`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:316`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:323`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:352`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:384`](../packages/core/session/src/types.ts)
## 事件
@@ -106,6 +117,22 @@ export type SessionEvent = {
来源:[`packages/core/agent/src/types.ts:19`](../packages/core/agent/src/types.ts)
+### `agent-preset/*`
+
+#### `agent-preset/selected` — log-only
+
+```ts persistence-catalog
+/**
+ * The session's agent preset was chosen after creation, while the session
+ * was still blank. Log-only: it records the composition later turns ran
+ * under, so a resumed or forked session rebuilds the same one instead of
+ * the header's creation-time value.
+ */
+'agent-preset/selected': { agentPreset: string }
+```
+
+来源:[`packages/preset/agent-presets/src/session.ts:26`](../packages/preset/agent-presets/src/session.ts)
+
### `approval/*`
#### `approval/asked` — log-only
@@ -178,7 +205,7 @@ export type SessionEvent = {
类型:[StreamChunk](subsystems/llm-streaming.md)
-来源:[`packages/core/session/src/types.ts:238`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:246`](../packages/core/session/src/types.ts)
#### `assistant/message` — surface
@@ -194,7 +221,7 @@ export type SessionEvent = {
类型:[TokenUsage](subsystems/llm-streaming.md)
-来源:[`packages/core/session/src/types.ts:245`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:253`](../packages/core/session/src/types.ts)
### `command/*`
@@ -345,12 +372,12 @@ export type SessionEvent = {
```ts persistence-catalog
/**
* One recorded human remark about this session. Log-only and independent
- * of its trigger; it never enters the model surface or derived history.
+ * of its trigger; it never enters model context or derived history.
*/
'feedback/record': { text: string }
```
-来源:[`packages/feedback/command-feedback/src/index.ts:24`](../packages/feedback/command-feedback/src/index.ts)
+来源:[`packages/feedback/command-feedback/src/index.ts:25`](../packages/feedback/command-feedback/src/index.ts)
### `goal/*`
@@ -460,7 +487,7 @@ export type SessionEvent = {
'plan/mode': { active: boolean }
```
-来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts)
+来源:[`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts)
### `request/*`
@@ -474,7 +501,7 @@ export type SessionEvent = {
'request/context': RequestContext
```
-来源:[`packages/core/session/src/types.ts:281`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:289`](../packages/core/session/src/types.ts)
#### `request/header` — log-only
@@ -486,7 +513,7 @@ export type SessionEvent = {
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
```
-来源:[`packages/core/session/src/types.ts:276`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:284`](../packages/core/session/src/types.ts)
### `sandbox/*`
@@ -555,7 +582,7 @@ export type SessionEvent = {
'session/end-seed': Record
```
-来源:[`packages/core/session/src/types.ts:304`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:312`](../packages/core/session/src/types.ts)
#### `session/title` — log-only
@@ -591,7 +618,7 @@ export type SessionEvent = {
'step/end': { turn: number; step: number }
```
-来源:[`packages/core/session/src/types.ts:228`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:236`](../packages/core/session/src/types.ts)
#### `step/start` — log-only
@@ -600,7 +627,7 @@ export type SessionEvent = {
'step/start': { turn: number; step: number }
```
-来源:[`packages/core/session/src/types.ts:226`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts)
### `subagent/*`
@@ -630,7 +657,7 @@ export type SessionEvent = {
类型:[TodoItem](subsystems/session.md)
-来源:[`packages/core/session/src/types.ts:271`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)
### `tool/*`
@@ -647,7 +674,7 @@ export type SessionEvent = {
类型:[CallId](subsystems/core.md)
-来源:[`packages/core/session/src/types.ts:251`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:259`](../packages/core/session/src/types.ts)
#### `tool/code-dispatch` — log-only
@@ -716,7 +743,57 @@ export type SessionEvent = {
}
```
-来源:[`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:271`](../packages/core/session/src/types.ts)
+
+### `tool-workflow/*`
+
+#### `tool-workflow/agent-end` — log-only
+
+```ts persistence-catalog
+/**
+ * Records one member settlement.
+ * @param data - run identity, paired member sequence, and outcome.
+ */
+'tool-workflow/agent-end': ToolWorkflowAgentEndData
+```
+
+来源:[`packages/workflow/tool-workflow/src/types.ts:57`](../packages/workflow/tool-workflow/src/types.ts)
+
+#### `tool-workflow/agent-start` — log-only
+
+```ts persistence-catalog
+/**
+ * Records one published workflow member.
+ * @param data - run identity, member sequence, display identity, and child Session.
+ */
+'tool-workflow/agent-start': ToolWorkflowAgentStartData
+```
+
+来源:[`packages/workflow/tool-workflow/src/types.ts:52`](../packages/workflow/tool-workflow/src/types.ts)
+
+#### `tool-workflow/run-end` — log-only
+
+```ts persistence-catalog
+/**
+ * Closes one workflow record after cleanup.
+ * @param data - stable run identity and terminal reason.
+ */
+'tool-workflow/run-end': ToolWorkflowRunEndData
+```
+
+来源:[`packages/workflow/tool-workflow/src/types.ts:62`](../packages/workflow/tool-workflow/src/types.ts)
+
+#### `tool-workflow/run-start` — log-only
+
+```ts persistence-catalog
+/**
+ * Opens one top-level workflow record.
+ * @param data - stable run identity and display name.
+ */
+'tool-workflow/run-start': ToolWorkflowRunStartData
+```
+
+来源:[`packages/workflow/tool-workflow/src/types.ts:47`](../packages/workflow/tool-workflow/src/types.ts)
### `turn/*`
@@ -736,7 +813,7 @@ export type SessionEvent = {
类型:[TurnEndReason](subsystems/session.md)
-来源:[`packages/core/session/src/types.ts:224`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:232`](../packages/core/session/src/types.ts)
#### `turn/start` — log-only
@@ -750,7 +827,7 @@ export type SessionEvent = {
'turn/start': { turn: number }
```
-来源:[`packages/core/session/src/types.ts:215`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:223`](../packages/core/session/src/types.ts)
### `user/*`
@@ -767,7 +844,7 @@ export type SessionEvent = {
'user/message': UserMessage
```
-来源:[`packages/core/session/src/types.ts:236`](../packages/core/session/src/types.ts)
+来源:[`packages/core/session/src/types.ts:244`](../packages/core/session/src/types.ts)
### `web/*`
diff --git a/docs/postmortem/0001-acp-default-export-drops-inject.i18n.yaml b/docs/postmortem/0001-acp-default-export-drops-inject.i18n.yaml
index e538f244b9..63484a6ba0 100644
--- a/docs/postmortem/0001-acp-default-export-drops-inject.i18n.yaml
+++ b/docs/postmortem/0001-acp-default-export-drops-inject.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/postmortem/0001-acp-default-export-drops-inject.md
-0001-acp-default-export-drops-inject.md: 2d36f24fa54814e39345d7fe68792023c2cf0194
-0001-acp-default-export-drops-inject.zh.md: 6ae7d45f58e09f205a5653f7c6d306014d4d393e
+0001-acp-default-export-drops-inject.md: f8474bde0b81b24573f813d9a0fb017962751f49
+0001-acp-default-export-drops-inject.zh.md: 1e64f123d1dd0b5e7e81d3a8c4a5e6f77e6411ff
diff --git a/docs/postmortem/0001-acp-default-export-drops-inject.md b/docs/postmortem/0001-acp-default-export-drops-inject.md
index 2d36f24fa5..f8474bde0b 100644
--- a/docs/postmortem/0001-acp-default-export-drops-inject.md
+++ b/docs/postmortem/0001-acp-default-export-drops-inject.md
@@ -18,7 +18,7 @@ The ACP server could not create or load a single session — the two RPCs an edi
## Timeline
-- The bridge (RFC 010) landed with a full unit suite (codec, in-memory transport, property-based protocol-shape, failure paths, HMR), a key-gated real-API e2e, and a no-key stdout-purity e2e. All green, 100% coverage.
+- The bridge (RFC 010) landed with a full unit suite for the codec, in-memory transport, generated protocol messages, failure paths, and HMR; a key-gated real-API e2e; and a no-key stdout-purity e2e. All green, 100% coverage.
- A real Zed session immediately failed on `session/new` with `cannot get property "agents" without inject`.
- Investigation initially pursued a Cordis "traceable/shadow" theory (plausible, and the mechanism is real — see Bug #2), then instrumented the actual fiber walk in vendored `reflect.ts` and ran the real subprocess. The trace showed the throw at `apply()` line 179 *at plugin load time*, on the ROOT fiber with no shadow — falsifying the shadow theory for `session/new`.
- Root cause #1 found: a stray `export default apply`. Removing it fixed `session/new`.
@@ -26,7 +26,7 @@ The ACP server could not create or load a single session — the two RPCs an edi
## Root cause #1 — `export default apply` drops the plugin's `inject` (broke `session/new`)
-`packages/acp/acp/src/index.ts` is a *namespace plugin*: it exports `name`, `inject`, `Config`, and `apply` as separate named exports — the same shape as every other plugin in the repo (`invariants`, `llm-deepseek`, `tool-bash`, `tui`, …). But it *also* ended with one extra line no other plugin had:
+`packages/acp/acp/src/index.ts` is a *namespace plugin*: it exports `name`, `inject`, `Config`, and `apply` as separate named exports, as every other plugin in the repo does (`invariants`, `llm-deepseek`, `tool-bash`, `tui`, …). But it *also* ended with one extra line no other plugin had:
```ts ignore-check
export const name = 'acp'
diff --git a/docs/postmortem/0001-acp-default-export-drops-inject.zh.md b/docs/postmortem/0001-acp-default-export-drops-inject.zh.md
index 6ae7d45f58..1e64f123d1 100644
--- a/docs/postmortem/0001-acp-default-export-drops-inject.zh.md
+++ b/docs/postmortem/0001-acp-default-export-drops-inject.zh.md
@@ -18,7 +18,7 @@ ACP 服务器无法创建或加载任何一个会话——而这正是编辑器
## 时间线
-- bridge(RFC 010)落地时附带完整的单元测试套件(codec、内存传输、基于属性的协议形状测试、失败路径、HMR(热模块替换))、一个需要 key 的真实 API e2e 测试,以及一个无需 key 的 stdout 纯净性 e2e 测试。全部绿色,100% 覆盖率。
+- bridge(RFC 010)落地时有一套完整的单元测试,覆盖 codec、内存传输、生成的协议消息、失败路径和 HMR(热模块替换);另有一个需要 key 的真实 API e2e 测试和一个无需 key 的 stdout 纯净性 e2e 测试。全部绿色,100% 覆盖率。
- 真实 Zed 会话在 `session/new` 上立即失败,报错 `cannot get property "agents" without inject`。
- 调查最初追踪了一个 Cordis「traceable/shadow」理论(看似合理,且该机制确实存在——见 Bug #2),随后在 vendor 目录中的 `reflect.ts` 里对实际 fiber 遍历做了插桩,并运行了真实子进程。跟踪结果显示,异常在 `apply()` 第 179 行、*插件加载时*抛出,位于 ROOT fiber 且没有 shadow——推翻了 shadow 理论对 `session/new` 的解释。
- 找到根因 #1:一行多余的 `export default apply`。删除后 `session/new` 修复。
@@ -26,7 +26,7 @@ ACP 服务器无法创建或加载任何一个会话——而这正是编辑器
## 根因 #1——`export default apply` 丢弃了插件的 `inject`(导致 `session/new` 崩溃)
-`packages/acp/acp/src/index.ts` 是一个*命名空间插件*:它将 `name`、`inject`、`Config` 和 `apply` 作为独立的命名导出——与仓库中其他所有插件(`invariants`、`llm-deepseek`、`tool-bash`、`tui` 等)形状相同。但它*还*多了一行其他插件都没有的代码:
+`packages/acp/acp/src/index.ts` 是一个*命名空间插件*:它将 `name`、`inject`、`Config` 和 `apply` 作为独立的命名导出,仓库中其他所有插件(`invariants`、`llm-deepseek`、`tool-bash`、`tui` 等)也是如此。但它*还*多了一行其他插件都没有的代码:
```ts ignore-check
export const name = 'acp'
diff --git a/docs/postmortem/0002-js-expression-disabled-filesystem-tools.i18n.yaml b/docs/postmortem/0002-js-expression-disabled-filesystem-tools.i18n.yaml
index b10d882b59..6f20cc55dc 100644
--- a/docs/postmortem/0002-js-expression-disabled-filesystem-tools.i18n.yaml
+++ b/docs/postmortem/0002-js-expression-disabled-filesystem-tools.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/postmortem/0002-js-expression-disabled-filesystem-tools.md
-0002-js-expression-disabled-filesystem-tools.md: 30ff9d920821a8d55c4bea5f120f1aeeca6634b3
-0002-js-expression-disabled-filesystem-tools.zh.md: 3c18a48d3b7e925a6e75c2d3edb3ec642e72e1b3
+0002-js-expression-disabled-filesystem-tools.md: 93c62261f54e273ea5dbb2ca1a236daf9b2a6f15
+0002-js-expression-disabled-filesystem-tools.zh.md: 7a26f8456c13ad22535cd04fdec061dccd2ed85d
diff --git a/docs/postmortem/0002-js-expression-disabled-filesystem-tools.md b/docs/postmortem/0002-js-expression-disabled-filesystem-tools.md
index 30ff9d9208..93c62261f5 100644
--- a/docs/postmortem/0002-js-expression-disabled-filesystem-tools.md
+++ b/docs/postmortem/0002-js-expression-disabled-filesystem-tools.md
@@ -16,7 +16,7 @@ Cordis Include parsed each `!!js` scalar into an expression object. The Loader r
## Impact
-Seven filesystem scenarios and the mixed workspace-edit scenario called tools that were absent from the registry. Their structured session logs carried `ToolNotFoundError` with code `UNKNOWN_TOOL`, while stdout rendered generic failed tool cards. The snapshot suite passed because both surfaces matched the refreshed fixtures; it proved deterministic replay of the regression rather than successful filesystem behavior.
+Seven filesystem scenarios and the mixed workspace-edit scenario called tools that were absent from the registry. Their structured session logs carried `ToolNotFoundError` with code `UNKNOWN_TOOL`, while stdout rendered generic failed tool cards. The snapshot suite passed because both outputs matched the refreshed fixtures; it proved deterministic replay of the regression rather than successful filesystem behavior.
The live confined default did not gain unintended filesystem access. A naive interpolation fix would have created that risk: permission presets update bash sandbox and approval state at runtime, but cannot mount, unmount, or confine the filesystem stack.
@@ -29,7 +29,7 @@ The live confined default did not gain unintended filesystem access. A naive int
## Root cause
-The implementation assumed `!!js` applied to an entire Loader entry. Its actual boundary is narrower: `Entry._resolveConfig()` interpolates only `entry.options.config`; `Entry.disabled` tests `entry.options.disabled` without interpolation. The YAML tag was syntactically valid, so loading produced no diagnostic.
+The implementation assumed `!!js` applied to an entire Loader entry. It applies only to `entry.options.config`: `Entry._resolveConfig()` interpolates that field, while `Entry.disabled` tests `entry.options.disabled` without interpolation. The YAML tag was syntactically valid, so loading produced no diagnostic.
The snapshot framework treated any deterministic transcript as valid behavior. Header pins verified the composed tool schemas, but the filesystem scenarios shared a pin from the default composition and therefore did not independently prove that their required tools were registered. Refresh rewrote the expected stdout and session logs before any semantic assertion rejected missing tools.
@@ -42,6 +42,6 @@ The snapshot framework treated any deterministic transcript as valid behavior. H
## Lessons
-- A syntactically accepted configuration value is not necessarily evaluated at that location; document and verify interpolation boundaries.
+- A syntactically accepted configuration value is not necessarily evaluated at that location; document and verify exactly which fields are interpolated.
- A snapshot refresh is fixture production, not correctness review. Semantic impossibilities such as a missing registered tool need assertions independent of the expected output.
- Permission controls must describe only the capabilities they actually govern. Composition-time filesystem access cannot follow a runtime bash-only preset safely.
diff --git a/docs/postmortem/0002-js-expression-disabled-filesystem-tools.zh.md b/docs/postmortem/0002-js-expression-disabled-filesystem-tools.zh.md
index 3c18a48d3b..7a26f8456c 100644
--- a/docs/postmortem/0002-js-expression-disabled-filesystem-tools.zh.md
+++ b/docs/postmortem/0002-js-expression-disabled-filesystem-tools.zh.md
@@ -29,7 +29,7 @@ Cordis Include 将每个 `!!js` 标量解析为一个表达式对象。Loader
## 根因
-实现时假设 `!!js` 适用于整个 Loader 配置项。其实际边界更窄:`Entry._resolveConfig()` 仅对 `entry.options.config` 进行插值;`Entry.disabled` 直接测试 `entry.options.disabled`,不经过插值。YAML 标签在语法上合法,因此加载过程不产生任何诊断信息。
+实现时假设 `!!js` 适用于整个 Loader 配置项。实际只有 `entry.options.config` 使用它:`Entry._resolveConfig()` 对该字段进行插值,而 `Entry.disabled` 直接测试 `entry.options.disabled`,不经过插值。YAML 标签在语法上合法,因此加载过程不产生任何诊断信息。
快照框架将任何确定性的 transcript(文本记录)视为有效行为。Header pin 验证了组合后的工具 schema,但文件系统场景共享来自默认组合的 pin,因此未独立证明其所需工具已注册。刷新在任何语义断言拒绝缺失工具之前,就已重写了预期的 stdout 和会话日志。
@@ -42,6 +42,6 @@ Cordis Include 将每个 `!!js` 标量解析为一个表达式对象。Loader
## 教训
-- 语法上被接受的配置值不一定在该位置被求值;应记录并验证插值边界。
+- 语法上被接受的配置值不一定在该位置被求值;应记录并验证具体对哪些字段进行插值。
- 快照刷新是 fixture 的生产过程,不是正确性审查。诸如已注册工具缺失这类语义上不可能的结果,需要独立于预期输出的断言。
- 权限控制只应描述其实际管辖的能力。组合时的文件系统访问无法安全地跟随运行时的 bash-only 预设。
diff --git a/docs/postmortem/0003-web-agent-gui-feedback-loop.i18n.yaml b/docs/postmortem/0003-web-agent-gui-feedback-loop.i18n.yaml
index ed77fc2f2e..4272783be3 100644
--- a/docs/postmortem/0003-web-agent-gui-feedback-loop.i18n.yaml
+++ b/docs/postmortem/0003-web-agent-gui-feedback-loop.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/postmortem/0003-web-agent-gui-feedback-loop.md
-0003-web-agent-gui-feedback-loop.md: 13d13a607babfe7f5ddfdb6773c94f973bbef0db
-0003-web-agent-gui-feedback-loop.zh.md: 44d4febc30344135932aad2394b3054587feca1d
+0003-web-agent-gui-feedback-loop.md: 0d8c07d9aca3305ea6bc85134e8578ae1bd8f387
+0003-web-agent-gui-feedback-loop.zh.md: 07faa3a302d1b124efcbdc57446995aefeb8bd87
diff --git a/docs/postmortem/0003-web-agent-gui-feedback-loop.md b/docs/postmortem/0003-web-agent-gui-feedback-loop.md
index 13d13a607b..0d8c07d9ac 100644
--- a/docs/postmortem/0003-web-agent-gui-feedback-loop.md
+++ b/docs/postmortem/0003-web-agent-gui-feedback-loop.md
@@ -31,7 +31,7 @@ No change in this investigation restarted or modified the read-only 3081 and 308
## Root cause
-The Web assembly had no model-visible identity for the current GUI, canonical URL, or runtime mode. The session cwd correctly represented the user's selected Workspace, but the model mistook that project boundary for the application boundary. No durable contract related the GUI source checkout, built artifacts, serving process, target origin, and browser acceptance.
+The Web assembly had no model-visible identity for the current GUI, canonical URL, or runtime mode. The session cwd correctly identified the user's selected Workspace, but the model treated that project directory as the application directory. No durable record related the GUI source checkout, built artifacts, serving process, target origin, and browser acceptance.
The wrong startup path looked legitimate because bare Vite returned HTTP 200. `window.__DSH_BOOT__` is injected only by the full host, so transport readiness did not imply application readiness. The first regression test repeated this mistake in another form: a timeout killed Vite and satisfied a nonzero-exit assertion. Live reproduction exposed that false positive.
diff --git a/docs/postmortem/0003-web-agent-gui-feedback-loop.zh.md b/docs/postmortem/0003-web-agent-gui-feedback-loop.zh.md
index 44d4febc30..07faa3a302 100644
--- a/docs/postmortem/0003-web-agent-gui-feedback-loop.zh.md
+++ b/docs/postmortem/0003-web-agent-gui-feedback-loop.zh.md
@@ -31,7 +31,7 @@ Web agent 修改了 GUI 源码,却不知道由哪个 URL 和进程承载当前
## 根因
-Web 组合没有向模型提供当前 GUI、规范 URL 或运行模式的身份信息。会话 cwd 正确表示了用户选择的 Workspace,但模型误把这个项目边界当成了应用边界。系统也没有持久约定将 GUI 源码检出目录、构建产物、服务进程、目标 origin 和浏览器验收关联起来。
+Web 组合没有向模型提供当前 GUI、规范 URL 或运行模式的身份信息。会话 cwd 正确标识了用户选择的 Workspace,但模型把这个项目目录当成了应用目录。系统也没有持久记录将 GUI 源码检出目录、构建产物、服务进程、目标 origin 和浏览器验收关联起来。
裸 Vite 返回 HTTP 200,使错误的启动路径看似合理。`window.__DSH_BOOT__` 只由完整宿主注入,因此传输层就绪不代表应用已就绪。首个回归测试以另一种方式重复了同样的错误:超时机制终止 Vite 后,非零退出断言仍会通过。真实复现暴露了这一误报。
diff --git a/docs/rescope.i18n.yaml b/docs/rescope.i18n.yaml
new file mode 100644
index 0000000000..2959c354da
--- /dev/null
+++ b/docs/rescope.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/rescope.md
+rescope.md: 3dde39875021e7a4161e1ae66550e9dedf5eb4fa
+rescope.zh.md: a7f355cf651fb063bf2d4c3cefe18babd4a57401
diff --git a/docs/rescope.md b/docs/rescope.md
new file mode 100644
index 0000000000..3dde398750
--- /dev/null
+++ b/docs/rescope.md
@@ -0,0 +1,53 @@
+# Vendored package rescope
+
+English | [中文](rescope.zh.md)
+
+The Cordis framework and its foundation libraries are vendored under [`vendor/`](../vendor/README.md) and published under the `@deepseek-ai` scope, because every harness package declares the framework as a peer dependency: publishing the harness publishes this layer with it, and under the upstream names that publication would squat them on the registry. This page is the name mapping; the decision and its consequences live in the [rescope Agent Note](../.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md), and the upstream commits in [`vendor/README.md`](../vendor/README.md).
+
+## Name mapping
+
+| Directory | Upstream name | Published name | Version | Role |
+|---|---|---|---|---|
+| `vendor/cordis/` | `cordis` | `@deepseek-ai/cordis` | 4.0.0-rc.7 | Framework core: `Context`, `Service`, `Fiber`, events |
+| `vendor/cosmokit/` | `cosmokit` | `@deepseek-ai/cosmokit` | 1.8.1 | Shared utilities the framework and Schemastery build on |
+| `vendor/schemastery/` | `schemastery` | `@deepseek-ai/schemastery` | 3.18.0 | Config schemas (`Schema`) behind every plugin's `Config` |
+| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | `cordis.yml` loading, plugin resolution, repository cache |
+| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.0.4 | Config includes and patch overlays |
+| `vendor/group/` | `@cordisjs/plugin-group` | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | Nested plugin groups |
+| `vendor/timer/` | `@cordisjs/plugin-timer` | `@deepseek-ai/cordis-plugin-timer` | 1.1.2 | Disposal-aware timers on `ctx` |
+| `vendor/hmr/` | `@cordisjs/plugin-hmr` | `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | Hot module replacement for plugins and config |
+| `vendor/logger-console/` | `@cordisjs/plugin-logger-console` | `@deepseek-ai/cordis-plugin-logger-console` | 1.0.0 | Console logger exporter |
+
+Subpath exports keep their path: `@cordisjs/plugin-loader/repository` becomes `@deepseek-ai/cordis-plugin-loader/repository`.
+
+## What the rename does not touch
+
+- **Directory names and versions.** `vendor/hmr/` stays `vendor/hmr/`, and every package keeps the upstream version its manifest table row records, so the vendored tree still reads as an upstream snapshot.
+- **Dependency ranges.** A dependency entry changes its key, never its range: `"cordis": "^4.0.0-rc.7"` becomes `"@deepseek-ai/cordis": "^4.0.0-rc.7"`. `linkWorkspacePackages` resolves those preserved ranges to the pinned workspaces.
+- **The Loader's `cordis:` builtin prefix.** `cordis:include` and `cordis:group` are a protocol prefix, not a package name.
+- **The `cordis.yml` configuration family**, including `*.cordis.yml`, `*.cordis.snapshot.yml`, and `cordis.patch.yml`.
+- **Harness packages whose own names contain the word**, such as `@deepseek-ai/dsh-tool-cordis`.
+- **Upstream runtime identifiers**, such as Schemastery's `Symbol.for('schemastery')` and its `vendor:` metadata field.
+- **Prose outside `docs/`.** `vendor/*/README.md`, package READMEs, and Agent Notes keep the names they were written with; a bare `cordis` there can also be the Python SDK's option name or an agent-preset id. Inside `docs/`, prose and every Markdown fence follow the rename.
+
+## What your code has to change
+
+| Site | Before | After |
+|---|---|---|
+| Module import | `import { Context } from 'cordis'` | `import { Context } from '@deepseek-ai/cordis'` |
+| Typed-event merge | `declare module 'cordis'` | `declare module '@deepseek-ai/cordis'` |
+| `package.json` dependency key | `"@cordisjs/plugin-hmr": "^1.0.15"` | `"@deepseek-ai/cordis-plugin-hmr": "^1.0.15"` |
+| `cordis.yml` plugin entry | `name: '@cordisjs/plugin-include'` | `name: '@deepseek-ai/cordis-plugin-include'` |
+
+## Applying, verifying, and reverting
+
+[`scripts/rescope-vendor.ts`](../scripts/rescope-vendor.ts) owns the mapping above and performs the rename, so no reference is renamed by hand:
+
+```sh
+pnpm run rescope-vendor # report what would change
+pnpm run rescope-vendor --apply # rewrite every reference
+pnpm run rescope-vendor:check # assert the post-state; runs in the hygiene gate
+pnpm run rescope-vendor --apply --reverse # return to the upstream names
+```
+
+Re-apply it after an upstream sync ([procedure](../vendor/README.md)), and follow it with the regeneration it prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`, and `pnpm run verify-translation-pairing --write` for the bilingual pairs it touched.
diff --git a/docs/rescope.zh.md b/docs/rescope.zh.md
new file mode 100644
index 0000000000..a7f355cf65
--- /dev/null
+++ b/docs/rescope.zh.md
@@ -0,0 +1,53 @@
+# Vendored 包改名
+
+[English](rescope.md) | 中文
+
+Cordis 框架及其基础库以源码形式 vendored 在 [`vendor/`](../vendor/README.md) 下,并以 `@deepseek-ai` scope 发布:每个 harness 包都把框架声明为 peer dependency,发布 harness 就会连带发布这一层,用上游名发布等于在 registry 上占用别人的名字。本页是名字映射表;决策与影响见 [改名 Agent Note](../.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md),上游 commit 见 [`vendor/README.md`](../vendor/README.md)。
+
+## 名字映射
+
+| 目录 | 上游名 | 发布名 | 版本 | 角色 |
+|---|---|---|---|---|
+| `vendor/cordis/` | `cordis` | `@deepseek-ai/cordis` | 4.0.0-rc.7 | 框架核心:`Context`、`Service`、`Fiber`、事件 |
+| `vendor/cosmokit/` | `cosmokit` | `@deepseek-ai/cosmokit` | 1.8.1 | 框架与 Schemastery 共用的基础工具 |
+| `vendor/schemastery/` | `schemastery` | `@deepseek-ai/schemastery` | 3.18.0 | 配置 schema(`Schema`),每个插件的 `Config` 都基于它 |
+| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | `cordis.yml` 装载、插件解析、repository 缓存 |
+| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.0.4 | 配置包含与 patch 叠加 |
+| `vendor/group/` | `@cordisjs/plugin-group` | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | 嵌套插件分组 |
+| `vendor/timer/` | `@cordisjs/plugin-timer` | `@deepseek-ai/cordis-plugin-timer` | 1.1.2 | `ctx` 上随 disposal 回收的定时器 |
+| `vendor/hmr/` | `@cordisjs/plugin-hmr` | `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | 插件与配置的热替换 |
+| `vendor/logger-console/` | `@cordisjs/plugin-logger-console` | `@deepseek-ai/cordis-plugin-logger-console` | 1.0.0 | 控制台日志导出 |
+
+子路径导出保持原路径:`@cordisjs/plugin-loader/repository` 变成 `@deepseek-ai/cordis-plugin-loader/repository`。
+
+## 改名不碰什么
+
+- **目录名与版本号。** `vendor/hmr/` 仍是 `vendor/hmr/`,每个包保留清单表那行记录的上游版本,所以 vendored 树依旧读作一份上游快照。
+- **依赖 range。** 依赖条目只换键、不换范围:`"cordis": "^4.0.0-rc.7"` 变成 `"@deepseek-ai/cordis": "^4.0.0-rc.7"`;`linkWorkspacePackages` 靠这些保留下来的范围把它们解析到固定的 workspace。
+- **Loader 的 `cordis:` 内建前缀。** `cordis:include`、`cordis:group` 是协议前缀,不是包名。
+- **`cordis.yml` 配置文件家族**,包括 `*.cordis.yml`、`*.cordis.snapshot.yml`、`cordis.patch.yml`。
+- **名字里带这个词的 harness 包**,例如 `@deepseek-ai/dsh-tool-cordis`。
+- **上游运行时标识符**,例如 Schemastery 的 `Symbol.for('schemastery')` 及其 `vendor:` 元数据字段。
+- **`docs/` 之外的散文。** `vendor/*/README.md`、各包 README 与 Agent Note 保留写作当时的名字;那里的裸 `cordis` 也可能是 Python SDK 的选项名或某个 agent-preset 的 id。`docs/` 之内,散文与所有 Markdown 围栏都跟着改。
+
+## 你的代码要改什么
+
+| 位置 | 改前 | 改后 |
+|---|---|---|
+| 模块 import | `import { Context } from 'cordis'` | `import { Context } from '@deepseek-ai/cordis'` |
+| 类型事件声明合并 | `declare module 'cordis'` | `declare module '@deepseek-ai/cordis'` |
+| `package.json` 依赖键 | `"@cordisjs/plugin-hmr": "^1.0.15"` | `"@deepseek-ai/cordis-plugin-hmr": "^1.0.15"` |
+| `cordis.yml` 插件条目 | `name: '@cordisjs/plugin-include'` | `name: '@deepseek-ai/cordis-plugin-include'` |
+
+## 施加、核验与回退
+
+上面这份映射由 [`scripts/rescope-vendor.ts`](../scripts/rescope-vendor.ts) 承载并执行改名,任何引用都不靠手改:
+
+```sh
+pnpm run rescope-vendor # report what would change
+pnpm run rescope-vendor --apply # rewrite every reference
+pnpm run rescope-vendor:check # assert the post-state; runs in the hygiene gate
+pnpm run rescope-vendor --apply --reverse # return to the upstream names
+```
+
+上游 sync 之后重跑它([流程](../vendor/README.md)),并接上它打印的重生成:`pnpm install` 重生成 lockfile、`pnpm run gen-third-party-notices`、以及对它触及的双语对跑 `pnpm run verify-translation-pairing --write`。
diff --git a/docs/subsystems/README.i18n.yaml b/docs/subsystems/README.i18n.yaml
index 390bbe911e..f34413f662 100644
--- a/docs/subsystems/README.i18n.yaml
+++ b/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: fea77566dd2cf07af04619203ef5a94dad6b2dc0
-README.zh.md: dfcd5a6360d375f710213b3cb944695f19f488d6
+README.md: b4049888106aeedbf6c94b937199d59dee964dd1
+README.zh.md: 09ce59e71d7be53f33553d93faef201357049422
diff --git a/docs/subsystems/README.md b/docs/subsystems/README.md
index fea77566dd..b404988810 100644
--- a/docs/subsystems/README.md
+++ b/docs/subsystems/README.md
@@ -2,12 +2,12 @@
English | [中文](README.zh.md)
-One page per subsystem of the DeepSeek Harness: what it is, the data structures it moves, and — where a `ctx` service or event scope backs it — a generated **Cordis surface** section carrying its service and event reference. The folder complements [architecture.md](../architecture.md), which describes *behavior* across subsystems (the service map, the session/turn/step lifecycle, the event taxonomy); each page here is the reference for one subsystem's vocabulary and wiring.
+One page per subsystem of the DeepSeek Harness: what it is, the data structures it moves, and — where a `ctx` service or event scope backs it — a generated **Cordis API** section carrying its service and event reference. The folder complements [architecture.md](../architecture.md), which describes *behavior* across subsystems (the service map, the session/turn/step lifecycle, the event taxonomy); each page here is the reference for one subsystem's vocabulary and wiring.
| Page | Owns |
|---|---|
-| [core.md](core.md) | the `packages/core` control spine: the package-by-package loop map, agent creation and ownership (`AgentHandle`), the `Agent` handle with its delivery/cancellation/interception contracts, and the repo-wide type patterns (`…Map → derived-union`, branded ids) |
-| [llm-streaming.md](llm-streaming.md) | the `packages/llm` conversation vocabulary — `Message`/`ContentBlock`, the assembled model request, the `StreamChunk` wire protocol + adapter contract, `BlockAssembler`, the `LlmAdapter` provider contract |
+| [core.md](core.md) | how `packages/core` controls the agent loop: the package-by-package loop description, agent creation and ownership (`AgentHandle`), the `Agent` handle's delivery/cancellation/interception contracts, and the repo-wide type patterns (`…Map → derived-union`, branded ids) |
+| [llm-streaming.md](llm-streaming.md) | the `packages/llm` conversation types — `Message`/`ContentBlock`, the assembled model request, the `StreamChunk` wire protocol and adapter contract, `BlockAssembler`, and the `LlmAdapter` provider contract |
| [token-meter.md](token-meter.md) | immutable scalar and positional replay measurements with consumed-log revisions |
| [scope.md](scope.md) | scoped registration identity, dispatch carriers, and the owned `Scope` context |
| [typert.md](typert.md) | Remote invocation descriptors, lookup/Context declarations, TypeRT registries, and the Host Gateway/Client API boundaries |
@@ -19,12 +19,14 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
| [settings.md](settings.md) | the user-settings seam: `SettingsNamespace` registration, layered resolution (defaults → composition `base` → user document), owner scopes, hot commits |
| [credentials.md](credentials.md) | the credential seam: `CredentialRef` references (never values) in configuration, per-operation resolution, UI-safe `CredentialInfo`, provider source layers |
| [session-query.md](session-query.md) | logical records, bounded exact-event reads, relationship traces, semantic filters/documents, and full-text result pages |
+| [feedback.md](feedback.md) | lifecycle-bound per-message feedback records, optimistic versions, sidecar persistence, and the Host Remote contract |
| [session-title.md](session-title.md) | durable title snapshots, cited source-message seqs, and the asynchronous provider contract |
| [session-reference.md](session-reference.md) | structured cross-session references: `SessionReferenceInput`/`Candidate`, prepared message contexts, the stable error taxonomy |
| [system-prompt.md](system-prompt.md) | per-assembly context, tool-provider results, prompt sections, and cooperative assembly |
| [tools.md](tools.md) | `ToolDefinition` full fields, the schema DSL, `ToolExecution`/`ToolResult`, tool-presentation UI types, and the guarded execution pipeline |
| [user-interaction.md](user-interaction.md) | the UI-backed human question/answer seam: `AskUserQuestionRequest`, answer/options vocabulary, provider API, error taxonomy |
-| [approval.md](approval.md) | the one-shot user-approval seam: `ApprovalRequest`, `ApprovalOutcome`, per-session policy, audit and answerer contracts |
+| [approval.md](approval.md) | the one-shot user-approval seam: `ApprovalRequest`, `ApprovalOutcome`, per-session policy, audit events, and answerer contracts |
+| [attachment.md](attachment.md) | durable image identity and metadata, validation inputs, verified reads, and the `AttachmentStore` seam |
| [bash.md](bash.md) | the bash executor seam: `BashExecRequest`/`Spec`, `BashRunResult`, background `BashProcess` handles |
| [subprocess.md](subprocess.md) | the subprocess seam: fully-explicit `SubprocessSpawnSpec`, offset-based output readers, unclassified `SubprocessOutcome`, and the managed `DSH_*` environment vocabulary |
| [pty.md](pty.md) | persistent terminal ids, backend/session contracts, send readiness, bounded reads, and owner-visible snapshots |
@@ -38,15 +40,15 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
| [web.md](web.md) | the web access seam: `WebSearchRequest`/`Result`, `WebFetchRequest`/`Result`, `WebFetchBody`, provider availability, `WebError` |
| [spill.md](spill.md) | the spill storage seam: `SaveTextSpill`, `SpillOwner`/`SpillSource`, `SpillRef`, the branded `SpillLocator` |
| [workflow.md](workflow.md) | the workflow seam: `WorkflowStartRequest`, `WorkflowMeta`, `WorkflowRun`/`Result`, the `workflow/*` event payloads, `WorkflowError` fatality |
-| [tasks.md](tasks.md) | the background-task runtime: branded `TaskId`s, the producer contract, consumer views, `ctx.tasks` service behavior |
+| [tasks.md](tasks.md) | the background-task runtime: branded `TaskId`s, the producer contract, consumer views, and `ctx.tasks` service behavior |
| [permission.md](permission.md) | the permission-preset layer: `PresetSpec`/`PresetOption`, the derived `custom` state, the log-only `permission/preset` event |
| [plan.md](plan.md) | plan mode: the log-only `plan/mode` state, pending-selection flush, `PlanModeConfig`, the `exit_plan_mode` review arc |
| [invariants.md](invariants.md) | the runtime-invariant registry: selection `Config`, `InvariantInstaller`/`InvariantFailure`, the empty-companion contract |
| [http-server.md](http-server.md) | the HTTP carrier: `WebRouteKind`/`WebRoute`, match order, the claimable fallback seat, index taps |
| [storage.md](storage.md) | the storage subsystem: the backend contract (`StorageBackend`), `StorageForms`, `DomainSpec`/`Domain`, `domain/changed` |
| [workspace.md](workspace.md) | the workspace registry: `Workspace`/`WorkspaceId`, registration and resolution, the session `cwd` relationship |
-| [client-modules.md](client-modules.md) | the web plugin table: `dshClient` declarations, `WebBootGraph` wire composition, the bundle route and index tap |
+| [client-modules.md](client-modules.md) | the web plugin table: `dsh.client` declarations, `WebBootGraph` wire composition, the bundle route and index tap |
| [session-projection.md](session-projection.md) | the projection seam: `SessionProjectionMap`, the pure `ProjectionDefinition` unit, `ProjectionSnapshot`'s consistent cut, the change feed |
-| [telemetry.md](telemetry.md) | the session-telemetry capability seam: `TelemetryRecord`/`TelemetrySeverity`, the `TelemetryBackend` contract, the `telemetry/record` redact waterfall |
+| [telemetry.md](telemetry.md) | the outbound session-reporting capability seam: `TelemetryRecord`/`TelemetrySeverity`, the `TelemetryBackend` contract, and the `telemetry/record` redact waterfall |
-> Type declarations and their JSDoc on these pages are source-equivalent and drift-checked by `pnpm run verify-type-equiv` (see [development.md](../development.md#documenting-types-verbatim-ts-type-equiv)). Ordinary blocks preserve complete declarations; `public-api` blocks preserve body-stripped public class declarations. Cordis services and events use each page's generated **Cordis surface** section.
+> Type declarations and their JSDoc on these pages are source-equivalent and drift-checked by `pnpm run verify-type-equiv` (see [development.md](../development.md#documenting-types-verbatim-ts-type-equiv)). Ordinary blocks preserve complete declarations; `public-api` blocks preserve body-stripped public class declarations. Cordis services and events use each page's generated **Cordis API** section.
diff --git a/docs/subsystems/README.zh.md b/docs/subsystems/README.zh.md
index dfcd5a6360..09ce59e71d 100644
--- a/docs/subsystems/README.zh.md
+++ b/docs/subsystems/README.zh.md
@@ -2,12 +2,12 @@
[English](README.md) | 中文
-每个子系统一页,覆盖 DeepSeek Harness 的全部子系统:它是什么、它操作哪些数据结构,以及——当它由某个 `ctx` 服务或事件作用域支撑时——一段生成的 **Cordis surface** 小节,承载其服务与事件参考。本目录与 [architecture.md](../architecture.md) 互补:后者描述跨子系统的*行为*(服务映射、会话/轮次/步骤生命周期、事件分类体系);这里的每一页是单个子系统词汇与接线的参考。
+每个子系统一页,覆盖 DeepSeek Harness 的全部子系统:它是什么、它操作哪些数据结构,以及——当它由某个 `ctx` 服务或事件作用域支撑时——一段生成的 **Cordis API** 小节,承载其服务与事件参考。本目录与 [architecture.md](../architecture.md) 互补:后者描述跨子系统的*行为*(服务映射、会话/轮次/步骤生命周期、事件分类体系);这里的每一页是单个子系统词汇与接线的参考。
| 页面 | 负责内容 |
|---|---|
-| [core.md](core.md) | `packages/core` 控制主干:逐包循环地图、agent 创建与所有权(`AgentHandle`)、`Agent` 句柄及其投递/取消/拦截约定,以及全仓通用类型模式(`…Map → 派生联合`、品牌化 id) |
-| [llm-streaming.md](llm-streaming.md) | `packages/llm` 的对话词汇——`Message`/`ContentBlock`、组装完成的模型请求、`StreamChunk` 协议格式(wire format)+ 适配器约定(adapter contract)、`BlockAssembler`、`LlmAdapter` 提供方约定 |
+| [core.md](core.md) | `packages/core` 如何控制 agent loop:逐包的循环说明、agent 创建与所有权(`AgentHandle`)、`Agent` 句柄的投递/取消/拦截约定,以及全仓通用类型模式(`…Map → 派生联合`、品牌化 id) |
+| [llm-streaming.md](llm-streaming.md) | `packages/llm` 的对话类型——`Message`/`ContentBlock`、组装完成的模型请求、`StreamChunk` wire protocol 和适配器约定(adapter contract)、`BlockAssembler`,以及 `LlmAdapter` 提供方约定 |
| [token-meter.md](token-meter.md) | 不可变的标量与位置回放度量,附带已消费日志修订号 |
| [scope.md](scope.md) | 作用域注册标识、dispatch 载体,以及拥有的 `Scope` 上下文 |
| [typert.md](typert.md) | 远程调用描述符、lookup/Context 声明、TypeRT 注册表,以及 Host Gateway/Client API 边界 |
@@ -19,12 +19,14 @@
| [settings.md](settings.md) | 用户设置 seam:`SettingsNamespace` 注册、分层解析(默认值 → 组合 `base` → 用户文档)、owner scope、热提交 |
| [credentials.md](credentials.md) | 凭据 seam:配置中的 `CredentialRef` 引用(绝不含值)、按操作解析、对 UI 安全的 `CredentialInfo`、provider 来源层 |
| [session-query.md](session-query.md) | 逻辑记录、有界精确事件读取、关系追踪、语义筛选器/文档与全文检索结果页 |
+| [feedback.md](feedback.md) | 绑定生命周期的逐消息反馈记录、乐观版本、伴随记录持久化与 Host Remote 契约 |
| [session-title.md](session-title.md) | 持久标题快照、被引用的来源消息 seq 与异步提供方约定 |
| [session-reference.md](session-reference.md) | 结构化跨会话引用:`SessionReferenceInput`/`Candidate`、prepared 消息上下文、稳定错误分类 |
| [system-prompt.md](system-prompt.md) | 逐次组装的上下文、工具提供方结果、提示词段落与协作式组装 |
| [tools.md](tools.md) | `ToolDefinition` 完整字段、schema DSL、`ToolExecution`/`ToolResult`、工具展示 UI 类型,以及受保护的执行流水线 |
| [user-interaction.md](user-interaction.md) | UI 支持的人工问答 seam:`AskUserQuestionRequest`、answer/options 词汇、提供方 API、错误分类体系 |
-| [approval.md](approval.md) | 一次性用户审批 seam:`ApprovalRequest`、`ApprovalOutcome`、逐会话策略、审计与 answerer 约定 |
+| [approval.md](approval.md) | 一次性用户审批 seam:`ApprovalRequest`、`ApprovalOutcome`、逐会话策略、审计事件和 answerer 约定 |
+| [attachment.md](attachment.md) | 持久图片标识与元数据、校验输入、经校验读取,以及 `AttachmentStore` seam |
| [bash.md](bash.md) | bash 执行器 seam:`BashExecRequest`/`Spec`、`BashRunResult`、后台 `BashProcess` 句柄 |
| [subprocess.md](subprocess.md) | 子进程 seam:完全显式的 `SubprocessSpawnSpec`、基于偏移的输出读取器、不含分类的 `SubprocessOutcome`,以及受管 `DSH_*` 环境词汇 |
| [pty.md](pty.md) | 持久化终端 ID、后端/会话约定、发送就绪状态、有界读取与 owner 可见快照 |
@@ -38,15 +40,15 @@
| [web.md](web.md) | Web 访问 seam:`WebSearchRequest`/`Result`、`WebFetchRequest`/`Result`、`WebFetchBody`、提供方可用性、`WebError` |
| [spill.md](spill.md) | spill 存储 seam:`SaveTextSpill`、`SpillOwner`/`SpillSource`、`SpillRef`、品牌类型 `SpillLocator` |
| [workflow.md](workflow.md) | 工作流 seam:`WorkflowStartRequest`、`WorkflowMeta`、`WorkflowRun`/`Result`、`workflow/*` 事件载荷、`WorkflowError` 致命性 |
-| [tasks.md](tasks.md) | 后台任务运行时:品牌化 `TaskId`、producer 约定、consumer 视图、`ctx.tasks` 服务行为 |
+| [tasks.md](tasks.md) | 后台任务运行时:品牌化 `TaskId`、producer 约定、consumer 视图和 `ctx.tasks` 服务行为 |
| [permission.md](permission.md) | 权限预设层:`PresetSpec`/`PresetOption`、派生的 `custom` 状态、仅记日志的 `permission/preset` 事件 |
| [plan.md](plan.md) | 计划模式:仅记日志的 `plan/mode` 状态、待定选择的冲刷、`PlanModeConfig`、`exit_plan_mode` 审阅流程 |
| [invariants.md](invariants.md) | 运行时不变式注册表:选择配置 `Config`、`InvariantInstaller`/`InvariantFailure`、空配套插件约定 |
| [http-server.md](http-server.md) | HTTP 载体:`WebRouteKind`/`WebRoute`、匹配顺序、可认领的回退席位、index 转换 |
| [storage.md](storage.md) | 存储子系统:后端约定(`StorageBackend`)、`StorageForms`、`DomainSpec`/`Domain`、`domain/changed` |
| [workspace.md](workspace.md) | 工作区注册表:`Workspace`/`WorkspaceId`、注册与解析、与会话 `cwd` 的关系 |
-| [client-modules.md](client-modules.md) | Web 插件表:`dshClient` 声明、`WebBootGraph` 线上组合、bundle 路由与 index 转换 |
+| [client-modules.md](client-modules.md) | Web 插件表:`dsh.client` 声明、`WebBootGraph` 线上组合、bundle 路由与 index 转换 |
| [session-projection.md](session-projection.md) | 投影 seam:`SessionProjectionMap`、纯函数 `ProjectionDefinition` 单元、`ProjectionSnapshot` 的一致切面、变更馈送 |
-| [telemetry.md](telemetry.md) | 会话遥测能力 seam:`TelemetryRecord`/`TelemetrySeverity`、`TelemetryBackend` 约定、`telemetry/record` 脱敏 waterfall |
+| [telemetry.md](telemetry.md) | 对外会话上报能力 seam:`TelemetryRecord`/`TelemetrySeverity`、`TelemetryBackend` 约定和 `telemetry/record` 脱敏 waterfall |
-> 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务与事件使用每页生成的 **Cordis surface** 小节。
+> 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务与事件使用每页生成的 **Cordis API** 小节。
diff --git a/docs/subsystems/approval.i18n.yaml b/docs/subsystems/approval.i18n.yaml
index 088b30ff32..bfc510ac16 100644
--- a/docs/subsystems/approval.i18n.yaml
+++ b/docs/subsystems/approval.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/approval.md
-approval.md: 660ced6d75886bbea41985fd1f357bf92e8a7838
-approval.zh.md: 56975914098978af99580694ca19f20a02ed4022
+approval.md: 6ade53d7755f1070a17513d9f3efd4252d6d255f
+approval.zh.md: 92138bee75369f81b69b3551ee47ea97be07441e
diff --git a/docs/subsystems/approval.md b/docs/subsystems/approval.md
index 660ced6d75..6ade53d775 100644
--- a/docs/subsystems/approval.md
+++ b/docs/subsystems/approval.md
@@ -91,9 +91,9 @@ The audit events are log-only and do not enter the model transcript. Model-visib
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/approval.zh.md b/docs/subsystems/approval.zh.md
index 5697591409..92138bee75 100644
--- a/docs/subsystems/approval.zh.md
+++ b/docs/subsystems/approval.zh.md
@@ -91,9 +91,9 @@ interface ApprovalRequest {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/attachment.i18n.yaml b/docs/subsystems/attachment.i18n.yaml
new file mode 100644
index 0000000000..330f2db253
--- /dev/null
+++ b/docs/subsystems/attachment.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/attachment.md
+attachment.md: ff7f14ceae8d4f8055d5cfd4367373729dc5ecbc
+attachment.zh.md: d7a9527788588d5504fdeffd8ae7849b0f8b1378
diff --git a/docs/subsystems/attachment.md b/docs/subsystems/attachment.md
new file mode 100644
index 0000000000..ff7f14ceae
--- /dev/null
+++ b/docs/subsystems/attachment.md
@@ -0,0 +1,115 @@
+# Durable Image Attachments
+
+English | [中文](attachment.zh.md)
+
+The attachment seam separates binary image ownership from the session log. A producer gives validated encoded bytes to [`ctx.attachments`](#ctxattachments--attachmentstore-abstract-seam); the service publishes an immutable content-addressed reference only after the object is durable. Session events and model-visible `ImageBlock`s contain that reference and metadata, never a browser object URL, host temporary path, provider URL, or base64 payload.
+
+Unsent browser drafts may stay in memory and native clients may stage them in operating-system temporary storage. Once the host accepts a user message, its images move below `/attachments/v1` before the user event is appended. Structured model image output follows the same persist-before-event rule.
+
+Source: [`packages/attachment/attachment/src/types.ts`](../../packages/attachment/attachment/src/types.ts)
+
+## Identity and verified metadata
+
+`AttachmentId` is a branded opaque string. The local backend currently emits `sha256:`, but consumers must neither parse that representation nor derive a filesystem path from it.
+
+```ts type-equiv
+/** Raster image formats accepted by the version-one attachment path. */
+type ImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
+```
+
+```ts type-equiv
+/** Durable, serializable metadata for one immutable image object. */
+interface ImageAttachmentRef {
+ /** Opaque storage identifier; never a filesystem path or bearer URL. */
+ attachmentId: AttachmentId
+ /** Media type verified from the stored bytes. */
+ mediaType: ImageMediaType
+ /** Exact encoded byte length. */
+ bytes: number
+ /** Intrinsic encoded width in pixels. */
+ width: number
+ /** Intrinsic encoded height in pixels. */
+ height: number
+ /** Optional display name stripped of local path information. */
+ name?: string
+}
+```
+
+```ts type-equiv
+/** Deployment-resolved limits used by upload admission and request buffering. */
+interface ImageAttachmentLimits {
+ maxImageBytes: number
+ maxImagesPerMessage: number
+ maxMessageImageBytes: number
+ maxImagePixels: number
+ mediaTypes: readonly ImageMediaType[]
+}
+```
+
+The reference records intrinsic dimensions and encoded length so clients can lay out history without decoding first, while every authoritative read still re-checks digest, media signature, dimensions, and metadata against the object.
+
+## Commit and verified-read payloads
+
+```ts type-equiv
+/** Request to validate and durably commit one image. */
+interface SaveImageAttachment {
+ data: Uint8Array
+ /** Caller-declared media type, checked against fully decoded bytes. */
+ mediaType: ImageMediaType
+ /** Optional browser/provider display name; it is never interpreted as a path. */
+ name?: string
+}
+```
+
+```ts type-equiv
+/** Stored image bytes returned after reference and digest verification. */
+interface StoredImageAttachment {
+ ref: ImageAttachmentRef
+ data: Uint8Array
+}
+```
+
+`saveImage()` validates bytes and atomically commits one object before returning its reference. `validateImage()` runs the same admission checks without persisting anything; batch callers validate every member through it before saving any member, so validation rejection leaves no partial objects behind. `readImage()` accepts a reference from an authorized session path and returns bytes only after integrity verification. The service is deliberately retention-neutral: resumed and forked sessions may share objects, so reference-aware garbage collection is deferred rather than tied to any one session's deletion.
+
+
+
+
+
+## Cordis API
+
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+
+
+
+### `ctx.attachments` — `AttachmentStore` (abstract seam)
+
+Immutable binary attachment service. Implementations validate bytes before publishing a reference.
+
+```ts cordis-catalog
+/**
+ * Validate one image without persisting it.
+ * Batch callers validate every member before saving any member.
+ * @param input - encoded bytes, declared media type, and optional display name.
+ * @returns completion after the encoded raster has been fully decoded.
+ */
+abstract validateImage(input: SaveImageAttachment): Promise
+
+/**
+ * Validate and durably commit one image before its owning session event is appended.
+ * @param input - encoded bytes, declared media type, and optional display name.
+ * @returns a durable content-addressed reference.
+ */
+abstract saveImage(input: SaveImageAttachment): Promise
+
+/**
+ * Read one image and verify that bytes still match the recorded reference.
+ * @param ref - durable reference from the session log.
+ * @param signal - optional cancellation for backend read and verification work.
+ * @returns the verified bytes and canonical reference.
+ * @throws the signal reason when aborted, or a storage error when verification fails.
+ */
+abstract readImage(ref: ImageAttachmentRef, signal?: AbortSignal): Promise
+```
+
+Source: [`packages/attachment/attachment/src/index.ts:29`](../../packages/attachment/attachment/src/index.ts)
+
diff --git a/docs/subsystems/attachment.zh.md b/docs/subsystems/attachment.zh.md
new file mode 100644
index 0000000000..d7a9527788
--- /dev/null
+++ b/docs/subsystems/attachment.zh.md
@@ -0,0 +1,115 @@
+# 持久图片附件
+
+[English](attachment.md) | 中文
+
+附件 seam 将二进制图片的所有权与会话日志分离。生成方把经过校验的编码字节交给 [`ctx.attachments`](#ctxattachments--attachmentstore-abstract-seam);只有对象完成持久化后,该服务才会发布不可变的内容寻址引用。会话事件和模型可见的 `ImageBlock` 包含该引用及其元数据,绝不包含浏览器对象 URL、宿主临时路径、提供方 URL 或 base64 数据。
+
+未发送的浏览器草稿可以保留在内存中,原生客户端也可以将其暂存于操作系统临时存储。宿主接受用户消息后,会先把消息中的图片移到 `/attachments/v1` 下,再追加用户事件。结构化模型图片输出遵循同样的先持久化、后追加事件规则。
+
+来源:[`packages/attachment/attachment/src/types.ts`](../../packages/attachment/attachment/src/types.ts)
+
+## 标识与经过校验的元数据
+
+`AttachmentId` 是带类型标记的不透明字符串。本地后端目前生成 `sha256:`,但消费方既不能解析这种表示,也不能据此派生文件系统路径。
+
+```ts type-equiv
+/** Raster image formats accepted by the version-one attachment path. */
+type ImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
+```
+
+```ts type-equiv
+/** Durable, serializable metadata for one immutable image object. */
+interface ImageAttachmentRef {
+ /** Opaque storage identifier; never a filesystem path or bearer URL. */
+ attachmentId: AttachmentId
+ /** Media type verified from the stored bytes. */
+ mediaType: ImageMediaType
+ /** Exact encoded byte length. */
+ bytes: number
+ /** Intrinsic encoded width in pixels. */
+ width: number
+ /** Intrinsic encoded height in pixels. */
+ height: number
+ /** Optional display name stripped of local path information. */
+ name?: string
+}
+```
+
+```ts type-equiv
+/** Deployment-resolved limits used by upload admission and request buffering. */
+interface ImageAttachmentLimits {
+ maxImageBytes: number
+ maxImagesPerMessage: number
+ maxMessageImageBytes: number
+ maxImagePixels: number
+ mediaTypes: readonly ImageMediaType[]
+}
+```
+
+引用记录固有尺寸和编码长度,使客户端无需先解码即可排布历史记录;每次权威读取仍会根据对象重新校验摘要、媒体签名、尺寸和元数据。
+
+## 提交与校验读取的数据
+
+```ts type-equiv
+/** Request to validate and durably commit one image. */
+interface SaveImageAttachment {
+ data: Uint8Array
+ /** Caller-declared media type, checked against fully decoded bytes. */
+ mediaType: ImageMediaType
+ /** Optional browser/provider display name; it is never interpreted as a path. */
+ name?: string
+}
+```
+
+```ts type-equiv
+/** Stored image bytes returned after reference and digest verification. */
+interface StoredImageAttachment {
+ ref: ImageAttachmentRef
+ data: Uint8Array
+}
+```
+
+`saveImage()` 校验字节并以原子方式提交一个对象,之后才返回其引用。`validateImage()` 执行相同的准入检查,但不持久化任何内容;批量调用方会在保存任何成员前通过它校验所有成员,因此校验拒绝不会留下部分对象。`readImage()` 接受来自已授权会话路径的引用,只在完整性校验通过后返回字节。该服务刻意不规定保留策略:恢复和 fork 后的会话可能共享对象,因此基于引用的垃圾回收会延期实现,而不是与任何一个会话的删除绑定。
+
+
+
+
+
+## Cordis API
+
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+
+
+
+### `ctx.attachments` — `AttachmentStore` (abstract seam)
+
+Immutable binary attachment service. Implementations validate bytes before publishing a reference.
+
+```ts cordis-catalog
+/**
+ * Validate one image without persisting it.
+ * Batch callers validate every member before saving any member.
+ * @param input - encoded bytes, declared media type, and optional display name.
+ * @returns completion after the encoded raster has been fully decoded.
+ */
+abstract validateImage(input: SaveImageAttachment): Promise
+
+/**
+ * Validate and durably commit one image before its owning session event is appended.
+ * @param input - encoded bytes, declared media type, and optional display name.
+ * @returns a durable content-addressed reference.
+ */
+abstract saveImage(input: SaveImageAttachment): Promise
+
+/**
+ * Read one image and verify that bytes still match the recorded reference.
+ * @param ref - durable reference from the session log.
+ * @param signal - optional cancellation for backend read and verification work.
+ * @returns the verified bytes and canonical reference.
+ * @throws the signal reason when aborted, or a storage error when verification fails.
+ */
+abstract readImage(ref: ImageAttachmentRef, signal?: AbortSignal): Promise
+```
+
+Source: [`packages/attachment/attachment/src/index.ts:29`](../../packages/attachment/attachment/src/index.ts)
+
diff --git a/docs/subsystems/bash.i18n.yaml b/docs/subsystems/bash.i18n.yaml
index dbfc52bf6e..b3cd579b22 100644
--- a/docs/subsystems/bash.i18n.yaml
+++ b/docs/subsystems/bash.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/bash.md
-bash.md: d2797c1e3ff73fe8ecb5a053ed4a1d13b13298dd
-bash.zh.md: b38a4f332978e94d2945f5fa49b38e8d7df8c9ca
+bash.md: 50e2051778dc0aca5c7b80e1a8b330ae62d4d309
+bash.zh.md: 40ab5b7eea04aeb7c85ab550a8d93ce352fe116e
diff --git a/docs/subsystems/bash.md b/docs/subsystems/bash.md
index d2797c1e3f..50e2051778 100644
--- a/docs/subsystems/bash.md
+++ b/docs/subsystems/bash.md
@@ -98,7 +98,7 @@ interface BashExecSpec {
}
```
-`stdin` and `env` are trusted in-process plugin inputs and are not exposed by `dsh-tool-bash`. The local executor scrubs ambient credentials before merging explicit caller-supplied env. See [the bash-stdin-env Agent Note](../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md).
+`stdin` and `env` are trusted in-process plugin inputs and are not exposed by `dsh-tool-bash`. The local executor scrubs ambient credentials before merging explicit caller-supplied env. See [the bash-stdin-env Agent Note](../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md).
`stdoutMaxBytes` is also trusted-plugin-only. It lets a foreground consumer request complete stdout up to a bounded parser budget without changing stderr, background tasks, or the model-facing bash tool's ordinary output cap.
@@ -136,7 +136,7 @@ interface BashRunResult {
}
```
-Each stream is a `CollectedOutput` — the (possibly truncated) text plus recovery info; when truncated, `text` is the **tail** and the complete stream spills to a private file. The shape is owned by the [subprocess seam](subprocess.md) and re-exported by `dsh-bash`.
+Each stream is a `CollectedOutput` — the (possibly truncated) text plus recovery info; when truncated, `text` is the **tail** and the complete stream spills to a private file. The fields are owned by the [subprocess seam](subprocess.md) and re-exported by `dsh-bash`.
## File sandbox: `BashSandboxInfo`
@@ -224,9 +224,9 @@ interface BashProcessRead {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/bash.zh.md b/docs/subsystems/bash.zh.md
index b38a4f3329..40ab5b7eea 100644
--- a/docs/subsystems/bash.zh.md
+++ b/docs/subsystems/bash.zh.md
@@ -98,7 +98,7 @@ interface BashExecSpec {
}
```
-`stdin` 和 `env` 是受信任的进程内插件输入,不由 `dsh-tool-bash` 暴露。本地执行器会先清除环境中的凭据,再合并调用方显式提供的 env。见 [bash-stdin-env Agent Note](../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)。
+`stdin` 和 `env` 是受信任的进程内插件输入,不由 `dsh-tool-bash` 暴露。本地执行器会先清除环境中的凭据,再合并调用方显式提供的 env。见 [bash-stdin-env Agent Note](../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md)。
`stdoutMaxBytes` 同样仅供受信任插件使用。它让前台消费方能在有界解析预算内请求完整 stdout,而不会改变 stderr、后台任务或面向模型的 bash 工具的常规输出上限。
@@ -136,7 +136,7 @@ interface BashRunResult {
}
```
-每个流是一个 `CollectedOutput`:(可能被截断的)文本加恢复信息;截断时,`text` 是**尾部**,完整流溢出到一个私有文件。该形状归[进程管理器 seam](subprocess.md)所有,由 `dsh-bash` 重导出。
+每个流是一个 `CollectedOutput`:(可能被截断的)文本加恢复信息;截断时,`text` 是**尾部**,完整流溢出到一个私有文件。这些字段归[进程管理器 seam](subprocess.md)所有,由 `dsh-bash` 重导出。
## 文件沙箱:`BashSandboxInfo`
@@ -224,9 +224,9 @@ interface BashProcessRead {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/client-modules.i18n.yaml b/docs/subsystems/client-modules.i18n.yaml
index 8c320d18e6..22b3d51e3e 100644
--- a/docs/subsystems/client-modules.i18n.yaml
+++ b/docs/subsystems/client-modules.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/client-modules.md
-client-modules.md: 808fa62b2e5a4351b1dc98bbe4f3d2af63c9dd2b
-client-modules.zh.md: dcab60bea7c54770cc5e6032afb39d6dd189d037
+client-modules.md: bd0ad83f6ebbbab3be8c91e8d51b1c3492e41e6c
+client-modules.zh.md: aea573ee99be2781427d6843b12936406d563283
diff --git a/docs/subsystems/client-modules.md b/docs/subsystems/client-modules.md
index 808fa62b2e..bd0ad83f6e 100644
--- a/docs/subsystems/client-modules.md
+++ b/docs/subsystems/client-modules.md
@@ -2,7 +2,7 @@
English | [中文](client-modules.zh.md)
-The web plugin table: the Node half of the client module system in [dsh-client-modules](../../packages/client/modules), provided as `ctx.clientModuleHost` (`ClientModuleHostService`). It scans the host Loader's entries for `dshClient` packages, composes the `window.__DSH_BOOT__` entry graph, serves each bundle at `/plugins//client.js`, and taps the index render to inject the boot manifest — the four faces of one service. It is an optional capability of the web GUI stack, not part of the agent-loop spine, and it is a consumer of [dsh-host-webserver](../../packages/host/webserver): the carrier described in [http-server.md](http-server.md) supplies the prefix route and index tap this service registers. The same package's browser half (`ctx.modules`, the lazy-CJS module table that fetches and materializes these bundles) is kernel machinery documented in the [package README](../../packages/client/modules/README.md), not here.
+The web plugin table: the Node half of the client module system in [dsh-client-modules](../../packages/client/modules), provided as `ctx.clientModuleHost` (`ClientModuleHostService`). It scans the host Loader's entries for packages declaring `dsh.client`, composes the `window.__DSH_BOOT__` entry graph, serves each bundle at `/plugins//client.js`, and taps the index render to inject the boot manifest — the four faces of one service. It is an optional capability of the web GUI stack, not part of the agent-loop spine, and it is a consumer of [dsh-host-webserver](../../packages/host/webserver): the carrier described in [http-server.md](http-server.md) supplies the prefix route and index tap this service registers. The same package's browser half (`ctx.modules`, the lazy-CJS module table that fetches and materializes these bundles) is kernel machinery documented in the [package README](../../packages/client/modules/README.md), not here.
Source: [`packages/client/modules/src/client/manifest.ts`](../../packages/client/modules/src/client/manifest.ts)
@@ -15,7 +15,7 @@ The graph is the wire single source between the Node and browser halves: the hos
* One composed client entry pushed by the host (a graph row). Wire
* single source: the host node half (package root) produces this same shape.
* `immediately` marks stage-one prefetch; `inject` is informational graph
- * metadata (the authoritative edges live in each package's dshClient
+ * metadata (the authoritative edges live in each package's `dsh.client`
* declaration and reach fibers through entry creation).
*/
interface WebBootEntry {
@@ -46,7 +46,7 @@ Each row's `rev` is the bundle's content hash and rides the URL as a cache-busti
## The scan
-A package joins the table by declaring `dshClient` (`platform: 'web'`, optional `inject` edges, optional `immediately`) in its package.json and exporting its built bundle at `exports["./client"]`. Package resolution anchors at the config tree's `ctx.baseUrl` — the cordis.yml directory, whose package declares every composed plugin as a dependency — and construction throws when that anchor is unset.
+A package joins the table by declaring `dsh.client` (`platform: 'web'`, optional `inject` edges, optional `immediately`) in its package.json and exporting its built bundle at `exports["./client"]`. Package resolution anchors at the config tree's `ctx.baseUrl` — the cordis.yml directory, whose package declares every composed plugin as a dependency — and construction throws when that anchor is unset.
Scanning is incremental per package; there is no full-rescan code path. Every cordis `internal/plugin` emission (fiber construction or disposal) marks the fiber's entry name dirty, and a microtask flush reconciles each dirty name against the live loader entries. The activation pass seeds the same dirty set with all current entries and flushes synchronously, so first scan and steady state share one implementation — with opposite failure postures. At activation, a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud `AggregateError` listing every broken package: the fiber FAILS and the boot's fail-loud sweep reports it. In steady state, a broken package logs a warning and must not poison the others.
@@ -66,15 +66,15 @@ In development, [dsh-client-hmr](../../packages/client/hmr/README.md) is the reg
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
### `ctx.clientModuleHost` — `ClientModuleHostService`
-The web plugin table service: incremental dshClient scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
+The web plugin table service: incremental `dsh.client` scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
```ts cordis-catalog
/**
diff --git a/docs/subsystems/client-modules.zh.md b/docs/subsystems/client-modules.zh.md
index dcab60bea7..aea573ee99 100644
--- a/docs/subsystems/client-modules.zh.md
+++ b/docs/subsystems/client-modules.zh.md
@@ -2,7 +2,7 @@
[English](client-modules.md) | 中文
-Web 插件表:[dsh-client-modules](../../packages/client/modules) 中 client 模块系统的 Node 半,以 `ctx.clientModuleHost`(`ClientModuleHostService`)形式提供。它扫描宿主 Loader 的 entry 找出 `dshClient` 包(package),组合出 `window.__DSH_BOOT__` entry 图,在 `/plugins//client.js` 提供各个 bundle,并经 index 转换(index tap)注入启动 manifest(元数据清单)——这是同一个服务的四个面。它是 Web GUI 栈的一项可选能力,不属于 agent loop(智能体循环)主干,并且是 [dsh-host-webserver](../../packages/host/webserver) 的消费方:[http-server.md](http-server.md) 所述的载体提供本服务注册的前缀路由与 index 转换。同一个包的浏览器半(`ctx.modules`,即拉取并物化这些 bundle 的 lazy CJS 模块表)属于内核机件,记录在[包 README](../../packages/client/modules/README.md)中,不在本页。
+Web 插件表:[dsh-client-modules](../../packages/client/modules) 中 client 模块系统的 Node 半,以 `ctx.clientModuleHost`(`ClientModuleHostService`)形式提供。它扫描宿主 Loader 的 entry,找出声明了 `dsh.client` 的包(package),组合出 `window.__DSH_BOOT__` entry 图,在 `/plugins//client.js` 提供各个 bundle,并经 index 转换(index tap)注入启动 manifest(元数据清单)——这是同一个服务的四个面。它是 Web GUI 栈的一项可选能力,不属于 agent loop(智能体循环)主干,并且是 [dsh-host-webserver](../../packages/host/webserver) 的消费方:[http-server.md](http-server.md) 所述的载体提供本服务注册的前缀路由与 index 转换。同一个包的浏览器半(`ctx.modules`,即拉取并物化这些 bundle 的 lazy CJS 模块表)属于内核机件,记录在[包 README](../../packages/client/modules/README.md)中,不在本页。
源码:[`packages/client/modules/src/client/manifest.ts`](../../packages/client/modules/src/client/manifest.ts)
@@ -15,7 +15,7 @@ Web 插件表:[dsh-client-modules](../../packages/client/modules) 中 client
* One composed client entry pushed by the host (a graph row). Wire
* single source: the host node half (package root) produces this same shape.
* `immediately` marks stage-one prefetch; `inject` is informational graph
- * metadata (the authoritative edges live in each package's dshClient
+ * metadata (the authoritative edges live in each package's `dsh.client`
* declaration and reach fibers through entry creation).
*/
interface WebBootEntry {
@@ -46,7 +46,7 @@ interface WebBootGraph {
## 扫描
-包加入这张表的方式,是在自己的 package.json 中声明 `dshClient`(`platform: 'web'`、可选的 `inject` 边、可选的 `immediately`),并在 `exports["./client"]` 导出构建好的 bundle。包解析锚定在配置树的 `ctx.baseUrl`——即 cordis.yml 所在目录,该目录的包把每个被组合的插件声明为依赖——这一锚点未设置时,构造即抛错。
+包加入这张表的方式,是在自己的 package.json 中声明 `dsh.client`(`platform: 'web'`、可选的 `inject` 边、可选的 `immediately`),并在 `exports["./client"]` 导出构建好的 bundle。包解析锚定在配置树的 `ctx.baseUrl`——即 cordis.yml 所在目录,该目录的包把每个被组合的插件声明为依赖——这一锚点未设置时,构造即抛错。
扫描是单包增量的;不存在全量重扫代码路径。fiber 构造或 dispose(资源释放)时的每次 cordis `internal/plugin` 发射都把该 fiber 的 entry 名标脏,一次微任务 flush 把每个脏名与实时 loader entry 对账。激活趟以全部当前 entry 灌入同一个脏集合并同步 flush,因此初扫与稳态共享一条实现——但失败姿态相反。激活时,已加载 entry 中的畸形声明或缺失 bundle 会聚合为一个大声的 `AggregateError`,列出每个损坏的包:该 fiber 进入 FAILED,由启动的大声失败 sweep 上报。稳态下,损坏的包只记录一条警告,且不得殃及其他包。
@@ -66,15 +66,15 @@ interface WebBootGraph {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
### `ctx.clientModuleHost` — `ClientModuleHostService`
-The web plugin table service: incremental dshClient scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
+The web plugin table service: incremental `dsh.client` scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
```ts cordis-catalog
/**
diff --git a/docs/subsystems/code-runtime.i18n.yaml b/docs/subsystems/code-runtime.i18n.yaml
index 47783d5f77..5b9f2d43b1 100644
--- a/docs/subsystems/code-runtime.i18n.yaml
+++ b/docs/subsystems/code-runtime.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/code-runtime.md
-code-runtime.md: 6d3fa7fa72891897155cb9c611c7aa472629de4f
-code-runtime.zh.md: 47516c7d21c24399498a05410ab9a46b61736fb4
+code-runtime.md: ef5f2b9b1f39c5e7218cc1f5d9ae6435ef725e3a
+code-runtime.zh.md: 3a136c4160eb30a38968ea4d482201d22ef62fde
diff --git a/docs/subsystems/code-runtime.md b/docs/subsystems/code-runtime.md
index 6d3fa7fa72..ef5f2b9b1f 100644
--- a/docs/subsystems/code-runtime.md
+++ b/docs/subsystems/code-runtime.md
@@ -36,7 +36,7 @@ interface CodeRunRequest {
}
```
-The result reports an error as a **field**, never a rejection of `run()` — reporting a failed program is the caller's job, not an exception path (mirroring `BashExecutor.run`'s resolve-on-failure contract):
+The result reports an error as a **field**, never a rejection of `run()` — reporting a failed program is the caller's job, not an exception path (matching `BashExecutor.run`'s resolve-on-failure contract):
```ts type-equiv
/**
@@ -164,9 +164,9 @@ interface CodeRunFailure {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/code-runtime.zh.md b/docs/subsystems/code-runtime.zh.md
index 47516c7d21..3a136c4160 100644
--- a/docs/subsystems/code-runtime.zh.md
+++ b/docs/subsystems/code-runtime.zh.md
@@ -164,9 +164,9 @@ interface CodeRunFailure {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/commands.i18n.yaml b/docs/subsystems/commands.i18n.yaml
index 32c5f15c15..f04e332ad9 100644
--- a/docs/subsystems/commands.i18n.yaml
+++ b/docs/subsystems/commands.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/commands.md
-commands.md: 5e280d659c2a74b3d8ba20f362922e9eb4e0b84c
-commands.zh.md: 24f099d1926f7dba45122bdc53c23a4274e3f507
+commands.md: 03cb068418ff3e157349989e421f33d76cd7afe6
+commands.zh.md: b900baab4b0fffb50ac10265f4ea3ac2d8f9d01b
diff --git a/docs/subsystems/commands.md b/docs/subsystems/commands.md
index 5e280d659c..03cb068418 100644
--- a/docs/subsystems/commands.md
+++ b/docs/subsystems/commands.md
@@ -51,7 +51,7 @@ The adapter owns cancellation and passes the exact target agent. `rawInput` begi
interface CommandInvocation {
/** Pairing id already written to this invocation's `command/run` event. */
readonly commandId: CommandId
- /** Exact agent whose human-facing surface received the command. */
+ /** Exact agent whose UI received the command. */
readonly agent: Agent
/** Exact text following the registered command name, including separator whitespace. */
readonly rawInput: string
@@ -104,9 +104,9 @@ interface ParsedCommand {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/commands.zh.md b/docs/subsystems/commands.zh.md
index 24f099d192..b900baab4b 100644
--- a/docs/subsystems/commands.zh.md
+++ b/docs/subsystems/commands.zh.md
@@ -51,7 +51,7 @@ interface CommandDefinition {
interface CommandInvocation {
/** Pairing id already written to this invocation's `command/run` event. */
readonly commandId: CommandId
- /** Exact agent whose human-facing surface received the command. */
+ /** Exact agent whose UI received the command. */
readonly agent: Agent
/** Exact text following the registered command name, including separator whitespace. */
readonly rawInput: string
@@ -104,9 +104,9 @@ interface ParsedCommand {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/compaction.i18n.yaml b/docs/subsystems/compaction.i18n.yaml
index d2fe7303fc..5cc4527f73 100644
--- a/docs/subsystems/compaction.i18n.yaml
+++ b/docs/subsystems/compaction.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/compaction.md
-compaction.md: fdff28ac2ec83966a03050ba8d54fe0ee26c4fc6
-compaction.zh.md: e4227320d9829cac695dc4ba0c5779086f77194b
+compaction.md: 9286a8be3f9030ac383701c6390c6c3582ec83a2
+compaction.zh.md: b4b353fef903b5e1a9cd29cee3147004b657c3bb
diff --git a/docs/subsystems/compaction.md b/docs/subsystems/compaction.md
index fdff28ac2e..9286a8be3f 100644
--- a/docs/subsystems/compaction.md
+++ b/docs/subsystems/compaction.md
@@ -20,7 +20,7 @@ The lock brackets the **whole** operation: `compact/start` is appended first, th
The markers are lock time points, not an exclusive container. An unrelated idle injection can appear between a standalone manual start and end while summarization is pending. The manual path revalidates only its selected positional span, so that injected context survives after the replacement checkpoint. A live unmatched start blocks every entry point; an unmatched start before a newer `session/end-seed` is stale evidence from a prior lifecycle and is ignored.
-These variants are merged inside a `declare module '@deepseek-ai/dsh-session/types'` block, so — unlike the top-level types on the other subsystem pages — they are not pasted as a drift-checked ` ```ts type-equiv ` block (the `verify-type-equiv` extractor matches only top-level declarations by name). The payload table above is the catalog entry; follow the source link for the authoritative shapes.
+These variants are merged inside a `declare module '@deepseek-ai/dsh-session/types'` block, so — unlike the top-level types on the other subsystem pages — they are not pasted as a drift-checked ` ```ts type-equiv ` block (the `verify-type-equiv` extractor matches only top-level declarations by name). The payload table above is the catalog entry; follow the source link for the authoritative fields.
## `CompactionResult`
@@ -85,7 +85,7 @@ type ManualCompactionErrorCode =
Pressure compaction runs at serial `agent/pre-step` before request derivation. Once pressure or canonical overflow qualifies, compact-basic invokes optional [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md) before range selection, remeasures through `ctx.tokenMeter`, and can advance the surface without a summary. Failed-request recovery runs through `agent/request-error` after the failed step closes and returns a retry action only when the surface replacement generation advances, even if later summary work throws after pruning; cancellation still wins. Region boundaries preserve tool-call/result pairing but not whole turns, allowing early closed steps of one oversized turn to compact. `dsh-compact-basic` owns thresholds, retained-tail policy, overflow caps, and failure handling.
-The Service Definition exports `toolPairingBalancedBefore(session, seq)` and `toolPairingBalancedAfter(session, seq)` for those edge checks. Both validate current surface membership and reject missing seqs and orphan results; the [package contract](../../packages/compact/compact/README.md#tool-pairing-boundaries) owns their cache semantics.
+The Service Definition exports `toolPairingBalancedBefore(session, seq)` and `toolPairingBalancedAfter(session, seq)` for the tool-call/result pairing checks before and after a seq. Both validate current surface membership and reject missing seqs and orphan results; the [package contract](../../packages/compact/compact/README.md#tool-pairing-boundaries) defines their cache behavior.
## Tool-result pruning outcomes
@@ -121,9 +121,9 @@ interface PruneResult {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/compaction.zh.md b/docs/subsystems/compaction.zh.md
index e4227320d9..b4b353fef9 100644
--- a/docs/subsystems/compaction.zh.md
+++ b/docs/subsystems/compaction.zh.md
@@ -20,7 +20,7 @@
这些标记表示锁的时间点,而不是排他的容器。摘要等待期间,不相关的空闲注入可以出现在独立的手动 start 与 end 之间。手动路径只重新验证所选位置 span,因此替换检查点之后仍保留该注入上下文。活动的未匹配 start 会阻塞所有入口点;较新 `session/end-seed` 之前的未匹配 start 是先前生命周期留下的陈旧证据,会被忽略。
-这些变体在 `declare module '@deepseek-ai/dsh-session/types'` 块内合并,因此——与其他子系统页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威形状请循源码链接查看。
+这些变体在 `declare module '@deepseek-ai/dsh-session/types'` 块内合并,因此——与其他子系统页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威字段请循源码链接查看。
## `CompactionResult`
@@ -85,7 +85,7 @@ type ManualCompactionErrorCode =
压力压缩在串行 `agent/pre-step` 中运行,先于请求推导。一旦压力或规范化溢出满足条件,compact-basic 会在选择范围前调用可选的 [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md),再通过 `ctx.tokenMeter` 重新测量,并且可以在不生成摘要的情况下推进 surface。失败请求的恢复在失败的步骤关闭后通过 `agent/request-error` 运行;仅当 surface replacement generation 前进时才返回重试动作,即便后续摘要工作在剪枝后抛异常亦如此;取消仍然优先。区域边界保持工具调用/结果配对,但不保持整个轮次,因此一个过大轮次中较早关闭的步骤可以被压缩。`dsh-compact-basic` 拥有阈值、保留尾部策略、溢出上限与失败处理。
-该 Service Definition 导出 `toolPairingBalancedBefore(session, seq)` 与 `toolPairingBalancedAfter(session, seq)`,用于这些边缘检查。两者都会验证当前 surface 成员关系,并拒绝缺失的 seq 与遗留结果;其缓存语义由[包约定](../../packages/compact/compact/README.md#tool-pairing-boundaries)规定。
+该 Service Definition 导出 `toolPairingBalancedBefore(session, seq)` 与 `toolPairingBalancedAfter(session, seq)`,用于检查 seq 之前与之后的工具调用/结果配对。两者都会验证当前 surface 成员关系,并拒绝缺失的 seq 与遗留结果;[包约定](../../packages/compact/compact/README.md#tool-pairing-boundaries)定义其缓存行为。
## 工具结果剪枝产出
@@ -121,9 +121,9 @@ interface PruneResult {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml
index 70ceac3b11..dc9105c6a8 100644
--- a/docs/subsystems/core.i18n.yaml
+++ b/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: 27c4359e360223d336cd94695bb45a79f0fd370c
-core.zh.md: 4e7519665b6d9efb8075546d93325debd961905d
+core.md: 1199943eb5770fd5fa7cc21cbed3023b844a55d6
+core.zh.md: 138d1d6292b89b2a9ff01cb925fdb81f008833fa
diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md
index 27c4359e36..1199943eb5 100644
--- a/docs/subsystems/core.md
+++ b/docs/subsystems/core.md
@@ -2,7 +2,7 @@
English | [中文](core.zh.md)
-The **core** subsystem is [`packages/core`](../../packages/core/README.md) — the control spine every composition boots: the event-sourced session log, system-prompt assembly, the tool registry, the agent vocabulary, and the concrete loop that drives them. This page owns what the `agent`/`agent-loop` pair declares — how an agent is created and owned, and the `Agent` handle with its delivery, cancellation, and interception contracts — plus the two type patterns every subsystem follows; the group's dedicated pages and the rest of the folder are indexed in the [subsystems README](README.md).
+The **core** subsystem is [`packages/core`](../../packages/core/README.md) — the packages every composition boots: the event-sourced session log, system-prompt assembly, the tool registry, the agent types, and the concrete loop that drives them. This page explains what the `agent`/`agent-loop` pair declares — how an agent is created and owned, and the `Agent` handle's delivery, cancellation, and interception contracts — plus the two type patterns every subsystem follows. The group's dedicated pages and the rest of the folder are indexed in the [subsystems README](README.md).
## The spine, package by package
@@ -31,7 +31,7 @@ Source: [`packages/core/agent/src/index.ts`](../../packages/core/agent/src/index
* {@link AgentRegistry.resume}. The disposer is a CAPABILITY: among consumers,
* only the holder can tear this agent down. The registered factory provider is
* also a structural owner because the scoped agent depends on that provider's
- * service surface; provider unload stops and drains every live handle it made.
+ * service API; provider unload stops and drains every live handle it made.
* `dispose()` stops the loop, awaits its exit, unregisters the agent, removes
* its session from the store, and finally unwinds its scoped world.
*
@@ -48,7 +48,7 @@ interface AgentHandle {
`CreateAgentOptions` carries the shared identity and everything a fresh agent needs before publication: session metadata (`meta` — validated `cwd`, fork lineage, seed boundary, origin classification, delegation depth), an optional `seed` replay prefix for forks, per-agent `AgentOptions`, a creation-only cancellation `signal`, and `setup`. `ResumeAgentOptions` is the persisted-identity counterpart: `resumeSessionId`, `agentOptions`, `signal`, and `setup`. The `setup` callback (`AgentSetup`) composes the agent's scoped world while both ids are still unpublished — everything registered through `agentCtx` exists before `agent/created` and the first prompt assembly — and may return a synchronous commit invoked immediately before publication; a setup rejection, commit throw, or owner disposal rolls the transaction back without publishing either id.
-`AgentFactory` is the creation contract behind the registry: the loop registers its factory via `ctx.agents.setFactory()`, so consumers program against `ctx.agents` without depending on the concrete loop package. The exact `create`/`resume` signatures and their rollback contracts are in the [generated section](#ctxagents--agentregistry) below.
+`AgentFactory` is the creation interface behind the registry: the loop registers its factory via `ctx.agents.setFactory()`, so consumers use `ctx.agents` without depending on the concrete loop package. The exact `create`/`resume` signatures and rollback contracts are in the [generated section](#ctxagents--agentregistry) below.
## The agent handle
@@ -206,11 +206,11 @@ The [event taxonomy](../architecture.md#event) owns the `agent/*` lifecycle, che
## Initiating Agent
-The process-local initiator carried by `ctx.agents` is the exact `Agent` above, not a separate frame or copied identity. Ambient presence is neither liveness proof nor authorization; the [initiator-scope decision](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns its lifetime and boundary rules.
+The process-local initiator carried by `ctx.agents` is the exact `Agent` above, not a separate frame or copied identity. Ambient presence is neither liveness proof nor authorization; the [initiator-scope decision](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md) defines its lifetime and scope rules.
## Interception decisions
-Pre-step decisions use the same identified `UserMessage` shape as durable user-role input. The entered batch is authoritative and preserves every message's id and source. Hook bridges map their native decision fields onto this typed result.
+Pre-step decisions use the same identified `UserMessage` type as durable user-role input. The entered batch is authoritative and preserves every message's `id` and `source`. Hook bridges map their native decision fields onto this typed result.
Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
@@ -232,7 +232,7 @@ type PreStepDecision =
type RequestErrorAction = { kind: 'retry' } | undefined
```
-`agent/pre-step` is the single serial boundary before request derivation. `agent/turn-stopping` runs when a turn has no tool or steering continuation, before one final steering drain.
+`agent/pre-step` is the only serial 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):
@@ -245,13 +245,13 @@ type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact'
A `Session` is an **append-only log** of typed `SessionEvent`s — the single source of truth. The LLM message history is *derived* from the log (`deriveMessages()`), not stored separately. Every entry carries a monotonic `seq`, a `time`, and a `type`-discriminated `data` payload; surface variants may also list cited earlier events in `sourceEventSeqs` and carry a `surfaceOp`.
-The `SessionEvent` envelope's exact conditional shape, the twelve event variants (`turn/start`, `turn/end`, `step/start`, `step/end`, `user/message`, `assistant/chunk`, `assistant/message`, `tool/call`, `tool/result`, `steering/message`, `todo/write`, `request/header`), the `deriveMessages()` projection rules, the `TurnTrigger`/`TurnEndReason` reasons, and the execution-enclosure and standalone-event rules are on **[session.md](session.md)**. How the log is made durable — the `SessionPersistence` seam, JSONL/SQLite backends, the `session/flush` checkpoint, crash recovery, and `SessionHeader` — is on **[persistence.md](persistence.md)**.
+The `SessionEvent` envelope's exact conditional fields, the twelve event variants (`turn/start`, `turn/end`, `step/start`, `step/end`, `user/message`, `assistant/chunk`, `assistant/message`, `tool/call`, `tool/result`, `steering/message`, `todo/write`, `request/header`), the `deriveMessages()` projection rules, the `TurnTrigger`/`TurnEndReason` reasons, and the execution-enclosure and standalone-event rules are on **[session.md](session.md)**. How the log is made durable — the `SessionPersistence` interface, JSONL/SQLite backends, the `session/flush` checkpoint, crash recovery, and `SessionHeader` — is on **[persistence.md](persistence.md)**.
## `ToolDefinition`
-The one pipeline-authoring type that is core: what every registered tool *is* — a model-facing `ToolSchema` plus an `execute` function and optional final-content and UI callbacks. A tool author rarely constructs it by hand (the `defineTool` DSL builds it with typed args), but it is the contract the registry holds and the loop dispatches through.
+The one pipeline-authoring type that is core: what every registered tool *is* — a model-facing `ToolSchema` plus an `execute` function and optional final-content and UI callbacks. A tool author rarely constructs it by hand (the `defineTool` DSL builds it with typed arguments), but it is the contract the registry holds and the loop dispatches through.
-Its full fields, the `defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` typed schema DSL, the `ToolExecution`/`ToolExecutionResult` waterfall shapes, and the tool-presentation UI vocabulary are on **[tools.md](tools.md)**.
+Its full fields, the `defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` typed schema DSL, the `ToolExecution`/`ToolExecutionResult` waterfall types, and the tool-presentation UI types are on **[tools.md](tools.md)**.
## Repo-wide type patterns
@@ -259,7 +259,7 @@ Two patterns recur across every subsystem and are documented once, here.
### The `…Map → derived-union` pattern
-Almost every extensible sum type in the harness follows one shape: an interface keyed by a discriminant tag (the `…Map`), from which the union is derived with `keyof`. Plugins add variants by **declaration merging** — no edit to the owning package.
+Almost every extensible sum type in the harness follows one pattern: an interface keyed by a discriminant tag (the `…Map`), from which the union is derived with `keyof`. Plugins add variants by **declaration merging** — no edit to the owning package.
```ts ignore-check
// The pattern, schematically:
@@ -293,7 +293,7 @@ Two large discriminated unions are the ones consumers `switch` over most: **`Str
### Branded IDs
-IDs that cross package boundaries are **branded** — structurally strings, but non-interchangeable at the type level (a `SessionId` cannot be passed where a `CallId` is expected). Construction goes through a per-type factory; comparison, logging, and JSON behave as ordinary strings.
+IDs passed between packages are **branded** — structurally strings, but non-interchangeable at the type level (a `SessionId` cannot be passed where a `CallId` is expected). Construction goes through a per-type factory; comparison, logging, and JSON behave as ordinary strings.
The `Branded` primitive lives in its own type-only package, [dsh-brand](../../packages/util/brand) (no runtime code, no harness-package dependency), so any package can brand the ids it owns without depending on an unrelated capability package.
@@ -310,9 +310,9 @@ The two core IDs are `CallId` (correlates a tool call with its result; dsh-llm)
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -330,7 +330,7 @@ currentSelection(): ModelSelection
/**
* Save the complete default model selection. A deployment without a settings
* provider keeps its composition entry.
- * @param next - resolved selection accepted by a front door.
+ * @param next - resolved selection accepted by an entry point.
* @returns fulfillment after the optional settings write settles.
*/
async saveSelection(next: ModelSelection): Promise
@@ -377,6 +377,177 @@ Types: [SessionHeader](persistence.md)
Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent-loop/src/index.ts)
+
+
+### `ctx.agentPresets` — `AgentPresets`
+
+Registry over the deployment's agent presets.
+
+Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every call so a preset authored while the process runs is visible immediately, and a preset deleted underneath a picker disappears from the next read.
+
+```ts cordis-catalog
+/**
+ * Every preset the configured roots currently supply.
+ * @returns the presets, first-root-wins per id.
+ */
+async list(): Promise
+
+/**
+ * Resolve one preset by id.
+ *
+ * A broken preset resolves — deleting one, reading one, and reporting one
+ * all need the row — and the mounting paths refuse it AFTER resolution
+ * through {@link resolveMountable}.
+ * @param id - the preset id, or `undefined` for {@link defaultId}.
+ * @returns the resolved preset.
+ * @throws when no configured root supplies that id.
+ */
+async resolve(id?: string): Promise
+
+/**
+ * Compose one agent from a preset: ensure the preset's standing mount, then
+ * parent the agent's scope key to it so the mount's registrations and
+ * listeners cover this agent.
+ *
+ * Call from the agent factory's `setup(agentCtx)`; a rejection there rolls
+ * the agent creation back, so a broken preset never yields a half-composed
+ * session.
+ * @param agentCtx - the agent's scope context.
+ * @param id - the preset id, or `undefined` for {@link defaultId}.
+ * @returns the preset that was composed, for the caller to record.
+ * @throws when the preset is unknown or its composition is unusable.
+ */
+async mount(agentCtx: Context, id?: string): Promise
+
+/**
+ * Join one agent to the SAME standing composition another already runs on.
+ *
+ * This is how a child agent inherits its parent's capabilities. It is a bind,
+ * not a mount: the parent's generation is already composed, so the child gets
+ * that exact instance — the same plugin objects, the same tool registrations,
+ * the same prompt sections. Re-resolving the parent's preset by id instead
+ * would re-read the roster, and a composition file edited since the parent
+ * started would hand the child a DIFFERENT generation than the one its
+ * parent's history was produced under (and a preset deleted since would fail
+ * the child outright while its parent keeps running).
+ *
+ * Synchronous, and with no composition failure mode of its own — it reads no
+ * roster, mounts nothing, and touches no file — which is what lets a child
+ * creation window use it: the two in-process subagent drivers compose their
+ * children inside a synchronous `setup`. It still rejects a caller error, as
+ * the `@throws` below record.
+ *
+ * A parent that joined no preset — a rosterless deployment — yields no join
+ * and no error: there, the model-facing rows sit in the host composition and
+ * the child already sees them through the global layer.
+ * @param agentCtx - the joining agent's scope context.
+ * @param parentCtx - the scope context of the agent whose composition to join.
+ * @returns the preset id joined, or undefined when the parent joined none.
+ * @throws when `agentCtx` carries no scope, or has already joined a preset.
+ */
+composeFrom(agentCtx: Context, parentCtx: Context): string | undefined
+
+/**
+ * The preset one live agent runs on.
+ *
+ * Read from the live scope chain rather than from the session, so it answers
+ * for an agent whose session has not recorded a preset yet — a child agent
+ * whose durable header is being built from its parent's composition.
+ * @param agentCtx - the agent's scope context.
+ * @returns the preset id, or undefined when the agent joined none.
+ */
+composedPreset(agentCtx: Context): string | undefined
+
+/**
+ * Read one preset's composition text.
+ * @param id - the preset id.
+ * @returns the composition exactly as stored.
+ * @throws when no configured root supplies that id.
+ */
+async read(id: string): Promise
+
+/**
+ * Create a locally authored preset by copying an existing one whole.
+ *
+ * Copy is the only authoring write. Composition text never crosses this
+ * seam: the source is named by id and its directory is copied as it stands,
+ * so the copy is exactly as loadable as its source and authoring grants no
+ * capability the roster did not already carry. The copy is NOT mounted to
+ * validate — a source that mounts today yields a copy that mounts today.
+ * @param from - the preset the copy starts from; shipped presets are the
+ * primary source, so any trust is accepted.
+ * @param id - the new preset's id, which becomes its directory name.
+ * @param name - display name for the copy; absent falls back to the id.
+ * @throws when the source is unknown, the id is unusable or already taken,
+ * or the deployment configures no writable root.
+ */
+async copy(from: string, id: string, name?: string): Promise
+
+/**
+ * Delete a locally authored preset.
+ * @param id - the preset id.
+ * @throws when the preset is unknown or ships with the deployment.
+ */
+async remove(id: string): Promise
+
+/**
+ * One agent's instance of a service its preset mounted.
+ *
+ * A preset publishes services behind `isolate` realms, which are invisible
+ * outside the group that declares them — including to the host. This is how a
+ * caller holding the agent reads one anyway: a request that is ABOUT a
+ * session but arrives from outside it, which is every browser RPC.
+ *
+ * Read addressing only. A host row that `inject`s a service cannot use this,
+ * because injection resolves before any session exists and has no agent to
+ * key by; such a service belongs on the host plane instead.
+ * @param agent - the agent whose composition to look inside.
+ * @param name - the service name as the preset's rows resolve it.
+ * @returns the agent's instance, or undefined when its preset mounts none.
+ */
+serviceFor(agent: { ctx: Context }, name: K): Context[K] | undefined
+
+/**
+ * Re-link one agent to a different preset's standing composition.
+ *
+ * Only valid while the agent has produced nothing: swapping tools mid
+ * conversation would leave logged tool calls the new composition cannot
+ * make. The CALLER owns that check — this method does not read session
+ * history.
+ *
+ * The swap is a parent re-link, not an unmount: standing mounts are shared
+ * and permanent, so the old composition stays for its other agents and the
+ * new one is ensured BEFORE the link moves. An unknown or unusable preset
+ * therefore throws with the agent exactly as it was — there is no torn-down
+ * state to restore. The re-link runs through the binding this roster kept
+ * from the agent's mount — dsh-scope's only re-link authority. An agent
+ * that never composed one has nothing to re-link: the switch is then the
+ * agent's first bind, exactly a mount.
+ * @param agentCtx - the agent's scope context.
+ * @param id - the preset to compose the agent from instead.
+ * @returns the preset now installed.
+ * @throws when the preset is unknown or its composition is unusable.
+ */
+async recompose(agentCtx: Context, id: string): Promise
+
+/**
+ * The standing scope key of one preset, for a host reader with no agent.
+ *
+ * A cold transcript read resolves tool presenters against the composition
+ * the session recorded, and the standing mount makes that possible without
+ * resuming anything: ensuring the mount composes plugins but starts no
+ * agent, no session, and no turn.
+ * @param id - the preset id, or `undefined` for {@link defaultId}.
+ * @returns the standing scope key readers pass as a registry view scope.
+ * @throws when the preset is unknown or its composition is unusable.
+ */
+async standingKeyFor(id?: string): Promise
+```
+
+Types: [ScopeKey](scope.md)
+
+Source: [`packages/preset/agent-presets/src/index.ts:80`](../../packages/preset/agent-presets/src/index.ts)
+
### `ctx.agents` — `AgentRegistry`
@@ -547,7 +718,7 @@ list(): Agent[]
roots(): Agent[]
```
-Source: [`packages/core/agent/src/index.ts:254`](../../packages/core/agent/src/index.ts)
+Source: [`packages/core/agent/src/index.ts:256`](../../packages/core/agent/src/index.ts)
diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md
index 4e7519665b..138d1d6292 100644
--- a/docs/subsystems/core.zh.md
+++ b/docs/subsystems/core.zh.md
@@ -2,7 +2,7 @@
[English](core.md) | 中文
-**核心**子系统即 [`packages/core`](../../packages/core/README.md)——每个组合都会启动的控制主干:事件溯源的会话日志、系统提示词组装、工具注册表、agent 词汇,以及驱动它们的具体循环。本页拥有 `agent`/`agent-loop` 这对包所声明的内容——agent 如何被创建与拥有,以及 `Agent` 句柄及其投递、取消与拦截约定——外加每个子系统都遵循的两个类型模式;该组的专属页面与目录其余部分见[子系统 README](README.md)。
+**核心**子系统即 [`packages/core`](../../packages/core/README.md),包含每个组合都会启动的包:事件溯源的会话日志、系统提示词组装、工具注册表、agent 类型,以及驱动它们的具体循环。本页说明 `agent`/`agent-loop` 这对包所声明的内容:agent 如何被创建与拥有,以及 `Agent` 句柄的投递、取消与拦截约定;本页还说明每个子系统都遵循的两个类型模式。该组的专属页面与目录其余部分见[子系统 README](README.md)。
## 主干逐包速览
@@ -33,7 +33,7 @@
* {@link AgentRegistry.resume}. The disposer is a CAPABILITY: among consumers,
* only the holder can tear this agent down. The registered factory provider is
* also a structural owner because the scoped agent depends on that provider's
- * service surface; provider unload stops and drains every live handle it made.
+ * service API; provider unload stops and drains every live handle it made.
* `dispose()` stops the loop, awaits its exit, unregisters the agent, removes
* its session from the store, and finally unwinds its scoped world.
*
@@ -50,7 +50,7 @@ interface AgentHandle {
`CreateAgentOptions` 携带共享标识以及新 agent 发布前所需的一切:会话元数据(`meta`——已校验的 `cwd`、fork 谱系、seed 边界、来源分类、委派深度)、fork 用的可选 `seed` 回放前缀、按 agent 的 `AgentOptions`、仅创建期有效的取消 `signal`,以及 `setup`。`ResumeAgentOptions` 是持久标识的对应物:`resumeSessionId`、`agentOptions`、`signal` 与 `setup`。`setup` 回调(`AgentSetup`)在两个 id 都尚未发布时组装 agent 的作用域世界——凡经 `agentCtx` 注册的内容都先于 `agent/created` 与第一次提示词组装存在——并可返回一个在发布前一刻调用的同步 commit;setup 拒绝、commit 抛出或所有者 dispose 都会回滚事务,两个 id 均不发布。
-`AgentFactory` 是注册表背后的创建约定:循环经 `ctx.agents.setFactory()` 注册其工厂,因此消费方面向 `ctx.agents` 编程,无需依赖具体循环包。确切的 `create`/`resume` 签名及其回滚约定见下方[生成区块](#ctxagents--agentregistry)。
+`AgentFactory` 是注册表背后的创建接口:循环经 `ctx.agents.setFactory()` 注册其工厂,因此消费方使用 `ctx.agents` 时无需依赖具体循环包。确切的 `create`/`resume` 签名及回滚约定见下方[生成区块](#ctxagents--agentregistry)。
@@ -206,17 +206,17 @@ type AgentCancelCause =
cause 是由 TypeScript 强制约束的同进程输入。活跃的取消持有者会将它复制到仅运行时的 `AbortSignal.reason`;signal 不授予协作监听器任何分类权限。持久 `turn/end` 保留粗粒度 `{ kind: 'aborted' }` 结果;若需记录谁请求了取消,应使用单独的持久事件,而不是让终态结果承担额外含义。
-[事件分类](../architecture.md#event)拥有 `agent/*` 生命周期、检查点与 waterfall(瀑布式事件)约定。轮次和步骤边界是持久会话事件,而不是 agent emit。
+[事件分类](../architecture.md#event)负责 `agent/*` 生命周期、检查点与 waterfall(瀑布式事件)约定。轮次和步骤边界是持久会话事件,而不是 agent emit。
## 发起 Agent
-`ctx.agents` 携带的进程本地 initiator 就是上面的确切 `Agent`,不是单独的 frame 或复制的标识。环境中存在该值既不能证明存活,也不代表授权;其生命周期与边界规则由 [initiator 作用域决策](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)规定。
+`ctx.agents` 携带的进程本地 initiator 就是上面的确切 `Agent`,不是单独的 frame 或复制的标识。环境中存在该值既不能证明存活,也不代表授权;[initiator 作用域决策](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)定义其生命周期和作用域规则。
## 拦截决策
-pre-step 决策使用与持久 user-role 输入相同、带标识的 `UserMessage` 形状。进入步骤的批次具有权威性,并保留每条消息的 id 和 source。钩子桥接层把其原生决策字段映射到这一类型化结果上。
+pre-step 决策使用与持久 user-role 输入相同、带标识的 `UserMessage` 类型。进入步骤的批次具有权威性,并保留每条消息的 `id` 和 `source`。钩子桥接层把其原生决策字段映射到这一类型化结果上。
源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
@@ -238,7 +238,7 @@ type PreStepDecision =
type RequestErrorAction = { kind: 'retry' } | undefined
```
-`agent/pre-step` 是请求推导前唯一的串行边界。`agent/turn-stopping` 在轮次没有工具或 steering(中途引导)后续时运行,先于最后一次 steering 排空。
+`agent/pre-step` 是请求推导前唯一的串行监听器链。`agent/turn-stopping` 在轮次没有工具或 steering(中途引导)后续时运行,先于最后一次 steering 排空。
`agent/session-start` 携带 `SessionStartSource`(会话生命周期为何开始;桥接层据此匹配其 SessionStart):
@@ -251,13 +251,13 @@ type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact'
`Session` 是一份类型化 `SessionEvent` 的**仅追加日志**——唯一的真源。LLM(大语言模型)消息历史从日志*派生*(`deriveMessages()`),而非单独存储。每个条目携带单调的 `seq`、`time` 与按 `type` 判别的 `data` payload;surface 变体还可以在 `sourceEventSeqs` 中列出被引用的较早事件,并携带 `surfaceOp`。
-`SessionEvent` 信封的确切条件形状、十二种事件变体(`turn/start`、`turn/end`、`step/start`、`step/end`、`user/message`、`assistant/chunk`、`assistant/message`、`tool/call`、`tool/result`、`steering/message`、`todo/write`、`request/header`)、`deriveMessages()` 投影规则、`TurnTrigger`/`TurnEndReason` 原因以及执行封闭和独立事件规则都在 **[session.md](session.md)** 中。日志如何持久化——`SessionPersistence` seam、JSONL/SQLite 后端、`session/flush` 检查点、崩溃恢复与 `SessionHeader`——则在 **[persistence.md](persistence.md)** 中。
+`SessionEvent` 信封的确切条件字段、十二种事件变体(`turn/start`、`turn/end`、`step/start`、`step/end`、`user/message`、`assistant/chunk`、`assistant/message`、`tool/call`、`tool/result`、`steering/message`、`todo/write`、`request/header`)、`deriveMessages()` 投影规则、`TurnTrigger`/`TurnEndReason` 原因以及执行封闭和独立事件规则都在 **[session.md](session.md)** 中。日志如何持久化——`SessionPersistence` 接口、JSONL/SQLite 后端、`session/flush` 检查点、崩溃恢复与 `SessionHeader`——则在 **[persistence.md](persistence.md)** 中。
## `ToolDefinition`
-唯一属于核心的流水线编写类型:每个已注册工具*是什么*——一个面向模型的 `ToolSchema` 加上一个 `execute` 函数,以及可选的最终内容回调与 UI 回调。工具作者很少手动构造它(`defineTool` DSL 会用类型化参数构建),但它是注册表持有、循环分发所经过的约定。
+唯一属于核心的流水线编写类型:每个已注册工具*是什么*——一个面向模型的 `ToolSchema` 加上一个 `execute` 函数,以及可选的最终内容回调与 UI 回调。工具作者很少手动构造它(`defineTool` DSL 会使用类型化参数构建),但它是注册表存储并由循环用于分发的约定。
-其完整字段、`defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` 类型化 schema DSL、`ToolExecution`/`ToolExecutionResult` waterfall 形状,以及工具展示 UI 词汇在 **[tools.md](tools.md)** 中。
+其完整字段、`defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` 类型化 schema DSL、`ToolExecution`/`ToolExecutionResult` waterfall 类型,以及工具展示 UI 类型都在 **[tools.md](tools.md)** 中。
## 全仓通用类型模式
@@ -265,7 +265,7 @@ type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact'
### `…Map → derived-union` 模式
-harness 中几乎所有可扩展的和类型都遵循同一形状:一个以判别标签为键的接口(`…Map`),联合类型由 `keyof` 派生。插件通过**声明合并**添加变体——无需修改拥有该类型的包。
+harness 中几乎所有可扩展的和类型都遵循同一模式:一个以判别标签为键的接口(`…Map`),联合类型由 `keyof` 派生。插件通过**声明合并**添加变体——无需修改拥有该类型的包。
```ts ignore-check
// The pattern, schematically:
@@ -301,7 +301,7 @@ declare module '@deepseek-ai/dsh-llm' {
### 品牌化 ID
-跨越包边界的 ID 都经过**品牌化**——结构上是字符串,但在类型层面不可互换(不能把 `SessionId` 传给需要 `CallId` 的位置)。每种类型通过各自的工厂构造;比较、日志记录和 JSON 行为与普通字符串相同。
+在包之间传递的 ID 都经过**品牌化**——结构上是字符串,但在类型层面不可互换(不能把 `SessionId` 传给需要 `CallId` 的位置)。每种类型通过各自的工厂构造;比较、日志记录和 JSON 行为与普通字符串相同。
`Branded` 原语位于独立的纯类型包 [dsh-brand](../../packages/util/brand) 中(没有运行时代码,也不依赖 Harness 包),因此任何包都能品牌化其拥有的 id,而无需依赖无关的能力包。
@@ -318,9 +318,9 @@ type Branded = string & { readonly [BRAND]: B }
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -338,7 +338,7 @@ currentSelection(): ModelSelection
/**
* Save the complete default model selection. A deployment without a settings
* provider keeps its composition entry.
- * @param next - resolved selection accepted by a front door.
+ * @param next - resolved selection accepted by an entry point.
* @returns fulfillment after the optional settings write settles.
*/
async saveSelection(next: ModelSelection): Promise
@@ -385,6 +385,177 @@ Types: [SessionHeader](persistence.md)
Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent-loop/src/index.ts)
+
+
+### `ctx.agentPresets` — `AgentPresets`
+
+Registry over the deployment's agent presets.
+
+Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every call so a preset authored while the process runs is visible immediately, and a preset deleted underneath a picker disappears from the next read.
+
+```ts cordis-catalog
+/**
+ * Every preset the configured roots currently supply.
+ * @returns the presets, first-root-wins per id.
+ */
+async list(): Promise
+
+/**
+ * Resolve one preset by id.
+ *
+ * A broken preset resolves — deleting one, reading one, and reporting one
+ * all need the row — and the mounting paths refuse it AFTER resolution
+ * through {@link resolveMountable}.
+ * @param id - the preset id, or `undefined` for {@link defaultId}.
+ * @returns the resolved preset.
+ * @throws when no configured root supplies that id.
+ */
+async resolve(id?: string): Promise
+
+/**
+ * Compose one agent from a preset: ensure the preset's standing mount, then
+ * parent the agent's scope key to it so the mount's registrations and
+ * listeners cover this agent.
+ *
+ * Call from the agent factory's `setup(agentCtx)`; a rejection there rolls
+ * the agent creation back, so a broken preset never yields a half-composed
+ * session.
+ * @param agentCtx - the agent's scope context.
+ * @param id - the preset id, or `undefined` for {@link defaultId}.
+ * @returns the preset that was composed, for the caller to record.
+ * @throws when the preset is unknown or its composition is unusable.
+ */
+async mount(agentCtx: Context, id?: string): Promise
+
+/**
+ * Join one agent to the SAME standing composition another already runs on.
+ *
+ * This is how a child agent inherits its parent's capabilities. It is a bind,
+ * not a mount: the parent's generation is already composed, so the child gets
+ * that exact instance — the same plugin objects, the same tool registrations,
+ * the same prompt sections. Re-resolving the parent's preset by id instead
+ * would re-read the roster, and a composition file edited since the parent
+ * started would hand the child a DIFFERENT generation than the one its
+ * parent's history was produced under (and a preset deleted since would fail
+ * the child outright while its parent keeps running).
+ *
+ * Synchronous, and with no composition failure mode of its own — it reads no
+ * roster, mounts nothing, and touches no file — which is what lets a child
+ * creation window use it: the two in-process subagent drivers compose their
+ * children inside a synchronous `setup`. It still rejects a caller error, as
+ * the `@throws` below record.
+ *
+ * A parent that joined no preset — a rosterless deployment — yields no join
+ * and no error: there, the model-facing rows sit in the host composition and
+ * the child already sees them through the global layer.
+ * @param agentCtx - the joining agent's scope context.
+ * @param parentCtx - the scope context of the agent whose composition to join.
+ * @returns the preset id joined, or undefined when the parent joined none.
+ * @throws when `agentCtx` carries no scope, or has already joined a preset.
+ */
+composeFrom(agentCtx: Context, parentCtx: Context): string | undefined
+
+/**
+ * The preset one live agent runs on.
+ *
+ * Read from the live scope chain rather than from the session, so it answers
+ * for an agent whose session has not recorded a preset yet — a child agent
+ * whose durable header is being built from its parent's composition.
+ * @param agentCtx - the agent's scope context.
+ * @returns the preset id, or undefined when the agent joined none.
+ */
+composedPreset(agentCtx: Context): string | undefined
+
+/**
+ * Read one preset's composition text.
+ * @param id - the preset id.
+ * @returns the composition exactly as stored.
+ * @throws when no configured root supplies that id.
+ */
+async read(id: string): Promise
+
+/**
+ * Create a locally authored preset by copying an existing one whole.
+ *
+ * Copy is the only authoring write. Composition text never crosses this
+ * seam: the source is named by id and its directory is copied as it stands,
+ * so the copy is exactly as loadable as its source and authoring grants no
+ * capability the roster did not already carry. The copy is NOT mounted to
+ * validate — a source that mounts today yields a copy that mounts today.
+ * @param from - the preset the copy starts from; shipped presets are the
+ * primary source, so any trust is accepted.
+ * @param id - the new preset's id, which becomes its directory name.
+ * @param name - display name for the copy; absent falls back to the id.
+ * @throws when the source is unknown, the id is unusable or already taken,
+ * or the deployment configures no writable root.
+ */
+async copy(from: string, id: string, name?: string): Promise
+
+/**
+ * Delete a locally authored preset.
+ * @param id - the preset id.
+ * @throws when the preset is unknown or ships with the deployment.
+ */
+async remove(id: string): Promise
+
+/**
+ * One agent's instance of a service its preset mounted.
+ *
+ * A preset publishes services behind `isolate` realms, which are invisible
+ * outside the group that declares them — including to the host. This is how a
+ * caller holding the agent reads one anyway: a request that is ABOUT a
+ * session but arrives from outside it, which is every browser RPC.
+ *
+ * Read addressing only. A host row that `inject`s a service cannot use this,
+ * because injection resolves before any session exists and has no agent to
+ * key by; such a service belongs on the host plane instead.
+ * @param agent - the agent whose composition to look inside.
+ * @param name - the service name as the preset's rows resolve it.
+ * @returns the agent's instance, or undefined when its preset mounts none.
+ */
+serviceFor(agent: { ctx: Context }, name: K): Context[K] | undefined
+
+/**
+ * Re-link one agent to a different preset's standing composition.
+ *
+ * Only valid while the agent has produced nothing: swapping tools mid
+ * conversation would leave logged tool calls the new composition cannot
+ * make. The CALLER owns that check — this method does not read session
+ * history.
+ *
+ * The swap is a parent re-link, not an unmount: standing mounts are shared
+ * and permanent, so the old composition stays for its other agents and the
+ * new one is ensured BEFORE the link moves. An unknown or unusable preset
+ * therefore throws with the agent exactly as it was — there is no torn-down
+ * state to restore. The re-link runs through the binding this roster kept
+ * from the agent's mount — dsh-scope's only re-link authority. An agent
+ * that never composed one has nothing to re-link: the switch is then the
+ * agent's first bind, exactly a mount.
+ * @param agentCtx - the agent's scope context.
+ * @param id - the preset to compose the agent from instead.
+ * @returns the preset now installed.
+ * @throws when the preset is unknown or its composition is unusable.
+ */
+async recompose(agentCtx: Context, id: string): Promise
+
+/**
+ * The standing scope key of one preset, for a host reader with no agent.
+ *
+ * A cold transcript read resolves tool presenters against the composition
+ * the session recorded, and the standing mount makes that possible without
+ * resuming anything: ensuring the mount composes plugins but starts no
+ * agent, no session, and no turn.
+ * @param id - the preset id, or `undefined` for {@link defaultId}.
+ * @returns the standing scope key readers pass as a registry view scope.
+ * @throws when the preset is unknown or its composition is unusable.
+ */
+async standingKeyFor(id?: string): Promise
+```
+
+Types: [ScopeKey](scope.md)
+
+Source: [`packages/preset/agent-presets/src/index.ts:80`](../../packages/preset/agent-presets/src/index.ts)
+
### `ctx.agents` — `AgentRegistry`
@@ -555,7 +726,7 @@ list(): Agent[]
roots(): Agent[]
```
-Source: [`packages/core/agent/src/index.ts:254`](../../packages/core/agent/src/index.ts)
+Source: [`packages/core/agent/src/index.ts:256`](../../packages/core/agent/src/index.ts)
diff --git a/docs/subsystems/credentials.i18n.yaml b/docs/subsystems/credentials.i18n.yaml
index 7e9e057c75..a7c9218e1b 100644
--- a/docs/subsystems/credentials.i18n.yaml
+++ b/docs/subsystems/credentials.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/credentials.md
-credentials.md: 0bc2224ac039addc795d3806e8c85004f2bb84a7
-credentials.zh.md: f236b0b2daef85784308f10dbcfc67db84c42234
+credentials.md: 5ac023231c7eac85edbeda1c72f871ca37c0a891
+credentials.zh.md: b3f6e19ca76fb680ab243715530cb03770b543dc
diff --git a/docs/subsystems/credentials.md b/docs/subsystems/credentials.md
index 0bc2224ac0..5ac023231c 100644
--- a/docs/subsystems/credentials.md
+++ b/docs/subsystems/credentials.md
@@ -8,7 +8,7 @@ Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credent
## Identity
-A reference names one credential as a POSIX-style environment-variable name. The brand keeps references from mixing with other cross-boundary strings; construction validates the shell-identifier shape.
+A reference names one credential as a POSIX-style environment-variable name. The brand prevents callers from mixing credential references with other strings passed between packages or processes; construction validates the shell-identifier syntax.
```ts type-equiv
/** Nominal reference to one credential: a POSIX-style environment-variable name. */
@@ -53,9 +53,9 @@ interface CredentialInfo {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/credentials.zh.md b/docs/subsystems/credentials.zh.md
index f236b0b2da..b3f6e19ca7 100644
--- a/docs/subsystems/credentials.zh.md
+++ b/docs/subsystems/credentials.zh.md
@@ -8,7 +8,7 @@
## 标识
-引用以 POSIX 风格环境变量名命名一条凭据。brand 使引用不与其他跨边界字符串混用;构造时校验 shell 标识符形态。
+引用以 POSIX 风格环境变量名命名一条凭据。brand 防止调用方将凭据引用与在包或进程之间传递的其他字符串混用;构造时校验 shell 标识符语法。
```ts type-equiv
/** Nominal reference to one credential: a POSIX-style environment-variable name. */
@@ -53,9 +53,9 @@ interface CredentialInfo {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/feedback.i18n.yaml b/docs/subsystems/feedback.i18n.yaml
new file mode 100644
index 0000000000..bef441ece2
--- /dev/null
+++ b/docs/subsystems/feedback.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/feedback.md
+feedback.md: a0daf47d093f3efb643950c0e124a8db0734fde4
+feedback.zh.md: 1163d30a6af5f818ab3be8ff754de20656e7e743
diff --git a/docs/subsystems/feedback.md b/docs/subsystems/feedback.md
new file mode 100644
index 0000000000..a0daf47d09
--- /dev/null
+++ b/docs/subsystems/feedback.md
@@ -0,0 +1,256 @@
+# Message Feedback
+
+English | [中文](feedback.zh.md)
+
+[`@deepseek-ai/dsh-message-feedback`](../../packages/feedback/message-feedback) owns editable feedback for individual assistant messages. It is deliberately separate from the immutable Session-level `feedback/record` event: message feedback is a local storage-domain sidecar, not Session-log content or a projection, and it performs no telemetry handoff.
+
+Source: [`packages/feedback/message-feedback/src/types.ts`](../../packages/feedback/message-feedback/src/types.ts)
+
+## Public types
+
+```ts type-equiv
+/** Opaque compare-and-set token for one exact feedback item revision. */
+type MessageFeedbackVersion = Branded<'MessageFeedbackVersion'>
+```
+
+```ts type-equiv
+/** The human's overall judgment of one assistant message. */
+type MessageFeedbackRating = 'positive' | 'negative'
+```
+
+```ts type-equiv
+/** One current feedback value and its opaque mutation token. */
+interface MessageFeedbackItem {
+ /** Stable identity of the assistant message inside the owning Session. */
+ readonly messageId: MessageId
+ /** Overall positive or negative judgment. */
+ readonly rating: MessageFeedbackRating
+ /** Optional explanation, preserved verbatim after validation. */
+ readonly note?: string
+ /** Equality-only token replaced by every material create or update. */
+ readonly version: MessageFeedbackVersion
+ /** Host-assigned creation time in Unix epoch milliseconds. */
+ readonly createdAt: number
+ /** Host-assigned time of the most recent material update. */
+ readonly updatedAt: number
+}
+```
+
+```ts type-equiv
+/** Read all message feedback belonging to one persisted Session lifecycle. */
+interface MessageFeedbackListRequest {
+ /** Persisted Session whose sidecar should be read. */
+ readonly sessionId: SessionId
+}
+```
+
+```ts type-equiv
+/** Current feedback values for one Session, in first-creation order. */
+interface MessageFeedbackListValue {
+ /** Fresh immutable item snapshots. */
+ readonly items: readonly MessageFeedbackItem[]
+}
+```
+
+```ts type-equiv
+/** Create or replace feedback for one assistant message. */
+interface MessageFeedbackPutRequest {
+ /** Persisted Session that owns the target message. */
+ readonly sessionId: SessionId
+ /** Target assistant-message identity. */
+ readonly messageId: MessageId
+ /** Desired overall judgment. */
+ readonly rating: MessageFeedbackRating
+ /** Optional non-blank explanation. */
+ readonly note?: string
+ /** Observed item version, or `null` to require that no item exists. */
+ readonly ifVersion: MessageFeedbackVersion | null
+}
+```
+
+```ts type-equiv
+/** Delete feedback for one message after observing its current version. */
+interface MessageFeedbackDeleteRequest {
+ /** Persisted Session that owns the sidecar. */
+ readonly sessionId: SessionId
+ /** Message whose feedback should be absent after this operation. */
+ readonly messageId: MessageId
+ /** Observed item version; ignored when the item is already absent. */
+ readonly ifVersion: MessageFeedbackVersion
+}
+```
+
+```ts type-equiv
+/** Idempotent deletion acknowledgement. */
+interface MessageFeedbackDeleteValue {
+ /** Stable postcondition shared by the first deletion and every retry. */
+ readonly absent: true
+}
+```
+
+```ts type-equiv
+/** No persisted Session header exists for the requested id. */
+interface MessageFeedbackSessionNotFound {
+ readonly code: 'session-not-found'
+ readonly sessionId: SessionId
+}
+```
+
+```ts type-equiv
+/** The id does not name a derived, append-origin assistant message. */
+interface MessageFeedbackTargetNotFound {
+ readonly code: 'target-not-found'
+ readonly sessionId: SessionId
+ readonly messageId: MessageId
+}
+```
+
+```ts type-equiv
+/** A material mutation did not match the addressed item's current version. */
+interface MessageFeedbackVersionConflict {
+ readonly code: 'version-conflict'
+ /** Authoritative current item, or `null` when it does not exist. */
+ readonly current: MessageFeedbackItem | null
+}
+```
+
+```ts type-equiv
+/** A supplied note contains no non-whitespace character. */
+interface MessageFeedbackNoteBlank {
+ readonly code: 'note-blank'
+}
+```
+
+```ts type-equiv
+/** A supplied note exceeds the configured UTF-8 byte limit. */
+interface MessageFeedbackNoteTooLarge {
+ readonly code: 'note-too-large'
+ readonly maxBytes: number
+ readonly actualBytes: number
+}
+```
+
+```ts type-equiv
+/** Failures shared by the public message-feedback operations. */
+type MessageFeedbackFailure =
+ | MessageFeedbackSessionNotFound
+ | MessageFeedbackTargetNotFound
+ | MessageFeedbackVersionConflict
+ | MessageFeedbackNoteBlank
+ | MessageFeedbackNoteTooLarge
+```
+
+```ts type-equiv
+/** Successful public operation result. */
+interface MessageFeedbackSuccess {
+ readonly ok: true
+ readonly value: T
+}
+```
+
+```ts type-equiv
+/** Rejected public operation result with a stable business failure. */
+interface MessageFeedbackRejected {
+ readonly ok: false
+ readonly error: E
+}
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `list` operation. */
+type MessageFeedbackListResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `put` operation. */
+type MessageFeedbackPutResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected<
+ | MessageFeedbackSessionNotFound
+ | MessageFeedbackTargetNotFound
+ | MessageFeedbackVersionConflict
+ | MessageFeedbackNoteBlank
+ | MessageFeedbackNoteTooLarge
+ >
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `delete` operation. */
+type MessageFeedbackDeleteResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected
+```
+
+## Data and concurrency
+
+One Session sidecar row contains its header identity `{createdAt, cwd}` and feedback items keyed by `MessageId`. Each item carries a positive or negative rating, an optional note, Host-assigned `createdAt`/`updatedAt` timestamps, and its own opaque version. Versions are compared only for equality and only against the addressed message; callers do not order or synthesize them.
+
+`put` uses strict optimistic concurrency: every request for an existing item must match its current `ifVersion`, including a no-op. A conflict returns the authoritative current item (or `null`), so a caller can reconcile a lost response or a concurrent edit without another read. Deleting an already absent item succeeds. A per-Session queue encloses inspection, read, conflict evaluation, and whole-row write, so these guarantees cover concurrent calls in one Host process.
+
+## Target and lifecycle authority
+
+`SessionPersistence.inspect()` supplies the target Session observation without publishing or resuming an Agent and without committing cold repair. A cold `listSnapshots()` preflight classifies definite absence; inspection failure for a catalogued Session propagates as infrastructure failure. `put` accepts only a non-empty, append-origin `assistant/message` with the requested `MessageId`; replacement-origin, usage-only empty, and non-assistant records are not feedback targets.
+
+The stored `{createdAt, cwd}` identity must match the inspected header. A mismatch is treated as absence: `list` returns no items, while `put` may replace the stale row with one bound to the current header identity. Forks use a new Session identity and receive no sidecar copy even when their seed contains the same messages.
+
+## Persistence and Remote contract
+
+The service stores whole Session rows in the `message_feedback` storage domain through `ctx.storageDomain`. Before `put` commits a row that references a target message, a matching live target passes through the canonical `ctx.sessions.flush` checkpoint; both live and cold paths are then physically read from sequence zero through `SessionPersistence.readFrom`. The resulting observation is revalidated before the sidecar write, so the durable target log always precedes its sidecar commit. `maxNoteBytes` is required and bounds note text by UTF-8 bytes; the Web Host composition sets `8192`. The package publishes the Host `messageFeedback.list`, `messageFeedback.put`, and `messageFeedback.delete` unary Remote contract through `GatewayService` and `@Remote`; the generated Cordis API below is the method-level authority.
+
+Plugin disposal closes mutation admission, drains accepted per-Session queue work, and then closes the storage domain.
+
+## Boundaries and limitations
+
+- The client Remote aggregate mount and UI consumer are separately owned and deferred.
+- The mutation queue is process-local. Storage-domain has no cross-process conditional write, so multiple Host writers to one storage root have no compare-and-swap or lost-update guarantee.
+- Session persistence has no durable deletion API. The service does not treat `session/disposed` or `host/session-removed` as deletion and therefore performs no fake cascade; orphan sidecar rows may remain after out-of-band log removal.
+- A request in the narrow interval after live detach but before the persistence catalog materializes the header can receive `session-not-found`; callers retry after retirement materialization.
+- Cold requests scan the complete Session snapshot catalog because persistence has no lookup-by-id metadata operation. One Session row also has no item-count or aggregate-byte cap; `maxNoteBytes` bounds only each note until a concrete consumer owns a row policy.
+- Header identity detects a reused id only when `{createdAt, cwd}` differs; a cloned log retaining the same header identity is indistinguishable by this contract.
+- The Host contract records no authenticated actor or audit identity and therefore assumes a trusted caller boundary.
+
+
+
+
+
+## Cordis API
+
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+
+
+
+### `ctx.messageFeedback` — `MessageFeedbackService`
+
+Storage-domain sidecar service. It inspects persisted Session history and never creates or resumes an Agent or Session.
+
+```ts cordis-catalog
+/**
+ * Read feedback belonging to the current persisted Session lifecycle.
+ * A stale row from a reused Session id is invisible.
+ * @param request - Session identity to inspect and list.
+ * @returns current immutable items or `session-not-found`.
+ */
+@Remote('list') async list(request: MessageFeedbackListRequest): Promise
+
+/**
+ * Create or replace feedback for one derived append-origin assistant
+ * message. Every request must match the addressed item's current version;
+ * a matching no-op returns the stored item without changing its revision.
+ * @param request - target, desired value, and observed item version.
+ * @returns the committed item or an explicit business failure.
+ */
+@Remote('put') put(request: MessageFeedbackPutRequest): Promise
+
+/**
+ * Delete one feedback item. Absence is successful regardless of the
+ * supplied version; an existing item requires an exact version match.
+ * @param request - Session, message, and observed item version.
+ * @returns the stable absent postcondition, or an explicit failure.
+ */
+@Remote('delete') delete(request: MessageFeedbackDeleteRequest): Promise
+```
+
+Source: [`packages/feedback/message-feedback/src/index.ts:150`](../../packages/feedback/message-feedback/src/index.ts)
+
diff --git a/docs/subsystems/feedback.zh.md b/docs/subsystems/feedback.zh.md
new file mode 100644
index 0000000000..1163d30a6a
--- /dev/null
+++ b/docs/subsystems/feedback.zh.md
@@ -0,0 +1,256 @@
+# 消息反馈
+
+[English](feedback.md) | 中文
+
+[`@deepseek-ai/dsh-message-feedback`](../../packages/feedback/message-feedback)拥有针对单条 assistant 消息的可编辑反馈。它刻意与不可变的 Session 级 `feedback/record` 事件分离:message feedback 是本地 storage-domain 伴随记录(sidecar),不是 Session 日志内容或投影,也不执行遥测交接。
+
+来源:[`packages/feedback/message-feedback/src/types.ts`](../../packages/feedback/message-feedback/src/types.ts)
+
+## 公开类型
+
+```ts type-equiv
+/** Opaque compare-and-set token for one exact feedback item revision. */
+type MessageFeedbackVersion = Branded<'MessageFeedbackVersion'>
+```
+
+```ts type-equiv
+/** The human's overall judgment of one assistant message. */
+type MessageFeedbackRating = 'positive' | 'negative'
+```
+
+```ts type-equiv
+/** One current feedback value and its opaque mutation token. */
+interface MessageFeedbackItem {
+ /** Stable identity of the assistant message inside the owning Session. */
+ readonly messageId: MessageId
+ /** Overall positive or negative judgment. */
+ readonly rating: MessageFeedbackRating
+ /** Optional explanation, preserved verbatim after validation. */
+ readonly note?: string
+ /** Equality-only token replaced by every material create or update. */
+ readonly version: MessageFeedbackVersion
+ /** Host-assigned creation time in Unix epoch milliseconds. */
+ readonly createdAt: number
+ /** Host-assigned time of the most recent material update. */
+ readonly updatedAt: number
+}
+```
+
+```ts type-equiv
+/** Read all message feedback belonging to one persisted Session lifecycle. */
+interface MessageFeedbackListRequest {
+ /** Persisted Session whose sidecar should be read. */
+ readonly sessionId: SessionId
+}
+```
+
+```ts type-equiv
+/** Current feedback values for one Session, in first-creation order. */
+interface MessageFeedbackListValue {
+ /** Fresh immutable item snapshots. */
+ readonly items: readonly MessageFeedbackItem[]
+}
+```
+
+```ts type-equiv
+/** Create or replace feedback for one assistant message. */
+interface MessageFeedbackPutRequest {
+ /** Persisted Session that owns the target message. */
+ readonly sessionId: SessionId
+ /** Target assistant-message identity. */
+ readonly messageId: MessageId
+ /** Desired overall judgment. */
+ readonly rating: MessageFeedbackRating
+ /** Optional non-blank explanation. */
+ readonly note?: string
+ /** Observed item version, or `null` to require that no item exists. */
+ readonly ifVersion: MessageFeedbackVersion | null
+}
+```
+
+```ts type-equiv
+/** Delete feedback for one message after observing its current version. */
+interface MessageFeedbackDeleteRequest {
+ /** Persisted Session that owns the sidecar. */
+ readonly sessionId: SessionId
+ /** Message whose feedback should be absent after this operation. */
+ readonly messageId: MessageId
+ /** Observed item version; ignored when the item is already absent. */
+ readonly ifVersion: MessageFeedbackVersion
+}
+```
+
+```ts type-equiv
+/** Idempotent deletion acknowledgement. */
+interface MessageFeedbackDeleteValue {
+ /** Stable postcondition shared by the first deletion and every retry. */
+ readonly absent: true
+}
+```
+
+```ts type-equiv
+/** No persisted Session header exists for the requested id. */
+interface MessageFeedbackSessionNotFound {
+ readonly code: 'session-not-found'
+ readonly sessionId: SessionId
+}
+```
+
+```ts type-equiv
+/** The id does not name a derived, append-origin assistant message. */
+interface MessageFeedbackTargetNotFound {
+ readonly code: 'target-not-found'
+ readonly sessionId: SessionId
+ readonly messageId: MessageId
+}
+```
+
+```ts type-equiv
+/** A material mutation did not match the addressed item's current version. */
+interface MessageFeedbackVersionConflict {
+ readonly code: 'version-conflict'
+ /** Authoritative current item, or `null` when it does not exist. */
+ readonly current: MessageFeedbackItem | null
+}
+```
+
+```ts type-equiv
+/** A supplied note contains no non-whitespace character. */
+interface MessageFeedbackNoteBlank {
+ readonly code: 'note-blank'
+}
+```
+
+```ts type-equiv
+/** A supplied note exceeds the configured UTF-8 byte limit. */
+interface MessageFeedbackNoteTooLarge {
+ readonly code: 'note-too-large'
+ readonly maxBytes: number
+ readonly actualBytes: number
+}
+```
+
+```ts type-equiv
+/** Failures shared by the public message-feedback operations. */
+type MessageFeedbackFailure =
+ | MessageFeedbackSessionNotFound
+ | MessageFeedbackTargetNotFound
+ | MessageFeedbackVersionConflict
+ | MessageFeedbackNoteBlank
+ | MessageFeedbackNoteTooLarge
+```
+
+```ts type-equiv
+/** Successful public operation result. */
+interface MessageFeedbackSuccess {
+ readonly ok: true
+ readonly value: T
+}
+```
+
+```ts type-equiv
+/** Rejected public operation result with a stable business failure. */
+interface MessageFeedbackRejected {
+ readonly ok: false
+ readonly error: E
+}
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `list` operation. */
+type MessageFeedbackListResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `put` operation. */
+type MessageFeedbackPutResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected<
+ | MessageFeedbackSessionNotFound
+ | MessageFeedbackTargetNotFound
+ | MessageFeedbackVersionConflict
+ | MessageFeedbackNoteBlank
+ | MessageFeedbackNoteTooLarge
+ >
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `delete` operation. */
+type MessageFeedbackDeleteResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected
+```
+
+## 数据与并发
+
+每个 Session 的一条伴随记录包含 header 身份 `{createdAt, cwd}` 和以 `MessageId` 为键的反馈条目。每个条目携带好评或差评、可选备注、Host 分配的 `createdAt`/`updatedAt` 时间戳及自己的 opaque version。version 只能用于相等比较,且只与目标消息比较;调用方不能排序或自行合成它。
+
+`put` 采用严格乐观并发:已有条目的每次请求都必须匹配当前 `ifVersion`,即使请求不会改变目标值。冲突会返回权威当前条目(不存在时为 `null`),因此调用方无需额外读取,即可协调丢失响应或并发编辑。删除已经不存在的条目同样成功。按 Session 划分的队列覆盖检查、读取、冲突判断与整行写入,因此这些保证适用于单个 Host 进程中的并发调用。
+
+## 目标与生命周期权威
+
+`SessionPersistence.inspect()` 提供目标 Session 的观测,且不会发布或恢复 Agent,也不会提交 cold repair。cold 路径先由 `listSnapshots()` 预检明确不存在;已进入目录的 Session 若检查失败,会按基础设施故障原样传播。`put` 只接受具有指定 `MessageId` 的非空、append-origin `assistant/message`;replacement-origin、仅承载 usage 的空记录和非 assistant 记录都不是反馈目标。
+
+存储的 `{createdAt, cwd}` 身份必须与检查所得 header 匹配。不匹配按不存在处理:`list` 返回空条目,`put` 则可用绑定当前 header 身份的新记录替换陈旧行。fork 使用新的 Session 身份,即使种子包含相同消息,也不获得伴随记录副本。
+
+## 持久化与 Remote 契约
+
+服务通过 `ctx.storageDomain` 在 `message_feedback` 存储域中保存完整 Session 行。`put` 提交引用目标消息的伴随记录前,身份匹配的 live 目标先经过权威 `ctx.sessions.flush` checkpoint;随后 live 与 cold 路径都会通过 `SessionPersistence.readFrom` 从序列零做物理复读。写入伴随记录前会再次校验所得观测,因此目标日志的持久提交始终先于其伴随记录。`maxNoteBytes` 为必填项,按 UTF-8 字节限制备注文本;Web Host 组合将其设为 `8192`。该包通过 `GatewayService` 与 `@Remote` 发布 Host `messageFeedback.list`、`messageFeedback.put` 和 `messageFeedback.delete` 一元 Remote 契约;下方生成的 Cordis API 是方法级权威。
+
+Plugin disposal 会先关闭变更接纳,排空已进入各 Session 队列的工作,然后才关闭 storage domain。
+
+## 边界与限制
+
+- 客户端 Remote 聚合挂载与 UI 消费方由各自边界负责并保持延后。
+- 变更队列仅在进程内生效。storage-domain 没有跨进程条件写,因此多个 Host 写入同一存储根目录时,不提供 compare-and-swap 或防止丢失更新的保证。
+- Session persistence 没有持久删除接口。服务不把 `session/disposed` 或 `host/session-removed` 当作删除,因此不伪造级联;在带外移除日志后,孤儿伴随记录可能继续存在。
+- 请求若恰好落在 live detach 之后、persistence catalog 物化 header 之前的极短窗口,可能收到 `session-not-found`;调用方应在 retirement materialization 后重试。
+- 由于 persistence 没有按 id 读取元数据的操作,cold 请求会扫描完整的 Session snapshot 目录。单个 Session 行也没有条目数或聚合字节上限;在具体消费方拥有行策略之前,`maxNoteBytes` 只限制每条备注。
+- 只有 `{createdAt, cwd}` 不同时,header 身份才能识别复用的 id;本契约无法区分保留相同 header 身份的克隆日志。
+- Host 契约不记录已认证的 actor 或审计身份,因此假设调用方边界可信。
+
+
+
+
+
+## Cordis API
+
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+
+
+
+### `ctx.messageFeedback` — `MessageFeedbackService`
+
+Storage-domain sidecar service. It inspects persisted Session history and never creates or resumes an Agent or Session.
+
+```ts cordis-catalog
+/**
+ * Read feedback belonging to the current persisted Session lifecycle.
+ * A stale row from a reused Session id is invisible.
+ * @param request - Session identity to inspect and list.
+ * @returns current immutable items or `session-not-found`.
+ */
+@Remote('list') async list(request: MessageFeedbackListRequest): Promise
+
+/**
+ * Create or replace feedback for one derived append-origin assistant
+ * message. Every request must match the addressed item's current version;
+ * a matching no-op returns the stored item without changing its revision.
+ * @param request - target, desired value, and observed item version.
+ * @returns the committed item or an explicit business failure.
+ */
+@Remote('put') put(request: MessageFeedbackPutRequest): Promise
+
+/**
+ * Delete one feedback item. Absence is successful regardless of the
+ * supplied version; an existing item requires an exact version match.
+ * @param request - Session, message, and observed item version.
+ * @returns the stable absent postcondition, or an explicit failure.
+ */
+@Remote('delete') delete(request: MessageFeedbackDeleteRequest): Promise
+```
+
+Source: [`packages/feedback/message-feedback/src/index.ts:150`](../../packages/feedback/message-feedback/src/index.ts)
+
diff --git a/docs/subsystems/filesystem.i18n.yaml b/docs/subsystems/filesystem.i18n.yaml
index fac61bde63..a1899ab15b 100644
--- a/docs/subsystems/filesystem.i18n.yaml
+++ b/docs/subsystems/filesystem.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/filesystem.md
-filesystem.md: e0edad514b0c4d9108b18cc0b472600b29024c51
-filesystem.zh.md: 81c2d87b6a96740c7a99449a27c16331f5a0645a
+filesystem.md: 5e491e08604f13b1862b44037a3e9a32d7ab0c96
+filesystem.zh.md: fadbf0178f7dde67d7d5b378c0760141dde28f7a
diff --git a/docs/subsystems/filesystem.md b/docs/subsystems/filesystem.md
index e0edad514b..5e491e0860 100644
--- a/docs/subsystems/filesystem.md
+++ b/docs/subsystems/filesystem.md
@@ -4,7 +4,7 @@ English | [中文](filesystem.zh.md)
The optional filesystem capability has four parts: [dsh-fs](../../packages/fs/fs) owns `ctx.fs` and atomic text operations with optional guards, [dsh-fs-local](../../packages/fs/fs-local) implements local disk, [dsh-fs-policy](../../packages/fs/fs-policy) records observed presence or absence and adds freshness rules through events rather than a service, and [dsh-tool-fs](../../packages/fs/tool-fs) directly executes model-facing read/write/edit calls and renders windows. It is outside the agent-loop spine; alternate backends do not change policy or tool schemas.
-The model is **additive, not subtractive**: `ctx.fs` alone is a complete, unconstrained text-storage seam (`write` unconditionally creates-or-overwrites, `edit` unconditionally replaces literal text). `dsh-fs-policy` is a plugin that *adds* policy on top by deciding the `fs/*` waterfalls; removing it leaves the bare provider rather than breaking the tool, because the tool is not method-coupled to the policy. A deployment that loads `dsh-tool-fs` is expected to also load `dsh-fs-policy` so the default behavior is read-before-write/edit.
+`dsh-fs-policy` is optional. Without it, the `FileSystem` Service Definition, a provider, and the `dsh-tool-fs` Consumer form the complete, unconstrained filesystem seam: `write` unconditionally creates or overwrites, and `edit` unconditionally replaces literal text. The policy plugin changes these operations by deciding the `fs/*` waterfalls. Removing it does not break the tool because the tool calls `ctx.fs` and dispatches events; it does not call policy methods. A deployment that loads `dsh-tool-fs` is expected to also load `dsh-fs-policy` so the default behavior is read-before-write/edit.
Provider source: [`packages/fs/fs/src/types.ts`](../../packages/fs/fs/src/types.ts) and [`packages/fs/fs/src/index.ts`](../../packages/fs/fs/src/index.ts). Policy source: [`packages/fs/fs-policy/src/types.ts`](../../packages/fs/fs-policy/src/types.ts). Read-rendering source: [`packages/fs/tool-fs/src/read-render.ts`](../../packages/fs/tool-fs/src/read-render.ts).
@@ -52,7 +52,7 @@ type FsTargetKey = Branded<'FsTargetKey'>
type FsVersion = Branded<'FsVersion'>
```
-`stat` returns metadata (never content), or `undefined` when the target is absent. `type` lets the tool reject directories/special files before reading, and `size` lets it choose `readText` vs `streamText` without probing by failure. A protocol consumer that needs a byte ceiling applies it while consuming `streamText`, so the filesystem seam needs no consumer-specific bounded-read primitive.
+`stat` returns metadata (never content), or `undefined` when the target is absent. `type` lets consumers reject directories and special files before reading, and `size` lets text consumers choose `readText` vs `streamText` without probing by failure. A text consumer applies its own retention ceiling while consuming `streamText`. Raw-byte consumers use `readBytes(target, signal, maxBytes)`; its required complete-content cap makes a known or discovered overflow fail with `FS_TOO_LARGE` instead of truncating or buffering without a bound.
```ts type-equiv
/**
@@ -113,7 +113,7 @@ interface FsDirEntry {
## Write and edit guards (provider contract)
-Both `writeText` and `editText` take their version guard OPTIONALLY: omit it for an unconditional (bare-provider) mutation, supply it to guard. `writeText`'s guard is an `FsWriteIntent` — `createIfAbsent` creates a missing target and rejects an existing one with `FS_NOT_OBSERVED`, including a target that appears after the provider's initial probe because publication itself must be no-replace; `replaceIfVersion` replaces only when the target exists at the observed version, else `FS_STALE_VERSION`. Omitting `expected` unconditionally creates-or-overwrites. The union itself carries only the two guarded intents; "no guard" is expressed by omission, so write and edit share one symmetric `expected?` shape.
+Both `writeText` and `editText` take their version guard OPTIONALLY: omit it for an unconditional (bare-provider) mutation, supply it to guard. `writeText`'s guard is an `FsWriteIntent` — `createIfAbsent` creates a missing target and rejects an existing one with `FS_NOT_OBSERVED`, including a target that appears after the provider's initial probe because publication itself must be no-replace; `replaceIfVersion` replaces only when the target exists at the observed version, else `FS_STALE_VERSION`. Omitting `expected` unconditionally creates-or-overwrites. The union itself carries only the two guarded intents; "no guard" is expressed by omission, so write and edit both use the same optional `expected` field.
```ts type-equiv
/**
@@ -136,10 +136,11 @@ interface FsWriteOutcome {
version: FsVersion
/**
* The file's content BEFORE the write, or `null` when the file did not exist
- * (a create) or was undiffable (binary/non-UTF-8). LF-normalized storage text
- * (the diff basis), never a diff — a consumer computes the result-time
- * contextual diff from `before`/`after` when `before` is present, else falls
- * back to a whole-file diff.
+ * (a create) or the backend declined a contextual basis (for example, a
+ * binary/non-UTF-8 prior file or either overwrite side reaching its exclusive limit).
+ * LF-normalized storage text (the diff basis), never a diff — a consumer
+ * computes the result-time contextual diff from `before`/`after` when
+ * `before` is present, else falls back to a whole-file diff.
*/
before: string | null
/** The file's content AFTER the write, LF-normalized to share `before`'s diff basis. */
@@ -196,15 +197,15 @@ type FsObservation =
## Execution context (policy plugin)
-The policy plugin needs just enough execution context to derive the observed-state owner by narrowing the opaque `object` actor the `fs/*` events carry. `ToolExecution` satisfies this shape, so `dsh-tool-fs` passes its execution object through as the actor without making `dsh-fs-policy` import the tool, agent, or session packages.
+The policy plugin needs just enough execution context to derive the observed-state owner by narrowing the opaque `object` actor the `fs/*` events carry. `ToolExecution` has the required fields, so `dsh-tool-fs` passes its execution object through as the actor without making `dsh-fs-policy` import the tool, agent, or session packages.
```ts type-equiv
/**
* Minimal structural view of a tool execution the policy plugin needs to derive
- * an observed-state owner. `@deepseek-ai/dsh-tools`' `ToolExecution` satisfies
- * this shape, so the tool passes its `exec` straight through as the opaque
- * `object` actor on the `fs/*` events; this plugin narrows that actor to this
- * shape without importing `dsh-tools`, `dsh-agent`, or `dsh-session`.
+ * an observed-state owner. `@deepseek-ai/dsh-tools`' `ToolExecution` contains
+ * these fields, so the tool passes its `exec` straight through as the opaque
+ * `object` actor on the `fs/*` events; this plugin narrows that actor to
+ * `FsPolicyExec` without importing `dsh-tools`, `dsh-agent`, or `dsh-session`.
*
* The owner is `agent.session` when present. It is treated as an opaque object
* identity (a `WeakMap` key); this package never reads any of its fields.
@@ -220,7 +221,7 @@ interface FsPolicyExec {
## Read outcome (consumer / read rendering)
-A text read is bounded by line window, byte cap, and backend limits. After the byte cap is reached, scanning continues without retaining more lines so `totalLines` remains exact. The outcome the model-facing `read` tool renders is purely presentational; there is no `full`/`partial` view — authorization is freshness-based (the tool emits a present `fs/observed` with the stat's version), so any windowed read can authorize a later write/edit when the file is unchanged. A metadata miss emits an absent observation before the tool returns `FS_NOT_FOUND`, allowing a later guarded write to recreate an externally deleted target without authorizing edit. Read windowing and this outcome shape live in `dsh-tool-fs` (the executor that owns the read), not in the policy plugin.
+A text read is bounded by line window, byte cap, and backend limits. After the byte cap is reached, scanning continues without retaining more lines so `totalLines` remains exact. The result the model-facing `read` tool renders is purely presentational; there is no `full`/`partial` view — authorization is freshness-based (the tool emits a present `fs/observed` directly with the stat's version), so any windowed read can authorize a later write/edit when the file is unchanged. A metadata miss emits an absent observation before the tool returns `FS_NOT_FOUND`, allowing a later guarded write to recreate an externally deleted target without authorizing edit. `dsh-tool-fs`, the executor that owns the read, implements read windowing and constructs this result; the policy plugin does not.
```ts type-equiv
/** Outcome of a bounded text read — what {@link formatReadOutput} renders. */
@@ -247,7 +248,7 @@ Filesystem failures use stable `FsErrorCode` strings carried by `FsError` (`Harn
```ts type-equiv
/**
* Stable, machine-routable codes for filesystem failures. Carried on
- * {@link FsError}; the tool registry surfaces `{ name, code }` on `isError`
+ * {@link FsError}; the tool registry exposes `{ name, code }` on `isError`
* results so retry/permission/UI layers can branch without parsing messages.
*/
type FsErrorCode =
@@ -255,6 +256,7 @@ type FsErrorCode =
| 'FS_NOT_DIRECTORY'
| 'FS_NOT_TEXT'
| 'FS_NOT_REGULAR_FILE'
+ | 'FS_TOO_LARGE'
| 'FS_PERMISSION_DENIED'
| 'FS_SANDBOX_DENIED'
| 'FS_IO_ERROR'
@@ -273,15 +275,15 @@ type FsErrorCode =
## The service and the plugin
-`FileSystem` (`ctx.fs`, abstract) owns the provider primitives: `resolve`, `processPath`, `fileUrl`, `contains`, `stat`, `lstat`, `readText`, `streamText`, `listDir`, `writeText`, and `editText`. `dsh-fs-policy` registers **no service** — it is a plugin that adds policy through the `fs/*` event gate: it decides the write/edit intent waterfalls from unseen/absent/present state and records `FsObservation` values. The executor is `dsh-tool-fs`: it reads/writes/edits through `ctx.fs`, dispatches the waterfalls, and emits the recording event. The generated [`ctx.fs` section](#ctxfs--filesystem-abstract-seam) below shows the exact signatures.
+`FileSystem` (`ctx.fs`, abstract) owns the provider primitives: `resolve`, `processPath`, `fileUrl`, `contains`, `stat`, `lstat`, `readText`, `streamText`, `readBytes`, `listDir`, `writeText`, and `editText`. `dsh-fs-policy` registers **no service** — it is a plugin that adds policy through the `fs/*` event gate: it decides the write/edit intent waterfalls from unseen/absent/present state and records `FsObservation` values. The executor is `dsh-tool-fs`: it reads/writes/edits through `ctx.fs`, dispatches the waterfalls, and emits the recording event. The generated [`ctx.fs` section](#ctxfs--filesystem-abstract-seam) below shows the exact signatures.
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -372,6 +374,18 @@ abstract readText(target: FsTarget, signal?: AbortSignal): Promise
*/
abstract streamText(target: FsTarget, signal?: AbortSignal): Promise>
+/**
+ * Read the whole regular file as raw bytes with no decoding or binary
+ * rejection. The bound lives at this seam so a backend can never buffer an
+ * unbounded file: a target known or discovered to exceed `maxBytes` fails
+ * with `FS_TOO_LARGE` instead of returning a truncated result.
+ * @param target - the resolved target to read.
+ * @param signal - aborts the read.
+ * @param maxBytes - inclusive byte cap on the complete content.
+ * @returns the full raw content, at most `maxBytes` long.
+ */
+abstract readBytes(target: FsTarget, signal: AbortSignal | undefined, maxBytes: number): Promise
+
/**
* List direct children of a directory in stable name order. Returns resolved
* child targets plus cheap metadata only; never reads file contents.
diff --git a/docs/subsystems/filesystem.zh.md b/docs/subsystems/filesystem.zh.md
index 81c2d87b6a..fadbf0178f 100644
--- a/docs/subsystems/filesystem.zh.md
+++ b/docs/subsystems/filesystem.zh.md
@@ -4,7 +4,7 @@
可选的文件系统能力由四个部分组成:[dsh-fs](../../packages/fs/fs) 拥有 `ctx.fs` 以及带可选守卫的原子文本操作;[dsh-fs-local](../../packages/fs/fs-local) 实现本地磁盘后端;[dsh-fs-policy](../../packages/fs/fs-policy) 记录观测到的存在或缺失状态,并通过事件(而非服务)添加新鲜度规则;[dsh-tool-fs](../../packages/fs/tool-fs) 直接执行面向模型的 read/write/edit 调用并渲染窗口。它位于 agent loop(智能体循环)主干之外;替换后端不会改变策略或工具 schema。
-该模型是**加法式而非减法式**的:`ctx.fs` 本身就是一个完整、无约束的文本存储 seam(`write` 无条件创建或覆盖,`edit` 无条件替换字面文本)。`dsh-fs-policy` 是一个插件,通过裁决 `fs/*` waterfall(瀑布式事件)在上层*叠加*策略;移除它只会暴露裸提供方,而不会破坏工具,因为工具与策略之间没有方法级耦合。加载了 `dsh-tool-fs` 的部署通常也应加载 `dsh-fs-policy`,使默认行为为「先读后写/编辑」。
+`dsh-fs-policy` 是可选插件。没有该插件时,`FileSystem` 服务定义、一个提供方和 `dsh-tool-fs` 消费方组成完整且不受约束的文件系统 seam:`write` 无条件创建或覆盖,`edit` 无条件替换字面文本。策略插件通过裁决 `fs/*` waterfall(瀑布式事件)来改变这些操作。移除该插件不会破坏工具,因为工具调用 `ctx.fs` 并分发事件,而不调用策略方法。加载了 `dsh-tool-fs` 的部署通常也应加载 `dsh-fs-policy`,使默认行为为「先读后写/编辑」。
提供方源码:[`packages/fs/fs/src/types.ts`](../../packages/fs/fs/src/types.ts) 与 [`packages/fs/fs/src/index.ts`](../../packages/fs/fs/src/index.ts)。策略源码:[`packages/fs/fs-policy/src/types.ts`](../../packages/fs/fs-policy/src/types.ts)。读取渲染源码:[`packages/fs/tool-fs/src/read-render.ts`](../../packages/fs/tool-fs/src/read-render.ts)。
@@ -52,7 +52,7 @@ type FsTargetKey = Branded<'FsTargetKey'>
type FsVersion = Branded<'FsVersion'>
```
-`stat` 返回元数据(从不返回内容),目标不存在时返回 `undefined`。`type` 让工具在读取前拒绝目录或特殊文件;`size` 让工具无需通过失败探测即可选择 `readText` 还是 `streamText`。需要字节上限的协议消费方在消费 `streamText` 时执行该上限,因此文件系统 seam 无需消费方专用的有界读取原语。
+`stat` 返回元数据(从不返回内容),目标不存在时返回 `undefined`。`type` 让消费方在读取前拒绝目录和特殊文件;`size` 让文本消费方无需通过失败探测即可选择 `readText` 还是 `streamText`。文本消费方在消费 `streamText` 时执行自己的保留量上限。原始字节消费方调用 `readBytes(target, signal, maxBytes)`;其必填的完整内容上限会使已知或读取中发现的超限以 `FS_TOO_LARGE` 失败,不会截断结果或无界缓冲。
```ts type-equiv
/**
@@ -113,7 +113,7 @@ interface FsDirEntry {
## 写入与编辑守卫(提供方约定)
-`writeText` 和 `editText` 的版本守卫都是可选的:省略守卫时执行无条件的裸提供方变更,提供守卫时则执行相应的条件检查。`writeText` 的守卫是 `FsWriteIntent`:`createIfAbsent` 在目标缺失时创建,目标已存在时以 `FS_NOT_OBSERVED` 拒绝;即使目标在提供方初始探测后才出现,也必须拒绝,因为发布操作本身不得替换。`replaceIfVersion` 仅在目标存在且版本匹配时替换,否则报 `FS_STALE_VERSION`。省略 `expected` 则无条件创建或覆盖。联合类型本身只包含两种有守卫的意图;「无守卫」通过省略表达,因此 write 和 edit 共享同一个对称的 `expected?` 形状。
+`writeText` 和 `editText` 的版本守卫都是可选的:省略守卫时执行无条件的裸提供方变更,提供守卫时则执行相应的条件检查。`writeText` 的守卫是 `FsWriteIntent`:`createIfAbsent` 在目标缺失时创建,目标已存在时以 `FS_NOT_OBSERVED` 拒绝;即使目标在提供方初始探测后才出现,也必须拒绝,因为发布操作本身不得替换。`replaceIfVersion` 仅在目标存在且版本匹配时替换,否则报 `FS_STALE_VERSION`。省略 `expected` 则无条件创建或覆盖。联合类型本身只包含两种有守卫的意图;「无守卫」通过省略表达,因此 write 和 edit 都使用同一个可选的 `expected` 字段。
```ts type-equiv
/**
@@ -136,10 +136,11 @@ interface FsWriteOutcome {
version: FsVersion
/**
* The file's content BEFORE the write, or `null` when the file did not exist
- * (a create) or was undiffable (binary/non-UTF-8). LF-normalized storage text
- * (the diff basis), never a diff — a consumer computes the result-time
- * contextual diff from `before`/`after` when `before` is present, else falls
- * back to a whole-file diff.
+ * (a create) or the backend declined a contextual basis (for example, a
+ * binary/non-UTF-8 prior file or either overwrite side reaching its exclusive limit).
+ * LF-normalized storage text (the diff basis), never a diff — a consumer
+ * computes the result-time contextual diff from `before`/`after` when
+ * `before` is present, else falls back to a whole-file diff.
*/
before: string | null
/** The file's content AFTER the write, LF-normalized to share `before`'s diff basis. */
@@ -196,15 +197,15 @@ type FsObservation =
## 执行上下文(策略插件)
-策略插件只需要足够的执行上下文,通过收窄 `fs/*` 事件携带的不透明 `object` actor 来推导观测状态的所有者。`ToolExecution` 满足此形状,因此 `dsh-tool-fs` 将其执行对象作为 actor 直接传递,而无需让 `dsh-fs-policy` 导入工具、agent 或会话包。
+策略插件只需要足够的执行上下文,通过收窄 `fs/*` 事件携带的不透明 `object` actor 来推导观测状态的所有者。`ToolExecution` 包含必需的字段,因此 `dsh-tool-fs` 将其执行对象作为 actor 直接传递,而无需让 `dsh-fs-policy` 导入工具、agent 或会话包。
```ts type-equiv
/**
* Minimal structural view of a tool execution the policy plugin needs to derive
- * an observed-state owner. `@deepseek-ai/dsh-tools`' `ToolExecution` satisfies
- * this shape, so the tool passes its `exec` straight through as the opaque
- * `object` actor on the `fs/*` events; this plugin narrows that actor to this
- * shape without importing `dsh-tools`, `dsh-agent`, or `dsh-session`.
+ * an observed-state owner. `@deepseek-ai/dsh-tools`' `ToolExecution` contains
+ * these fields, so the tool passes its `exec` straight through as the opaque
+ * `object` actor on the `fs/*` events; this plugin narrows that actor to
+ * `FsPolicyExec` without importing `dsh-tools`, `dsh-agent`, or `dsh-session`.
*
* The owner is `agent.session` when present. It is treated as an opaque object
* identity (a `WeakMap` key); this package never reads any of its fields.
@@ -220,7 +221,7 @@ interface FsPolicyExec {
## 读取结果(消费方 / 读取渲染)
-文本读取受行窗口、字节上限和后端限制约束。达到字节上限后,扫描仍会继续,但不再保留更多行,因此 `totalLines` 仍为精确值。面向模型的 `read` 工具渲染的结果纯粹是展示性的;不存在 `full`/`partial` 视图区分——授权基于新鲜度(工具以 stat 的版本 emit 表示存在的 `fs/observed`),因此任何窗口化读取在文件未变时都能授权后续的 write/edit。元数据未命中时,工具会在返回 `FS_NOT_FOUND` 前 emit 缺失观测,使后续带防护的写入可以重新创建外部删除的目标,但不会授权 edit。读取窗口化与此结果形状位于 `dsh-tool-fs`(拥有读取操作的执行器)中,而非策略插件中。
+文本读取受行窗口、字节上限和后端限制约束。达到字节上限后,扫描仍会继续,但不再保留更多行,因此 `totalLines` 仍为精确值。面向模型的 `read` 工具渲染的结果纯粹是展示性的;不存在 `full`/`partial` 视图区分——授权基于新鲜度(工具 emit 表示存在的 `fs/observed`,并直接携带 stat 的版本),因此任何窗口化读取在文件未变时都能授权后续的 write/edit。元数据未命中时,工具会在返回 `FS_NOT_FOUND` 前 emit 缺失观测,使后续带守卫的写入可以重新创建外部删除的目标,但不会授权 edit。拥有读取操作的执行器 `dsh-tool-fs` 实现读取窗口化并构造该结果;策略插件不执行这些操作。
```ts type-equiv
/** Outcome of a bounded text read — what {@link formatReadOutput} renders. */
@@ -247,7 +248,7 @@ interface FileReadOutcome {
```ts type-equiv
/**
* Stable, machine-routable codes for filesystem failures. Carried on
- * {@link FsError}; the tool registry surfaces `{ name, code }` on `isError`
+ * {@link FsError}; the tool registry exposes `{ name, code }` on `isError`
* results so retry/permission/UI layers can branch without parsing messages.
*/
type FsErrorCode =
@@ -255,6 +256,7 @@ type FsErrorCode =
| 'FS_NOT_DIRECTORY'
| 'FS_NOT_TEXT'
| 'FS_NOT_REGULAR_FILE'
+ | 'FS_TOO_LARGE'
| 'FS_PERMISSION_DENIED'
| 'FS_SANDBOX_DENIED'
| 'FS_IO_ERROR'
@@ -273,15 +275,15 @@ type FsErrorCode =
## 服务与插件
-`FileSystem`(`ctx.fs`,abstract)拥有提供方原语:`resolve`、`processPath`、`fileUrl`、`contains`、`stat`、`lstat`、`readText`、`streamText`、`listDir`、`writeText` 与 `editText`。`dsh-fs-policy` **不注册服务**——它是一个通过 `fs/*` 事件门禁添加策略的插件:根据未见/缺失/存在状态对写入与编辑意图 waterfall 作出决策,并记录 `FsObservation` 值。执行器是 `dsh-tool-fs`:它通过 `ctx.fs` 读取/写入/编辑,分发 waterfall,并 emit 记录事件。下方生成的 [`ctx.fs` 小节](#ctxfs--filesystem-abstract-seam) 展示确切的 `ctx.fs` 签名。
+`FileSystem`(`ctx.fs`,abstract)拥有提供方原语:`resolve`、`processPath`、`fileUrl`、`contains`、`stat`、`lstat`、`readText`、`streamText`、`readBytes`、`listDir`、`writeText` 与 `editText`。`dsh-fs-policy` **不注册服务**——它是一个通过 `fs/*` 事件门禁添加策略的插件:根据未见/缺失/存在状态对写入与编辑意图 waterfall 作出决策,并记录 `FsObservation` 值。执行器是 `dsh-tool-fs`:它通过 `ctx.fs` 读取/写入/编辑,分发 waterfall,并 emit 记录事件。下方生成的 [`ctx.fs` 小节](#ctxfs--filesystem-abstract-seam) 展示确切的 `ctx.fs` 签名。
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -372,6 +374,18 @@ abstract readText(target: FsTarget, signal?: AbortSignal): Promise
*/
abstract streamText(target: FsTarget, signal?: AbortSignal): Promise>
+/**
+ * Read the whole regular file as raw bytes with no decoding or binary
+ * rejection. The bound lives at this seam so a backend can never buffer an
+ * unbounded file: a target known or discovered to exceed `maxBytes` fails
+ * with `FS_TOO_LARGE` instead of returning a truncated result.
+ * @param target - the resolved target to read.
+ * @param signal - aborts the read.
+ * @param maxBytes - inclusive byte cap on the complete content.
+ * @returns the full raw content, at most `maxBytes` long.
+ */
+abstract readBytes(target: FsTarget, signal: AbortSignal | undefined, maxBytes: number): Promise
+
/**
* List direct children of a directory in stable name order. Returns resolved
* child targets plus cheap metadata only; never reads file contents.
diff --git a/docs/subsystems/goal.i18n.yaml b/docs/subsystems/goal.i18n.yaml
index cdd932726e..4eeb224596 100644
--- a/docs/subsystems/goal.i18n.yaml
+++ b/docs/subsystems/goal.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/goal.md
-goal.md: 6f54a5261cb44c3fda389e37cb689a00061ab241
-goal.zh.md: ea5a0fe7923648aead5e6f54ab31162a7abdb954
+goal.md: 676b2f49d00681ac7f05b894cda0157ebba4cfcd
+goal.zh.md: 7f83e6f58461e99b05f04794e83c0dfa9bd58d1b
diff --git a/docs/subsystems/goal.md b/docs/subsystems/goal.md
index 6f54a5261c..676b2f49d0 100644
--- a/docs/subsystems/goal.md
+++ b/docs/subsystems/goal.md
@@ -2,7 +2,7 @@
English | [中文](goal.zh.md)
-Types shared by the event-sourced goal domain and its policy consumers. The [goal-domain Agent Note](../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md) owns the persistence and activation decisions; this page records the literal shapes from [`packages/goal/goal/src/types.ts`](../../packages/goal/goal/src/types.ts).
+Types shared by the event-sourced goal service and its policy consumers. The [goal-domain Agent Note](../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md) owns the persistence and activation decisions; this page records the exact fields and variants from [`packages/goal/goal/src/types.ts`](../../packages/goal/goal/src/types.ts).
## Identity and lifecycle
@@ -142,15 +142,15 @@ interface GoalChanged {
## Service behavior
-[`GoalService`](../../packages/goal/goal/src/index.ts) resolves creation defaults, folds strict replay from durable `goal/change` events, enforces exact-live-agent identity and compare-and-set mutations, and emits contained `goal/changed` notifications. The package [README](../../packages/goal/goal/README.md) owns the callable and model-visible contract.
+[`GoalService`](../../packages/goal/goal/src/index.ts) resolves creation defaults, folds strict replay from durable `goal/change` events, enforces exact-live-agent identity and compare-and-set mutations, and emits contained `goal/changed` notifications. The package [README](../../packages/goal/goal/README.md) defines the callable API and model-visible contract.
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/goal.zh.md b/docs/subsystems/goal.zh.md
index ea5a0fe792..7f83e6f584 100644
--- a/docs/subsystems/goal.zh.md
+++ b/docs/subsystems/goal.zh.md
@@ -2,7 +2,7 @@
[English](goal.md) | 中文
-事件溯源目标领域及其策略消费方共享的类型。[目标领域 Agent Note](../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md)负责记录持久化与激活决策;本页记录 [`packages/goal/goal/src/types.ts`](../../packages/goal/goal/src/types.ts) 中的字面形态。
+事件溯源目标服务及其策略消费方共享的类型。[目标领域 Agent Note](../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md)负责记录持久化与激活决策;本页记录 [`packages/goal/goal/src/types.ts`](../../packages/goal/goal/src/types.ts) 中的确切字段和变体。
## 标识与生命周期
@@ -142,15 +142,15 @@ interface GoalChanged {
## 服务行为
-[`GoalService`](../../packages/goal/goal/src/index.ts) 解析创建默认值、从持久 `goal/change` 事件执行严格回放折叠、校验确切的活跃 agent 身份、以比较并设置方式执行变更,并发出 `goal/changed` 通知;监听器故障会被隔离。包 [README](../../packages/goal/goal/README.md) 负责记录可调用约定和面向模型的约定。
+[`GoalService`](../../packages/goal/goal/src/index.ts) 解析创建默认值、从持久 `goal/change` 事件执行严格回放折叠、校验确切的活跃 agent 身份、以比较并设置方式执行变更,并发出 `goal/changed` 通知;监听器故障会被隔离。包 [README](../../packages/goal/goal/README.md) 定义可调用 API 和面向模型的约定。
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/http-server.i18n.yaml b/docs/subsystems/http-server.i18n.yaml
index 6f61102676..d4969ead3f 100644
--- a/docs/subsystems/http-server.i18n.yaml
+++ b/docs/subsystems/http-server.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/http-server.md
-http-server.md: b9795fe98b432b6ef5f7d01a4d3e115c809fe642
-http-server.zh.md: 8d55c48ae2882bd471ca4e64e011c7d79c4ebb40
+http-server.md: a8a8aece5fd49cda6db0111ac882d83ae07135c4
+http-server.zh.md: 66fc6a6cfa468a3f96a29200bb2fe0aae581f44b
diff --git a/docs/subsystems/http-server.md b/docs/subsystems/http-server.md
index b9795fe98b..a8a8aece5f 100644
--- a/docs/subsystems/http-server.md
+++ b/docs/subsystems/http-server.md
@@ -2,7 +2,7 @@
English | [中文](http-server.zh.md)
-[dsh-host-webserver](../../packages/host/webserver) is the web-shape HTTP carrier for the GUI host: a single `node:http` plugin providing `ctx.httpServer`, a named-route registry, index.html transform taps, and a single claimable fallback seat. It is not part of the agent-loop spine and not a capability seam — it knows no harness concepts, and every feature surface (the `/api` bridge, plugin bundles, the HMR event stream) is a route some other plugin registers ([layering note](../../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)). Web (browser) shape only: Electron loads dist over `file://` and carries fetch over an IPC bridge, not this server.
+[dsh-host-webserver](../../packages/host/webserver) is the browser HTTP carrier for the GUI host: a single `node:http` plugin providing `ctx.httpServer`, a named-route registry, index.html transform callbacks, and one fallback handler that a plugin may claim. It is not part of the agent loop and not a capability seam; it knows no harness concepts, and another plugin registers every feature route, including the `/api` bridge, plugin bundles, and the HMR event stream ([layering note](../../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)). It serves browsers only: Electron loads the built files over `file://` and sends fetch requests through an IPC bridge instead of this server.
Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
@@ -42,7 +42,7 @@ interface Config {
## The service
-`HttpServerService` (`ctx.httpServer`) listens immediately on activation; a listen failure (EADDRINUSE…) throws out of init — a FAILED fiber the boot's fail-loud sweep reports. `register(route)` adds one named route and returns its disposer; a duplicate `(kind, path)` throws, because route patterns are a composition-level contract and a collision is a misconfiguration. `tapIndex(transform)` adds a pure html-to-html transform applied to every index response — `/` and each SPA fallback — in registration order; [dsh-client-modules](../../packages/client/modules) uses it to inject the boot manifest. `port` reads the listening port, the OS-assigned value when `config.port` is 0.
+`HttpServerService` (`ctx.httpServer`) listens immediately on activation; a listen failure (EADDRINUSE…) rejects initialization, and the boot process reports the failed fiber. `register(route)` adds one named route and returns its disposer; a duplicate `(kind, path)` throws because route patterns are a composition-level contract and a collision is a misconfiguration. `tapIndex(transform)` adds a pure html-to-html transform applied to every index response — `/` and each SPA fallback — in registration order; [dsh-client-modules](../../packages/client/modules) uses it to inject the boot manifest. `port` reads the listening port, including the port assigned by the OS when `config.port` is 0.
A request whose handling throws (a malformed %-escape hitting `decodeURIComponent`, a client dropping mid-body) is logged as a warning and answered 400 — or the socket destroyed when headers are already out — never a process exit. Disposal pairs `close()` with `closeAllConnections()` because a handler may hold its response open (SSE) and such connections never end on their own; without the force-close, teardown would hang. The package never prints: the URL line belongs to the shell. Per-package operational detail, including the dev-mode bundle watch pipeline, stays in the [README](../../packages/host/webserver/README.md).
@@ -50,15 +50,15 @@ A request whose handling throws (a malformed %-escape hitting `decodeURIComponen
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
### `ctx.httpServer` — `HttpServerService`
-The web-shape HTTP carrier service. Activation listens immediately (route registration order carries no request-facing semantics: named routes are composed to be disjoint, and the fallback seat answers anything not yet claimed during the boot window — 404 until its owner registers). A listen failure throws out of init — a FAILED fiber the boot's fail-loud sweep reports.
+The browser HTTP carrier service. Activation listens immediately. Route registration order does not affect requests because configured named routes must be distinct, and the fallback handler answers anything not yet claimed during startup with 404 until its owner registers. A listen failure rejects initialization, and the boot process reports the failed fiber.
```ts cordis-catalog
/**
@@ -104,5 +104,5 @@ tapIndex(transform: (html: string) => string): () => void
applyIndexTaps(html: string): string
```
-Source: [`packages/host/webserver/src/index.ts:60`](../../packages/host/webserver/src/index.ts)
+Source: [`packages/host/webserver/src/index.ts:59`](../../packages/host/webserver/src/index.ts)
diff --git a/docs/subsystems/http-server.zh.md b/docs/subsystems/http-server.zh.md
index 8d55c48ae2..66fc6a6cfa 100644
--- a/docs/subsystems/http-server.zh.md
+++ b/docs/subsystems/http-server.zh.md
@@ -2,7 +2,7 @@
[English](http-server.md) | 中文
-[dsh-host-webserver](../../packages/host/webserver) 是 GUI 宿主 web 形态的 HTTP 载体:单个提供 `ctx.httpServer` 的 `node:http` 插件,由具名路由注册表、index.html 转换挂点与单一可认领的回退席位组成。它不属于 agent loop(智能体循环)主干,也不是能力 seam:它不了解任何 harness 概念,每个功能表面(`/api` 桥接、插件 bundle、HMR(热模块替换)事件流)都是由其他插件注册的一条路由([分层说明](../../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md))。仅限 web(浏览器)形态:Electron 通过 `file://` 加载 dist,并经 IPC 桥接承载 fetch,不经过本服务器。
+[dsh-host-webserver](../../packages/host/webserver) 是 GUI 宿主的浏览器 HTTP 载体:它是一个提供 `ctx.httpServer` 的 `node:http` 插件,包含具名路由注册表、index.html 转换回调,以及一个可由插件认领的回退处理器。它不属于 agent loop(智能体循环),也不是能力 seam;它不了解任何 harness 概念。其他插件负责注册所有功能路由,包括 `/api` 桥接、插件 bundle 和 HMR(热模块替换)事件流([分层说明](../../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md))。该服务器只服务浏览器:Electron 通过 `file://` 加载已构建文件,并经 IPC 桥接发送 fetch 请求,不使用本服务器。
源码:[`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts)
@@ -42,7 +42,7 @@ interface Config {
## 服务
-`HttpServerService`(`ctx.httpServer`)在激活时立即监听;监听失败(EADDRINUSE 等)会从 init 抛出,形成一个 FAILED fiber,由启动的大声失败 sweep 上报。`register(route)` 添加一条具名路由并返回其 disposer;重复的 `(kind, path)` 抛出异常,因为路由模式是组合层约定,冲突即配置错误。`tapIndex(transform)` 添加一个纯的 html 到 html 转换,按注册顺序应用于每个 index 响应(`/` 和每次 SPA 回退);[dsh-client-modules](../../packages/client/modules) 用它注入启动 manifest(元数据清单)。`port` 读取监听端口,`config.port` 为 0 时读到的是操作系统分配的值。
+`HttpServerService`(`ctx.httpServer`)在激活时立即监听;监听失败(EADDRINUSE 等)会使初始化被拒绝,启动进程会报告失败的 fiber。`register(route)` 添加一条具名路由并返回其 disposer;重复的 `(kind, path)` 抛出异常,因为路由模式是组合层约定,冲突即配置错误。`tapIndex(transform)` 添加一个纯的 html 到 html 转换,按注册顺序应用于每个 index 响应(`/` 和每次 SPA 回退);[dsh-client-modules](../../packages/client/modules) 用它注入启动 manifest(元数据清单)。`port` 读取监听端口,包括 `config.port` 为 0 时操作系统分配的端口。
处理过程中抛出异常的请求(畸形的 % 转义撞上 `decodeURIComponent`、客户端在请求体中途断开)会记录为警告并应答 400(响应头已发出时则销毁 socket),绝不导致进程退出。dispose(资源释放)把 `close()` 与 `closeAllConnections()` 配对使用,因为处理器可能像 SSE(Server-Sent Events)那样保持响应打开,而这类连接永远不会自行结束;没有强制关闭,拆卸就会挂起。该包(package)从不打印输出:URL 行归 shell 所有。逐包运维细节(含开发模式的 bundle 监视流水线)留在 [README](../../packages/host/webserver/README.md) 中。
@@ -50,15 +50,15 @@ interface Config {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
### `ctx.httpServer` — `HttpServerService`
-The web-shape HTTP carrier service. Activation listens immediately (route registration order carries no request-facing semantics: named routes are composed to be disjoint, and the fallback seat answers anything not yet claimed during the boot window — 404 until its owner registers). A listen failure throws out of init — a FAILED fiber the boot's fail-loud sweep reports.
+The browser HTTP carrier service. Activation listens immediately. Route registration order does not affect requests because configured named routes must be distinct, and the fallback handler answers anything not yet claimed during startup with 404 until its owner registers. A listen failure rejects initialization, and the boot process reports the failed fiber.
```ts cordis-catalog
/**
@@ -104,5 +104,5 @@ tapIndex(transform: (html: string) => string): () => void
applyIndexTaps(html: string): string
```
-Source: [`packages/host/webserver/src/index.ts:60`](../../packages/host/webserver/src/index.ts)
+Source: [`packages/host/webserver/src/index.ts:59`](../../packages/host/webserver/src/index.ts)
diff --git a/docs/subsystems/invariants.i18n.yaml b/docs/subsystems/invariants.i18n.yaml
index 2ff4b67060..b103214416 100644
--- a/docs/subsystems/invariants.i18n.yaml
+++ b/docs/subsystems/invariants.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/invariants.md
-invariants.md: ccf81f0252dd2db4eec666df6fab8c7d6a830f25
-invariants.zh.md: 046aa4a56d48fa0386fba3a51d850ab21ce5145d
+invariants.md: d6f5f175fb277af09e620b94e8604e04ce2a9d26
+invariants.zh.md: b8e08fcf05f76cd69d0f14d64f38dfd15aa6346a
diff --git a/docs/subsystems/invariants.md b/docs/subsystems/invariants.md
index ccf81f0252..d6f5f175fb 100644
--- a/docs/subsystems/invariants.md
+++ b/docs/subsystems/invariants.md
@@ -62,9 +62,9 @@ Every workspace package owns a `./invariant` companion ([package contract](../..
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/invariants.zh.md b/docs/subsystems/invariants.zh.md
index 046aa4a56d..b8e08fcf05 100644
--- a/docs/subsystems/invariants.zh.md
+++ b/docs/subsystems/invariants.zh.md
@@ -62,9 +62,9 @@ interface InvariantInstaller {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml
index 86ec4e3c3f..376b1a976c 100644
--- a/docs/subsystems/llm-streaming.i18n.yaml
+++ b/docs/subsystems/llm-streaming.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/llm-streaming.md
-llm-streaming.md: 4d450c19ec2bbfacabcefc83466e67c8a6c82bd6
-llm-streaming.zh.md: 777c44eaff2b1ee6e5939e04e580d6ad34b1ad1a
+llm-streaming.md: 2063ba2deadada4689161110200af177320fcc15
+llm-streaming.zh.md: c3d1c4d895f35032bfe7602a2880a7922b984a1f
diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md
index 4d450c19ec..2063ba2dea 100644
--- a/docs/subsystems/llm-streaming.md
+++ b/docs/subsystems/llm-streaming.md
@@ -2,7 +2,7 @@
English | [中文](llm-streaming.zh.md)
-The conversation and streaming vocabulary of [`packages/llm`](../../packages/llm/README.md): the `Message`/`ContentBlock` shapes every request and durable history share, the fully-assembled model request, the raw `StreamChunk` protocol, the adapter contract every adapter must obey, and the shared assembler. The [core spine](core.md) holds and logs these values on every turn; this page declares them.
+The conversation and streaming types from [`packages/llm`](../../packages/llm/README.md): the `Message`/`ContentBlock` variants every request and durable history share, the fully assembled model request, the raw `StreamChunk` protocol, the adapter contract every adapter must implement, and the shared assembler. The [core packages](core.md) hold and log these values on every turn; this page declares them.
Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts)
@@ -22,12 +22,13 @@ Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts)
interface ContentBlockMap {
'text': TextBlock
'reasoning': ReasoningBlock
+ 'image': ImageBlock
'tool-call': ToolCallBlock
'tool-result': ToolResultBlock
}
```
-The block interfaces (full fields in source): `TextBlock` (`text`), `ReasoningBlock` (thinking, distinct from visible text), `ToolCallBlock` (`id: CallId`, `name`, raw-JSON `arguments`), `ToolResultBlock` (`toolCallId`, nested `content: ContentBlock[]`, `isError?`). `ContentBlock = ContentBlockMap[ContentBlockType]`. The core set is limited to blocks every shipping path honors — multimodal content (images, audio, …) has no core block type; a feature that needs one adds it via the merge-extensible map together with the adapter/UI/compaction support that honors it.
+The block interfaces (full fields in source): `TextBlock` (`text`), `ReasoningBlock` (thinking, distinct from visible text), `ImageBlock` (a durable [image attachment](attachment.md)), `ToolCallBlock` (`id: CallId`, `name`, raw-JSON `arguments`), and `ToolResultBlock` (`toolCallId`, nested `content: ContentBlock[]`, `isError?`). `ContentBlock = ContentBlockMap[ContentBlockType]`. A new modality belongs in the merge-extensible map only when its adapter, UI, compaction, and durable replay paths honor it.
Source: [`packages/llm/llm/src/message.ts`](../../packages/llm/llm/src/message.ts)
@@ -78,12 +79,12 @@ interface MessageSourceMap {
}
```
-Producer identity and content shape are independent. `kind` answers *who produced this*; the optional `form` a producer mixes in answers *what shape of information it is*, so several producers may share one presentation and one producer may emit more than one shape over a session. The vocabulary is semantic and grows one value at a time; an absent or unrecognized value is the documented default, presented as opaque content:
+Producer identity and presentation form are independent. `kind` answers *who produced this*; the optional `form` answers *what kind of information this is*, and consumers decide how to present it. Several producers may share one form, and one producer may emit more than one form over a session. The values are semantic and grow one at a time; an absent or unrecognized value uses the documented default and is presented as opaque content:
```ts type-equiv
/**
- * What SHAPE of information a producer-supplied context carries, declared by
- * the producer beside the source fields it supplied.
+ * The kind of information in producer-supplied context, declared by the
+ * producer beside its provenance.
*
* `MessageSource.kind` answers *who produced this*; `form` answers *what kind
* of thing it is*, and the two axes are deliberately independent — several
@@ -125,10 +126,10 @@ interface ContextSnapshotSection {
```ts type-equiv
/**
* Producer-declared {@link ContextForm} and the fields that form requires,
- * mixed into the source shapes that carry one.
+ * mixed into the source types that carry one.
*
- * Discriminated by `form` so a producer cannot declare a shape without the
- * facts that shape is presented from: a `notice` must record its one-line
+ * Discriminated by `form` so a producer cannot select a form without the
+ * fields needed to present it: a `notice` must record its one-line
* account, a `snapshot` its sections. Omitting `form` stays valid — an
* undeclared context is the documented default.
*/
@@ -183,13 +184,13 @@ type StreamChunk =
Every thrown or in-band final-adapter failure normalizes to one serializable provider-neutral payload. `providerRetryAfterMs` is a validated positive delay requested by the provider, not a retry decision; `ProviderRequestId` is an opaque branded string for diagnostics.
```ts type-equiv
-/** Serializable provider-boundary facts; policy decides whether they are retryable. */
+/** Serializable provider or transport failure facts; policy decides whether they are retryable. */
interface LlmFailure {
/** Human-readable provider or transport failure. */
readonly message: string
/** Stable provider-neutral machine-routing code. */
readonly code: string
- /** HTTP status observed at the provider boundary, when available. */
+ /** HTTP status returned by the provider, when available. */
readonly status?: number
/** Provider-requested delay in milliseconds, when valid and available. */
readonly providerRetryAfterMs?: number
@@ -204,7 +205,7 @@ Every adapter MUST obey these, and every consumer may rely on them:
- **`usage` before `finish`, nothing after `finish`.** Defer both to the provider's end-of-stream marker so a trailing usage-only chunk can't violate the ordering.
- **Tool-call `arguments` stay raw JSON strings end-to-end.** Partial fragments stream via `argumentsDelta`; a provider that hands back parsed objects re-stringifies at `block-end`.
-- **Two sanctioned error paths, one fact shape.** A failure may either THROW from `stream()` (transport/protocol errors) **or** end the stream with `finish {kind:'error'|'aborted', failure}` (provider in-band errors, for adapters that can't throw mid-stream). `LlmError.failure` carries the same `LlmFailure`. The final adapter boundary preserves the exact thrown `Error` object and associates immutable facts plus the serving registration's immutable retry policy with that call; the agent loop closes the failed step and offers the error, facts, immutable prior-retried facts, serving policy, and turn signal to `agent/request-error`. A handling listener returns `{ kind: 'retry' }` after its awaited repair; absent recovery the structured failure becomes the turn error, and no normal assistant message or tool side effect is committed for that attempt.
+- **Two sanctioned error paths, one `LlmFailure` type.** A failure may either THROW from `stream()` (transport/protocol errors) **or** end the stream with `finish {kind:'error'|'aborted', failure}` (provider in-band errors, for adapters that can't throw mid-stream). `LlmError.failure` carries the same `LlmFailure`. After the call selects its adapter, the stream preserves the exact thrown `Error` object and associates immutable facts plus the serving registration's immutable retry policy with that call; the agent loop closes the failed step and offers the error, facts, immutable prior-retried facts, serving policy, and turn signal to `agent/request-error`. A handling listener returns `{ kind: 'retry' }` after its awaited repair; absent recovery the structured failure becomes the turn error, and no normal assistant message or tool side effect is committed for that attempt.
- **One adapter call is one provider attempt.** Adapters disable library retries. Agent-level recovery opens another durable numbered turn; direct `ctx.llm.stream()` callers remain single-attempt.
- **Provider stalls are bounded at the transport.** Both shipping remote adapters expose positive finite `streamIdleTimeoutMs` with a five-minute default. The watchdog arms only while iterator `next()` is outstanding, uses one stable signal for the whole request, maps its own expiry to `TIMEOUT`, and keeps an earlier caller abort as `ABORTED`.
- **Context overflow has one canonical code.** Both DeepSeek adapters classify explicit provider detail through `isContextWindowExceededError()` and surface `CONTEXT_WINDOW_EXCEEDED`, whether the failure arrives as a thrown HTTP `LlmError` or an in-band finish error. Consumers route on the code, never provider text.
@@ -214,7 +215,7 @@ Every adapter MUST obey these, and every consumer may rely on them:
## `ResolvedRetryPolicy`
-Provider configuration resolves before route registration into an immutable discriminated union. Normal mode carries `mode: 'normal'`, finite `maxRetries`, `retryableCodes`, and required `initialDelayMs`, `maxDelayMs`, and `jitterRatio`; always mode carries `mode: 'always'` and the same required backoff fields without a finite maximum. `LlmService.providerRetryPolicy(provider)` returns the currently registered value and supplies normal defaults when the adapter omits one; `llmRetryPolicyOf(stream)` returns the exact serving registration's captured value after that call enters its final adapter boundary, so later route disposal or replacement cannot change an in-flight failure's recovery policy. The [generated config catalog](../config-catalog.md) owns the optional input shapes.
+Provider configuration resolves before route registration into an immutable discriminated union. Normal mode carries `mode: 'normal'`, finite `maxRetries`, `retryableCodes`, and required `initialDelayMs`, `maxDelayMs`, and `jitterRatio`; always mode carries `mode: 'always'` and the same required backoff fields without a finite maximum. `LlmService.providerRetryPolicy(provider)` returns the currently registered value and supplies normal defaults when the adapter omits one; `llmRetryPolicyOf(stream)` returns the value captured from the serving registration after the call selects that registration, so later route disposal or replacement cannot change an in-flight failure's recovery policy. The [generated config catalog](../config-catalog.md) lists the optional input fields.
## `AppIdentity` — app attribution
@@ -233,7 +234,7 @@ interface AppIdentity {
product: string
/** Product version; sourced from package metadata, never hand-copied. */
version: string
- /** Public home URL of the app, used as the `User-Agent` comment. */
+ /** Repository home URL of the app, used as the `User-Agent` comment. */
url: string
}
```
@@ -396,6 +397,8 @@ interface LlmModelInfo {
name: string
/** Optional user-facing distinction from otherwise similar models. */
description?: string
+ /** Accepted request modalities; absent means unknown, while an explicit omission is negative capability. */
+ inputModalities?: readonly ModelModality[]
}
```
@@ -530,7 +533,7 @@ interface ToolSchema {
}
```
-The model-facing `ToolSchema` is the wire shape; the registered `ToolDefinition` that produces it (schema + `execute`) is on [tools.md](tools.md).
+The model-facing `ToolSchema` is the wire type; the registered `ToolDefinition` that produces it (schema + `execute`) is on [tools.md](tools.md).
A provider a surface is still drafting has no route and no catalog, so interrogation is described separately: the request carries the draft the user is editing, and the reply is candidates a surface may adopt rather than a catalog it must serve.
@@ -587,7 +590,7 @@ The loop builds each request from logged state. `EpochHeader` records call confi
`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, reasoning effort, or sampling. Before the waterfall, the loop removes values marked as adapter defaults so exact-model preparation materializes the selected route's current values; unmarked explicit settings remain in the proposal. After the waterfall, preparation rejects unsupported explicit effort ids without clamping and logs the effective config plus the fields supplied by adapter defaults under the turn signal. The prepared call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests.
-On the wire, a loop-built request reads the `system` slot (the rendered prompt assembly) followed by the derived history — the boundary snapshot, whose tail is the newest `user/message` on a turn's first step and the previous step's tool results on later steps. The dev invariant recomputes exactly this equation against every loop-built request.
+On the wire, a loop-built request reads the `system` slot (the rendered prompt assembly) followed by the derived history. The logged request snapshot ends with the newest `user/message` on a turn's first step and the previous step's tool results on later steps. The dev invariant recomputes exactly this equation against every loop-built request.
FIXME(call-config-shape): revisit which remaining fields are genuinely epoch-level for cache purposes (`model` and the model-owned reasoning effort are explicit; the sampling scalars sit here out of caution).
@@ -649,8 +652,8 @@ interface PreparedLlmCall {
/**
* Provider-wire adapter for the harness message and stream vocabulary. Register implementations
* with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include
- * `attributionHeaders()`; prove that at the wire or library header-hook boundary. The direct-fetch
- * DeepSeek and library-backed pi-ai adapters intentionally exercise this contract through different internals.
+ * `attributionHeaders()`; prove the headers are added in the wire request or library header hook. The direct-fetch
+ * DeepSeek and library-backed pi-ai adapters meet this contract through different internals.
*/
declare abstract class LlmAdapter {
/**
@@ -702,15 +705,15 @@ declare abstract class LlmAdapter {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
### `ctx.llm` — `LlmService`
-The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall.
+The abstract `llm` service: an adapter registry plus a streaming model-call API, interceptable via the `llm/stream` waterfall.
```ts cordis-catalog
/**
@@ -830,7 +833,7 @@ async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise
```
-Source: [`packages/llm/llm/src/index.ts:292`](../../packages/llm/llm/src/index.ts)
+Source: [`packages/llm/llm/src/index.ts:294`](../../packages/llm/llm/src/index.ts)
@@ -855,7 +858,7 @@ The provider topology changed: an adapter registered or unregistered routes, or
'llm/adapters-updated'(): void
```
-Source: [`packages/llm/llm/src/index.ts:73`](../../packages/llm/llm/src/index.ts)
+Source: [`packages/llm/llm/src/index.ts:75`](../../packages/llm/llm/src/index.ts)
@@ -879,5 +882,5 @@ Waterfall around every streaming model call (retry, replay, routing). Bound to t
'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable): AsyncIterable
```
-Source: [`packages/llm/llm/src/index.ts:62`](../../packages/llm/llm/src/index.ts)
+Source: [`packages/llm/llm/src/index.ts:64`](../../packages/llm/llm/src/index.ts)
diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md
index 777c44eaff..c3d1c4d895 100644
--- a/docs/subsystems/llm-streaming.zh.md
+++ b/docs/subsystems/llm-streaming.zh.md
@@ -2,7 +2,7 @@
[English](llm-streaming.md) | 中文
-[`packages/llm`](../../packages/llm/README.md) 的对话与流式输出词汇:每个请求与持久历史共享的 `Message`/`ContentBlock` 形状、完整组装的模型请求、原始 `StreamChunk` 协议、每个适配器必须遵守的适配器约定(adapter contract),以及共享的 assembler。[核心主干](core.md)在每个轮次持有并记录这些值;本页声明它们。
+[`packages/llm`](../../packages/llm/README.md) 提供对话与流式输出类型:每个请求和持久历史共用的 `Message`/`ContentBlock` 变体、完整组装的模型请求、原始 `StreamChunk` 协议、每个适配器必须实现的适配器约定(adapter contract),以及共享的 assembler。[核心包](core.md)在每个轮次持有并记录这些值;本页声明它们。
源码:[`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts)
@@ -22,12 +22,13 @@
interface ContentBlockMap {
'text': TextBlock
'reasoning': ReasoningBlock
+ 'image': ImageBlock
'tool-call': ToolCallBlock
'tool-result': ToolResultBlock
}
```
-各块接口(完整字段见源码):`TextBlock`(`text`)、`ReasoningBlock`(thinking,区别于可见文本)、`ToolCallBlock`(`id: CallId`、`name`、原始 JSON `arguments`)、`ToolResultBlock`(`toolCallId`、嵌套 `content: ContentBlock[]`、`isError?`)。`ContentBlock = ContentBlockMap[ContentBlockType]`。核心集仅限于每条交付路径都尊重的块——多模态内容(图像、音频等)没有核心块类型;需要的功能通过可合并扩展的 map 添加,同时提供适配器/UI/压缩支持。
+各块接口(完整字段见源码):`TextBlock`(`text`)、`ReasoningBlock`(thinking,区别于可见文本)、`ImageBlock`(一个持久的[图片附件](attachment.md))、`ToolCallBlock`(`id: CallId`、`name`、原始 JSON `arguments`),以及 `ToolResultBlock`(`toolCallId`、嵌套 `content: ContentBlock[]`、`isError?`)。`ContentBlock = ContentBlockMap[ContentBlockType]`。仅当适配器、UI、压缩和持久回放路径均支持某种新模态时,才将其纳入可合并扩展的 map。
源码:[`packages/llm/llm/src/message.ts`](../../packages/llm/llm/src/message.ts)
@@ -78,12 +79,12 @@ interface MessageSourceMap {
}
```
-生产方标识与内容形态相互独立。`kind` 回答「由谁产生」;生产方可选混入的 `form` 回答「这是何种形态的信息」,因此多个生产方可以共用一种呈现,一个生产方在一次会话中也可以发出多种形态。该词汇表是语义的,逐个取值增长;未声明或无法识别的取值是有文档的默认,按不透明内容呈现:
+生产方标识与呈现形式相互独立。`kind` 回答「由谁产生」;可选的 `form` 回答「这是什么类型的信息」,消费方决定如何呈现。多个生产方可以共用一种 `form`,一个生产方在一次会话中也可以发出多种 `form`。这些取值描述语义,并逐个增加;未声明或无法识别的值使用文档规定的默认值,按不透明内容呈现:
```ts type-equiv
/**
- * What SHAPE of information a producer-supplied context carries, declared by
- * the producer beside the source fields it supplied.
+ * The kind of information in producer-supplied context, declared by the
+ * producer beside its provenance.
*
* `MessageSource.kind` answers *who produced this*; `form` answers *what kind
* of thing it is*, and the two axes are deliberately independent — several
@@ -125,10 +126,10 @@ interface ContextSnapshotSection {
```ts type-equiv
/**
* Producer-declared {@link ContextForm} and the fields that form requires,
- * mixed into the source shapes that carry one.
+ * mixed into the source types that carry one.
*
- * Discriminated by `form` so a producer cannot declare a shape without the
- * facts that shape is presented from: a `notice` must record its one-line
+ * Discriminated by `form` so a producer cannot select a form without the
+ * fields needed to present it: a `notice` must record its one-line
* account, a `snapshot` its sections. Omitting `form` stays valid — an
* undeclared context is the documented default.
*/
@@ -187,13 +188,13 @@ type StreamChunk =
每个抛出的失败或最终适配器的带内失败都会规范化为一种可序列化、提供方无关的 payload。`providerRetryAfterMs` 是经校验、由提供方请求的正数延迟,而不是重试决策;`ProviderRequestId` 是用于诊断的不透明品牌字符串。
```ts type-equiv
-/** Serializable provider-boundary facts; policy decides whether they are retryable. */
+/** Serializable provider or transport failure facts; policy decides whether they are retryable. */
interface LlmFailure {
/** Human-readable provider or transport failure. */
readonly message: string
/** Stable provider-neutral machine-routing code. */
readonly code: string
- /** HTTP status observed at the provider boundary, when available. */
+ /** HTTP status returned by the provider, when available. */
readonly status?: number
/** Provider-requested delay in milliseconds, when valid and available. */
readonly providerRetryAfterMs?: number
@@ -208,7 +209,7 @@ interface LlmFailure {
- **`usage` 在 `finish` 之前,`finish` 之后不再有任何分片。** 将两者都推迟到提供方的流结束标记,这样尾部的 usage-only 分片就不会违反顺序。
- **工具调用的 `arguments` 全程保持原始 JSON 字符串。** 部分片段通过 `argumentsDelta` 流式传输;如果提供方返回的是已解析的对象,适配器在 `block-end` 时重新序列化为字符串。
-- **两条受支持的错误路径,一种事实形状。** 失败可以从 `stream()` 抛出(传输/协议错误),**或者**以 `finish {kind:'error'|'aborted', failure}` 结束流(无法在流中途抛异常的适配器用它表示提供方带内错误)。`LlmError.failure` 携带同一个 `LlmFailure`。最终适配器边界保留被抛出的确切 `Error` 对象,并将不可变事实以及实际服务注册所对应的不可变重试策略关联到该调用;agent loop(智能体循环)关闭失败步骤,再把错误、事实、不可变的先前已重试失败事实、实际服务策略和轮次信号提供给 `agent/request-error`。处理该错误的 listener 在其 await 的修复完成后返回 `{ kind: 'retry' }`;若未恢复,结构化失败会成为轮次错误,并且该次尝试不会提交正常 assistant 消息或工具副作用。
+- **两条受支持的错误路径,共用一个 `LlmFailure` 类型。** 失败可以从 `stream()` 抛出(传输/协议错误),**或者**以 `finish {kind:'error'|'aborted', failure}` 结束流(无法在流中途抛异常的适配器用它表示提供方带内错误)。`LlmError.failure` 携带同一个 `LlmFailure`。调用选定适配器后,流会保留被抛出的确切 `Error` 对象,并将不可变事实以及实际服务注册所对应的不可变重试策略关联到该调用;agent loop(智能体循环)关闭失败步骤,再把错误、事实、不可变的先前已重试失败事实、实际服务策略和轮次信号提供给 `agent/request-error`。处理该错误的 listener 在其 await 的修复完成后返回 `{ kind: 'retry' }`;若未恢复,结构化失败会成为轮次错误,并且该次尝试不会提交正常 assistant 消息或工具副作用。
- **一次适配器调用就是一次提供方尝试。** 适配器禁用库重试。agent 层恢复会打开另一个持久、带编号的轮次;直接调用 `ctx.llm.stream()` 的调用方仍然只尝试一次。
- **提供方停顿在传输层受到时限约束。** 两个已交付的远程适配器都暴露正数且有限的 `streamIdleTimeoutMs`,默认五分钟。watchdog 只在 iterator `next()` 尚未完成时启动,整个请求使用同一个稳定 signal,把自身到期映射为 `TIMEOUT`,并把更早发生的调用方中止保留为 `ABORTED`。
- **上下文溢出只有一个规范 code。** 两个 DeepSeek 适配器都通过 `isContextWindowExceededError()` 对提供方的显式细节分类并暴露 `CONTEXT_WINDOW_EXCEEDED`,无论失败以抛出的 HTTP `LlmError` 还是带内 finish error 到达。消费方按 code 路由,绝不依赖提供方文本。
@@ -218,7 +219,7 @@ interface LlmFailure {
## `ResolvedRetryPolicy`
-提供方配置会在路由注册前解析为不可变的可辨识联合。normal mode 携带 `mode: 'normal'`、有限的 `maxRetries`、`retryableCodes`,以及必填的 `initialDelayMs`、`maxDelayMs` 与 `jitterRatio`;always mode 携带 `mode: 'always'` 和相同的必填退避字段,但没有有限上限。`LlmService.providerRetryPolicy(provider)` 返回当前注册的值,并在适配器省略策略时提供 normal 默认值;调用进入最终适配器边界后,`llmRetryPolicyOf(stream)` 返回为其提供服务的确切注册所捕获的值,因此之后释放或替换路由都无法改变进行中失败的恢复策略。可选输入形状由[生成的配置目录](../config-catalog.md)规定。
+提供方配置会在路由注册前解析为不可变的可辨识联合。normal mode 携带 `mode: 'normal'`、有限的 `maxRetries`、`retryableCodes`,以及必填的 `initialDelayMs`、`maxDelayMs` 与 `jitterRatio`;always mode 携带 `mode: 'always'` 和相同的必填退避字段,但没有有限上限。`LlmService.providerRetryPolicy(provider)` 返回当前注册的值,并在适配器省略策略时提供 normal 默认值;调用选定该注册后,`llmRetryPolicyOf(stream)` 返回为该调用服务的注册所捕获的值,因此之后释放或替换路由都无法改变进行中失败的恢复策略。可选配置输入字段由[生成的配置目录](../config-catalog.md)列出。
## `AppIdentity`:应用归属
@@ -237,7 +238,7 @@ interface AppIdentity {
product: string
/** Product version; sourced from package metadata, never hand-copied. */
version: string
- /** Public home URL of the app, used as the `User-Agent` comment. */
+ /** Repository home URL of the app, used as the `User-Agent` comment. */
url: string
}
```
@@ -404,6 +405,8 @@ interface LlmModelInfo {
name: string
/** Optional user-facing distinction from otherwise similar models. */
description?: string
+ /** Accepted request modalities; absent means unknown, while an explicit omission is negative capability. */
+ inputModalities?: readonly ModelModality[]
}
```
@@ -538,7 +541,7 @@ interface ToolSchema {
}
```
-面向模型的 `ToolSchema` 是协议格式;产出它的已注册 `ToolDefinition`(schema + `execute`)在 [tools.md](tools.md) 中。
+面向模型的 `ToolSchema` 是协议类型;产出它的已注册 `ToolDefinition`(schema + `execute`)在 [tools.md](tools.md) 中。
界面正在起草的提供方既没有路由也没有 catalog,因此询问被单独描述:请求携带用户正在编辑的草稿,回复是界面可以采纳的候选,而不是它必须服务的 catalog。
@@ -595,7 +598,7 @@ interface LlmDiscoveredModel {
`agent/request` 接收冻结的调用配置种子,并可返回替代值以切换提供方、模型、推理强度或采样参数。waterfall 开始前,循环会移除标记为适配器默认值的值,使确切模型准备过程填入所选路由的当前值;未带标记的显式设置仍保留在提议中。waterfall 结束后,准备过程会在轮次信号控制下拒绝显式指定但不受支持的推理强度 ID(不自动调整),并记录生效配置以及由适配器默认值提供的字段。准备完成的调用直至分派完成始终持有同一项适配器注册。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。
-在协议格式上,循环构建的请求先读取 `system` 槽位(渲染后的提示词组装),再读取派生历史——边界快照,其尾部在轮次首步是最新的 `user/message`,在后续步骤是上一步的工具结果。开发不变式针对每个循环构建的请求精确重算此等式。
+在协议中,循环构建的请求先读取 `system` 槽位(渲染后的提示词组装),再读取派生历史。已记录的请求快照会以最新的 `user/message`(轮次首步)或上一步的工具结果(后续步骤)结尾。开发不变式针对每个循环构建的请求精确重算此等式。
FIXME(call-config-shape):重新审视其余哪些字段出于缓存目的确实属于 epoch 层级(`model` 和模型持有的推理强度已明确属于;采样标量目前出于谨慎保留在此)。
@@ -657,8 +660,8 @@ interface PreparedLlmCall {
/**
* Provider-wire adapter for the harness message and stream vocabulary. Register implementations
* with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include
- * `attributionHeaders()`; prove that at the wire or library header-hook boundary. The direct-fetch
- * DeepSeek and library-backed pi-ai adapters intentionally exercise this contract through different internals.
+ * `attributionHeaders()`; prove the headers are added in the wire request or library header hook. The direct-fetch
+ * DeepSeek and library-backed pi-ai adapters meet this contract through different internals.
*/
declare abstract class LlmAdapter {
/**
@@ -710,15 +713,15 @@ declare abstract class LlmAdapter {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
### `ctx.llm` — `LlmService`
-The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall.
+The abstract `llm` service: an adapter registry plus a streaming model-call API, interceptable via the `llm/stream` waterfall.
```ts cordis-catalog
/**
@@ -838,7 +841,7 @@ async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise
```
-Source: [`packages/llm/llm/src/index.ts:292`](../../packages/llm/llm/src/index.ts)
+Source: [`packages/llm/llm/src/index.ts:294`](../../packages/llm/llm/src/index.ts)
@@ -863,7 +866,7 @@ The provider topology changed: an adapter registered or unregistered routes, or
'llm/adapters-updated'(): void
```
-Source: [`packages/llm/llm/src/index.ts:73`](../../packages/llm/llm/src/index.ts)
+Source: [`packages/llm/llm/src/index.ts:75`](../../packages/llm/llm/src/index.ts)
@@ -887,5 +890,5 @@ Waterfall around every streaming model call (retry, replay, routing). Bound to t
'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable): AsyncIterable
```
-Source: [`packages/llm/llm/src/index.ts:62`](../../packages/llm/llm/src/index.ts)
+Source: [`packages/llm/llm/src/index.ts:64`](../../packages/llm/llm/src/index.ts)
diff --git a/docs/subsystems/permission.i18n.yaml b/docs/subsystems/permission.i18n.yaml
index 3f9c9b533c..ea67c1431c 100644
--- a/docs/subsystems/permission.i18n.yaml
+++ b/docs/subsystems/permission.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/permission.md
-permission.md: 6f9a37dbb784ccbaea74056519992e40f9c07c6d
-permission.zh.md: 8eda50fae09a67c41003d7a914fb139c4132a900
+permission.md: 88fd03877d27ba02ef4f449b0500b6395f67977d
+permission.zh.md: e7459844487793a51188e27e2b87b64060d83bcc
diff --git a/docs/subsystems/permission.md b/docs/subsystems/permission.md
index 6f9a37dbb7..88fd03877d 100644
--- a/docs/subsystems/permission.md
+++ b/docs/subsystems/permission.md
@@ -71,9 +71,9 @@ interface PresetOption {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/permission.zh.md b/docs/subsystems/permission.zh.md
index 8eda50fae0..e745984448 100644
--- a/docs/subsystems/permission.zh.md
+++ b/docs/subsystems/permission.zh.md
@@ -71,9 +71,9 @@ interface PresetOption {
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
diff --git a/docs/subsystems/persistence.i18n.yaml b/docs/subsystems/persistence.i18n.yaml
index f198d0a25e..03460eae89 100644
--- a/docs/subsystems/persistence.i18n.yaml
+++ b/docs/subsystems/persistence.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/persistence.md
-persistence.md: 640e2c0122b01ae869d20c1805742abad782c95b
-persistence.zh.md: 7e28d6d1f63a78741840fb74194d3249696423ae
+persistence.md: fde8348d64a200eda5133abf66deedee6be09857
+persistence.zh.md: 7a334501ee7fcbefda9d1381dfe41a79679d33b3
diff --git a/docs/subsystems/persistence.md b/docs/subsystems/persistence.md
index 640e2c0122..fde8348d64 100644
--- a/docs/subsystems/persistence.md
+++ b/docs/subsystems/persistence.md
@@ -77,12 +77,23 @@ interface SessionHeader {
* resume — a runtime-only depth would reset a resumed child to top-level.
*/
readonly delegationDepth?: number
+ /**
+ * Id of the agent preset this session's agent was composed from, when the
+ * deployment composes per session. Durable because the preset decides the
+ * session's tools and prompt: a resume that restored a different composition
+ * would replay history the model can no longer act on.
+ */
+ readonly agentPreset?: string
}
```
+## Format refusal — logs a build cannot faithfully read
+
+A backend refuses a log it cannot faithfully interpret with `SessionFormatUnsupportedError`, distinct from `SessionPersistenceCorruptionError` because nothing is damaged. A header `version` ahead of `SESSION_FORMAT_VERSION` names the direction ("written by a newer harness — upgrade the harness to open it"); one behind it states that this build ships no upgrade path. After legacy-shape normalization, an event type outside this build's generated vocabulary (`KNOWN_SESSION_EVENT_TYPES`, emitted by `gen-persistence-catalog`) refuses the same way unless the event's envelope carries `ignorable: true` — silently skipping an unrecognized required event could change how the rest of the log must be read. The message appends the raw log path when the backend keeps one artifact per session, so the refused text stays reachable. The JSONL backend refuses a foreign version straight from the raw header line, before validating today's header shape or decoding any event row — a structurally different future format still reports the upgrade direction, never "corrupt"; SQLite gates whole-file structure through its own `SCHEMA_VERSION` pragma first. Design rationale and the deferred upgrader chain live in the [session-log-version-mechanism note](../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md).
+
## `CreateSessionOptions` — seeding and metadata
-Creating a `Session` through the store takes a `seed` (initial replay or fork history) and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller may supply the validated absolute `cwd`, the `parentSession` lineage, the `seedLength` seed boundary, the optional coarse `origin`, the `delegationDepth`, and an existing `createdAt`. `origin: 'subagent'` lets product navigation hide duplicate child rows; it does not prove that a descriptor is valid or that the child can resume.
+Creating a `Session` through the store takes a `seed` (initial replay or fork history) and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller may supply the validated absolute `cwd`, the `parentSession` lineage, the `seedLength` seed boundary, the optional coarse `origin`, the `delegationDepth`, the `agentPreset` the agent was composed from, and an existing `createdAt`. `origin: 'subagent'` lets product navigation hide duplicate child rows; it does not prove that a descriptor is valid or that the child can resume.
```ts type-equiv
/**
@@ -104,12 +115,29 @@ interface CreateSessionOptions {
readonly seedLength?: number
readonly origin?: 'subagent'
readonly delegationDepth?: number
+ readonly agentPreset?: string
}
}
```
Replay/fork is therefore `ctx.sessions.create(id, { seed: seedEvents })`; resuming a *persisted* session into a live agent is `ctx.agents.resume({ resumeSessionId })`.
+## `SessionRawArtifact` — verbatim stored artifact text
+
+A backend's own artifact text for one session, byte-identical to what it durably wrote (decoded from its physical encoding). `readRaw` returns it without reconstructing from parsed events, so backend-specific serialization (chunk packing, key order, line breaks) survives. Consumers first test `supportsRawArtifacts`: `false` means the backend does not provide this capability (for example SQLite), while `readRaw(...) === undefined` means a supported backend has no materialized artifact for that session.
+
+```ts type-equiv
+/** A backend's own raw artifact text for one session, verbatim. */
+interface SessionRawArtifact {
+ /** The session header parsed from the artifact's own first line. */
+ readonly meta: SessionHeader
+ /** The artifact's base filename on disk, without any physical encoding suffix. */
+ readonly filename: string
+ /** The artifact's full text content, decoded from the backend's physical encoding. */
+ readonly content: string
+}
+```
+
## Preparation and restoration ownership
`SessionStore.prepare()` accepts ordinary creation options or fresh persistence graphs transferred through `RestoredSessionOptions`. The restoration branch validates and freezes the transferred header and events in place, so callers must retain no mutable aliases. `SessionPreparation` then owns the exact unpublished Session until publication or rollback; disposal is synchronous and idempotent. Persistence inspection exposes only `SessionInspection`, an immutable logical view borrowed from the same prepared Session.
@@ -203,15 +231,15 @@ interface SessionPersistenceSnapshot {
Both implement the same abstract `SessionPersistence` (locate/create/append/prepare/load/inspect/readFrom/list/listSnapshots over `SessionEvent`, with optional cancellation on observation methods) and pass the shared `runPersistenceContract` suite:
- **[dsh-session-persistence-jsonl](../../packages/session/session-persistence-jsonl)** — an append-only logical JSONL log per session, stored as checksummed concatenated Zstandard frames by default or raw lines by configuration, with crash-safe atomic writes, interrupted-turn recovery, and a read/replay path.
-- **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)** — `node:sqlite`, one row per `SessionEvent`. The row shape `(session_id, seq, type, time, data, source_event_seqs, surface_op)` maps 1:1 onto the event, including optional surface metadata, so there is no parallel persisted schema to keep in sync.
+- **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)** — `node:sqlite`, one row per `SessionEvent`. The row fields `(session_id, seq, type, time, data, source_event_seqs, surface_op)` map 1:1 onto the event, including optional surface metadata, so there is no parallel persisted schema to keep in sync.
-## Cordis surface
+## Cordis API
-Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
@@ -229,6 +257,23 @@ Durable append-only session storage. Implementations preserve contiguous, lossle
*/
abstract locate(meta: SessionHeader): SessionLocation | undefined
+/**
+ * Read a session's backend-owned artifact text verbatim — the exact durable
+ * bytes the backend wrote (decoded from its physical encoding, e.g. a
+ * decompressed JSONL). The returned `content` is the raw text, not a
+ * reconstruction from parsed events, so it preserves backend-specific
+ * serialization (chunk packing, key order, line breaks). Callers first test
+ * {@link supportsRawArtifacts}; `undefined` then means only that the requested
+ * session has no materialized artifact.
+ * @param _id - the persisted session to read (unused by the default: no
+ * per-session artifact).
+ * @param signal - optional cancellation for backend read work.
+ * @returns the raw artifact plus its parsed header, or `undefined` when the
+ * session is absent.
+ * @throws when this backend does not expose per-session raw artifacts.
+ */
+readRaw(_id: SessionId, signal?: AbortSignal): Promise
+
/**
* Register a new session's metadata. A backend MAY defer the physical write
* until the first {@link append} (lazy materialization), in which case a
@@ -334,5 +379,5 @@ abstract listSnapshots(signal?: AbortSignal): Promise
diff --git a/docs/subsystems/persistence.zh.md b/docs/subsystems/persistence.zh.md
index 7e28d6d1f6..7a334501ee 100644
--- a/docs/subsystems/persistence.zh.md
+++ b/docs/subsystems/persistence.zh.md
@@ -77,12 +77,23 @@ interface SessionHeader {
* resume — a runtime-only depth would reset a resumed child to top-level.
*/
readonly delegationDepth?: number
+ /**
+ * Id of the agent preset this session's agent was composed from, when the
+ * deployment composes per session. Durable because the preset decides the
+ * session's tools and prompt: a resume that restored a different composition
+ * would replay history the model can no longer act on.
+ */
+ readonly agentPreset?: string
}
```
+## 格式拒绝:本构建无法可靠读取的日志
+
+后端用 `SessionFormatUnsupportedError` 拒绝无法可靠解读的日志,它与 `SessionPersistenceCorruptionError` 区分,因为数据没有损坏。header 的 `version` 比 `SESSION_FORMAT_VERSION` 新时,消息说明方向("由更新的 harness 写入,请升级 harness 后打开");比它旧时说明本构建没有升级路径。经过 legacy 形状归一化后,本构建生成词汇表(`KNOWN_SESSION_EVENT_TYPES`,由 `gen-persistence-catalog` 生成)之外的事件类型同样被拒绝,除非该事件的信封带 `ignorable: true`:静默跳过一个不认识的必需事件可能改变日志其余部分的解读方式。后端为每个会话保留独立文件时,消息附上原始日志路径,被拒绝的文本仍然可读。JSONL 后端直接从原始 header 行拒绝外来版本,先于当前 header 形状校验和任何事件行解码,因此结构完全不同的未来格式仍会报告升级方向,绝不会报"损坏";SQLite 则先由自己的 `SCHEMA_VERSION` pragma 把关整个文件的结构。设计理由与推迟建设的升级器链见 [session-log 版本机制 Agent Note](../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md)。
+
## `CreateSessionOptions`:seed 与元数据
-通过 store 创建 `Session` 时会接收 `seed`(初始回放或 fork 历史)与 `meta`(store 折叠进 `SessionHeader` 的存储层字段)。store 填充 `version`/`id` 并为 `createdAt` 提供默认值;调用方可以提供已校验的绝对 `cwd`、`parentSession` 谱系、`seedLength` 种子边界、可选的粗粒度 `origin`、`delegationDepth` 以及已有的 `createdAt`。`origin: 'subagent'` 让产品导航能够隐藏重复的 child 行;它不证明描述符有效,也不证明 child 可以恢复。
+通过 store 创建 `Session` 时会接收 `seed`(初始回放或 fork 历史)与 `meta`(store 折叠进 `SessionHeader` 的存储层字段)。store 填充 `version`/`id` 并为 `createdAt` 提供默认值;调用方可以提供已校验的绝对 `cwd`、`parentSession` 谱系、`seedLength` 种子边界、可选的粗粒度 `origin`、`delegationDepth`、该 agent 所依据组装的 `agentPreset` 以及已有的 `createdAt`。`origin: 'subagent'` 让产品导航能够隐藏重复的 child 行;它不证明描述符有效,也不证明 child 可以恢复。
```ts type-equiv
/**
@@ -104,12 +115,29 @@ interface CreateSessionOptions {
readonly seedLength?: number
readonly origin?: 'subagent'
readonly delegationDepth?: number
+ readonly agentPreset?: string
}
}
```
因此,回放/fork 的调用方式为 `ctx.sessions.create(id, { seed: seedEvents })`;将一个*持久化*会话恢复为活跃 agent 的调用方式为 `ctx.agents.resume({ resumeSessionId })`。
+## `SessionRawArtifact`——逐字存储工件文本
+
+后端为单个会话自持的工件文本,与其持久化写入的字节逐字一致(按物理编码解码)。`readRaw` 返回它而不从解析后事件重建,因此后端特定的序列化(chunk 打包、键序、换行)得以保留。Consumer 须先检查 `supportsRawArtifacts`:`false` 表示后端不提供此能力(如 SQLite),而 `readRaw(...) === undefined` 表示受支持的后端没有该会话的已实体化工件。
+
+```ts type-equiv
+/** A backend's own raw artifact text for one session, verbatim. */
+interface SessionRawArtifact {
+ /** The session header parsed from the artifact's own first line. */
+ readonly meta: SessionHeader
+ /** The artifact's base filename on disk, without any physical encoding suffix. */
+ readonly filename: string
+ /** The artifact's full text content, decoded from the backend's physical encoding. */
+ readonly content: string
+}
+```
+
## 准备与恢复所有权
`SessionStore.prepare()` 接收普通创建选项,或通过 `RestoredSessionOptions` 转移所有权的新鲜持久化对象图。恢复分支会直接验证并冻结转移来的 header 与事件,因此调用方不得保留可变别名。`SessionPreparation` 随后持有该精确的未发布 Session,直至发布或回滚;dispose 是同步且幂等的。持久化检查只暴露 `SessionInspection`,即从同一个已准备 Session 借用的不可变逻辑视图。
@@ -203,15 +231,15 @@ interface SessionPersistenceSnapshot {
两者都实现同一个抽象 `SessionPersistence`(在 `SessionEvent` 上执行 locate/create/append/prepare/load/inspect/readFrom/list/listSnapshots,观察方法可选支持取消),并通过共享的 `runPersistenceContract` 套件:
- **[dsh-session-persistence-jsonl](../../packages/session/session-persistence-jsonl)**——每个会话一份仅追加的逻辑 JSONL 日志,默认存储为带 checksum 的连续 Zstandard frame,也可配置为原始行;支持崩溃安全的原子写入、被中断轮次的恢复以及读取/回放路径。
-- **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)**:基于 `node:sqlite`,每个 `SessionEvent` 一行。行结构 `(session_id, seq, type, time, data, source_event_seqs, surface_op)` 与事件 1:1 映射(包含可选的 surface 元数据),因此没有需要保持同步的并行持久化 schema。
+- **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)**:基于 `node:sqlite`,每个 `SessionEvent` 一行。行字段 `(session_id, seq, type, time, data, source_event_seqs, surface_op)` 与事件 1:1 映射(包含可选的 surface 元数据),因此没有需要保持同步的并行持久化 schema。