diff --git a/.agents/notes/README.i18n.yaml b/.agents/notes/README.i18n.yaml
index f92ea2f66d..faf4d69b5b 100644
--- a/.agents/notes/README.i18n.yaml
+++ b/.agents/notes/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/README.md
README.md: ae8e4724d610c97d74910d7dec5c95af69e93281
-README.zh.md: a16403ae87cd16d758b8303f42f60ad34b9c1eb3
+README.zh.md: 8cecc7068d9efc5e2ebb78d2301d03633f70f137
diff --git a/.agents/notes/README.zh.md b/.agents/notes/README.zh.md
index a16403ae87..8cecc7068d 100644
--- a/.agents/notes/README.zh.md
+++ b/.agents/notes/README.zh.md
@@ -16,13 +16,13 @@
文件名中的日期是该主题**首次提出**的时间(以 git 历史为准)。Agent Note 之间的交叉引用使用相对 Markdown 链接(`[topic](../../implemented/architecture/2026-…-….md)`),从不使用纯文字或编号,这样既可机械检查,也能在文件夹间移动时保持有效。
-活跃生命周期目录树就是工作清单:浏览其生命周期/类别文件夹,或搜索仓库即可。请勿添加集中式 `INDEX.md`;设计理由见[不设索引的 Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.md)。未来指导价值较低的已实施记录会移至下文所述、单独冻结的 [`archived/`](archived/AGENTS.md) 目录树。
+活跃生命周期目录树就是工作清单:浏览其生命周期/类别文件夹,或搜索仓库即可。请勿添加集中式 `INDEX.md`;设计理由见[不设索引的 Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.zh.md)。未来指导价值较低的已实施记录会移至下文所述、单独冻结的 [`archived/`](archived/AGENTS.md) 目录树。
## 分类
-每份 Agent Note 属于 `scripts/agent-note-tree.ts` 中封闭集合里的一个路径编码类别;分类门禁拒绝其他文件夹。新增类别需要同时更新规范集合与本节。见[分类 Agent Note](implemented/process/2026-06-20-agent-note-classification.md)。
+每份 Agent Note 属于 `scripts/agent-note-tree.ts` 中封闭集合里的一个路径编码类别;分类门禁拒绝其他文件夹。新增类别需要同时更新规范集合与本节。见[分类 Agent Note](implemented/process/2026-06-20-agent-note-classification.zh.md)。
| 类别 | 覆盖范围 |
|---|---|
@@ -41,7 +41,9 @@
归档路径编码为 `archived/{class}/yyyy-mm-dd-topic-title.md`;其中有意省略 `implemented`,因为只有 implemented Agent Note 可以进入归档。归档变更会移动完整的英文、中文和伴随记录三个文件,保留 `Status: implemented`,在两种语言的文件中紧接该状态行插入相同的 `Archived: YYYY-MM-DD` 行,重新记录伴随记录,并修复或删除入站链接。归档时只允许对内容做这些更改。
-封存后,每组归档文件都永久冻结。禁止编辑、翻译、重新格式化、更新、移动或删除,也不得将其视为当前行为的权威依据。文档门禁会跳过归档源文件,包括其中的出站链接;当活跃文档有意引用历史时,仍可链接到归档 Agent Note。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 强制执行封闭的类别目录树、完整的三文件配对、归档元数据、伴随记录 hash,以及仅追加的冻结内容 manifest(元数据清单)。[归档政策 Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.md) 记录了设计依据。
+封存后,每组归档文件都永久冻结。禁止编辑、翻译、重新格式化、更新、移动或删除,也不得将其视为当前行为的权威依据。文档门禁会跳过归档源文件,包括其中的出站链接;当活跃文档有意引用历史时,仍可链接到归档 Agent Note。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 强制执行封闭的类别目录树、完整的三文件配对、归档元数据、伴随记录 hash,以及仅追加的冻结内容 manifest(元数据清单)。[归档政策 Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.zh.md) 记录了设计依据。
+
+
## 何时需要写一份
@@ -57,7 +59,7 @@
## 文件格式
-每份活跃 Agent Note 遵循统一的文件内格式,由 `pnpm run verify-agent-note-format`([scripts/verify-agent-note-format.ts](../../scripts/verify-agent-note-format.ts),`doc-sync`(文档同步门禁)的一环)强制执行;该格式的设计动机及其否决的替代方案见[统一格式 Agent Note](implemented/process/2026-07-05-uniform-agent-note-format.md)。归档记录保留封存时的格式,并增加上述归档日期行。
+每份活跃 Agent Note 遵循统一的文件内格式,由 `pnpm run verify-agent-note-format`([scripts/verify-agent-note-format.ts](../../scripts/verify-agent-note-format.ts),`doc-sync`(文档同步门禁)的一环)强制执行;该格式的设计动机及其否决的替代方案见[统一格式 Agent Note](implemented/process/2026-07-05-uniform-agent-note-format.zh.md)。归档记录保留封存时的格式,并增加上述归档日期行。
### 头部块
@@ -126,4 +128,4 @@ Status:
### 中文对侧文件
-`.zh.md` 对侧文件按 [i18n 约定](../../docs/i18n/README.md)逐章节与其英文对侧文件保持相同结构;机器检查的头部标记(`# Agent Note: ` 和 `Status:` 行)保持英文原样不翻译。格式门禁跳过 `.zh.md` 文件;配对门禁检查它们的一致性。
+`.zh.md` 对侧文件按 [i18n 约定](../../docs/i18n/README.zh.md)逐章节与其英文对侧文件保持相同结构;机器检查的头部标记(`# Agent Note: ` 和 `Status:` 行)保持英文原样不翻译。格式门禁跳过 `.zh.md` 文件;配对门禁检查它们的一致性。
diff --git a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml
index 0c0e629d26..dac9ce6b5c 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md
2026-06-11-content-block-vocabulary.md: a31df6a7d16ea7cba649702fdb474dab34533c1b
-2026-06-11-content-block-vocabulary.zh.md: 5ac882e9de7dea02cc6534aee99c46869cc9363f
+2026-06-11-content-block-vocabulary.zh.md: da387b179816cda64791e71ca7affa1fbdfd195b
diff --git a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md
index 5ac882e9de..da387b1798 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md
@@ -12,7 +12,7 @@ harness 需要一套统一的内部消息语言,供 agent loop(智能体循
自主拥有词汇:消息是类型化内容块的数组(`text`、`reasoning`、`tool-call`、`tool-result`),其联合类型派生自可合并扩展的 `ContentBlockMap`,插件通过声明合并添加新的块类型。同一可合并扩展映射模式为所有「字符串化」字段提供类型(`MessageSource`、`FinishReason`、`TurnTrigger`、`TurnEndReason`)。流式输出采用原始分片协议;`BlockAssembler` 是唯一的共享组装实现。适配器负责转换为提供方的协议格式(wire format)——映射成本留在适配器中,正是它该在的地方。
-会话内上下文注入(`context/message`)和轮次中途 steering(中途引导)最初渲染为带标签的 user-role 信封(system-reminder 模式),而非引入新角色,因此适配器无需承担额外负担。如今两者都投影为无包装的普通用户内容;见[注入内容信封 Agent Note](../simplification/2026-07-20-unwrap-injected-content-envelopes.md)。实际适配器验证已确认此渲染方式符合当前 DeepSeek 的行为;如果未来某提供方出现不兼容,应在该适配器内处理,而非引入新的规范角色。
+会话内上下文注入(`context/message`)和轮次中途 steering(中途引导)最初渲染为带标签的 user-role 信封(system-reminder 模式),而非引入新角色,因此适配器无需承担额外负担。如今两者都投影为无包装的普通用户内容;见[注入内容信封 Agent Note](../simplification/2026-07-20-unwrap-injected-content-envelopes.zh.md)。实际适配器验证已确认此渲染方式符合当前 DeepSeek 的行为;如果未来某提供方出现不兼容,应在该适配器内处理,而非引入新的规范角色。
## 曾考虑的替代方案
diff --git a/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml
index e5c589dbf9..44272f7ca1 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md
2026-06-11-dev-invariants-over-deep-readonly.md: 66980f1ee09c6112f72786d6c3a147aadbc57f6c
-2026-06-11-dev-invariants-over-deep-readonly.zh.md: 0d694ea84e1b7c265491e9adb64757378b4185f1
+2026-06-11-dev-invariants-over-deep-readonly.zh.md: 576e53e0b27e65f6fa071ff649509223a7bc30ff
diff --git a/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.zh.md b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.zh.md
index 0d694ea84e..576e53e0b2 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.zh.md
@@ -32,7 +32,7 @@ TypeScript readonly 类型不是充分的运行时边界。它们在程序运行
### 包拥有的不变式配套插件检查关系
-`dsh-invariants` 注册可配置的 `ctx.invariants` 服务,本身不包含产品检查。每个包发布一个 `./invariant` 所有权配套插件;`dsh-session`、`dsh-agent`、`dsh-scope` 和 `dsh-agent-loop` 目前添加需要跟踪状态或观察另一个 seam 的规则:单调递增的序列号、轮次与步骤嵌套、工具调用/结果配对、合法的 agent(智能体)状态转换、主体正确的作用域分发,以及循环构建的请求与从其会话日志前缀重建的请求之间的相等性。全局启用和包名 regex 过滤器归该服务所有(见[包拥有的不变式服务](2026-07-19-package-owned-invariant-service.md))。
+`dsh-invariants` 注册可配置的 `ctx.invariants` 服务,本身不包含产品检查。每个包发布一个 `./invariant` 所有权配套插件;`dsh-session`、`dsh-agent`、`dsh-scope` 和 `dsh-agent-loop` 目前添加需要跟踪状态或观察另一个 seam 的规则:单调递增的序列号、轮次与步骤嵌套、工具调用/结果配对、合法的 agent(智能体)状态转换、主体正确的作用域分发,以及循环构建的请求与从其会话日志前缀重建的请求之间的相等性。全局启用和包名 regex 过滤器归该服务所有(见[包拥有的不变式服务](2026-07-19-package-owned-invariant-service.zh.md))。
当会话配套插件附加到已有会话或以种子记录初始化的会话时,它回放不可变日志以重建跟踪状态。服务为每项贡献提供一个可 dispose(资源释放)的子 fiber,因此轮次中途热重载是安全的,同时不赋予诊断逻辑对会话存储的所有权。
diff --git a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml
index 760c3fd099..868d215253 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md
2026-06-11-event-sourced-sessions.md: b6d17d2db1d9b489f3d4224683b61e62be485014
-2026-06-11-event-sourced-sessions.zh.md: a78349ea2385b3c9d32870757bb607f15996196d
+2026-06-11-event-sourced-sessions.zh.md: 11e5740dc2636fc002ed7158167b1dfbb39a971d
diff --git a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md
index a78349ea23..11e5740dc2 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md
@@ -24,5 +24,5 @@ MVP 要求严格的基于事件的追踪,以及完全可回放的会话(严
- 回放、追踪与遥测在结构上得到保证,而非事后附加。
- 持久化仍是插件关注点;内存存储随 dsh-session 一起提供。
-- 事件词汇可通过合并扩展(插件可添加如压缩(compaction)事件);[会话持久化](2026-06-14-session-persistence.md)在日志具备持久性后固定了其结构。
+- 事件词汇可通过合并扩展(插件可添加如压缩(compaction)事件);[会话持久化](2026-06-14-session-persistence.zh.md)在日志具备持久性后固定了其结构。
- 派生成本随日志长度增长,压缩(dsh-compaction)是预期的缓解手段,而不是改写日志。
diff --git a/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml
index 8e97d86ccf..57843018c6 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md
2026-06-11-microkernel-event-taxonomy.md: 6cacbc30f29c863de0f8ddfa4df7251a64bc80ca
-2026-06-11-microkernel-event-taxonomy.zh.md: 2e43b57d62d8fca692ffab379d1841e6a3e61fad
+2026-06-11-microkernel-event-taxonomy.zh.md: 1c7919392b835c491b9dc606d46f638a708d2948
diff --git a/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.zh.md b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.zh.md
index 2e43b57d62..1c7919392b 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.zh.md
@@ -25,7 +25,7 @@ Status: implemented
## 后果
-- 每个 MVP 功能都映射到一个监听器([功能→机制映射](../../../../docs/cookbook/extension-cookbook.md#the-feature--mechanism-map)是证明义务,保持更新)。
+- 每个 MVP 功能都映射到一个监听器([功能→机制映射](../../../../docs/cookbook/extension-cookbook.zh.md#the-feature--mechanism-map)是证明义务,保持更新)。
- HMR 与 dispose 无需额外工作:监听器和注册均为 Cordis effect。
- waterfall 语义(调用 `next()` 或短路)不直观,需要教学——在 AGENTS.md 中记录,并由组合测试覆盖。
- 循环必须具备防御性:插件异常在轮次级别被隔离,来自任何扩展点的 steering(中途引导)永远不会被搁置(有回归测试保障)。
diff --git a/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml
index 2e6f1ad2df..821a220d9a 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md
2026-06-11-runtime-arg-validation.md: 520823c76f147205805d306e97b5461f38b84c35
-2026-06-11-runtime-arg-validation.zh.md: 0a6ef40c93ec3550d6e991836950be849e86ede7
+2026-06-11-runtime-arg-validation.zh.md: bc2f743300be78cc2a01f027d5aca81fcb18b468
diff --git a/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md
index 0a6ef40c93..bc2f743300 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md
@@ -6,7 +6,7 @@ Status: implemented
## 问题
-`defineTool`([统一 schema DSL](2026-07-20-unified-json-value-schema-dsl.md))为工具作者的 `execute(args)` 提供了经 `InferArgs` 映射的类型化参数。但该类型只是对运行时值的编译期声明,而这个值实际上是模型生成的 JSON:没有任何机制强制模型遵守 schema,因此畸形调用(缺少必需键、声明为数字的位置传入字符串,或字面量超出声明的集合)会以「仅在名义上类型化」的状态到达 `execute`。工具函数体随后要么在处理结构错误的数据时崩溃,要么在不报错的情况下行为异常。
+`defineTool`([统一 schema DSL](2026-07-20-unified-json-value-schema-dsl.zh.md))为工具作者的 `execute(args)` 提供了经 `InferArgs` 映射的类型化参数。但该类型只是对运行时值的编译期声明,而这个值实际上是模型生成的 JSON:没有任何机制强制模型遵守 schema,因此畸形调用(缺少必需键、声明为数字的位置传入字符串,或字面量超出声明的集合)会以「仅在名义上类型化」的状态到达 `execute`。工具函数体随后要么在处理结构错误的数据时崩溃,要么在不报错的情况下行为异常。
## 决策
@@ -17,8 +17,8 @@ Status: implemented
## 后果
- 模型会收到有关自身畸形调用的可操作反馈,而不是遭遇不透明的崩溃,弥合了 `InferArgs` 的承诺与运行时现实之间的鸿沟。
-- 校验器与 `InferArgs` 必须保持一致;一项[属性测试](../testing/2026-06-11-property-based-testing.md)生成满足 spec 的参数并断言它们通过 `validateArgs`(同时通过针对性改坏参数来断言其会被拒绝),通过自动化检查消除这种漂移风险。
-- `ToolArgsError` 是[结构化错误分类体系](2026-06-11-structured-error-taxonomy.md)中 `HarnessError` 的子类,保留其 `code` 字段;读取 `.message` 的调用方不受该层级结构影响。
+- 校验器与 `InferArgs` 必须保持一致;一项[属性测试](../testing/2026-06-11-property-based-testing.zh.md)生成满足 spec 的参数并断言它们通过 `validateArgs`(同时通过针对性改坏参数来断言其会被拒绝),通过自动化检查消除这种漂移风险。
+- `ToolArgsError` 是[结构化错误分类体系](2026-06-11-structured-error-taxonomy.zh.md)中 `HarnessError` 的子类,保留其 `code` 字段;读取 `.message` 的调用方不受该层级结构影响。
- 校验开销相对于一次模型调用可忽略不计。
diff --git a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml
index 106164a71c..b7a0ea7192 100644
--- a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-13-capability-seams.md
2026-06-13-capability-seams.md: 2a166278ea454895177fa12b58f5493276f19cd1
-2026-06-13-capability-seams.zh.md: 0874af5826960ab9e718eb07b00c12b446edfd78
+2026-06-13-capability-seams.zh.md: 28b45cbbc7f65a0b783db3d91a2e559132b5779f
diff --git a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md
index 0874af5826..28b45cbbc7 100644
--- a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md
@@ -26,7 +26,7 @@ Service Provider 与 Consumer 由此独立演进:沙箱化执行器替换 `dsh
## 术语:seam 指三者组合,而非接口
-一个 **seam** 是完整的能力——三个角色合在一起:**Service Definition**(拥有 `ctx.` 和词汇的 Cordis `Service`)、一个或多个 **Service Provider**,以及一个或多个 **Consumer**。`packages/shell` 是规范范例——`dsh-shell` / `dsh-bash-local`+`dsh-bash-sandbox` / `dsh-tool-bash`。一个包可以承担多个角色,但单个角色本身不是 seam。「seam」一词严格保留给这种完整能力;命名其中一个组成部分时,应使用其角色、类、服务、约定或扩展点。[术语表](../../../../docs/glossary.md#capability-seam)是规范条目。
+一个 **seam** 是完整的能力——三个角色合在一起:**Service Definition**(拥有 `ctx.` 和词汇的 Cordis `Service`)、一个或多个 **Service Provider**,以及一个或多个 **Consumer**。`packages/shell` 是规范范例——`dsh-shell` / `dsh-bash-local`+`dsh-bash-sandbox` / `dsh-tool-bash`。一个包可以承担多个角色,但单个角色本身不是 seam。「seam」一词严格保留给这种完整能力;命名其中一个组成部分时,应使用其角色、类、服务、约定或扩展点。[术语表](../../../../docs/glossary.zh.md#capability-seam)是规范条目。
## 曾考虑的替代方案
@@ -35,4 +35,4 @@ Service Provider 与 Consumer 由此独立演进:沙箱化执行器替换 `dsh
## 后果
-分离角色会增加包和样板代码(`package.json`、`tsconfig`、README 和注入接线)。换来的是:Service Provider 与 Consumer 独立发布和版本管理,新后端永远不会波及面向模型的约定。[AGENTS.md](../../../../AGENTS.md) 和 [architecture.md](../../../../docs/architecture.md) 载有这项规则;bash 三件套是参考模板。本 Agent Note 记录为什么独立变化的角色通常需要拆分,而确实共享的关注点可以保持合并。
+分离角色会增加包和样板代码(`package.json`、`tsconfig`、README 和注入接线)。换来的是:Service Provider 与 Consumer 独立发布和版本管理,新后端永远不会波及面向模型的约定。[AGENTS.md](../../../../AGENTS.md) 和 [architecture.md](../../../../docs/architecture.zh.md) 载有这项规则;bash 三件套是参考模板。本 Agent Note 记录为什么独立变化的角色通常需要拆分,而确实共享的关注点可以保持合并。
diff --git a/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml
index d44279eb42..3af4789414 100644
--- a/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md
2026-06-13-twin-llm-adapters.md: a4c87325a0b0d1ebe6cf8f95672e5de74ef37d57
-2026-06-13-twin-llm-adapters.zh.md: 2af7e7cca4d4c40e18cece1a4dd5c9cdb11901c8
+2026-06-13-twin-llm-adapters.zh.md: 36996750a16cc95393d727cffee3bcc53573eaf2
diff --git a/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md
index 2af7e7cca4..36996750a1 100644
--- a/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md
@@ -6,7 +6,7 @@ Status: implemented
## 问题
-`dsh-llm` 拥有一套提供方无关的流式词汇:`StreamChunk` 协议(`block-start`、`text-delta`、`reasoning-delta`、`tool-call-delta`、`block-end`、`usage`、`finish`)以及内容块类型([内容块词汇](2026-06-11-content-block-vocabulary.md))。如果词汇仅针对单个适配器定义,就有可能将该适配器的特异行为固化到「中立」约定中:唯一实现碰巧做了什么,什么就成为事实上的规范;在第二个提供方到来之前,抽象层未经验证——而届时修复这种泄漏的代价已经很高。
+`dsh-llm` 拥有一套提供方无关的流式词汇:`StreamChunk` 协议(`block-start`、`text-delta`、`reasoning-delta`、`tool-call-delta`、`block-end`、`usage`、`finish`)以及内容块类型([内容块词汇](2026-06-11-content-block-vocabulary.zh.md))。如果词汇仅针对单个适配器定义,就有可能将该适配器的特异行为固化到「中立」约定中:唯一实现碰巧做了什么,什么就成为事实上的规范;在第二个提供方到来之前,抽象层未经验证——而届时修复这种泄漏的代价已经很高。
## 决策
diff --git a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml
index 61f9754431..8f9aa62e49 100644
--- a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-14-session-persistence.md
2026-06-14-session-persistence.md: 62228bd2f5b25b13880a563818d08f3a2d52d956
-2026-06-14-session-persistence.zh.md: b10ceebd95d4d05ae3f7ea620183ad8fffd074ee
+2026-06-14-session-persistence.zh.md: ebf004333c383336cd025aa8a4aabc9d1e07f0e5
diff --git a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md
index b10ceebd95..ebf004333c 100644
--- a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md
@@ -8,28 +8,28 @@ Status: implemented
会话此前仅存在于内存中。示例插件 `session-jsonl.ts`(在两个示例中逐字节重复)是只写的遥测:它缓冲 `session/event` 并追加 JSON 行,没有读取/回放路径,没有崩溃安全性(无 fsync、无原子写入、dispose(资源释放)时采用 fire-and-forget 方式排空),没有列表功能,也没有格式版本控制。没有任何机制能将磁盘上的历史会话重新注入到活跃的 agent(智能体)中,因此持久恢复、持久 fork 以及宿主侧的会话浏览都无法实现。
-[事件溯源模型](2026-06-11-event-sourced-sessions.md)将仅追加日志作为唯一真源,并从中派生 LLM(大语言模型)历史。持久化必须忠实于这一设计:直接持久化现有的 `SessionEvent`,不引入需要来回转换的并行「持久化消息」类型。后端也必须可替换——当前用文件存储,以后用数据库存储——并由同一接口封装。
+[事件溯源模型](2026-06-11-event-sourced-sessions.zh.md)将仅追加日志作为唯一真源,并从中派生 LLM(大语言模型)历史。持久化必须忠实于这一设计:直接持久化现有的 `SessionEvent`,不引入需要来回转换的并行「持久化消息」类型。后端也必须可替换——当前用文件存储,以后用数据库存储——并由同一接口封装。
## 决策
-持久化是一个具有抽象 Service Definition 的**能力 seam**([能力 seam](2026-06-13-capability-seams.md),`dsh-shell` 模板),而非循环或核心逻辑:
+持久化是一个具有抽象 Service Definition 的**能力 seam**([能力 seam](2026-06-13-capability-seams.zh.md),`dsh-shell` 模板),而非循环或核心逻辑:
1. **接口**(`dsh-session-persistence`,`ctx.sessionPersistence`):一个抽象的 `SessionPersistence` 服务,提供 `locate`/`create`/`append`/`prepare`/`load`/`inspect`/`readFrom`/`list`/`listSnapshots`。其持久化单元就是现有的 `SessionEvent`(`{ type, seq, time, data }`),原样复用,无转换类型。
-2. **实现**(`dsh-session-persistence-jsonl`):每个会话一个仅追加的逻辑 JSONL 日志:先是一行 `SessionHeader`,随后是无损表示连续 `SessionEvent` 流的存储记录。符合条件的 `assistant/chunk` 增量连续段默认使用打包行;[带校验和的 Zstandard 帧](2026-07-19-zstandard-jsonl-session-logs.md)是默认物理编码,也可通过配置使用原始行。
+2. **实现**(`dsh-session-persistence-jsonl`):每个会话一个仅追加的逻辑 JSONL 日志:先是一行 `SessionHeader`,随后是无损表示连续 `SessionEvent` 流的存储记录。符合条件的 `assistant/chunk` 增量连续段默认使用打包行;[带校验和的 Zstandard 帧](2026-07-19-zstandard-jsonl-session-logs.zh.md)是默认物理编码,也可通过配置使用原始行。
长期有效、存在争议的关键选择:
- **规范的持久日志无损保留每个 `SessionEvent`,包括 `assistant/chunk`。** JSONL 存储可以将一段连续的增量事件编码为一条打包行,但逻辑读取方会重建精确的事件边界、序号与时间戳。`deriveMessages()` 跳过分片,而过滤分片的方案(Codex 的 `policy.rs`)很有吸引力,但 `seq = log.length` 以及 `events[i].seq === i` 验证要求*连续*的逻辑日志;过滤掉分片会留下空洞,同时破坏约定和恢复功能。基于分片过滤的投影可以作为派生视图在后续实现(带有自己的重新编号),但它不是规范日志。
-- **仅追加;崩溃的轮次被关闭,而非截断。** 已刷写的事件永不被重写。[语义检查点策略](../bug-fix/2026-07-21-semantic-session-checkpoints.md)会在调用模型前排空请求、在调用工具前排空已记录的顶层调用,并在步骤结束后排空完整的响应/结果批次;循环则排空最终轮次边界。由于一个被中断的轮次可能包含大量有效工作,冷检查会保留其连续、可解析的事件,并在内存逻辑视图中为未应答的 assistant 调用添加按风险分类的错误结果、补一个缺失的 `step/end`,以及带 `{ kind: 'interrupted' }` 的 `turn/end`。`prepare` 或 `load` 在返回可恢复视图前提交这些收尾事件;合成结果保证恢复后的提供方 transcript(文本记录)仍然有效。只有不完整的最后一条记录会在提交修复时被丢弃;在最后一个真实 `turn/end` 处或之前出现解析错误或序号间隙,属于数据损坏,会使该会话不可加载。
+- **仅追加;崩溃的轮次被关闭,而非截断。** 已刷写的事件永不被重写。[语义检查点策略](../bug-fix/2026-07-21-semantic-session-checkpoints.zh.md)会在调用模型前排空请求、在调用工具前排空已记录的顶层调用,并在步骤结束后排空完整的响应/结果批次;循环则排空最终轮次边界。由于一个被中断的轮次可能包含大量有效工作,冷检查会保留其连续、可解析的事件,并在内存逻辑视图中为未应答的 assistant 调用添加按风险分类的错误结果、补一个缺失的 `step/end`,以及带 `{ kind: 'interrupted' }` 的 `turn/end`。`prepare` 或 `load` 在返回可恢复视图前提交这些收尾事件;合成结果保证恢复后的提供方 transcript(文本记录)仍然有效。只有不完整的最后一条记录会在提交修复时被丢弃;在最后一个真实 `turn/end` 处或之前出现解析错误或序号间隙,属于数据损坏,会使该会话不可加载。
- **文件后端为规范实现,数据库后端为经过验证的直接替换。** `SessionEvent` 1:1 映射到一行 `(session_id, seq, type, time, data)`:`append` 是 INSERT(在一个断言连续 seq 约定的事务中),读取使用 SELECT … ORDER BY seq。`dsh-session-persistence-sqlite` 正是如此:一个 `SessionPersistence` 子类,接口无变化(opencode 在 SQLite/WAL 上采用的正是这种接口形态),且通过与 JSONL 后端相同的 `runPersistenceContract` 测试套件。该约定以相同的语义约束两个后端(惰性物化、逻辑关闭中断轮次、修复只提交一次、连续 seq),一次表达在文件字节上,一次表达在数据库行上。其数据库拥有专用的 application id 与单调递增的 schema 版本。系统会在一个事务中为全新文件创建所有表并写入这两个 header 值;未版本化文件若带有任何用户定义的 schema 对象或应用标识、当前版本文件若带有外部应用标识,以及任何非当前版本文件,都会在修改日志模式之前被拒绝。
-- **元数据在日志之外。** 格式版本、cwd 和谱系是存储关注点,不是可回放的对话状态,因此它们存放在 `dsh-session` 拥有的 `SessionHeader` 中,并通过新的只读属性 `session.header` 附加到 `Session` 上——永远不进入 `SessionEventMap`,永远不到达 `deriveMessages()`。`createdAt` 是以 Unix epoch 毫秒表示的非负安全整数:运行时创建和持久化注册会拒绝小数值,JSONL 会验证解码后的 header,SQLite 则将其存入严格的 `INTEGER` 列。替代方案(一个可合并扩展的 `session/meta` 事件作为日志第 0 行)被否决:日志内事件会自然随 seed/fork 的会话携带,但元数据不是可回放状态,因此显式的日志外 header 边界是更清晰的取舍。(header 最初被拆分为不可变的 `SessionHeader` 加可变的 `SessionSummary`,二者的联合类型为 `SessionMeta`;可变 summary 后来因属于死状态而被移除——见 [移除可变会话摘要](../simplification/2026-06-19-drop-mutable-session-summary.md)。)
-- **`ctx.agents.create()` 和 `ctx.agents.resume()` 是异步工厂;恢复还跨越持久化边界。** `ctx.agents.resume({ resumeSessionId })` 通过 `ctx.sessionPersistence.prepare()` 取得精确的未发布 Session,以持久化 id 发布它,并继续其投影。[Session 准备阶段决策](2026-08-05-session-preparation.md)定义历史检查与恢复之间的复用。agent loop(智能体循环)不会硬注入 `sessionPersistence`(那样会让非持久化的演示永远挂起);当它不存在时,`resume` 会以明确的错误拒绝。
+- **元数据在日志之外。** 格式版本、cwd 和谱系是存储关注点,不是可回放的对话状态,因此它们存放在 `dsh-session` 拥有的 `SessionHeader` 中,并通过新的只读属性 `session.header` 附加到 `Session` 上——永远不进入 `SessionEventMap`,永远不到达 `deriveMessages()`。`createdAt` 是以 Unix epoch 毫秒表示的非负安全整数:运行时创建和持久化注册会拒绝小数值,JSONL 会验证解码后的 header,SQLite 则将其存入严格的 `INTEGER` 列。替代方案(一个可合并扩展的 `session/meta` 事件作为日志第 0 行)被否决:日志内事件会自然随 seed/fork 的会话携带,但元数据不是可回放状态,因此显式的日志外 header 边界是更清晰的取舍。(header 最初被拆分为不可变的 `SessionHeader` 加可变的 `SessionSummary`,二者的联合类型为 `SessionMeta`;可变 summary 后来因属于死状态而被移除——见 [移除可变会话摘要](../simplification/2026-06-19-drop-mutable-session-summary.zh.md)。)
+- **`ctx.agents.create()` 和 `ctx.agents.resume()` 是异步工厂;恢复还跨越持久化边界。** `ctx.agents.resume({ resumeSessionId })` 通过 `ctx.sessionPersistence.prepare()` 取得精确的未发布 Session,以持久化 id 发布它,并继续其投影。[Session 准备阶段决策](2026-08-05-session-preparation.zh.md)定义历史检查与恢复之间的复用。agent loop(智能体循环)不会硬注入 `sessionPersistence`(那样会让非持久化的演示永远挂起);当它不存在时,`resume` 会以明确的错误拒绝。
## 曾考虑的替代方案
上述每个关键选择都在陈述处记录了被否决的替代方案:**过滤分片的规范日志**(Codex 的 `policy.rs` 形式)破坏连续 seq 约定;**截断崩溃的轮次**会静默销毁长时间自主运行中的真实工作;**日志内 `session/meta` 事件作为第 0 行**——元数据不是可回放状态;**有限的非整数 `createdAt` 值**没有生产方,且与整数 Unix 毫秒存储及查询列不一致;**接受非全新的未版本化 SQLite 文件**可能覆盖无关对象或应用标识;**将 `sessionPersistence` 硬注入循环**会让非持久化的演示永远挂起。
-格式版本控制:header 携带一个 `version`;冷读取拒绝任何非当前版本。预发布阶段的会话格式仍固定为 `SESSION_FORMAT_VERSION = 0`,不承诺广泛兼容;当持久化用户数据确有需要时,协调器可以负责显式且范围受限的导入升级([消息标识机制引入前的消息恢复](../bug-fix/2026-07-28-load-pre-identity-session-messages.md))。仅追加 + 刷写对尾部的不完整写入具有健壮性(冷准备时可容忍),但无法抵御未使用 fsync 时在行写入中途断电;数据库/WAL 后端是该场景下更强的选项。
+格式版本控制:header 携带一个 `version`;冷读取拒绝任何非当前版本。预发布阶段的会话格式仍固定为 `SESSION_FORMAT_VERSION = 0`,不承诺广泛兼容;当持久化用户数据确有需要时,协调器可以负责显式且范围受限的导入升级([消息标识机制引入前的消息恢复](../bug-fix/2026-07-28-load-pre-identity-session-messages.zh.md))。仅追加 + 刷写对尾部的不完整写入具有健壮性(冷准备时可容忍),但无法抵御未使用 fsync 时在行写入中途断电;数据库/WAL 后端是该场景下更强的选项。
## 后果
diff --git a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml
index 9898684b9e..c3984a9fa9 100644
--- a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md
2026-06-17-filesystem-capability-seam.md: 6265aebf5e7ffdd4ec4dc0083aa55adb34ee78a1
-2026-06-17-filesystem-capability-seam.zh.md: 024421ed59fa1e6c29bcc12ab51f69ac19a0c231
+2026-06-17-filesystem-capability-seam.zh.md: f0f1cb132fe06ebf669f0310f0066f4fdd36ad0b
diff --git a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md
index 024421ed59..f0f1cb132f 100644
--- a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md
@@ -20,7 +20,7 @@ harness 已有一个具体的 `bash` 能力 seam(`dsh-shell` / `dsh-bash-local
## 决策
-文件系统访问是一个一等的能力 seam,遵循[能力 seam Agent Note](2026-06-13-capability-seams.md):
+文件系统访问是一个一等的能力 seam,遵循[能力 seam Agent Note](2026-06-13-capability-seams.zh.md):
1. `@deepseek-ai/dsh-fs`(`packages/fs/fs`)拥有抽象的 `ctx.fs` 服务、文件系统词汇类型,以及 `fs/*` 策略事件词汇。
2. `@deepseek-ai/dsh-fs-local`(`packages/fs/fs-local`)提供第一个实现,以本地文件系统为后端。
@@ -28,7 +28,7 @@ harness 已有一个具体的 `bash` 能力 seam(`dsh-shell` / `dsh-bash-local
Consumer 包仅依赖 Service Definition 包,从不依赖 `dsh-fs-local`。需要不同后端的部署只需为 `ctx.fs` 加载不同的提供方,无需改动工具 schema 或面向模型的提示词引导。
-读后写/编辑与观测状态策略是第四个包 `@deepseek-ai/dsh-fs-observation-policy`(`packages/fs/fs-observation-policy`),通过 `fs/*` 事件门控贡献,而非挂在 `ctx.fs` 上;加载 `dsh-tool-fs` 的部署同时加载 `dsh-fs-observation-policy` 以获得读后写/编辑能力。本决策确立了由三个包构成的边界;策略从提供方基类拆出的决策由 [拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) 做出,其以事件门控插件(而非方法服务)实现的方式由 [事件门控 Agent Note](2026-06-26-file-context-as-event-gate.md) 做出。
+读后写/编辑与观测状态策略是第四个包 `@deepseek-ai/dsh-fs-observation-policy`(`packages/fs/fs-observation-policy`),通过 `fs/*` 事件门控贡献,而非挂在 `ctx.fs` 上;加载 `dsh-tool-fs` 的部署同时加载 `dsh-fs-observation-policy` 以获得读后写/编辑能力。本决策确立了由三个包构成的边界;策略从提供方基类拆出的决策由 [拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.zh.md) 做出,其以事件门控插件(而非方法服务)实现的方式由 [事件门控 Agent Note](2026-06-26-file-context-as-event-gate.zh.md) 做出。
第一个后端有意仅限本地:`dsh-fs-local` 基于宿主文件系统实现 `ctx.fs`。未来的兄弟后端可在同一接口之后提供沙箱、远程、虚拟或项目作用域的文件系统。
@@ -36,7 +36,7 @@ Consumer 包仅依赖 Service Definition 包,从不依赖 `dsh-fs-local`。需
文件系统权限和沙箱并非此拆分所隐含。本地后端从其配置的基目录解析相对路径,但路径包含约束策略是独立的决策:要么由更严格的 `ctx.fs` 实现强制执行,要么由权限/沙箱插件包装 `tools/execute` 并在调用到达消费方之前否决。
-读后写/编辑与观测状态属于 `dsh-fs-observation-policy`,而非 `ctx.fs`。通过 `fs/*` 事件门控,策略按不透明 actor 记录版本,并提供可选的变更期望;提供方原子性地强制新鲜度。`dsh-tool-fs` 发出事件但不依赖策略。见[拆分文件系统 seam](../simplification/2026-06-26-fsspec-style-fs-seam.md)和[事件门控插件](2026-06-26-file-context-as-event-gate.md) Agent Note。
+读后写/编辑与观测状态属于 `dsh-fs-observation-policy`,而非 `ctx.fs`。通过 `fs/*` 事件门控,策略按不透明 actor 记录版本,并提供可选的变更期望;提供方原子性地强制新鲜度。`dsh-tool-fs` 发出事件但不依赖策略。见[拆分文件系统 seam](../simplification/2026-06-26-fsspec-style-fs-seam.zh.md)和[事件门控插件](2026-06-26-file-context-as-event-gate.zh.md) Agent Note。
## 包拓扑
@@ -84,7 +84,7 @@ Consumer 包仅依赖 Service Definition 包,从不依赖 `dsh-fs-local`。需
- 不透明的 `targetKey`,用于陈旧守护和文件状态查找。本地后端可能使用类似 realpath 的键;远程后端可能使用工作区 URI 或文件 id。消费方禁止解析或假设它是本地绝对路径。
- `displayPath`,用于面向模型/UI 的输出。根据后端不同,它可能是本地绝对路径、工作区相对路径或远程 URI。
-即使另一项能力共享提供方的执行环境,`targetKey` 仍保持不透明。这类消费方通过提供方的 `processPath(target)`、`fileUrl(target)` 或 `contains(parent, child)` 获取所需事实;[可移植执行环境决策](2026-07-28-portable-execution-world-consumers.md)说明这些事实为何属于文件系统 seam。
+即使另一项能力共享提供方的执行环境,`targetKey` 仍保持不透明。这类消费方通过提供方的 `processPath(target)`、`fileUrl(target)` 或 `contains(parent, child)` 获取所需事实;[可移植执行环境决策](2026-07-28-portable-execution-world-consumers.zh.md)说明这些事实为何属于文件系统 seam。
读取和变更结果必须包含不透明的文件 `version`。本地后端从 bigint stat 元数据(`dev`、`ino`、`size`、`mtimeNs` 和 `ctimeNs`)派生令牌,因此同大小重写和 inode 替换都会可靠地使消费方失效;远程后端可以使用 revision id 或类似 hash 的令牌。`dsh-fs-observation-policy` 插件记录版本用于陈旧检查;消费方可以展示相关元数据但禁止解释版本令牌。
@@ -140,7 +140,9 @@ Consumer 包仅依赖 Service Definition 包,从不依赖 `dsh-fs-local`。需
- **面向模型的工具直接基于 `node:fs`**:工具包将同时承担执行策略、路径解析、原子写入、文本解码和编辑语义,耦合问题部分所列的三个独立变化的关注点,且任何后端替换都会搅动 schema。
- **单一合并包 `dsh-fs-tools`**:seam 之前的形态;以与 bash 相同的 Service Definition / Service Provider / Consumer 拆分理由否决,且合并名称从未成为公开 API。
-- **观测状态放在 `ctx.fs` 上**:本 Agent Note 最初落地的形态;被 [拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) 和 [事件门控 Agent Note](2026-06-26-file-context-as-event-gate.md) 取代:沙箱/远程后端不应继承面向模型的观测策略,因此提供方只保留版本令牌和可选的版本守护变更。
+- **观测状态放在 `ctx.fs` 上**:本 Agent Note 最初落地的形态;被 [拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.zh.md) 和 [事件门控 Agent Note](2026-06-26-file-context-as-event-gate.zh.md) 取代:沙箱/远程后端不应继承面向模型的观测策略,因此提供方只保留版本令牌和可选的版本守护变更。
+
+
## 后果
diff --git a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.i18n.yaml
index 0ecda2484f..03a0440ca1 100644
--- a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.md
2026-06-18-agent-lifecycle-and-ownership-contracts.md: 9bc558bfce75892b0ebdb80e9f8735d440cabaf4
-2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md: 65e0018c3f2ba56043898050c8e0b49929f8e7e7
+2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md: bbd0a7219f902e5ead4d190c58e9a5d4da2131fd
diff --git a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md
index 65e0018c3f..bbd0a7219f 100644
--- a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md
@@ -14,7 +14,7 @@ ACP(Agent Client Protocol)与 tool-bash 的若干限制是同一个所有权
### 1. 队列感知的 `Agent.cancel(cause?)`
-`Agent` 接口新增 `cancel()` 动词——唯一的公开停止原语。(它最初与范围更窄、仅作用于步骤的 `abort()` 一同交付;后者后来因无人使用而移除,使 `cancel()` 成为唯一公开的停止工作方式。)它清空 inbox 的 queued + steering FIFO,在存在活跃轮次时中止它,并保留一个不带 cause 的 pre-run 标记,使在被领取前被取消的提示词永不运行,而后来的提示词仍保持独立。有效调用会在清空或中止前发出 `agent/cancel-requested`,携带类型化的 `user | parent` cause;空闲取消不发出任何事件,也不会使下一条提示词搁浅。`whenIdle()` 会在取消后达到完全停稳,ACP 的 `session/cancel` 映射到 `user`。[显式轮次取消决策](2026-07-16-explicit-turn-cancellation.md)规定了当前的 cause、signal 生命周期与协作式结算约定。
+`Agent` 接口新增 `cancel()` 动词——唯一的公开停止原语。(它最初与范围更窄、仅作用于步骤的 `abort()` 一同交付;后者后来因无人使用而移除,使 `cancel()` 成为唯一公开的停止工作方式。)它清空 inbox 的 queued + steering FIFO,在存在活跃轮次时中止它,并保留一个不带 cause 的 pre-run 标记,使在被领取前被取消的提示词永不运行,而后来的提示词仍保持独立。有效调用会在清空或中止前发出 `agent/cancel-requested`,携带类型化的 `user | parent` cause;空闲取消不发出任何事件,也不会使下一条提示词搁浅。`whenIdle()` 会在取消后达到完全停稳,ACP 的 `session/cancel` 映射到 `user`。[显式轮次取消决策](2026-07-16-explicit-turn-cancellation.zh.md)规定了当前的 cause、signal 生命周期与协作式结算约定。
### 2. `AgentHandle` 异步释放器
@@ -43,7 +43,7 @@ bash 所有者 token 比较依赖共享的 `Agent.id`/`SessionId` 在存活 agen
- **公开的 `BashTask.owner` 字段**而非 `ShellExecutor.ownerOf(id)` Service Definition 方法:否决。一条读取路径即可,无需冗余 API。
- **为 agent 的会话生命周期使用兄弟 Cordis effect**:否决。fiber 卸载时并发释放兄弟 effect(`Promise.all`),store 拥有的 append 发布钩子的移除与循环的关闭 `session/flush` 产生竞争;单一复合 effect 的有序 LIFO 链才能在两条释放路径上都捕获关闭的 `turn/end`。
-- **在 `cancel()` 之外另设一个仅中止步骤的 `abort()`**:最初发布过,后因无人使用而移除;`cancel()` 是唯一的公开停止原语(见[公开停止接口 Agent Note](../simplification/2026-06-20-public-agent-stop-api.md))。
+- **在 `cancel()` 之外另设一个仅中止步骤的 `abort()`**:最初发布过,后因无人使用而移除;`cancel()` 是唯一的公开停止原语(见[公开停止接口 Agent Note](../simplification/2026-06-20-public-agent-stop-api.zh.md))。
## 后果
diff --git a/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.i18n.yaml
index 85a8dd684b..8097b8cbb0 100644
--- a/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md
2026-06-18-shared-persistence-write-coordinator.md: 286bbb7d5cd3720109db0d0abc0bb72ddbfcbdcd
-2026-06-18-shared-persistence-write-coordinator.zh.md: d24398c4bc9445739b4cdeb9f2ed176577060932
+2026-06-18-shared-persistence-write-coordinator.zh.md: 70db616b0a71826c648072228fff936ad423ad8f
diff --git a/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.zh.md b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.zh.md
index d24398c4bc..70db616b0a 100644
--- a/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.zh.md
@@ -14,9 +14,9 @@ Status: implemented
组合,而非继承。协调器是后端持有的具体类,不是后端继承的基类。协调器让非常规后端与继承层级作斗争的风险由此规避:后端只暴露钩子,无法触及协调器的私有编排状态。第三方后端仍然可以完全不使用协调器、直接实现抽象服务,包括不可变逻辑检查,以及通过 `load` 实现的默认准备回退。
-协调器为每个存活的 `Session` 实例持有一个生命周期条目:初始化,加上一个包私有写入控制器,后者负责待处理事件、固定批处理截止时间、活跃写入、失败保留和共享 flush 屏障。每个 `session/event` 都进入这条有界写入路径,`session/flush` 则绕过等待以观察完全停稳。控制器归并由 [flush 控制器简化](../simplification/2026-07-23-collapse-persistence-flush-state.md)定义;调度节奏由[有界批处理决策](2026-08-08-bounded-session-persistence-write-batching.md)定义。
+协调器为每个存活的 `Session` 实例持有一个生命周期条目:初始化,加上一个包私有写入控制器,后者负责待处理事件、固定批处理截止时间、活跃写入、失败保留和共享 flush 屏障。每个 `session/event` 都进入这条有界写入路径,`session/flush` 则绕过等待以观察完全停稳。控制器归并由 [flush 控制器简化](../simplification/2026-07-23-collapse-persistence-flush-state.zh.md)定义;调度节奏由[有界批处理决策](2026-08-08-bounded-session-persistence-write-batching.zh.md)定义。
-创建流程将 `Session.events` 的原始快照借作持久化种子。`Session` 已经分离、验证并深度冻结每个事件,后续追加会替换缓存视图,因此该快照数组保持稳定。协调器及其后端钩子只读取这个有类型的进程内值;再次克隆完整日志会重复 [agent scope 运行时决策](2026-07-12-agent-scope-runtime-design.md#session-append-materialize-validate-commit-notify)规定的所有权工作。持久化服务的公开 `append()` 仍在 API 边界为调用方拥有的输入创建快照。
+创建流程将 `Session.events` 的原始快照借作持久化种子。`Session` 已经分离、验证并深度冻结每个事件,后续追加会替换缓存视图,因此该快照数组保持稳定。协调器及其后端钩子只读取这个有类型的进程内值;再次克隆完整日志会重复 [agent scope 运行时决策](2026-07-12-agent-scope-runtime-design.zh.md#session-append-materialize-validate-commit-notify)规定的所有权工作。持久化服务的公开 `append()` 仍在 API 边界为调用方拥有的输入创建快照。
已准备 Session 的后缀,以及进入 write-behind 队列的事件,仍保留现有复制。这些路径会逐个后缀或事件建立异步队列所有权,且没有已测得的完整日志克隆成本;移除这些复制属于单独的所有权审计,不属于创建种子的借用决策。
@@ -48,4 +48,4 @@ Status: implemented
## 后果
-协调器增加了一层间接、一个不透明的 torn marker、脱离会话生命周期的退役任务,以及有界的已准备 Session 状态,但将此前每个后端重复的、对正确性要求很高的编排逻辑集中到一处。会话 dispose 仍是仅观察事件,因此会话所有者不会等待持久化退役;协调器会收容失败、在存活控制器中保留待处理事件,并以后端 teardown 为完全停稳边界。其钩子面保持窄小:标识校验、接管、碰撞检查、准备与不可变检查共用 `loadStored`;物化保持在 `appendBatch` 内原子完成;列举绕过协调器。读模型使用 `inspect` 而非 `load`,因此观察已持久化但仍开放的轮次时不会提交中断收尾事件;复用、预留与发布由 [Session 准备阶段决策](2026-08-05-session-preparation.md)定义。新后端只需实现存储原语,而无需复制有界写入生命周期。
+协调器增加了一层间接、一个不透明的 torn marker、脱离会话生命周期的退役任务,以及有界的已准备 Session 状态,但将此前每个后端重复的、对正确性要求很高的编排逻辑集中到一处。会话 dispose 仍是仅观察事件,因此会话所有者不会等待持久化退役;协调器会收容失败、在存活控制器中保留待处理事件,并以后端 teardown 为完全停稳边界。其钩子面保持窄小:标识校验、接管、碰撞检查、准备与不可变检查共用 `loadStored`;物化保持在 `appendBatch` 内原子完成;列举绕过协调器。读模型使用 `inspect` 而非 `load`,因此观察已持久化但仍开放的轮次时不会提交中断收尾事件;复用、预留与发布由 [Session 准备阶段决策](2026-08-05-session-preparation.zh.md)定义。新后端只需实现存储原语,而无需复制有界写入生命周期。
diff --git a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml
index b7e2678928..ed11425f4b 100644
--- a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-branded-ids.md
2026-06-20-branded-ids.md: 29b258b21240c92e74051339f0939a8e70933099
-2026-06-20-branded-ids.zh.md: 288125764d4b279c79b7469080bb0c9f2efdc709
+2026-06-20-branded-ids.zh.md: 2f960bac3172f1e83161f1af8f4e9d2cc0cda7bd
diff --git a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md
index 288125764d..2f960bac31 100644
--- a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md
@@ -6,11 +6,11 @@ Status: implemented
## 问题
-harness 使用 `Branded = string & { readonly [BRAND]: B }` 机制,为 `CallId`(`packages/llm/llm/src/brand.ts`)和 agent(智能体)/会话共享的 `SessionId`(`packages/core/session/src/types.ts`)做 brand 处理;该机制由纯类型包 `@deepseek-ai/dsh-brand` 拥有,位于 `packages/util/brand/`,见其 [README](../../../../packages/util/brand/README.md),并为每个类型提供零开销的 cast 工厂。`dsh-brand` 还声明了治理策略:*「Branding 用于跨包边界且可能被混淆的 id;不是每个 string 都需要 brand。」* 这条策略是正确的;问题在于它只落实了一半。两处缺口使得结构相同但语义错误的 string 今天仍能通过类型检查器。
+harness 使用 `Branded = string & { readonly [BRAND]: B }` 机制,为 `CallId`(`packages/llm/llm/src/brand.ts`)和 agent(智能体)/会话共享的 `SessionId`(`packages/core/session/src/types.ts`)做 brand 处理;该机制由纯类型包 `@deepseek-ai/dsh-brand` 拥有,位于 `packages/util/brand/`,见其 [README](../../../../packages/util/brand/README.zh.md),并为每个类型提供零开销的 cast 工厂。`dsh-brand` 还声明了治理策略:*「Branding 用于跨包边界且可能被混淆的 id;不是每个 string 都需要 brand。」* 这条策略是正确的;问题在于它只落实了一半。两处缺口使得结构相同但语义错误的 string 今天仍能通过类型检查器。
**缺口 1:bash seam 中未 brand 的跨边界 ID。** 后台 job id 是普通 `string`:`BashTask.id: string`(`packages/shell/shell/src/types.ts`),作为 `string` 贯穿整个执行器 seam(`packages/shell/shell/src/index.ts` 中的 `ShellExecutor.get`/`ownerOf`/`readOutput`/`kill(id: string)`),再由面向模型的工具以 `string` 校验并传递(`validateJobId`、`assertTaskAccess`、`packages/shell/tool-bash/src/index.ts` 中 `job_id` 的 schema 参数)。它由每执行器计数器生成——`packages/shell/bash-local/src/index.ts` 中的 `` `bash-${this.nextTaskId++}` ``——其形状与 `SessionId` 的默认值**完全相同,都是 `name-N`**(`packages/core/session/src/index.ts` 中的 `` `session-${++counter}` ``)。bash job id 和会话 id 在调用点轻易就能互换,而编译器毫无反应。它是面向模型的 id(模型会把 `job_id` 传回 `bash_output`/`bash_kill`),所以该混淆可由不受信任的输入触达。
-bash **owner token** 是相关的子情形:`ShellExecRequest.owner?: string` 和 `ShellExecSpec.owner: string | undefined`(`packages/shell/shell/src/types.ts`)被文档描述为刻意*不透明*的隔离键,但在所有实际调用方中,该值就是所属 agent 共享的 `Agent.id`/`SessionId`(`callerToken = (exec) => exec.agent?.id`,位于 `packages/shell/tool-bash/src/index.ts`),只是披着另一个 seam 本地名称。它被用于访问控制比较(`owner !== callerToken(exec)`),因此一个不匹配但类型正确的 string 在此处就是跨会话隔离 bug,而当前类型系统无法捕获。这正是[统一 agent/session 标识决策](../simplification/2026-06-20-unify-agent-and-session-id.md)覆盖的共享 id 别名。
+bash **owner token** 是相关的子情形:`ShellExecRequest.owner?: string` 和 `ShellExecSpec.owner: string | undefined`(`packages/shell/shell/src/types.ts`)被文档描述为刻意*不透明*的隔离键,但在所有实际调用方中,该值就是所属 agent 共享的 `Agent.id`/`SessionId`(`callerToken = (exec) => exec.agent?.id`,位于 `packages/shell/tool-bash/src/index.ts`),只是披着另一个 seam 本地名称。它被用于访问控制比较(`owner !== callerToken(exec)`),因此一个不匹配但类型正确的 string 在此处就是跨会话隔离 bug,而当前类型系统无法捕获。这正是[统一 agent/session 标识决策](../simplification/2026-06-20-unify-agent-and-session-id.zh.md)覆盖的共享 id 别名。
**缺口 2:*已经 brand* 的 ID 在边界处被侵蚀。** 就连 `CallId` 和 `SessionId` 也恰好在最容易混淆的地方退化为裸 `string`:注册表/store 键类型和公开方法参数。代表性位置包括会话存储、agent 注册表(二者都以共享的 `SessionId` 为键)、工具展示层的 call-id map、ACP(Agent Client Protocol)的会话记录,以及持久化协调器。在集合键处丢弃 brand,会让既有 brand 在查找时毫无价值;它们的价值只实现了一部分。
@@ -64,6 +64,6 @@ export function OwnerToken(id: string): OwnerToken {
## 后果
-- **两个接口面的机械性改动。** 传播 brand 涉及 bash seam(Service Definition + Service Provider + Consumer)以及 ACP 会话 id 接口和持久化协调器。改动面广但严重度低:遗漏的位置是编译错误而非静默 bug。从可观察行为看,这是一项纯类型变更——无快照或 e2e 行为差异。它与[统一 agent/会话标识决策](../simplification/2026-06-20-unify-agent-and-session-id.md)相邻,因为二者都触及会话 id / owner-token 边界;`OwnerToken` 出于上述解耦理由仍与统一后的 id 保持独立。
+- **两个接口面的机械性改动。** 传播 brand 涉及 bash seam(Service Definition + Service Provider + Consumer)以及 ACP 会话 id 接口和持久化协调器。改动面广但严重度低:遗漏的位置是编译错误而非静默 bug。从可观察行为看,这是一项纯类型变更——无快照或 e2e 行为差异。它与[统一 agent/会话标识决策](../simplification/2026-06-20-unify-agent-and-session-id.zh.md)相邻,因为二者都触及会话 id / owner-token 边界;`OwnerToken` 出于上述解耦理由仍与统一后的 id 保持独立。
- **Brand 不做校验。** Brand 是混淆防护,不是正确性证明:一个*错误的*会话 id 只要仍是格式正确的 string,就和以前一样能通过类型检查器。本决策不关闭这个缺口(见「不在范围内」)——它只阻止这类*类别*错误:传入错误*种类*的 id。
- **「在哪里停下」仍是判断题。** 为 `BashTaskId` 加 brand 但不为 `ToolName` 加,为 `OwnerToken` 加但不为 `ModelId` 加,是对哪些 string「可能被混淆」的品味判断。合理的评审者可能想要更多或更少;`brand.ts` 中的策略是裁决依据,本决策倾向于面向模型或用于访问控制的 id。
diff --git a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml
index 477c6df3ae..7d4ccbd4e1 100644
--- a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md
2026-06-20-generic-long-running-tool-runtime.md: 7db43323dd83f8e99698a7163412c9b33a607dfb
-2026-06-20-generic-long-running-tool-runtime.zh.md: efd306cc8d3b82749635dde302af45235c27b431
+2026-06-20-generic-long-running-tool-runtime.zh.md: 040ea6a01e97c9dc1d93e8f3a0fd9c1f4b0f31fc
diff --git a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md
index efd306cc8d..040ea6a01e 100644
--- a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md
@@ -19,13 +19,13 @@ Status: implemented
长时间运行工具是生产方。`dsh-tool-bash` 将 `ShellProcess` 适配为增量输出与进程取消;`dsh-tool-subagent` 将子运行适配为最终输出与子运行释放。bash 与 subagent 能力 seam 保持独立,不依赖会话或任务注册表。
-`JobRegistry` 是 `@deepseek-ai/dsh-jobs` 中的 Service Definition;进程内 Service Provider 是 `@deepseek-ai/dsh-jobs-local` 中的 `LocalJobRegistry`(该拆分记录在[任务注册表约定 Agent Note](2026-07-26-job-registry-seam.md)中)。
+`JobRegistry` 是 `@deepseek-ai/dsh-jobs` 中的 Service Definition;进程内 Service Provider 是 `@deepseek-ai/dsh-jobs-local` 中的 `LocalJobRegistry`(该拆分记录在[任务注册表约定 Agent Note](2026-07-26-job-registry-seam.zh.md)中)。
## 运行时约定
-字面类型见[任务子系统页面](../../../../docs/subsystems/jobs.md)。生产方调用 `ctx.jobs.start()`,传入 kind、label、可选的所属 `Agent`、可选的正数 `outputLimitBytes` 与一个 `run()` 函数。运行时会在调用 `run()` 前完成所有可能失败的预检工作,并且只调用一次。`run()` 返回钩子后,注册过程不会再执行可能失败的步骤而直接提交;生产方无法启动没有可收集 job id 的工作。
+字面类型见[任务子系统页面](../../../../docs/subsystems/jobs.zh.md)。生产方调用 `ctx.jobs.start()`,传入 kind、label、可选的所属 `Agent`、可选的正数 `outputLimitBytes` 与一个 `run()` 函数。运行时会在调用 `run()` 前完成所有可能失败的预检工作,并且只调用一次。`run()` 返回钩子后,注册过程不会再执行可能失败的步骤而直接提交;生产方无法启动没有可收集 job id 的工作。
-进程内 Service Provider 还拥有有界准入,其理由记录在[有界后台任务准入决策](../bug-fix/2026-08-11-bounded-background-job-admission.md)中。它的 `maxConcurrentJobsPerOwner` 配置必须是正的安全整数,默认值为 `10`;`start()` 从 `running` 与 `stopping` 记录派生每个确切 `Agent` 对象的活动数量,而全部无 owner 任务共享一个服务级桶。容量拒绝发生在 `run()` 与 id 分配之前,处于 stopping 的任务只有在生产方 `done` 结算时才释放名额。Service Provider 不排队或抢占任务,也不保留第二份可变计数。
+进程内 Service Provider 还拥有有界准入,其理由记录在[有界后台任务准入决策](../bug-fix/2026-08-11-bounded-background-job-admission.zh.md)中。它的 `maxConcurrentJobsPerOwner` 配置必须是正的安全整数,默认值为 `10`;`start()` 从 `running` 与 `stopping` 记录派生每个确切 `Agent` 对象的活动数量,而全部无 owner 任务共享一个服务级桶。容量拒绝发生在 `run()` 与 id 分配之前,处于 stopping 的任务只有在生产方 `done` 结算时才释放名额。Service Provider 不排队或抢占任务,也不保留第二份可变计数。
`outputLimitBytes` 是生产方拥有的呈现策略,而非注册表缓冲区。注册表校验该值,并将其原样投影到 `JobSnapshot`;通用任务控制器添加自身的状态或通知元数据后,再将该上限应用于完整的面向模型输出。省略该值时保持现有控制器行为,因此运行时不会向无关的生产方类别施加隐式默认值。
@@ -79,7 +79,7 @@ job id 在运行时全局可见且可预测,因此注册表会授权每次访
流式读取共享一个任务作用域内的消费游标,因为所属模型是预期读取方。UI 或多个独立读取方需要单独的非消费式观察 API;共享该游标会让读取方彼此消费对方的输出。
-系统提示词要求模型保留 job id、在后台工作运行时继续处理独立工作而非忙轮询或重复启动同一任务、在给出最终答案前收集相关任务,并终止不再重要的工作。完成时,系统会向确切所有者的会话交付一条已记录的消息:繁忙的所有者走注入,空闲的所有者会被唤醒,其有界策略由[空闲所有者唤醒决策](../feature/2026-08-11-background-job-completion-wakes-an-idle-owner.md)负责。
+系统提示词要求模型保留 job id、在后台工作运行时继续处理独立工作而非忙轮询或重复启动同一任务、在给出最终答案前收集相关任务,并终止不再重要的工作。完成时,系统会向确切所有者的会话交付一条已记录的消息:繁忙的所有者走注入,空闲的所有者会被唤醒,其有界策略由[空闲所有者唤醒决策](../feature/2026-08-11-background-job-completion-wakes-an-idle-owner.zh.md)负责。
当读取或等待交付终止任务、尚在等待的等待方在结算时认领了投递,或模型显式终止任务时,运行时将终止任务标为 `reported`。已报告的任务不会注入冗余的完成通知。监听器失败会独立记录,不会阻止后续监听器,也不会被等待方或资源销毁过程等待。当快照携带 `outputLimitBytes` 时,`dsh-tool-jobs` 会保持 UTF-8 边界,并复用生产方已有的截断标记,而不会重复添加。读取会为状态后缀预留空间并保留输出尾部;完成通知会先为稳定的 `background job ` 前缀与 `job_output` 指令预留空间,再截断可变的 kind、label、status、detail,乃至截断标记本身,因此 PTY 的最小上限仍能标识需要收集的任务。任务控制器在策略有机会拒绝或短路分发之前,于最先执行的 pre-execute 监听器中解析调用方可见的生产方上限;随后通过任务定义最后一道的 `finalizeContent` 回调应用该上限,使规范化的工具错误、外层流水线失败与单文本策略结果都无法绕过该边界;经特意结构化的多块策略结果仍由策略拥有其形状与大小。
@@ -105,7 +105,7 @@ bash seam 暴露 `resolve`、`run` 和 `start`。`start(spec)` 返回一个 `She
### 立即抽象任务运行时后端
-当前 `JobStart.run()` 约定传入进程内回调与确切的 `Agent` 对象。持久化后端会改变身份、重启、所有权与观察语义,因此在引入之时注册表保持为单一具体服务,而非固化错误的边界。[任务注册表约定 Agent Note](2026-07-26-job-registry-seam.md)后来在不改变这些进程内语义的前提下,将约定与进程内实现分离。
+当前 `JobStart.run()` 约定传入进程内回调与确切的 `Agent` 对象。持久化后端会改变身份、重启、所有权与观察语义,因此在引入之时注册表保持为单一具体服务,而非固化错误的边界。[任务注册表约定 Agent Note](2026-07-26-job-registry-seam.zh.md)后来在不改变这些进程内语义的前提下,将约定与进程内实现分离。
### 由消费方负责授权或清理事件
@@ -131,7 +131,7 @@ bash seam 暴露 `resolve`、`run` 和 `start`。`start(spec)` 返回一个 `She
## 后果
-bash 命令与 subagent 共享一套 id 词汇、列表、通知格式、提示词习惯和控制工具。新的长时间运行生产方只需实现执行钩子,而不必再实现一套注册表与工具族。[工具实操手册](../../../../docs/cookbook/adding-a-tool.md)将生产方指向本约定。
+bash 命令与 subagent 共享一套 id 词汇、列表、通知格式、提示词习惯和控制工具。新的长时间运行生产方只需实现执行钩子,而不必再实现一套注册表与工具族。[工具实操手册](../../../../docs/cookbook/adding-a-tool.zh.md)将生产方指向本约定。
单个确切 owner 无法再无限增加进程内由 Task 承载的工作,另一个 owner 也不会消耗它的额度。取消请求会继续占用容量,直到生产方真正释放资源,因此用新工作替换缓慢停止的任务不会突破已配置的实时资源预算。
diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml
index f9b830c3b7..dcaeb8c34b 100644
--- a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md
-2026-06-21-bounded-llm-request-recovery.md: 3a91d77fe9c08fbcc3afdce4dad9a38e288b4723
-2026-06-21-bounded-llm-request-recovery.zh.md: a6b342b19236251e638242c1e9df6b3d6ed557c0
+2026-06-21-bounded-llm-request-recovery.md: e725a025f2d8b0d5e8eaf4137f07d8eab4448bf4
+2026-06-21-bounded-llm-request-recovery.zh.md: 9e2263b05888797eaaaeb82859730d1c4a728cea
diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md
index 3a91d77fe9..e725a025f2 100644
--- a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md
+++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md
@@ -4,13 +4,13 @@ Status: implemented
English | [中文](2026-06-21-bounded-llm-request-recovery.zh.md)
-The [per-provider request retry policy](../feature/2026-07-24-provider-retry-policies.md) extends this foundation with exact-provider configuration and an explicit unbounded mode. This note continues to own structured failure facts, the closed-step recovery boundary, normal mode's transient defaults, visible single attempts, and durable retry status. [Terminal LLM stream failures](2026-07-29-terminal-llm-stream-failures.md) supersedes its thrown-error identity and stream-sidecar mechanism.
+The [per-provider request retry policy](../feature/2026-07-24-provider-retry-policies.md) extends this foundation with exact-provider configuration and an explicit unbounded mode. This note continues to own structured failure facts, the failed-attempt recovery boundary, normal mode's transient defaults, visible single attempts, and durable retry status. [Terminal LLM stream failures](2026-07-29-terminal-llm-stream-failures.md) supersedes its thrown-error identity and stream-sidecar mechanism.
## Problem
Provider adapters can fail by throwing during dispatch or iteration or by ending with `finish { kind: 'error' | 'aborted' }`. The final adapter boundary normalizes thrown values to that terminal finish protocol before `dsh-agent-loop` receives them; middleware and result-processing defects remain thrown. The loop offers a terminal model-request failure to `agent/request-error`. An unhandled failure is terminal; a handling listener repairs policy-owned state, returns `{ kind: 'retry' }`, and stops waterfall delegation. The [retry-action decision](../simplification/2026-07-27-request-error-retry-action.md) owns this return contract.
-That boundary is already safe for another request attempt. Raw `assistant/chunk` events carry the failed `turn` and `step`, message derivation ignores them unless a successful `assistant/message` cites them, tool calls are dispatched only after a successful terminal finish and assembly, and a retry opens a new numbered turn from the durable log. The harness therefore does not need a second response lifecycle or tentative-output protocol to keep two attempts separate.
+That boundary is already safe for another request attempt. Raw `assistant/chunk` events carry the failed `turn` and `step`, message derivation ignores them unless a successful `assistant/message` cites them, tool calls are dispatched only after a successful terminal finish and assembly, and a retry reconstructs its next attempt from the durable log. The harness therefore does not need a second response lifecycle or tentative-output protocol to keep two attempts separate.
The prior boundary left three narrower gaps.
@@ -52,7 +52,7 @@ The shared transient-code set is intentionally small: adapter mappings for `RATE
`@deepseek-ai/dsh-llm-retry` is a function plugin that listens to `agent/request-error`. It introduces no service or new loop branch; the agent-loop package changes only the data carried through its existing failed-step recovery control flow.
-The `agent/request-error` waterfall carries the current `LlmFailure`, an immutable list of prior failures that authorized retry turns in the consecutive recovery sequence, and the serving registration's immutable retry policy. The loop transports but does not interpret that policy, owns the consecutive failure history, and clears it after a successful model request. Normal `dsh-llm-retry` policy counts durable retry records scheduled by the same exact-provider policy, while `dsh-compaction-basic` keeps its own context-overflow budget. Alternating transient and context-overflow failures therefore consume their owning finite budgets independently; the maximum request count is one plus the sum of the loaded finite budgets.
+The `agent/request-error` waterfall carries the current `LlmFailure`, an immutable list of prior failures that authorized retries in the consecutive recovery sequence, and the serving registration's immutable retry policy. The loop transports but does not interpret that policy, owns the consecutive failure history, and clears it after a successful model request. Normal `dsh-llm-retry` policy counts durable retry records scheduled by the same exact-provider policy, while `dsh-compaction-basic` keeps its own context-overflow budget. Alternating transient and context-overflow failures therefore consume their owning finite budgets independently; the maximum request count is one plus the sum of the loaded finite budgets.
The [provider-policy decision](../feature/2026-07-24-provider-retry-policies.md) owns the current configuration shape. Provider adapters register their nested `retryPolicy`; omission uses normal defaults: two transient retries, a 500 millisecond initial delay, a 10 second delay cap, 10 percent jitter, and the five transient codes above. The count and delay bounds match the conservative edge of the inspected implementations: [OpenCode uses two request retries with 500 ms/10 s bounds](https://github.com/anomalyco/opencode/blob/9976269ab1accfc9f9dc98a4a688c516934de422/%70ackages/llm/src/route/executor.ts#L36-L39), [Pi separates three agent-level retries from provider retries and defaults provider retries to zero](https://github.com/earendil-works/pi/blob/3da591ab74ab9ab407e72ed882600b2c851fae21/%70ackages/coding-agent/docs/settings.md#L139-L147), and [Codex uses finite request/stream budgets plus a five-minute idle timeout](https://github.com/openai/codex/blob/0fb559f0f6e231a88ac02ea002d3ecd248e2b515/codex-rs/model-provider-info/src/lib.rs#L25-L33). Ten percent follows [Codex's bounded jitter](https://github.com/openai/codex/blob/0fb559f0f6e231a88ac02ea002d3ecd248e2b515/codex-rs/codex-client/src/retry.rs#L40-L47).
@@ -68,7 +68,7 @@ The agent-spine demo bundle loads the plugin so the shared stdio/TUI, one-shot C
### Make one layer own visible attempts
-Adapters perform one provider request per `stream()` call. The pi-ai adapter removes public `maxRetries` and `maxRetryDelayMs` profile fields and disables library retries; the hand-written adapter keeps its current single-attempt behavior. This prevents an SDK budget from multiplying the agent budget and ensures every transient retry is represented by a closed failed step plus `llm/retry`.
+Adapters perform one provider request per `stream()` call. The pi-ai adapter removes public `maxRetries` and `maxRetryDelayMs` profile fields and disables library retries; the hand-written adapter keeps its current single-attempt behavior. This prevents an SDK budget from multiplying the agent budget and ensures every transient retry is represented by its recorded failed attempt plus `llm/retry`.
`ctx.llm.stream()` remains the raw one-attempt waterfall. Direct callers such as compaction summarization receive the structured failure but do not gain automatic retry, because they have no agent step boundary or general durable place to separate attempts. A future direct-call consumer may justify a buffering helper that retries only before emitting a chunk; this decision adds no such helper.
@@ -82,9 +82,9 @@ Boundary tests prove termination at both actual transports. The hand-written ada
### Keep attempts separate in the existing log
-A failed attempt may leave `assistant/chunk` events in its closed step, but it never appends `assistant/message` and never dispatches a tool. A retry closes the failed turn, opens the next numbered turn, reconstructs the request from the durable surface, and produces its own chunks. UIs may render live chunks while a step is open, then mark or clear that transient view when `llm/retry` identifies the failed step or `turn/end` records failure. Web validates the complete retry payload contract, clears the failed partial at `llm/retry`, projects consecutive retry-turn events into one stable row updated to the latest attempt, and derives scheduled, started, or cancelled status from subsequent turn facts. Its countdown anchors the scheduled delay to browser receipt rather than the Host event clock, uses ceiling-rounded seconds with a one-second floor, animates only while unresolved, and keeps exact latest failure details collapsed behind the row. Retry nodes anchor their own trajectory turn even when the failed attempt has no assistant node. Message derivation continues to ignore the failed chunks, and Web applies the same projection during history rebuild so refreshing cannot resurrect discarded partials or duplicate retry rows.
+A failed attempt may leave `assistant/chunk` events in its step, but it never appends `assistant/message` and never dispatches a tool. A retry continues inside the failing turn and step, reconstructs the request from the durable surface, and produces its own chunks; only the final outcome closes the turn. UIs may render live chunks while a step is open, then mark or clear that transient view when `llm/retry` identifies the failed attempt or `turn/end` records failure. Web validates the complete retry payload contract, clears the failed partial at `llm/retry`, projects each producer-correlated `retryId` chain into one stable row updated to the latest attempt, and derives scheduled, started, or cancelled status from `llm/retry-started` and the owning turn and step boundaries' closure. Its countdown anchors the scheduled delay to browser receipt rather than the Host event clock, uses ceiling-rounded seconds with a one-second floor, animates only while unresolved, and keeps exact latest failure details collapsed behind the row. Retry nodes anchor their own trajectory turn even when the failed attempt has no assistant node. Message derivation continues to ignore the failed chunks, and Web applies the same projection during history rebuild so refreshing cannot resurrect discarded partials or duplicate retry rows.
-If recovery is exhausted, the final failure is stored once on `turn/end.reason` with the structured facts. Web derives one `turn-error` node at that sequence position and renders its display-safe message and optional code inline; AUTH projections replace provider copy that may echo credential fragments with `API key is invalid`, while the raw diagnostic remains in the session log. The same fold runs for live events and history replay. If transient recovery continues, `llm/retry` is the durable home for that attempt's failure and delay, so its failed turn does not also gain a terminal error row. No standalone final-error event or response-id vocabulary is added.
+If recovery is exhausted, the final failure is stored once on `turn/end.reason` with the structured facts. Web derives one `turn-error` node at that sequence position and renders its display-safe message and optional code inline; AUTH projections replace provider copy that may echo credential fragments with `API key is invalid`, while the raw diagnostic remains in the session log. The same fold runs for live events and history replay. While transient recovery continues, `llm/retry` is the durable home for each intermediate failure and delay; the terminal row exists only once `turn/end` records the error, and because exhausted recovery shares the failing turn, the turn's retry history never suppresses that row — the settled retry chain and the terminal error render side by side. No standalone final-error event or response-id vocabulary is added.
## Out of scope
@@ -114,15 +114,15 @@ If recovery is exhausted, the final failure is stored once on `turn/end.reason`
- Each provider adapter validates its nested retry policy at Loader startup, and `ctx.llm` captures it with the route; normal mode delegates ineligible paths and makes at most `maxRetries + 1` provider requests when no other policy applies.
- HMR-during-backoff tests prove disposal unregisters the listener, aborts and awaits its captured callbacks, emits no retry decision after disposal, and leaves no timer or promise alive.
- Pure unit tests cover transient-code selection, exponential backoff and jitter bounds, valid and over-cap `Retry-After`, exhausted budgets, deterministic timer/random hooks, and abort during backoff.
-- Real agent-loop tests cover failure before chunks, partial chunks then failure, thrown and in-band failures, retry to success in a new turn, exhaustion to structured `turn/end.reason`, and composition with `dsh-compaction-basic` context-overflow recovery.
+- Real agent-loop tests cover failure before chunks, partial chunks then failure, thrown and in-band failures, retry to success inside the same turn, exhaustion to structured `turn/end.reason`, and composition with `dsh-compaction-basic` context-overflow recovery.
- The partial-chunk integration test proves failed chunks remain attributed to the failed step, no assistant message or tool side effect is committed for that step, and the successful retry records its own chunk seqs and provider/model route.
-- The plugin-owned `llm/retry` event is non-surface, survives JSONL and SQLite round trips, is ignored by message derivation, and drives TUI and Web retraction plus scheduled-retry rendering. Client tests cover complete wire validation, clock-independent countdown, cancellation versus completed retry labels, and trajectory attribution; keyless UI snapshots cover Web scheduling and success, a real Web composition test covers partial transport failure through recovery, and ACP automation snapshots confirm that a discarded attempt stays off the wire while the recovered reply is emitted.
+- The plugin-owned `llm/retry` event is non-surface, survives JSONL and SQLite round trips, is ignored by message derivation, and drives TUI and Web retraction plus scheduled-retry rendering. Client tests cover complete wire validation, clock-independent countdown, cancellation versus completed retry labels, and trajectory attribution; keyless UI snapshots cover Web scheduling and success, real Web composition tests cover partial transport failure through recovery and exhausted recovery's terminal error row beside the settled retry chain, and ACP automation snapshots confirm that a discarded attempt stays off the wire while the recovered reply is emitted.
- Idle-watchdog tests prove the stable signal is rearmed only while `next()` is outstanding, disarmed during consumer think time and in `finally`, and classified separately from a total-call deadline and an earlier caller abort; adapter tests prove the signal stops the underlying request rather than merely detaching it.
- Direct `ctx.llm.stream()` callers remain single-attempt and receive the same structured failure facts.
## Consequences
-- Every retry attempt is visible as a closed failed turn plus `llm/retry`, and adapter-level single-attempt behavior prevents hidden SDK retries from multiplying policy decisions. A retry can still duplicate provider billing even when no chunk arrived; normal mode limits that risk, while explicit always mode accepts it until cancellation or success.
+- Every retry attempt is visible inside its owning turn as the failed attempt's chunks plus `llm/retry`, and adapter-level single-attempt behavior prevents hidden SDK retries from multiplying policy decisions. A retry can still duplicate provider billing even when no chunk arrived; normal mode limits that risk, while explicit always mode accepts it until cancellation or success.
- Provider SDKs may hide status or retry headers. Those adapters retain the stable facts they expose and otherwise use a coarse code rather than letting recovery policy parse fragile text.
- Durable retry events expand the session protocol and UI state machine. Shipping the event and its consumer together prevents an unused telemetry vocabulary, but later schema changes still require persistence and replay work.
- Clearing a failed step's live chunks can visibly retract output. That is preferable to presenting discarded text or partial tool JSON as committed history, and snapshots pin the transition.
@@ -136,3 +136,4 @@ If recovery is exhausted, the final failure is stored once on `turn/end.reason`
- [Timeout deadline library](../../implemented/architecture/2026-07-06-timeout-deadline-library.md) separates shared deadline classification from capability-owned termination.
- [After-call compaction pressure and context-overflow recovery](../../implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md) owns the current closed-step request-recovery extension point and bounded overflow retry.
- [Provider-routed LLM adapters](../../implemented/architecture/2026-07-14-provider-routed-llm-adapters.md) owns explicit provider/model routing and the one-adapter-per-provider invariant.
+- [Terminal turn errors survive same-turn retry history](../bug-fix/2026-08-20-turn-error-survives-same-turn-retry-history.md) owns the removal of the Web retry-history suppression that hid exhausted recovery's terminal error row.
diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md
index a6b342b192..9e2263b058 100644
--- a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md
@@ -4,13 +4,13 @@ Status: implemented
[English](2026-06-21-bounded-llm-request-recovery.md) | 中文
-[按提供方配置的请求重试策略](../feature/2026-07-24-provider-retry-policies.md)在此基础上增加了确切提供方配置与显式无界 mode。本说明继续负责结构化失败事实、已关闭步骤的恢复边界、normal mode 的暂时性默认值、可见的单次尝试和持久重试状态。[LLM(大语言模型)流的终止失败](2026-07-29-terminal-llm-stream-failures.md)取代了其中关于抛出错误身份和流 sidecar 的机制。
+[按提供方配置的请求重试策略](../feature/2026-07-24-provider-retry-policies.zh.md)在此基础上增加了确切提供方配置与显式无界 mode。本说明继续负责结构化失败事实、失败尝试的恢复边界、normal mode 的暂时性默认值、可见的单次尝试和持久重试状态。[LLM(大语言模型)流的终止失败](2026-07-29-terminal-llm-stream-failures.zh.md)取代了其中关于抛出错误身份和流 sidecar 的机制。
## 问题
-提供方适配器可能在分发或迭代时抛出异常,也可能以 `finish { kind: 'error' | 'aborted' }` 结束。最终适配器边界会在 `dsh-agent-loop` 接收前把抛出值规范化为该终止 finish 协议;middleware 与结果处理缺陷仍会抛出。loop 会将终止模型请求失败交给 `agent/request-error`。未被处理的失败是终态;处理失败的监听器修复策略自有状态,返回 `{ kind: 'retry' }`,并停止 waterfall(瀑布式事件)委托。[重试动作决策](../simplification/2026-07-27-request-error-retry-action.md)规定这一返回约定。
+提供方适配器可能在分发或迭代时抛出异常,也可能以 `finish { kind: 'error' | 'aborted' }` 结束。最终适配器边界会在 `dsh-agent-loop` 接收前把抛出值规范化为该终止 finish 协议;middleware 与结果处理缺陷仍会抛出。loop 会将终止模型请求失败交给 `agent/request-error`。未被处理的失败是终态;处理失败的监听器修复策略自有状态,返回 `{ kind: 'retry' }`,并停止 waterfall(瀑布式事件)委托。[重试动作决策](../simplification/2026-07-27-request-error-retry-action.zh.md)规定这一返回约定。
-该边界已能安全地再次发起请求。原始 `assistant/chunk` 事件携带失败的 `turn` 和 `step`;除非某条成功的 `assistant/message` 引用这些事件,否则消息派生会忽略它们。只有终止性 finish 成功且组装完成后,系统才会分发工具调用;重试则会从持久日志开启新的编号轮次。因此,harness 无需引入第二套响应生命周期或暂定输出协议,即可分隔两次尝试。
+该边界已能安全地再次发起请求。原始 `assistant/chunk` 事件携带失败的 `turn` 和 `step`;除非某条成功的 `assistant/message` 引用这些事件,否则消息派生会忽略它们。只有终止性 finish 成功且组装完成后,系统才会分发工具调用;重试则会从持久日志重建下一次尝试。因此,harness 无需引入第二套响应生命周期或暂定输出协议,即可分隔两次尝试。
此前的边界还留有三个较窄的缺口。
@@ -46,15 +46,15 @@ agent loop(智能体循环)会将终止 finish 的 `LlmFailure` 传给 `agen
适配器会先提取结构化事实,再回退到消息检查。它们会验证 HTTP 状态,将 `Retry-After` 的秒数或日期解析为正的有限毫秒延迟,在提供方公开请求 id 时将其品牌化,并区分自身超时与调用方中止。提供方专用 code 和消息可以细化映射,但恢复监听器不会解析它们。
-共享的暂时性 code 集有意保持很小:适配器针对 `RATE_LIMIT` 和 `SERVER` 的映射,远程失败使用的显式 `TIMEOUT` 和 `TRANSPORT` code,以及提供方响应已完成却没有内容块时使用的 `EMPTY_RESPONSE`。两个适配器都会把最后一种情况归类为错误 finish;详见[空模型响应可重试](../bug-fix/2026-07-24-empty-model-response-is-retryable.md)。身份验证、配额、无效请求、上下文溢出、协议、中止和未知失败都保留不同的稳定 code,且默认不属于暂时性失败。新增 code 需要适配器 fixture(测试前置数据)和已记录的策略决策;无需扩展第二个失败类枚举。
+共享的暂时性 code 集有意保持很小:适配器针对 `RATE_LIMIT` 和 `SERVER` 的映射,远程失败使用的显式 `TIMEOUT` 和 `TRANSPORT` code,以及提供方响应已完成却没有内容块时使用的 `EMPTY_RESPONSE`。两个适配器都会把最后一种情况归类为错误 finish;详见[空模型响应可重试](../bug-fix/2026-07-24-empty-model-response-is-retryable.zh.md)。身份验证、配额、无效请求、上下文溢出、协议、中止和未知失败都保留不同的稳定 code,且默认不属于暂时性失败。新增 code 需要适配器 fixture(测试前置数据)和已记录的策略决策;无需扩展第二个失败类枚举。
### 将重试策略放在现有失败步骤扩展点上
`@deepseek-ai/dsh-llm-retry` 是监听 `agent/request-error` 的函数插件。它不引入服务或新的循环分支;agent-loop 包仅会更改通过现有失败步骤恢复控制流携带的数据。
-`agent/request-error` waterfall 携带当前 `LlmFailure`、在连续恢复序列中授权重试轮次的不可变先前失败列表,以及提供服务的注册项所携带的不可变重试策略。循环只传递而不解释该策略;它拥有连续失败历史,并在模型请求成功后清除。`dsh-llm-retry` 的 normal 策略统计由同一项确切提供方策略安排的持久重试记录,`dsh-compaction-basic` 则维护自己的上下文溢出预算。因此,暂时性失败与上下文溢出交替出现时,会各自独立消耗其有限预算;最大请求数等于 1 加上所有已加载有限预算之和。
+`agent/request-error` waterfall 携带当前 `LlmFailure`、在连续恢复序列中授权重试的不可变先前失败列表,以及提供服务的注册项所携带的不可变重试策略。循环只传递而不解释该策略;它拥有连续失败历史,并在模型请求成功后清除。`dsh-llm-retry` 的 normal 策略统计由同一项确切提供方策略安排的持久重试记录,`dsh-compaction-basic` 则维护自己的上下文溢出预算。因此,暂时性失败与上下文溢出交替出现时,会各自独立消耗其有限预算;最大请求数等于 1 加上所有已加载有限预算之和。
-当前配置形状由[提供方策略决策](../feature/2026-07-24-provider-retry-policies.md)规定。提供方适配器会注册嵌套的 `retryPolicy`;省略时使用 normal 默认值:两次暂时性重试、500 毫秒初始延迟、10 秒延迟上限、10% 抖动,以及上述五个暂时性 code。计数与延迟边界参考了所调查实现中较保守的一端:[OpenCode 使用两次请求重试,延迟边界为 500 毫秒/10 秒](https://github.com/anomalyco/opencode/blob/9976269ab1accfc9f9dc98a4a688c516934de422/%70ackages/llm/src/route/executor.ts#L36-L39);[Pi 将三次 agent 级重试与提供方重试分开,且提供方重试默认为零](https://github.com/earendil-works/pi/blob/3da591ab74ab9ab407e72ed882600b2c851fae21/%70ackages/coding-agent/docs/settings.md#L139-L147);[Codex 使用有限请求/流预算以及五分钟空闲超时](https://github.com/openai/codex/blob/0fb559f0f6e231a88ac02ea002d3ecd248e2b515/codex-rs/model-provider-info/src/lib.rs#L25-L33)。10% 抖动参考 [Codex 的有界抖动](https://github.com/openai/codex/blob/0fb559f0f6e231a88ac02ea002d3ecd248e2b515/codex-rs/codex-client/src/retry.rs#L40-L47)。
+当前配置形状由[提供方策略决策](../feature/2026-07-24-provider-retry-policies.zh.md)规定。提供方适配器会注册嵌套的 `retryPolicy`;省略时使用 normal 默认值:两次暂时性重试、500 毫秒初始延迟、10 秒延迟上限、10% 抖动,以及上述五个暂时性 code。计数与延迟边界参考了所调查实现中较保守的一端:[OpenCode 使用两次请求重试,延迟边界为 500 毫秒/10 秒](https://github.com/anomalyco/opencode/blob/9976269ab1accfc9f9dc98a4a688c516934de422/%70ackages/llm/src/route/executor.ts#L36-L39);[Pi 将三次 agent 级重试与提供方重试分开,且提供方重试默认为零](https://github.com/earendil-works/pi/blob/3da591ab74ab9ab407e72ed882600b2c851fae21/%70ackages/coding-agent/docs/settings.md#L139-L147);[Codex 使用有限请求/流预算以及五分钟空闲超时](https://github.com/openai/codex/blob/0fb559f0f6e231a88ac02ea002d3ecd248e2b515/codex-rs/model-provider-info/src/lib.rs#L25-L33)。10% 抖动参考 [Codex 的有界抖动](https://github.com/openai/codex/blob/0fb559f0f6e231a88ac02ea002d3ecd248e2b515/codex-rs/codex-client/src/retry.rs#L40-L47)。
对于预算未耗尽的合格失败,从 1 开始的暂时性重试计数使用有界指数退避。有效的 `providerRetryAfterMs` 只有在不超过 `maxDelayMs` 时才会取代指数退避;提供方延迟更长时,系统会委托给下一监听器,而不会违反提供方指令提前重试。本地退避乘以 `[1 - jitterRatio, 1 + jitterRatio]` 内的注入随机因子,并将最终值限制到 `maxDelayMs`;提供方延迟不加抖动。
@@ -68,7 +68,7 @@ agent-spine 演示组合包加载该插件,因此共享的 stdio/TUI、一次
### 由单一层负责可见的尝试
-适配器每次调用 `stream()` 只执行一次提供方请求。pi-ai 适配器移除公开的 `maxRetries` 和 `maxRetryDelayMs` profile 字段,并禁用库内部重试;手写适配器保持现有的单次尝试行为。这样既避免 SDK 预算成倍放大 agent 预算,又能确保每次暂时性重试都由一个已关闭的失败步骤加 `llm/retry` 表示。
+适配器每次调用 `stream()` 只执行一次提供方请求。pi-ai 适配器移除公开的 `maxRetries` 和 `maxRetryDelayMs` profile 字段,并禁用库内部重试;手写适配器保持现有的单次尝试行为。这样既避免 SDK 预算成倍放大 agent 预算,又能确保每次暂时性重试都由其记录在案的失败尝试加 `llm/retry` 表示。
`ctx.llm.stream()` 仍是原始的单次尝试 waterfall。压缩(compaction)摘要等直接调用方会收到结构化失败,但不会自动获得重试,因为它们没有 agent 步骤边界,也没有可供分隔尝试的通用持久位置。未来的直接调用消费方可能会需要一个缓冲辅助函数,仅在尚未发出任何分片时重试;本决策不增加此类辅助函数。
@@ -82,9 +82,9 @@ agent-spine 演示组合包加载该插件,因此共享的 stdio/TUI、一次
### 在现有日志中分隔尝试
-一次失败尝试可以在已关闭的步骤中留下 `assistant/chunk` 事件,但绝不会追加 `assistant/message`,也不会分发工具。重试会关闭失败轮次,开启下一个编号轮次,从持久表层重建请求,并生成自己的分片。步骤仍处于打开状态时,UI 可以渲染实时分片;当 `llm/retry` 标识失败步骤,或 `turn/end` 记录失败时,UI 再标记或清除这份暂时视图。Web 会验证完整的重试载荷约定,在 `llm/retry` 到达时清除失败的部分输出,将连续重试轮次的事件投影为稳定的一行,并用最新一次尝试更新该行,再从后续轮次事实派生 scheduled、started 或 cancelled 状态。倒计时以浏览器收到事件的时刻为计划延迟的起点,而不是使用 Host 事件时钟;它按向上取整且不低于 1 秒的秒数显示,仅在重试尚未结束时显示动画,并把最近一次失败的准确详情折叠在该行之后。即使失败尝试没有 assistant 节点,重试节点也会锚定自身的轨迹轮次。消息派生仍会忽略失败分片;Web 在重建历史时也会应用同一投影,因此刷新页面不会让已丢弃的部分输出重新出现,也不会生成重复的重试行。
+一次失败尝试可以在其步骤中留下 `assistant/chunk` 事件,但绝不会追加 `assistant/message`,也不会分发工具。重试在失败的轮次与步骤内继续,从持久表层重建请求,并生成自己的分片;只有最终结果才会关闭该轮次。步骤仍处于打开状态时,UI 可以渲染实时分片;当 `llm/retry` 标识失败尝试,或 `turn/end` 记录失败时,UI 再标记或清除这份暂时视图。Web 会验证完整的重试载荷约定,在 `llm/retry` 到达时清除失败的部分输出,将每条生产方关联的 `retryId` 重试链投影为稳定的一行,并用最新一次尝试更新该行,再从 `llm/retry-started` 与所属轮次、步骤边界的关闭派生 scheduled、started 或 cancelled 状态。倒计时以浏览器收到事件的时刻为计划延迟的起点,而不是使用 Host 事件时钟;它按向上取整且不低于 1 秒的秒数显示,仅在重试尚未结束时显示动画,并把最近一次失败的准确详情折叠在该行之后。即使失败尝试没有 assistant 节点,重试节点也会锚定自身的轨迹轮次。消息派生仍会忽略失败分片;Web 在重建历史时也会应用同一投影,因此刷新页面不会让已丢弃的部分输出重新出现,也不会生成重复的重试行。
-如果恢复预算耗尽,最终失败会连同结构化事实在 `turn/end.reason` 中存储一次。Web 会在该序列位置派生一个 `turn-error` 节点,并内联渲染适合展示的消息与可选错误码;AUTH 投影会把可能回显凭据片段的提供方文案替换为 `API key is invalid`,原始诊断仍保留在会话日志中。实时事件和历史回放使用同一套折叠逻辑。如果暂时性恢复继续,`llm/retry` 就是该次尝试的失败与延迟的持久归属位置,因此该失败轮次不会再获得终态错误行。本决策不增加独立的最终错误事件或响应 id 词汇。
+如果恢复预算耗尽,最终失败会连同结构化事实在 `turn/end.reason` 中存储一次。Web 会在该序列位置派生一个 `turn-error` 节点,并内联渲染适合展示的消息与可选错误码;AUTH 投影会把可能回显凭据片段的提供方文案替换为 `API key is invalid`,原始诊断仍保留在会话日志中。实时事件和历史回放使用同一套折叠逻辑。暂时性恢复继续期间,`llm/retry` 是每次中间失败与延迟的持久归属位置;终态错误行只在 `turn/end` 记录错误后才存在,而由于耗尽的恢复与失败共享同一轮次,该轮次的重试历史绝不会抑制这一行——定格的重试链与终态错误并列渲染。本决策不增加独立的最终错误事件或响应 id 词汇。
## 不在范围内
@@ -100,7 +100,7 @@ agent-spine 演示组合包加载该插件,因此共享的 stdio/TUI、一次
- **向 `dsh-llm` 增加响应开始、中断、丢弃、失败和提交事件**:拒绝采用,因为 agent 日志已经分隔原始分片、成功消息和编号尝试。第二套状态机会重复归属关系,又不能支持有界的同路由重试。
- **增加逻辑路由、能力矩阵和故障转移选择**:拒绝采用,因为当前请求已经显式指定提供方和模型,每个提供方由一个适配器负责,而且没有当前消费方要求自动回退或能够证明语义兼容性。
- **把 `retryable` 或 `failover` 放在 `LlmFailure` 上**:拒绝采用,因为适配器报告事实,部署策略决定动作。同一个 429 可以在交互式组合包中重试,也可以在成本受限的批处理中被拒绝。
-- **只要调用方仍处于活跃状态就无限重试**:[按提供方配置的策略](../feature/2026-07-24-provider-retry-policies.md)对显式 `always` 配置项推翻了这项拒绝,同时保留有界的 normal mode 作为默认值。
+- **只要调用方仍处于活跃状态就无限重试**:[按提供方配置的策略](../feature/2026-07-24-provider-retry-policies.zh.md)对显式 `always` 配置项推翻了这项拒绝,同时保留有界的 normal mode 作为默认值。
- **只通过进程 logger 记录重试状态**:拒绝采用,因为进程日志无法重建会话行为,也不能驱动回放后的 UI 状态。
- **只保留扁平 code**:拒绝采用,因为重试延迟和提供方请求 id 是结构化的提供方事实,而当不同协议失败共用一个稳定 code 时,诊断还需要 HTTP 状态。
@@ -114,15 +114,15 @@ agent-spine 演示组合包加载该插件,因此共享的 stdio/TUI、一次
- 每个提供方适配器都在 Loader 启动时验证其嵌套重试策略,`ctx.llm` 则将该策略与路由一同捕获;normal mode 会委托不合格路径,而且在没有其他策略时最多发起 `maxRetries + 1` 次提供方请求。
- 退避期间执行 HMR 的测试证明:dispose 过程会注销监听器、中止并等待其捕获的回调,dispose 后不发出重试决策,也不留下存活的定时器或 promise。
- 纯单元测试覆盖暂时性 code 选择、指数退避和抖动边界、有效及超出上限的 `Retry-After`、耗尽的预算、确定性定时器/随机数钩子,以及退避期间中止。
-- 真实 agent-loop 测试覆盖分片前失败、部分分片后失败、抛出及带内失败、在新轮次中重试至成功、耗尽后写入结构化 `turn/end.reason`,以及与 `dsh-compaction-basic` 上下文溢出恢复的组合。
+- 真实 agent-loop 测试覆盖分片前失败、部分分片后失败、抛出及带内失败、在同一轮次内重试至成功、耗尽后写入结构化 `turn/end.reason`,以及与 `dsh-compaction-basic` 上下文溢出恢复的组合。
- 部分分片集成测试证明:失败分片仍归属于失败步骤,该步骤不会提交 assistant 消息或工具副作用,成功的重试会记录自己的分片 seq 和提供方/模型路由。
-- 插件拥有的不进入表层的 `llm/retry` 事件可在 JSONL 和 SQLite 往返后保留,被消息派生忽略,并驱动 TUI 和 Web 撤回及计划重试渲染。客户端测试覆盖完整的 wire 验证、独立于时钟的倒计时、已取消与已完成重试标签的区别以及轨迹归属;无密钥 UI 快照覆盖 Web 的调度与成功,真实 Web 组合测试覆盖部分传输失败直至恢复,ACP 自动化快照确认,被丢弃的尝试不会通过协议发出,而恢复后的回复会正常发出。
+- 插件拥有的不进入表层的 `llm/retry` 事件可在 JSONL 和 SQLite 往返后保留,被消息派生忽略,并驱动 TUI 和 Web 撤回及计划重试渲染。客户端测试覆盖完整的 wire 验证、独立于时钟的倒计时、已取消与已完成重试标签的区别以及轨迹归属;无密钥 UI 快照覆盖 Web 的调度与成功,真实 Web 组合测试覆盖部分传输失败直至恢复,以及耗尽后终态错误行与定格重试链并列的画面,ACP 自动化快照确认,被丢弃的尝试不会通过协议发出,而恢复后的回复会正常发出。
- 空闲看门狗测试证明:只有 `next()` 尚未完成时才会重新布防稳定信号;在消费方思考期间及 `finally` 中会解除布防;它与总调用 deadline 以及更早发生的调用方中止分开分类。适配器测试证明该信号会终止底层请求,而不只是与其脱离。
- `ctx.llm.stream()` 的直接调用方仍只尝试一次,并收到相同的结构化失败事实。
## 后果
-- 每次重试尝试都以一个已关闭失败轮次加 `llm/retry` 的形式可见,适配器级的单次尝试行为会防止隐藏的 SDK 重试成倍增加策略决策。即使没有分片到达,重试仍可能造成提供方重复计费;normal mode 会限制此风险,而显式 always mode 会接受它,直至取消或成功。
+- 每次重试尝试都在其所属轮次内以失败尝试的分片加 `llm/retry` 的形式可见,适配器级的单次尝试行为会防止隐藏的 SDK 重试成倍增加策略决策。即使没有分片到达,重试仍可能造成提供方重复计费;normal mode 会限制此风险,而显式 always mode 会接受它,直至取消或成功。
- 提供方 SDK 可能隐藏状态或重试标头。适配器会保留 SDK 公开的稳定事实,否则使用粗粒度 code,而不会让恢复策略解析脆弱的文本。
- 持久重试事件扩展了会话协议和 UI 状态机。事件与其消费方一同交付,可避免产生无人使用的遥测词汇;但以后更改 schema 仍需要同步完成持久化和回放工作。
- 清除失败步骤的实时分片可能会明显撤回输出。与把丢弃的文本或不完整工具 JSON 呈现为已提交历史相比,这是更好的选择;快照固定这一转换。
@@ -131,8 +131,9 @@ agent-spine 演示组合包加载该插件,因此共享的 stdio/TUI、一次
## 相关资料
-- [结构化错误分类体系](../../implemented/architecture/2026-06-11-structured-error-taxonomy.md)负责稳定、可供机器路由的 code 与 cause chaining。
-- [可重建请求](../../implemented/architecture/2026-07-05-reconstructable-requests.md)使提供方/模型和完整请求输入在分发前持久化。
-- [超时 deadline 库](../../implemented/architecture/2026-07-06-timeout-deadline-library.md)将共享的 deadline 分类与能力自身拥有的终止操作分开。
-- [调用后压缩压力与上下文溢出恢复](../../implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)负责当前已关闭步骤的请求恢复扩展点与有界溢出重试。
-- [提供方路由的 LLM 适配器](../../implemented/architecture/2026-07-14-provider-routed-llm-adapters.md)负责显式提供方/模型路由与每个提供方仅有一个适配器的不变量。
+- [结构化错误分类体系](../../implemented/architecture/2026-06-11-structured-error-taxonomy.zh.md)负责稳定、可供机器路由的 code 与 cause chaining。
+- [可重建请求](../../implemented/architecture/2026-07-05-reconstructable-requests.zh.md)使提供方/模型和完整请求输入在分发前持久化。
+- [超时 deadline 库](../../implemented/architecture/2026-07-06-timeout-deadline-library.zh.md)将共享的 deadline 分类与能力自身拥有的终止操作分开。
+- [调用后压缩压力与上下文溢出恢复](../../implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md)负责当前已关闭步骤的请求恢复扩展点与有界溢出重试。
+- [提供方路由的 LLM 适配器](../../implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md)负责显式提供方/模型路由与每个提供方仅有一个适配器的不变量。
+- [Terminal turn errors survive same-turn retry history](../bug-fix/2026-08-20-turn-error-survives-same-turn-retry-history.zh.md)负责移除曾藏掉耗尽恢复终态错误行的 Web 重试历史抑制。
diff --git a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml
index 687743c0a4..1b5420b35a 100644
--- a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md
2026-06-21-mandatory-app-attribution-headers.md: 479d3a46dc41c5cc9ae9b77b81dbef3d6524370b
-2026-06-21-mandatory-app-attribution-headers.zh.md: 75162604623099d50ca87aafd5d45567e2121789
+2026-06-21-mandatory-app-attribution-headers.zh.md: 1b11cb6ef1e96609c6777134a85de298ca979c58
diff --git a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md
index 7516260462..1b11cb6ef1 100644
--- a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md
@@ -6,7 +6,7 @@ Status: implemented
## 问题
-LLM(大语言模型)提供方请求应当标识发出请求的产品。这对提供方侧的技术支持、滥用调查、兼容性调试和流量分析都有价值。在本 Agent Note 之前,harness 只做了部分工作:手写的 DeepSeek 适配器发送了一个手动复制的 `User-Agent` 常量(`packages/llm/llm-deepseek/src/adapter.ts`),而基于 pi-ai 的孪生适配器则完全不发送 harness 自有的头部(`packages/llm/llm-pi-ai/src/adapter.ts`)。因此新适配器可以悄无声息地省略归属标识,而基于库的适配器也可能与手写适配器产生偏差——尽管[孪生适配器 Agent Note](2026-06-13-twin-llm-adapters.md) 的存在正是为了确保两种实现中的提供方约定真实可靠。
+LLM(大语言模型)提供方请求应当标识发出请求的产品。这对提供方侧的技术支持、滥用调查、兼容性调试和流量分析都有价值。在本 Agent Note 之前,harness 只做了部分工作:手写的 DeepSeek 适配器发送了一个手动复制的 `User-Agent` 常量(`packages/llm/llm-deepseek/src/adapter.ts`),而基于 pi-ai 的孪生适配器则完全不发送 harness 自有的头部(`packages/llm/llm-pi-ai/src/adapter.ts`)。因此新适配器可以悄无声息地省略归属标识,而基于库的适配器也可能与手写适配器产生偏差——尽管[孪生适配器 Agent Note](2026-06-13-twin-llm-adapters.zh.md) 的存在正是为了确保两种实现中的提供方约定真实可靠。
直接触发因素来自 OpenRouter 的[应用归属](https://openrouter.ai/docs/app-attribution)文档。OpenRouter 根据 `HTTP-Referer` 加上用于展示和分类的头部来创建应用页面和排名。这有价值,但它不是 HTTP 标准中的应用身份机制。风险在于:把 OpenRouter 的精确头部集当作通用标准来采纳,然后将提供方特有的头部泄漏到直连 DeepSeek 的请求、未来的 OpenAI/Anthropic/Vertex 适配器、测试服务器或无限期记录未知字段的代理中。
@@ -24,7 +24,7 @@ LLM(大语言模型)提供方请求应当标识发出请求的产品。这
## 决策
-在 LLM 适配器边界,提供方无关的应用归属是强制的,且仅使用标准 `User-Agent` 头部。规则:每个产品级 LLM 适配器在每个提供方 HTTP 请求上发送一个静态、非机密的应用身份,且每个适配器都有测试证明 `User-Agent` 到达了线路(mock 服务器断言收到的头部;对于基于库的适配器,通过库的头部钩子馈入同一个 mock 服务器断言)。这条规则约束应用归属,不约束提供方特有的请求身份;[DeepSeek 请求身份决策](../feature/2026-08-11-deepseek-request-user-id-header.md)另行负责其用户与会话头部。
+在 LLM 适配器边界,提供方无关的应用归属是强制的,且仅使用标准 `User-Agent` 头部。规则:每个产品级 LLM 适配器在每个提供方 HTTP 请求上发送一个静态、非机密的应用身份,且每个适配器都有测试证明 `User-Agent` 到达了线路(mock 服务器断言收到的头部;对于基于库的适配器,通过库的头部钩子馈入同一个 mock 服务器断言)。这条规则约束应用归属,不约束提供方特有的请求身份;[DeepSeek 请求身份决策](../feature/2026-08-11-deepseek-request-user-id-header.zh.md)另行负责其用户与会话头部。
OpenRouter 应用归属刻意未实现。`HTTP-Referer`、`X-OpenRouter-Title`、`X-Title` 和 `X-OpenRouter-Categories` 是 OpenRouter 特有的产品展示头部,不是提供方无关的模型请求归属。它们可以后续由 OpenRouter 适配器或显式 OpenRouter 模式提出,附带自己的隐私/产品决策、测试和文档。在此之前,即使请求指向 OpenRouter,也只发送本决策定义的共享 `User-Agent` 归属。
diff --git a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml
index bc92549f44..f0e60bbc20 100644
--- a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md
2026-06-24-web-capability-seam.md: 7e7b09f19864bd2ad8ad9d69579c1d5c79600cde
-2026-06-24-web-capability-seam.zh.md: d6051eec498edb640773ba367038581cebd1f654
+2026-06-24-web-capability-seam.zh.md: dbb41ee42d2c7503955ead2df32abe80b3a4f641
diff --git a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md
index d6051eec49..dbb41ee42d 100644
--- a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md
@@ -16,7 +16,7 @@ harness 需要面向模型的 web 工具,但不能将模型约定绑定到某
## 决策
-Web 访问是一个一等能力 seam,遵循[能力 seam Agent Note](2026-06-13-capability-seams.md):
+Web 访问是一个一等能力 seam,遵循[能力 seam Agent Note](2026-06-13-capability-seams.zh.md):
1. `@deepseek-ai/dsh-web`(`packages/web/web`)拥有 `ctx.web`、提供方注册、提供方选择、共享的请求/结果词汇,以及 web 特有的错误。
2. 提供方包实现具体后端并向 `ctx.web` 注册能力,例如 `@deepseek-ai/dsh-web-search-exa`、`@deepseek-ai/dsh-web-search-perplexity`、`@deepseek-ai/dsh-web-search-deepseek` 和 `@deepseek-ai/dsh-web-fetch-http`。
@@ -280,7 +280,7 @@ SSRF/私有网络防护(阻断私有、回环、链路本地、多播及其他
## 测试
-每一层在自己的边界处固定:`dsh-web` 中的注册/选择/截断/abort 约定与 `WebError` 码;每个提供方基于录制的 fixture(测试前置数据)的请求/响应映射(Perplexity fixture 包含纯 URL 引用,以保持可选 source 字段的诚实性),加上每个真实提供方的自跳过带密钥冒烟测试;`web-fetch-http` 中的真实本地 HTTP 行为;`dsh-tool-web` 中通过真实工具注册表的启用驱动注册、结构化执行错误和结果格式化。一个真实 Loader 冒烟测试守护两种导出形状([事故复盘(postmortem) 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)):`dsh-web` 是默认导出的服务,而提供方和 `tool-web` 是命名空间插件,误加 `export default` 会丢失 `inject`。
+每一层在自己的边界处固定:`dsh-web` 中的注册/选择/截断/abort 约定与 `WebError` 码;每个提供方基于录制的 fixture(测试前置数据)的请求/响应映射(Perplexity fixture 包含纯 URL 引用,以保持可选 source 字段的诚实性),加上每个真实提供方的自跳过带密钥冒烟测试;`web-fetch-http` 中的真实本地 HTTP 行为;`dsh-tool-web` 中通过真实工具注册表的启用驱动注册、结构化执行错误和结果格式化。一个真实 Loader 冒烟测试守护两种导出形状([事故复盘(postmortem) 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.zh.md)):`dsh-web` 是默认导出的服务,而提供方和 `tool-web` 是命名空间插件,误加 `export default` 会丢失 `inject`。
## 曾考虑的替代方案
@@ -329,10 +329,10 @@ SSRF/私有网络防护(阻断私有、回环、链路本地、多播及其他
- `web_fetch` 的 SSRF/私有网络防护:阻断私有、回环、链路本地、多播及其他非公开目的地,使 `web_fetch` 不再是 SSRF 原语。正确实现不仅仅是 URL 字符串检查——需要先 DNS 解析再连接到已验证的 IP(防御 DNS rebinding/TOCTOU)、跨重定向的每跳重新验证,以及 IPv6 边缘处理(私有范围、IPv4 映射地址)。所调研的参考实现均未做 IP 级阻断(OpenCode 做前缀检查后直接 fetch;Claude Code 依赖集中式主机名黑名单加「私有 URL 会失败」的提示词),因此没有可复制的实现,且这是 harness 唯一的 SSRF 防线——值得一次专门的设计/spike。在其落地之前,`web_fetch` 只能在无法触达敏感内部目标的部署中启用。
- `pdf` `WebFetchBody` 类别:`http` 提供方将可文本提取的 PDF 解码(尽力而为、有上限、`truncated`)为 `{ kind: 'pdf'; content; pageCount? }` 分支,`tool-web` 渲染它。这是 fetch 而非 `web_extract`——PDF 获取是具体的 HTTP 200 加确定性的本地解码,不是提供方侧对非 HTTP 资源的提取。添加它是跨 `dsh-web`(声明分支)、提供方(解码 + 将「二进制拒绝」收窄为「拒绝二进制,但可文本提取的 PDF 除外」;需要 OCR 的扫描/图片 PDF 不在范围内)和 `tool-web`(渲染)的协调变更。封闭的 `WebFetchBody` 联合类型使消费方在新分支被处理之前编译失败。
- 提供方支撑的提取作为独立的 `web_extract` 能力,而非静默扩展 `web_fetch`。
-- 权限策略集成:权限系统现已存在([沙箱与审批](../feature/2026-07-06-sandbox.md)、[web 权限预设](../feature/2026-07-23-web-permission-and-approval.md)),但只捆绑了沙箱模式与审批策略;web 权限策略仍未集成。
+- 权限策略集成:权限系统现已存在([沙箱与审批](../feature/2026-07-06-sandbox.zh.md)、[web 权限预设](../feature/2026-07-23-web-permission-and-approval.zh.md)),但只捆绑了沙箱模式与审批策略;web 权限策略仍未集成。
- `query` 和 `maxResults` 之外的提供方无关搜索控制,待 Exa 和 Perplexity 都能诚实遵守时再添加。
## 开放问题
- 产品应用包是否应在启动时探测 web 配置(当 web 被显式配置时将 `WEB_PROVIDER_CONFIGURED_MISSING`、`WEB_PROVIDER_CONFIGURED_UNAVAILABLE` 和 `WEB_PROVIDER_AMBIGUOUS` 视为致命错误),还是将配置错误留到首次执行时浮出?
-- 在已交付的权限系统([沙箱与审批](../feature/2026-07-06-sandbox.md)、[web 权限预设](../feature/2026-07-23-web-permission-and-approval.md))中,公开 web 访问的权限策略应放在哪里:`tools/execute` 上的专用 web 权限插件、提供方配置,还是两者兼有?
+- 在已交付的权限系统([沙箱与审批](../feature/2026-07-06-sandbox.zh.md)、[web 权限预设](../feature/2026-07-23-web-permission-and-approval.zh.md))中,公开 web 访问的权限策略应放在哪里:`tools/execute` 上的专用 web 权限插件、提供方配置,还是两者兼有?
diff --git a/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.i18n.yaml
index c49cd144b9..421739cb57 100644
--- a/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md
2026-06-26-file-context-as-event-gate.md: a8316741c3ce8548db5b5def431425589994995c
-2026-06-26-file-context-as-event-gate.zh.md: 78b1b759edc0340fd7be103fc9bfec0b125e0800
+2026-06-26-file-context-as-event-gate.zh.md: 618985f33073cf9fd69cf2c7fda7112d9752468f
diff --git a/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.zh.md b/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.zh.md
index 78b1b759ed..618985f330 100644
--- a/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.zh.md
@@ -6,7 +6,7 @@ Status: implemented
## 问题
-[拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) 在面向模型的工具与 `ctx.fs` 提供方之间放置了 `ctx.fileContext`:`dsh-tool-fs` 注入 `fileContext`,并将每次 `read`/`write`/`edit` 路由到它的方法。这使得 `fileContext` **位于关键路径上且不可省略**。工具不经过它就无法访问 `ctx.fs`,策略层掌控着 fs I/O 和读取窗口,而一个不需要观测状态策略的部署也无法简单地移除该包——`dsh-tool-fs` 会因无法解析 `ctx.fileContext` 而失败。
+[拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.zh.md) 在面向模型的工具与 `ctx.fs` 提供方之间放置了 `ctx.fileContext`:`dsh-tool-fs` 注入 `fileContext`,并将每次 `read`/`write`/`edit` 路由到它的方法。这使得 `fileContext` **位于关键路径上且不可省略**。工具不经过它就无法访问 `ctx.fs`,策略层掌控着 fs I/O 和读取窗口,而一个不需要观测状态策略的部署也无法简单地移除该包——`dsh-tool-fs` 会因无法解析 `ctx.fileContext` 而失败。
这把三件本应可分离的事情耦合在了一起:
@@ -33,7 +33,7 @@ provider dsh-fs-local local implementation of ctx.fs
该模型是叠加式的:裸 `ctx.fs` 执行原子化、无约束的文本 I/O,而 `dsh-fs-observation-policy` 叠加观测状态、先读后编辑和版本守卫。因此移除策略层后工具仍可用,只是不受约束。正式发布的 agent(智能体)配置会加载策略;裸模式的存在是为了让策略在服务边界保持可选,而非作为正常部署姿态。
-[文件系统缺失观测后续决策](../bug-fix/2026-08-09-filesystem-absence-observation.md)把记录载荷从仅表示成功的版本细化为显式的存在/缺失状态,并要求带防护的创建以不替换方式发布。事件门控归属与无 I/O 策略边界保持不变。
+[文件系统缺失观测后续决策](../bug-fix/2026-08-09-filesystem-absence-observation.zh.md)把记录载荷从仅表示成功的版本细化为显式的存在/缺失状态,并要求带防护的创建以不替换方式发布。事件门控归属与无 I/O 策略边界保持不变。
`dsh-tool-fs` 不再注入 `fileContext`。它注入 `fs` 和 `tools`/`systemPrompt`。
@@ -152,7 +152,7 @@ interface Events {
## 取代关系
-本 Agent Note 修正——而非推翻——[拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md)。四层拆分、提供方约定和新鲜度*策略*均保留。变更的是**工具与策略层之间的耦合方式**:强制性方法服务变为插件拥有的事件门控,fs I/O + 读取窗口从 `fileContext` 上移至 `dsh-tool-fs`。拆分文件系统 seam Agent Note 中关于 `dsh-tool-fs` 注入 `fileContext` 以及 `fileContext` 拥有 `read`/`write`/`edit` 的描述已在同一变更中更新。
+本 Agent Note 修正——而非推翻——[拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.zh.md)。四层拆分、提供方约定和新鲜度*策略*均保留。变更的是**工具与策略层之间的耦合方式**:强制性方法服务变为插件拥有的事件门控,fs I/O + 读取窗口从 `fileContext` 上移至 `dsh-tool-fs`。拆分文件系统 seam Agent Note 中关于 `dsh-tool-fs` 注入 `fileContext` 以及 `fileContext` 拥有 `read`/`write`/`edit` 的描述已在同一变更中更新。
## 验证
@@ -160,7 +160,7 @@ interface Events {
## 曾考虑的替代方案
-- **保留 `ctx.fileContext` 作为关键路径上的方法服务**——[拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) 最初落地的形态;否决,因为工具无法在没有策略层的情况下运行,使策略对基本操作是承重性的,而非可选的收紧。
+- **保留 `ctx.fileContext` 作为关键路径上的方法服务**——[拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.zh.md) 最初落地的形态;否决,因为工具无法在没有策略层的情况下运行,使策略对基本操作是承重性的,而非可选的收紧。
- **策略侧版本检查**(`dsh-fs-observation-policy` 在其 waterfall 处理器中 stat 并比较版本)——否决,因为该检查与工具实际写入之间存在 TOCTOU 间隙;提供方的 mutation 临界区是唯一无竞态的位置,因此策略只选择 CAS 基准并对先前观测进行门控。
- **每工具 `/read`/`/write`/`/edit` 子路径插件**——实现时放弃:没有消费方需要单工具部署,且子路径发布迫使引入兄弟工具包都不需要的定制 `tsdown`/`tsconfig`/`files`/workspace-constraint 处理;每工具的注册辅助函数仍作为根插件组合的内部模块保留。
diff --git a/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.i18n.yaml
index 7a5d35f882..04eec8e0e1 100644
--- a/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md
2026-06-30-bash-stdin-env-trusted-plugin-api.md: 41be63fff598587ee9b873cf7edf51da788bc02a
-2026-06-30-bash-stdin-env-trusted-plugin-api.zh.md: 4f0338cbb6a265ff629a81e9c9a12c4456941ba0
+2026-06-30-bash-stdin-env-trusted-plugin-api.zh.md: 60721539d9d37857e145e289bb262482588139bf
diff --git a/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.zh.md b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.zh.md
index 4f0338cbb6..60721539d9 100644
--- a/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.zh.md
@@ -8,7 +8,7 @@ Status: implemented
钩子子系统以 Claude Code 和 Codex 的方式运行外部钩子命令:钩子是一条 shell 命令,通过 **stdin 上的 JSON** 接收事件载荷,并从若干**环境变量**(`CLAUDE_PROJECT_DIR`、`CLAUDE_PLUGIN_ROOT`、`PLUGIN_ROOT`……)读取上下文。harness 已经在 `ctx.shell` 能力 seam 后面有一个完善的命令执行器([dsh-shell](../../../../packages/shell/shell) → [dsh-bash-local](../../../../packages/shell/bash-local)),具备进程组终止、输出截断/spill 处理和凭证擦除功能。复用它来执行钩子意味着钩子桥接层无需重新实现子进程底层机制——但该 seam 此前无法写入 stdin 或设置额外 env。本次变更添加这两个输入。
-`stdin` 和 `env` 不构成新的模型能力,因为普通 shell 语法已经能提供两者。环境凭证由 `dsh-bash-local` 的子环境擦除机制保护,而非靠隐藏这些 Service Definition 字段;模型工具参数是静态 JSON,不会展开 shell 变量。因此这些字段服务于受信的进程内调用方(如钩子桥接层),它们需要传递结构化输入和 `CLAUDE_*` 变量,而不必将其嵌入模型可见的 shell 文本。环境变量规则见 [defensive-patterns.md](../../../../docs/defensive-patterns.md)。
+`stdin` 和 `env` 不构成新的模型能力,因为普通 shell 语法已经能提供两者。环境凭证由 `dsh-bash-local` 的子环境擦除机制保护,而非靠隐藏这些 Service Definition 字段;模型工具参数是静态 JSON,不会展开 shell 变量。因此这些字段服务于受信的进程内调用方(如钩子桥接层),它们需要传递结构化输入和 `CLAUDE_*` 变量,而不必将其嵌入模型可见的 shell 文本。环境变量规则见 [defensive-patterns.md](../../../../docs/defensive-patterns.zh.md)。
## 决策
@@ -16,7 +16,7 @@ Status: implemented
三个有意为之的选择:
-1. **模型侧工具不暴露 `stdin` 和 `env`。** Shell 语法已覆盖这些需求,重复参数只会增加接口面而不带来权限隔离。工具仅从声明的模型参数、signal 和 owner 构建请求;受信的进程内调用方可以直接设置请求字段。harness 自有变量使用[托管环境决策](../feature/2026-07-10-agent-session-identity-and-log-location.md)规定的独立 `dshEnv` 通道,因此普通 `env` 无法替换它们。
+1. **模型侧工具不暴露 `stdin` 和 `env`。** Shell 语法已覆盖这些需求,重复参数只会增加接口面而不带来权限隔离。工具仅从声明的模型参数、signal 和 owner 构建请求;受信的进程内调用方可以直接设置请求字段。harness 自有变量使用[托管环境决策](../feature/2026-07-10-agent-session-identity-and-log-location.zh.md)规定的独立 `dshEnv` 通道,因此普通 `env` 无法替换它们。
2. **`env` 在凭证擦除之后合并,因此调用方显式设置的条目即使具有凭证形态的名称也会胜出。** 后续的托管命名空间决策负责管理 `DSH_*`:这类环境条目会被移除,受信的 `dshEnv` 最后合并,因此普通 `env` 条目永远无法顶掉托管值。完整顺序为 `scrub(process.env, including DSH_*)` → `ENV_OVERRIDES` → 普通 `env` → `dshEnv`。
@@ -30,4 +30,4 @@ Status: implemented
## 后果
-钩子桥接层通过既有的 bash seam 传递 JSON 载荷和钩子特定变量,保留其进程组终止、截断和 spill 行为。面向模型的行为不变,bash 工具仍是模型调用请求构建的唯一所有者。相关词汇定义见 [bash 数据结构参考](../../../../docs/subsystems/shell.md)。
+钩子桥接层通过既有的 bash seam 传递 JSON 载荷和钩子特定变量,保留其进程组终止、截断和 spill 行为。面向模型的行为不变,bash 工具仍是模型调用请求构建的唯一所有者。相关词汇定义见 [bash 数据结构参考](../../../../docs/subsystems/shell.zh.md)。
diff --git a/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml
index 171f33fe17..feb512101f 100644
--- a/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.md
2026-06-30-event-domain-semantics.md: 70da718b5471ce309a090c8aade3e7290cc949dc
-2026-06-30-event-domain-semantics.zh.md: 6795e1f6f583d6fe7d34efc5e1619da38ab1c89f
+2026-06-30-event-domain-semantics.zh.md: c3b12a167da0a41b792914d82a675a98b3a0b860
diff --git a/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.zh.md b/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.zh.md
index 6795e1f6f5..c3b12a167d 100644
--- a/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.zh.md
@@ -6,7 +6,7 @@ Status: implemented
## 问题
-harness 通过 Cordis 事件分类体系扩展 agent loop(智能体循环)(见[微内核事件分类体系 Agent Note](2026-06-11-microkernel-event-taxonomy.md))。随着该分类体系的增长,三个事件域之间的界限变得模糊:
+harness 通过 Cordis 事件分类体系扩展 agent loop(智能体循环)(见[微内核事件分类体系 Agent Note](2026-06-11-microkernel-event-taxonomy.zh.md))。随着该分类体系的增长,三个事件域之间的界限变得模糊:
- `session/*` 承载持久的、事件溯源的日志(`SessionEventMap`)。
- `agent/*` 承载运行时实时信号,向插件传递 `Agent` 句柄。
@@ -26,14 +26,14 @@ harness 通过 Cordis 事件分类体系扩展 agent loop(智能体循环)
**边界规则:** 持久的、可回放的事实是 `SessionEvent`;实时拦截或瞬态/活对象信号是 `agent`/`tools` Cordis 事件。轮次或步骤边界是持久事实,因此存在于会话日志中并从 `session/event` 源读取——不会被镜像为 `agent/*` emit。
-**将规则应用于边界镜像:** 全部四个边界镜像——`agent/turn-start`、`agent/turn-end`、`agent/step-start`、`agent/step-end`——被**移除**。没有生产消费方需要在边界处获取活的 `Agent`:ACP(Agent Client Protocol)桥接将其进行中的提示词与精确对应的 `session/event` `turn/start`/`turn/end` 事件对关联,其他 transcript 消费方同样从持久流派生边界。见[移除边界镜像事件 Agent Note](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md),该决策由它负责。移除 emit 也简化了循环的 `closeStep`/`closeTurn`(各只需一次 append,无需配对 emit)。
+**将规则应用于边界镜像:** 全部四个边界镜像——`agent/turn-start`、`agent/turn-end`、`agent/step-start`、`agent/step-end`——被**移除**。没有生产消费方需要在边界处获取活的 `Agent`:ACP(Agent Client Protocol)桥接将其进行中的提示词与精确对应的 `session/event` `turn/start`/`turn/end` 事件对关联,其他 transcript 消费方同样从持久流派生边界。见[移除边界镜像事件 Agent Note](../simplification/2026-06-20-remove-agent-boundary-mirror-events.zh.md),该决策由它负责。移除 emit 也简化了循环的 `closeStep`/`closeTurn`(各只需一次 append,无需配对 emit)。
## 后果
- 循环不再 emit 任何边界镜像;`closeStep` 仅追加 `step/end`,`closeTurn` 仅追加 `turn/end`。`Session.append` 负责 post-commit observer 隔离,因此抛出异常的边界 observer 无法改变轮次结果或饿死后续消费方;事件接纳失败或内部校验失败仍会在边界进入日志之前向外抛出。
- 之前通过已移除 emit 观察边界的测试,现在观察持久的 `turn/start`/`turn/end`/`step/start`/`step/end` 会话事件——它们所锁定的行为(边界顺序、步骤计数)不变;只是读取的源移到了规范源。那些测试*抛出异常的轮次边界 emit 监听器*的用例被删除,因为该代码路径不再存在(没有 emit 可供抛出)。按照 [AGENTS.md「测试记录行为,而非黄金真相」](../../../../AGENTS.md),行为与其测试一同迁移(或一同消亡)。
- 循环仅在 `append('step/start')` 返回后才标记步骤已打开(`stepOpen = true`)。内部分发校验在日志推入之前运行,可能在不打开步骤的情况下拒绝;post-commit `session/event` observer 的失败被隔离在 `Session.append` 内部。因此该标记精确表示已提交的、欠一个后续 `step/end` 的边界。
-- 完整实现见[简化 Agent Note「停止将持久边界镜像为 agent 事件」](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md):全部四个边界镜像被移除,所有消费方从 `session/event` 读取边界。`agent/steering`(不是边界镜像)不在该 Agent Note 范围内,由其后续 Agent Note [移除 `agent/steering` 镜像 emit](../../archived/simplification/2026-07-04-remove-agent-steering-mirror.md) 单独移除——它镜像的是持久的中途 steering `user/message`。
+- 完整实现见[简化 Agent Note「停止将持久边界镜像为 agent 事件」](../simplification/2026-06-20-remove-agent-boundary-mirror-events.zh.md):全部四个边界镜像被移除,所有消费方从 `session/event` 读取边界。`agent/steering`(不是边界镜像)不在该 Agent Note 范围内,由其后续 Agent Note [移除 `agent/steering` 镜像 emit](../../archived/simplification/2026-07-04-remove-agent-steering-mirror.md) 单独移除——它镜像的是持久的中途 steering `user/message`。
- 生成的 Cordis 事件表面(`docs/subsystems/` 各页)不再列出镜像事件。
diff --git a/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.i18n.yaml
index fa8401fdc8..933b3d2826 100644
--- a/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md
2026-07-02-tool-render-intent-union.md: 52626d3d9200146df95aee7836d37bbaf7e6a9ec
-2026-07-02-tool-render-intent-union.zh.md: 43fda366694aecda07b276d34c882e75d6b4aa95
+2026-07-02-tool-render-intent-union.zh.md: bc91b89c53c3c43b6c483dc0d4876a00f5a936b7
diff --git a/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.zh.md b/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.zh.md
index 43fda36669..bc91b89c53 100644
--- a/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.zh.md
@@ -4,7 +4,7 @@ Status: implemented
[English](2026-07-02-tool-render-intent-union.md) | 中文
-> render-intent 联合类型对 UI 传输层仍然有效;其 ACP(Agent Client Protocol)映射已被 [ACP 作为仅面向自动化的协议](../simplification/2026-07-23-acp-automation-only-protocol.md)取代。
+> render-intent 联合类型对 UI 传输层仍然有效;其 ACP(Agent Client Protocol)映射已被 [ACP 作为仅面向自动化的协议](../simplification/2026-07-23-acp-automation-only-protocol.zh.md)取代。
## 问题
@@ -78,5 +78,5 @@ terminal 意图只用于展示。harness 仍通过自身的 bash 服务执行命
## 相关
- 取代早先被否决的折叠工具自有呈现提案(已否决——「等两个真实工具和两个真实消费方,然后做带标签 render-intent 联合类型」)中的推迟决定。该条件现已满足;本 Agent Note 即为那个联合类型。
-- 被[结果时已应用 hunk 差异](../../archived/architecture/2026-07-02-result-time-applied-hunk-diffs.md)(已归档)扩展:后者添加了一个持久化的 `meta` 通道,使 write/edit 在结果时输出 `DiffResultView`(应用后的变更:带上下文行的 contextual hunk / 每个 `replace_all` 位点一个,或创建时的整文件 diff)——值/呈现拆分与持久化的 `presentationMeta` 通道现由[规范工具输出约定](2026-07-20-canonical-tool-output-contract.md)拥有。
+- 被[结果时已应用 hunk 差异](../../archived/architecture/2026-07-02-result-time-applied-hunk-diffs.md)(已归档)扩展:后者添加了一个持久化的 `meta` 通道,使 write/edit 在结果时输出 `DiffResultView`(应用后的变更:带上下文行的 contextual hunk / 每个 `replace_all` 位点一个,或创建时的整文件 diff)——值/呈现拆分与持久化的 `presentationMeta` 通道现由[规范工具输出约定](2026-07-20-canonical-tool-output-contract.zh.md)拥有。
- 将 `ToolTerminal` 折入当前 UI 传输层使用的带标签 `terminal` 视图。
diff --git a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml
index 800449486f..13871d1cff 100644
--- a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md
2026-07-05-prompt-variables-and-tool-guidance-ownership.md: 9a53619d9510e3f4fa561f8420b2da3bedbbf4bb
-2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: 6174f277f0a4e8c46ff8d732411f28b4ea97728c
+2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: dc1164198df0f92b843c75b618f140d8aef86e4f
diff --git a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md
index 6174f277f0..dc1164198d 100644
--- a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md
@@ -40,7 +40,7 @@ Status: implemented
### Subagent 对话历史描述符
-`SubagentProvider.inheritsParentContext` 描述的是对话历史初始化,而非作用域、服务、工具或权限。spawn 和 ACP 将其设为 `false`;fork 设为 `true`。`dsh-tool-subagent` 根据该标志派生工具描述和提示词参数描述,包括 fork 继承已完成轮次但不继承进行中轮次这一点。提供方生命周期事件使该措辞与响应式提供方注册保持同步;其设计动机见[提供方生命周期事件 Agent Note](2026-07-05-subagent-provider-lifecycle-events.md)。
+`SubagentProvider.inheritsParentContext` 描述的是对话历史初始化,而非作用域、服务、工具或权限。spawn 和 ACP 将其设为 `false`;fork 设为 `true`。`dsh-tool-subagent` 根据该标志派生工具描述和提示词参数描述,包括 fork 继承已完成轮次但不继承进行中轮次这一点。提供方生命周期事件使该措辞与响应式提供方注册保持同步;其设计动机见[提供方生命周期事件 Agent Note](2026-07-05-subagent-provider-lifecycle-events.zh.md)。
## 曾考虑的替代方案
@@ -49,7 +49,7 @@ Status: implemented
- **在每个 persona 中手写模型名称**:与上方一行的 `model:` 键重复,配置修改后静默失实;正是本决策要治愈的病症。
- **宽松插值(未知引用保留原样或替换为空)**:一个拼写错误 `{{modle}}`(或一个空洞)会被发送给模型,直到 transcript(文本记录)审查时才会被发现。
- **在配置中为每个 subagent 实例编写措辞**:面向模型的行文回到每个部署 × 实例中,重蹈在 leaf YAML 中手写指导的漂移。**根据提供方名称选择措辞**:`providerName` 本身是配置,重命名提供方后会静默获得错误的措辞。
-- **在 `apply` 时解析提供方(加载顺序要求)**与**仅用 section 承载 subagent 措辞(在 assemble 时惰性解析)**:提供方生命周期事件的替代方案;两者均在[提供方生命周期事件 Agent Note](2026-07-05-subagent-provider-lifecycle-events.md)中被否决。
+- **在 `apply` 时解析提供方(加载顺序要求)**与**仅用 section 承载 subagent 措辞(在 assemble 时惰性解析)**:提供方生命周期事件的替代方案;两者均在[提供方生命周期事件 Agent Note](2026-07-05-subagent-provider-lifecycle-events.zh.md)中被否决。
## 不在范围内
diff --git a/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.i18n.yaml
index 336d9669ed..a66c6f6065 100644
--- a/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md
2026-07-05-subagent-provider-lifecycle-events.md: 503c0d638785e3a4944c8903b8d7d469c68b1881
-2026-07-05-subagent-provider-lifecycle-events.zh.md: e7d1d8c398a6d77efb2dbccc365e0040f38f2d33
+2026-07-05-subagent-provider-lifecycle-events.zh.md: d9d15b4ff688506b84b81c8649e1de4ce50e086d
diff --git a/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.zh.md b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.zh.md
index e7d1d8c398..d9d15b4ff6 100644
--- a/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.zh.md
@@ -6,9 +6,9 @@ Status: implemented
## 问题
-[提示词变量 Agent Note](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) 让 `dsh-tool-subagent` 从其提供方派生面向模型的措辞:`SubagentProvider.inheritsParentContext`(spawn 和 ACP(Agent Client Protocol)为 `false`,fork 为 `true`)同时驱动工具描述和 `prompt` 参数描述,使 fork 工具不再在上下文继承问题上对模型撒谎。这一修复引入了跨 fiber 的数据依赖:工具描述在工具注册时固定(这是有意为之——描述是 tool-choice 引导所在之处),但提供方在自己的插件 fiber 上到达,时机不确定。
+[提示词变量 Agent Note](2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md) 让 `dsh-tool-subagent` 从其提供方派生面向模型的措辞:`SubagentProvider.inheritsParentContext`(spawn 和 ACP(Agent Client Protocol)为 `false`,fork 为 `true`)同时驱动工具描述和 `prompt` 参数描述,使 fork 工具不再在上下文继承问题上对模型撒谎。这一修复引入了跨 fiber 的数据依赖:工具描述在工具注册时固定(这是有意为之——描述是 tool-choice 引导所在之处),但提供方在自己的插件 fiber 上到达,时机不确定。
-如果在工具插件的 `apply` 时刻解析提供方,就会产生一个隐式的加载顺序要求(「在 cordis.yml 中把后端列在工具前面」)。这个要求不成立,因为 Cordis Loader 并发启动同级条目,且 `Entry.init()` 不会等待激活完成:延迟到达的后端即使列在前面,也可能让工具 fiber 失败。Loader 不提供同级顺序保证——「异步状态不是同步状态」(见[防御性模式](../../../../docs/defensive-patterns.md))。
+如果在工具插件的 `apply` 时刻解析提供方,就会产生一个隐式的加载顺序要求(「在 cordis.yml 中把后端列在工具前面」)。这个要求不成立,因为 Cordis Loader 并发启动同级条目,且 `Entry.init()` 不会等待激活完成:延迟到达的后端即使列在前面,也可能让工具 fiber 失败。Loader 不提供同级顺序保证——「异步状态不是同步状态」(见[防御性模式](../../../../docs/defensive-patterns.zh.md))。
## 决策
@@ -31,6 +31,6 @@ Status: implemented
## 后果
- 从命名提供方派生状态的消费方响应 `subagent/provider-added`/`-removed` 事件,而非在 `apply` 时读取注册表;`dsh-tool-subagent` 是参考实现。
-- **添加时大声失败;移除时按监听器隔离。** 添加监听器可以回滚注册。移除在 disposal 期间运行,因此单个监听器抛异常只会被记录日志,不会饿死后续镜像或干扰拆解流程。`start()` 仍在每次运行时按名称解析提供方,防止陈旧工具调用已移除的后端。见[事件目录](../../../../docs/subsystems/subagent.md#cordis-surface)与[生产者/消费方映射](../../../../docs/event-producer-consumer.md)。
+- **添加时大声失败;移除时按监听器隔离。** 添加监听器可以回滚注册。移除在 disposal 期间运行,因此单个监听器抛异常只会被记录日志,不会饿死后续镜像或干扰拆解流程。`start()` 仍在每次运行时按名称解析提供方,防止陈旧工具调用已移除的后端。见[事件目录](../../../../docs/subsystems/subagent.zh.md#cordis-surface)与[生产者/消费方映射](../../../../docs/event-producer-consumer.zh.md)。
- **工具不存在的窗口期。** 在后端 disposal 与重新注册之间(HMR 重载期间),模型看不到 subagent 工具。这是诚实的状态——替代方案是一个向空处分发的工具——工具注册表发出的 `tools/change` 事件会使提示词组装保持最新状态。
- **两个等待中的 fiber 共享同一 `toolName` 是无效配置,被延迟捕获。** 如果两个 `dsh-tool-subagent` 加载实例分别指定了不同的提供方但相同的 `toolName`,两者都会等待,先到达的提供方先注册;第二次注册仅在其提供方到达时才抛异常。插件中的 `TODO(subagent-dup-toolname)` 记录了这一影响范围;工具注册表的重名拒绝机制仍是最终防线。
diff --git a/.agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.i18n.yaml
index cf8eaf225c..cc278292d9 100644
--- a/.agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.md
2026-07-05-windows-jsonl-durable-publish.md: 546cde6086f3c84e22b4d4de144a48bb3425cd4e
-2026-07-05-windows-jsonl-durable-publish.zh.md: 205460bcd374bd351cfdf541eb4041c09461229d
+2026-07-05-windows-jsonl-durable-publish.zh.md: b99032e9d7e72e2028f39c5af91659278fd81343
diff --git a/.agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.zh.md b/.agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.zh.md
index 205460bcd3..b99032e9d7 100644
--- a/.agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.zh.md
@@ -28,7 +28,7 @@ Windows 通过持久的暂存发布来创建缺失目录:创建一个以固定
## 影响
-该后端在各平台上维持同一项外部约定:首次追加要么把完整日志发布到最终名称,要么失败且不覆盖已有日志。平台分流只是实现细节;`SessionPersistence` API 和 JSONL 逻辑记录格式均不改变。后续的 [Zstandard 编码决策](2026-07-19-zstandard-jsonl-session-logs.md)会先作用于不透明字节,然后才由任一平台执行发布。
+该后端在各平台上维持同一项外部约定:首次追加要么把完整日志发布到最终名称,要么失败且不覆盖已有日志。平台分流只是实现细节;`SessionPersistence` API 和 JSONL 逻辑记录格式均不改变。后续的 [Zstandard 编码决策](2026-07-19-zstandard-jsonl-session-logs.zh.md)会先作用于不透明字节,然后才由任一平台执行发布。
Windows 测试会在原生 Windows 上执行真实的 Win32 发布路径。断电行为属于 API 约定属性,单元测试无法证明;可测试的不变量包括:Windows 物化不会调用目录 fsync、最终路径冲突会失败、达到最大长度的目标路径组件仍可物化、临时日志在发布前已经执行 fsync,并且生成的日志可以正常加载。
diff --git a/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml
index e7e49aa115..580e74f217 100644
--- a/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md
2026-07-06-timeout-deadline-library.md: 38f048d16ecba0e5278ae34b0c88b7889dcfa47a
-2026-07-06-timeout-deadline-library.zh.md: e8b0ab62b19d931c33027d5e6c9919804e80aa99
+2026-07-06-timeout-deadline-library.zh.md: c8d189c2a7588ee57b0f7fe02137b78c9ad7ff9d
diff --git a/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md
index e8b0ab62b1..c8d189c2a7 100644
--- a/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md
@@ -103,7 +103,7 @@ export function timeoutOf(x: AbortSignal | { reason?: unknown }, code?: string):
- `AbortSignal.any` 和 `using`/`Symbol.dispose` 在此首次进入本仓库(Node ≥ 24 基线,已满足)。
- 模型流现在共享一个可重启的定时器约定,不会把滑动的空闲间隔变成总调用截止时间,也不会计入消费方思考时间。能够观察到带外传输活动的适配器可以对尚未结算的 demand 调用 `pulse()`;被屏蔽的活动对 watchdog 仍不可见。该原语仍然只做通知;适配器测试证明其传输观察到稳定信号并终止。
-以下内容不在本次范围内,列出以标明边界:`web_search` 可以在其工具 schema 和快照覆盖规划完成后获得可选的面向模型的 `timeout_ms`;基于 ripgrep 的文件系统发现工具([打包的 ripgrep 搜索](2026-08-01-packaged-ripgrep-search.md))通过 `dsh-tool-call-timeout-policy` 和 `exec.signal` 消费同样的提供方自有 deadline 形状;`tools/execute` waterfall(瀑布式事件)中间件可以通过驱动 `exec.signal` 为每次工具调用设置默认 deadline——那将是一个*消费*本库的插件,仍然只做通知,硬终止仍是各能力自己的事。
+以下内容不在本次范围内,列出以标明边界:`web_search` 可以在其工具 schema 和快照覆盖规划完成后获得可选的面向模型的 `timeout_ms`;基于 ripgrep 的文件系统发现工具([打包的 ripgrep 搜索](2026-08-01-packaged-ripgrep-search.zh.md))通过 `dsh-tool-call-timeout-policy` 和 `exec.signal` 消费同样的提供方自有 deadline 形状;`tools/execute` waterfall(瀑布式事件)中间件可以通过驱动 `exec.signal` 为每次工具调用设置默认 deadline——那将是一个*消费*本库的插件,仍然只做通知,硬终止仍是各能力自己的事。
## 曾考虑的替代方案
diff --git a/.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.i18n.yaml
index 5e943fed2b..398eaab528 100644
--- a/.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md
2026-07-06-tool-result-retention-library.md: 8d938db8f5fa78398a39e97cc877308200f7d60f
-2026-07-06-tool-result-retention-library.zh.md: 0b2fe841beed21511c4088732713d3fd9fd9048f
+2026-07-06-tool-result-retention-library.zh.md: 747b545ee8c900c13d80c7aef6caecc8ae8dc0ad
diff --git a/.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.zh.md b/.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.zh.md
index 0b2fe841be..747b545ee8 100644
--- a/.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.zh.md
@@ -103,7 +103,7 @@ type TextRetentionStrategy =
`grep` 在分组前使用 `ItemRetainer`,并将其配置为 `{ kind: 'head', maxItems: grepMaxMatches }`。执行器解析 ripgrep 输出、映射路径、应用逐行预览截断,并输入扁平匹配项。调用 `finish()` 后,工具按文件对保留的匹配项分组;如果行内结果达到上限,还可以通过 spill seam 保存完整匹配列表。分组不属于 retainer,因为上限针对匹配总数,而不是文件数;逐匹配项的预览截断和 `incomplete` 也与结果级保留相互独立。
-`bash` 可以使用 `TextRetainer`,配置为 `tail` 或 `headTail`,并读取至进程结束。bash 执行器仍负责 spill 文件、退出状态、信号、超时与后台任务行为;保留辅助函数只在需要该行为时替换临时实现的内存首尾核算。长时间运行任务的所有权与[通用长时间运行工具的运行时](2026-06-20-generic-long-running-tool-runtime.md)相互独立。
+`bash` 可以使用 `TextRetainer`,配置为 `tail` 或 `headTail`,并读取至进程结束。bash 执行器仍负责 spill 文件、退出状态、信号、超时与后台任务行为;保留辅助函数只在需要该行为时替换临时实现的内存首尾核算。长时间运行任务的所有权与[通用长时间运行工具的运行时](2026-06-20-generic-long-running-tool-runtime.zh.md)相互独立。
`web_fetch` 可以使用 `TextRetainer`,配置为 `head` 或 `headTail`;如果提供方必须在内部读取和解码,也可以保留由提供方负责的正文上限。无论采用哪种方式,fetch 结果中的 `truncated` 仍是提供方/工具事实,该库只提供保留文本与省略元数据。
@@ -138,7 +138,7 @@ const formatGrepNotice = (notice: RetentionNotice): string =>
**已交付内容。** `@deepseek-ai/dsh-output-retention` 导出 `ItemRetainer`、`TextRetainer`、结果类型(`RetainedItems`、`RetainedText`)、策略类型(`ItemRetentionStrategy`、`TextRetentionStrategy`)、`Omitted`、`PushDecision`、`RetentionNotice`,以及中性的提示辅助函数 `describeOmitted`/`formatRetentionNotice`,且不依赖 Cordis 或任何工具包。单元测试覆盖具有精确省略计数的条目头部保留、文本头部保留、文本尾部保留、首尾字节保留、零预算、UTF-8 边界处理(2、3、4 字节码位,以及每个裁切位置上的无效起始字节)和未知省略量的措辞。
-**已记录但尚未迁移的内容。** `glob`、`grep`、`bash`、`web_fetch` 与 `web_search` 的映射已记录在[包 README](../../../../packages/util/output-retention/README.md) 中,但本次改动并未把每个工具都迁移到该库;迁移工作刻意留作独立的后续任务。`read` 被明确记录为不在范围内:其 `read-render` 行窗口约定(`offset`/`limit`、`totalLines`、offset 范围错误、逐行预览截断,以及针对所选窗口的字节上限)不属于通用保留,而一个 `Omitted` 计数也无法同时表达行窗口两侧。
+**已记录但尚未迁移的内容。** `glob`、`grep`、`bash`、`web_fetch` 与 `web_search` 的映射已记录在[包 README](../../../../packages/util/output-retention/README.zh.md) 中,但本次改动并未把每个工具都迁移到该库;迁移工作刻意留作独立的后续任务。`read` 被明确记录为不在范围内:其 `read-render` 行窗口约定(`offset`/`limit`、`totalLines`、offset 范围错误、逐行预览截断,以及针对所选窗口的字节上限)不属于通用保留,而一个 `Omitted` 计数也无法同时表达行窗口两侧。
**该库维持的边界。** `truncated` 表示 retainer 因预算省略了原本可用的内容,绝不表示上游不完整。工具专用状态,包括 `incomplete`、权限失败、提供方局部失败、跳过二进制文件、bash spill 路径恢复和无效 UTF-8,均留在工具领域字段中、位于 retainer 之外。未来改动迁移某项工具时,该包的 README 与测试必须证明,除了有意改变的提示措辞外,模型可见的结果文本没有变化。
@@ -152,6 +152,6 @@ const formatGrepNotice = (notice: RetentionNotice): string =>
**把 `read` 窗口交给 `ItemRetainer`。** v1 不予采纳:`read` 是当前唯一的窗口消费方,其语义属于文件分页,而不是通用保留。一个 `Omitted` 计数无法表示行窗口两侧,而且 `read` 还携带 `totalLines`、offset 范围错误、逐行预览截断和针对所选输出的字节上限。让 `read-render` 由工具所有,可以避免共享库围绕一项特例膨胀。
-**让截断成为 `ToolExecutionResult` 的一部分。** 不予采纳:工具注册表将不得不理解工具专用的恢复指引、分组、行号、退出状态和提供方语义。保留是由工具的 Native renderer(原生渲染器)使用的库;模型可见投影继续由工具所有,而[规范值](2026-07-20-canonical-tool-output-contract.md)可以保留完整的已采集结果。
+**让截断成为 `ToolExecutionResult` 的一部分。** 不予采纳:工具注册表将不得不理解工具专用的恢复指引、分组、行号、退出状态和提供方语义。保留是由工具的 Native renderer(原生渲染器)使用的库;模型可见投影继续由工具所有,而[规范值](2026-07-20-canonical-tool-output-contract.zh.md)可以保留完整的已采集结果。
**在每个面向模型的工具 schema 中公开上限。** 不作为默认方案:Claude Code 的 grep 公开 `head_limit`/`offset`,但本 harness 会把常规预算保留为部署配置,除非模型确实需要控制分页。未来可以为具体工具增加类似 read 的续传字段;它不属于共享保留原语。
diff --git a/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml
index 10a29ee499..373b1293ce 100644
--- a/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md
2026-07-07-tool-call-timeout-policy.md: 92618cc8c761b38d7e516c9d00eb3de1c37831a8
-2026-07-07-tool-call-timeout-policy.zh.md: cc633ceaa3840331826f6475e603e78a98f0afd2
+2026-07-07-tool-call-timeout-policy.zh.md: 0303fadd134c6eb8c41449823b62f1458f4523ba
diff --git a/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md
index cc633ceaa3..0303fadd13 100644
--- a/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md
@@ -6,7 +6,7 @@ Status: implemented
## 问题
-[超时/截止时间 Agent Note](2026-07-06-timeout-deadline-library.md) 将计时与分类原语提取到了 `@deepseek-ai/dsh-timeout`,但超时策略仍然附着在各个能力和面向模型的 schema 上。`bash` 暴露了 `timeoutMs`;`web_fetch` 暴露了 `timeout_ms`;`web_search` 没有面向模型的超时参数,尽管提供方已经遵循 `exec.signal`;未来的 grep/glob 工具要么直接导入超时库,要么自行发明超时策略。对于一个插件 SDK 来说,这是错误的编写范式:工具作者通常只需将 `exec.signal` 转发给其调用的实现,而部署策略来决定预算。
+[超时/截止时间 Agent Note](2026-07-06-timeout-deadline-library.zh.md) 将计时与分类原语提取到了 `@deepseek-ai/dsh-timeout`,但超时策略仍然附着在各个能力和面向模型的 schema 上。`bash` 暴露了 `timeoutMs`;`web_fetch` 暴露了 `timeout_ms`;`web_search` 没有面向模型的超时参数,尽管提供方已经遵循 `exec.signal`;未来的 grep/glob 工具要么直接导入超时库,要么自行发明超时策略。对于一个插件 SDK 来说,这是错误的编写范式:工具作者通常只需将 `exec.signal` 转发给其调用的实现,而部署策略来决定预算。
与此同时,仓库中并非所有超时都是面向模型的工具调用预算。钩子通过直接调用 `ctx.shell` 执行命令钩子,而非通过 `ctx.tools.execute()`;`bash` 模型工具通过同一个后端复用前台执行、后台启动、后台轮询和钩子复用。一步到位地将所有超时移入工具插件会混淆这些路径,并有破坏钩子超时语义的风险。
@@ -16,7 +16,7 @@ Status: implemented
- `@deepseek-ai/dsh-timeout` 仍是拥有 `deadline()` 和 `timeoutOf()` 的共享库。
- `@deepseek-ai/dsh-tools` 在 `tools/pre-execute` 和 `tools/post-execute` 之间有一个环绕分发的 waterfall(瀑布式事件)`tools/execute`。
-- [仓库命名约定](2026-08-11-repository-naming-contract-and-rename-ledger.md)使用 `@deepseek-ai/dsh-tool-call-timeout-policy`,准确说明该策略所限制的操作。插件从 runtime 读取每个工具声明的 `timeoutMs`,并通过派生新的 `exec.signal` 来包装有此声明的调用。
+- [仓库命名约定](2026-08-11-repository-naming-contract-and-rename-ledger.zh.md)使用 `@deepseek-ai/dsh-tool-call-timeout-policy`,准确说明该策略所限制的操作。插件从 runtime 读取每个工具声明的 `timeoutMs`,并通过派生新的 `exec.signal` 来包装有此声明的调用。
执行流水线如下:
@@ -52,7 +52,7 @@ catch 是基础 `next`(而非 waterfall 之外的东西)这一点至关重
searchTimeoutMs: 30000
```
-超时放在工具定义上而非自由文本名称映射中,消除了拼错名称导致策略不生效的问题。`defineTool` 校验预算为正有限数。分发期间,执行器派生截止信号并将其赋给 `exec.signal`;注册表依据[工具取消约定](2026-07-19-cooperative-tool-cancellation.md),在执行工具体之前将该截止信号与调用方的原始信号融合。执行器随后恢复调用方信号,并将自身的超时转换为 `TOOL_TIMEOUT`;没有预算的工具原样通过。
+超时放在工具定义上而非自由文本名称映射中,消除了拼错名称导致策略不生效的问题。`defineTool` 校验预算为正有限数。分发期间,执行器派生截止信号并将其赋给 `exec.signal`;注册表依据[工具取消约定](2026-07-19-cooperative-tool-cancellation.zh.md),在执行工具体之前将该截止信号与调用方的原始信号融合。执行器随后恢复调用方信号,并将自身的超时转换为 `TOOL_TIMEOUT`;没有预算的工具原样通过。
信号替换采用**就地修改 `exec.signal`** 的方式,而非向 `next()` 传递新对象。Cordis 的 waterfall `next()` 忽略传入的任何参数,并以共享的 payload 数组重新调用下游监听器(`vendor/cordis/src/events.ts`),因此修改共享对象是包装器向注册表提供截止信号的方式。注册表会在进入工具体前再次融合已捕获的调用方信号;插件则在 `finally` 中将 `exec.signal` 恢复为调用方的原始值,使 `tools/post-execute` 永远不会看到本插件的截止信号。
diff --git a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml
index efed9ca5ea..20d0e4f85c 100644
--- a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md
2026-07-08-agent-scope-contexts.md: eb3f6f247bac1a1d81aa2644132c7b9cc04d602c
-2026-07-08-agent-scope-contexts.zh.md: fe82ac18d07de97330e86461e6bdf86edc37d2ae
+2026-07-08-agent-scope-contexts.zh.md: a0f4ffb0ef80dd2fc1ee61c9ab3f4730c28c687e
diff --git a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md
index fe82ac18d0..a0f4ffb0ef 100644
--- a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md
@@ -16,7 +16,7 @@ Status: implemented
每个存活的 agent 拥有一个扁平的注册层,通过 `agent.ctx` 暴露。代码通过拥有某项贡献的上下文进行注册;具备作用域感知的服务将部署全局注册与恰好一个匹配的 agent 层合并;操作从其真实 agent 选择该层;该层在 agent 的完整发布生命周期内存在。
-Cordis 是 SDK 底层的插件框架。Cordis **上下文**是插件用来访问服务和注册效果的对象,效果的清理跟随该上下文。[Cordis 入门](../../../../docs/cordis-primer.md)对该框架有更详细的说明。
+Cordis 是 SDK 底层的插件框架。Cordis **上下文**是插件用来访问服务和注册效果的对象,效果的清理跟随该上下文。[Cordis 入门](../../../../docs/cordis-primer.zh.md)对该框架有更详细的说明。
对大多数贡献者而言,完整约定是四条规则:
@@ -45,7 +45,7 @@ flowchart LR
缺失的交叉边即隔离规则:Agent A 的本地注册不会进入 Agent B 的视图,父级的注册也不会仅因父级拥有子级的生命周期就进入子级。
-配套的[运行时设计 Agent Note](2026-07-12-agent-scope-runtime-design.md) 阐述了实现与正确性推理。[subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) 负责独立的 `persona`、`toolFilter` 和 `maxDepth` 功能。
+配套的[运行时设计 Agent Note](2026-07-12-agent-scope-runtime-design.zh.md) 阐述了实现与正确性推理。[subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md) 负责独立的 `persona`、`toolFilter` 和 `maxDepth` 功能。
### 注册来源决定可见性与清理
@@ -104,7 +104,7 @@ setup 接收一个完整的受信 Cordis 上下文,因此可以组合普通插
在 Cordis 层面,`Scoped` 是一个不透明的路由接收器。它携带用于选择监听器的过滤器,但本身不是领域对象。因此事件签名将真实的 `Agent`、工具执行、审批请求或其他主体作为显式参数保留,供监听器检查。
-以 `{ global: true }` 注册的监听器有意绕过上下文受众过滤,但其清理仍跟随注册上下文。注册表成员变更通知保持不过滤,因为它们描述的是共享注册表状态而非某个 agent 的操作。详尽的事件参考是各[子系统页面](../../../../docs/subsystems/core.md)上生成的 `cordis-surface` 区块的集合——每个事件作用域在其所属页面上(`agent/*` 与 `agent-loop/*` 在 core.md 本页)。
+以 `{ global: true }` 注册的监听器有意绕过上下文受众过滤,但其清理仍跟随注册上下文。注册表成员变更通知保持不过滤,因为它们描述的是共享注册表状态而非某个 agent 的操作。详尽的事件参考是各[子系统页面](../../../../docs/subsystems/core.zh.md)上生成的 `cordis-surface` 区块的集合——每个事件作用域在其所属页面上(`agent/*` 与 `agent-loop/*` 在 core.md 本页)。
### 创建最后发布,dispose 最后撤销
@@ -138,6 +138,8 @@ flowchart TB
detach --> revoke["Dispose the agent scope"]
```
+
+
## 安全与权限是非目标
agent 作用域组合的是受信的同进程注册。它不沙箱化插件、不定义父到子的权限格、不在创建时冻结授权、也不保证子级不能做超出父级的事。
@@ -146,6 +148,8 @@ agent 作用域组合的是受信的同进程注册。它不沙箱化插件、
需要非升权保证的部署需要独立的权限表示、传播规则和执行检查。父级子集授权、创建时授权快照、显式未来授权 API,以及通用的能力/输出/终止标签均不在本决策范围内。
+
+
## 曾考虑的替代方案
被否决的设计要么将可见性与清理分离,要么只覆盖一类注册,要么重复共享基础设施,要么将生命周期所有权与继承混为一谈。
diff --git a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.i18n.yaml
index 5f9f8ba421..15abdc91f0 100644
--- a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md
2026-07-08-tool-output-spill-files.md: 4d1d4b7b665f34b362df2f8c8aeb06d96bf2668f
-2026-07-08-tool-output-spill-files.zh.md: 1a3387830861c6d05318024870325523b8eeee36
+2026-07-08-tool-output-spill-files.zh.md: 3e7ce4f57a0078c6a4b919436946be8e172fa7cb
diff --git a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md
index 1a33878308..3e7ce4f57a 100644
--- a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md
@@ -8,9 +8,9 @@ Status: implemented
工具输出需要有界的模型可见预览,但部分超大结果仍可能在之后有用。抓取的页面正文或冗长的工具响应不应完整占用下一次模型请求,但模型应能使用现有文件读取工具,在之后查看经过格式化的完整结果。
-这项改动之前的行为并不一致。`dsh-bash-local` 已经会在内存尾部溢出时,把完整 stdout/stderr 流写入私有的临时 spill 文件;普通文本工具结果则仍以内联形式返回,除非工具自行实现上限。[工具结果保留库](2026-07-06-tool-result-retention-library.md)负责预览机制,但不负责存储,也不负责把这些机制应用于最终工具结果的执行流水线策略。
+这项改动之前的行为并不一致。`dsh-bash-local` 已经会在内存尾部溢出时,把完整 stdout/stderr 流写入私有的临时 spill 文件;普通文本工具结果则仍以内联形式返回,除非工具自行实现上限。[工具结果保留库](2026-07-06-tool-result-retention-library.zh.md)负责预览机制,但不负责存储,也不负责把这些机制应用于最终工具结果的执行流水线策略。
-其形态与超时策略设计一致:工具作者声明规范值与 Native renderer(原生渲染器),由策略插件在渲染后的内容上执行部署默认的上下文预算。工具仍可在提供方采集上限处提前 spill;由工具负责的展示 spill 可以保留已完整采集的规范值,而只替换展示内容。[规范工具输出约定](2026-07-20-canonical-tool-output-contract.md)规定了这项区分。
+其形态与超时策略设计一致:工具作者声明规范值与 Native renderer(原生渲染器),由策略插件在渲染后的内容上执行部署默认的上下文预算。工具仍可在提供方采集上限处提前 spill;由工具负责的展示 spill 可以保留已完整采集的规范值,而只替换展示内容。[规范工具输出约定](2026-07-20-canonical-tool-output-contract.zh.md)规定了这项区分。
## 决策
diff --git a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml
index 0321d494a0..ec2f5318d6 100644
--- a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md
2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md: 89c8917484c11057ff2e6b55e2311e83f724f7ed
-2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: 96f7aa1b0ea4e44ca6318caca2c785d898b1d00d
+2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: 48f954f2c4720894548ecab387965d3a3d1799b0
diff --git a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md
index 96f7aa1b0e..48f954f2c4 100644
--- a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md
@@ -22,9 +22,9 @@ Compact-basic 会在每个拟议请求之前包装 `agent/pre-step`。在续步
### 请求恢复只覆盖最终模型边界
-`agent/request-error` 表示来自最终适配器边界的终止失败。适配器选择、分发、iterator 构造与迭代抛出会在 agent loop(智能体循环)消费前成为终止 `error` 或 `aborted` finish;适配器直接发出的终止 finish 进入同一路径。提示词装配、请求 middleware、请求日志、结果处理、工具、step 监听器与清理仍属于普通失败。[LLM(大语言模型)流的终止失败](2026-07-29-terminal-llm-stream-failures.md)规定这一规范化边界。
+`agent/request-error` 表示来自最终适配器边界的终止失败。适配器选择、分发、iterator 构造与迭代抛出会在 agent loop(智能体循环)消费前成为终止 `error` 或 `aborted` finish;适配器直接发出的终止 finish 进入同一路径。提示词装配、请求 middleware、请求日志、结果处理、工具、step 监听器与清理仍属于普通失败。[LLM(大语言模型)流的终止失败](2026-07-29-terminal-llm-stream-failures.zh.md)规定这一规范化边界。
-恢复运行前,失败 step 已经关闭。负责处理的监听器修复持久状态、返回 `{ kind: 'retry' }`,并停止 waterfall(瀑布式事件)委托。循环随后关闭失败 turn,并从持久日志开启一个重试 turn,中间不发布空闲通知。重试策略与尝试计数由插件自己拥有;compaction-basic 在链路到达终态 `agent/settled` 时清除对应 agent 的溢出计数。两个 DeepSeek 适配器都把识别出的提供方上下文限制错误规范化为 `CONTEXT_WINDOW_EXCEEDED`。[重试动作决策](../simplification/2026-07-27-request-error-retry-action.md)规定这一返回边界。
+恢复运行前,失败 step 已经关闭。负责处理的监听器修复持久状态、返回 `{ kind: 'retry' }`,并停止 waterfall(瀑布式事件)委托。循环随后关闭失败 turn,并从持久日志开启一个重试 turn,中间不发布空闲通知。重试策略与尝试计数由插件自己拥有;compaction-basic 在链路到达终态 `agent/settled` 时清除对应 agent 的溢出计数。两个 DeepSeek 适配器都把识别出的提供方上下文限制错误规范化为 `CONTEXT_WINDOW_EXCEEDED`。[重试动作决策](../simplification/2026-07-27-request-error-retry-action.zh.md)规定这一返回边界。
如果取消发生在 assistant 工具调用已经持久化之后、所有调用完成分发之前,循环会为每个尚未分发的调用记录一对合成的 `tool/call` 与 aborted `tool/result`,随后进入正常中止路径。因此,表层不会仅因取消赢得竞态而留下孤立的持久工具调用。
@@ -58,4 +58,4 @@ Compact-basic 会在每个拟议请求之前包装 `agent/pre-step`。在续步
代价是在共享 pre-step waterfall 中执行压力工作,并需要适配器持续维护溢出分类。提供方措辞与启发式字符密度仍是维护风险。表层压缩依然无法修复仅信封本身就超出窗口的情况,也不能拆分不可分割的非工具节点,或修复不可剪枝的剩余部分仍然过大的工具单元。若可移除的文本工具结果是主要体积,可选剪枝器仍可修复原本不可分割的工具配对。
-[已领取 pre-step 生命周期](2026-07-31-claimed-pre-step-inbox-lifecycle.md)取代了本记录原先的 post-step 触发方式。服务拆分、独立 token meter、平衡范围约定、日志中记录的锁、摘要替换与唯一 `summarize()` 子类 hook 均保持不变。
+[已领取 pre-step 生命周期](2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md)取代了本记录原先的 post-step 触发方式。服务拆分、独立 token meter、平衡范围约定、日志中记录的锁、摘要替换与唯一 `summarize()` 子类 hook 均保持不变。
diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
index 21c607072c..b8bcc4246e 100644
--- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
2026-07-10-single-file-executable-sdk-runtime-distribution.md: 40433d99e5d1aa569c3fdf094a280d3de62ad588
-2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 54030fa4b0742fbc282bc327b0ca22747e6a20bd
+2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: cff72ae10eb82c65c499123cc559cc6ad7e440ab
diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
index 54030fa4b0..cff72ae10e 100644
--- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
@@ -27,8 +27,8 @@ exe 使用 [@yao-pkg/pkg](https://github.com/yao-pkg/pkg)(vercel/pkg 归档后
确定性协议实现(`server.ts` / `transport.ts`)按 `acp/acp` + `examples/acp-demo` 的既有模式落为两包——对外服务接口本身也是插件:
-- [`packages/sdk/server`](../../../../packages/sdk/server/README.md)(`@deepseek-ai/dsh-sdk-jsonrpc-server`):纯协议插件;执行 `apply` 时,在进程 stdio 上挂载 `HarnessSdkJsonRpcServer` 与按行分隔的 JSON-RPC 传输层,资源释放走 `ctx.effect()`。是否提供服务由 `cordis.yml` 决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答并确保 `shutdown` 响应发送完毕后,对根运行时执行 dispose(资源释放),让待处理的持久化操作完成,再调用 `exit(0)`;HMR(热模块替换)式卸载只停止服务,不退出进程)。
-- [`packages/examples/jsonrpc-demo`](../../../../packages/examples/jsonrpc-demo/README.md)(`@deepseek-ai/dsh-sdk-jsonrpc-demo`):轻量应用入口——`installFailLoud` + `loadEnv` + 配置发现 + [`dsh-app-boot`](../../../../packages/boot/app-boot/src/index.ts) 的 `boot()`;`boot()` 完成后入口即完成,服务器由 `cordis.yml` 中的 `dsh-sdk-jsonrpc-server` 条目启动。它只依赖 `app-boot`。进程级退出归 `bin` 所有(stdin EOF/SIGTERM → dispose 后返回 0,SIGINT → 130)。
+- [`packages/sdk/server`](../../../../packages/sdk/server/README.zh.md)(`@deepseek-ai/dsh-sdk-jsonrpc-server`):纯协议插件;执行 `apply` 时,在进程 stdio 上挂载 `HarnessSdkJsonRpcServer` 与按行分隔的 JSON-RPC 传输层,资源释放走 `ctx.effect()`。是否提供服务由 `cordis.yml` 决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答并确保 `shutdown` 响应发送完毕后,对根运行时执行 dispose(资源释放),让待处理的持久化操作完成,再调用 `exit(0)`;HMR(热模块替换)式卸载只停止服务,不退出进程)。
+- [`packages/examples/jsonrpc-demo`](../../../../packages/examples/jsonrpc-demo/README.zh.md)(`@deepseek-ai/dsh-sdk-jsonrpc-demo`):轻量应用入口——`installFailLoud` + `loadEnv` + 配置发现 + [`dsh-app-boot`](../../../../packages/boot/app-boot/src/index.ts) 的 `boot()`;`boot()` 完成后入口即完成,服务器由 `cordis.yml` 中的 `dsh-sdk-jsonrpc-server` 条目启动。它只依赖 `app-boot`。进程级退出归 `bin` 所有(stdin EOF/SIGTERM → dispose 后返回 0,SIGINT → 130)。
配置发现有两个通道,均缺失时立即报错:优先使用 `DSH_CORDIS_CONFIG` 环境变量(SDK 客户端约定),其次使用 argv 位置参数;没有默认路径或内置回退——「实际启动的插件由外部 `cordis.yml` 决定」是硬语义。
@@ -44,11 +44,11 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 恢复被 legacy deploy 提升回源 manifest 的 `node_modules` 下的任何直接工作区包,同时省略其包内依赖树,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的每个符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-sdk-jsonrpc-demo/lib/packaged-bin.js`;`assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg--` 写入 `dist-exe/`,并拷回运行时目录。Linux 安装会从源码构建 `pty.node`;CI 会在打包前进入匹配架构的 manylinux 2.28 容器重新构建该 addon,而 `--legacy` 部署会省略这一副作用目录,因此构建器会把它从根安装目录复制到暂存闭包。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。
-CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[必需的 Python 运行时拉取请求验证](../testing/2026-08-12-required-python-runtime-pull-request-ci.md)调用它构建 linux-x64,手动派发 `workflow_dispatch` 或 PR(Pull Request)的 `build-exe` 标签可以显式选择构建目标,[公开发布工作流](../process/2026-08-11-python-publication-workflow.md)则调用它构建全部目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)和 macos-arm64 三个平台分别进行原生构建,并缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都使用 mock SSE(Server-Sent Events)模型,分别通过默认配置和自定义 `cordis.yml` 驱动 SDK,再通过 NDJSON JSON-RPC 直接驱动 exe,校验 JSONL 与最终响应;最后把发布形态的 wheel 包安装到干净的 venv 中,并在不传 `runtime_bin` 的情况下运行。Linux 还会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并在 manylinux 2.28 容器中运行;macOS 则验证可执行文件的部署目标符合 wheel 包标签。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。
+CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[必需的 Python 运行时拉取请求验证](../testing/2026-08-12-required-python-runtime-pull-request-ci.zh.md)调用它构建 linux-x64,手动派发 `workflow_dispatch` 或 PR(Pull Request)的 `build-exe` 标签可以显式选择构建目标,[公开发布工作流](../process/2026-08-11-python-publication-workflow.zh.md)则调用它构建全部目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)和 macos-arm64 三个平台分别进行原生构建,并缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都使用 mock SSE(Server-Sent Events)模型,分别通过默认配置和自定义 `cordis.yml` 驱动 SDK,再通过 NDJSON JSON-RPC 直接驱动 exe,校验 JSONL 与最终响应;最后把发布形态的 wheel 包安装到干净的 venv 中,并在不传 `runtime_bin` 的情况下运行。Linux 还会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并在 manylinux 2.28 容器中运行;macOS 则验证可执行文件的部署目标符合 wheel 包标签。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。
### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发
-Python SDK 位于 [`python/`](../../../../python/README.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 及其必需的 `-rg` 伴随文件和可选的 macOS helper,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**;`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-sdk-jsonrpc-demo/lib/packaged-bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。
+Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 及其必需的 `-rg` 伴随文件和可选的 macOS helper,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**;`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-sdk-jsonrpc-demo/lib/packaged-bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的 `X.Y.Z` 或预发布版本,把预发布版本转换为 PEP 440 写法,并以该 wheel 包版本暂存两个包,让 `deepseek-harness-sdk` 精确依赖匹配版本的 `deepseek-harness-runtime-bin`。可选的 `python-v` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。暂存过程还会把仓库许可证放入两个 wheel 包,并把第三方声明放入内置运行时 wheel 包。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe 及其架构匹配的 `-rg` 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`,或针对 Node 24 可执行文件 macOS 13.5 部署目标而保守选择的 `py3-none-macosx_14_0_arm64` 标签;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。
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 02aeec5ad6..7d8d2db1cf 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
@@ -3,4 +3,4 @@
# 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: 9d70b8048b1d2bb50290d34158d9deb329d5e15e
-2026-07-12-agent-scope-runtime-design.zh.md: a505f5337559294795c1080937c6079cee235baa
+2026-07-12-agent-scope-runtime-design.zh.md: 278bcede47fee9f67d3d2d2d7135e5357c120161
diff --git a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md
index a505f53375..278bcede47 100644
--- a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md
@@ -6,7 +6,7 @@ Status: implemented
## 问题
-[agent(智能体)作用域约定](2026-07-08-agent-scope-contexts.md)对贡献者而言很简单:通过 `agent.ctx` 注册,解析出一个全局加单 agent 的视图,仅在 setup 完成后发布,并保持作用域直到工作停止。运行时必须在协作式插件框架、异步创建、可重入监听器、持久化会话提交以及 worker 或进程故障等场景下维护这份约定。
+[agent(智能体)作用域约定](2026-07-08-agent-scope-contexts.zh.md)对贡献者而言很简单:通过 `agent.ctx` 注册,解析出一个全局加单 agent 的视图,仅在 setup 完成后发布,并保持作用域直到工作停止。运行时必须在协作式插件框架、异步创建、可重入监听器、持久化会话提交以及 worker 或进程故障等场景下维护这份约定。
主要的设计风险是为每个竞态条件引入第二套机制。独立的预留、就绪哨兵、取消中继、快照层和保护注册表可能镜像同一个事实,直到没有读者能分辨哪个才是权威的。这些机制还会诱使运行时把可信的类型化调用当作敌对的序列化边界来处理。
@@ -30,7 +30,7 @@ Status: implemented
本 Agent Note 余下部分按依赖顺序展开这些选择:Cordis 机制、作用域路由、创建与会话提交、工具与提示词、subagent 与工作流,最后是可执行检查。
-[7 月 8 日 Agent Note](2026-07-08-agent-scope-contexts.md) 仍然是贡献者约定。独立的 [subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) 拥有 `persona`、`toolFilter` 和 `maxDepth`;本文仅讨论它们的 setup 如何融入生命周期。
+[7 月 8 日 Agent Note](2026-07-08-agent-scope-contexts.zh.md) 仍然是贡献者约定。独立的 [subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md) 拥有 `persona`、`toolFilter` 和 `maxDepth`;本文仅讨论它们的 setup 如何融入生命周期。
## Cordis 模型:上下文、fiber、effect、receiver 与 waterfall
@@ -58,6 +58,8 @@ Cordis 使用 dispatch receiver(`this`)过滤监听器,而 harness 的监
Cordis waterfall 是中间件风格的 dispatch。每个监听器接收 `next()`:调用它则委托给剩余监听器和基础操作,不调用则短路或替换下游结果。Waterfall 驱动提示词组装和工具策略;普通 emit 事件同步通知,parallel 事件等待所有监听器但没有否决结果。
+
+
## 作用域路由:一个不透明键选择一层
scope 包实现了 Cordis 路由所需的最小对象。其载体仅持有一个组合的服务过滤器和作用域谓词,而包私有地记录不透明键,并单独暴露会等待作用域 fiber 完全停稳的 disposer。
@@ -72,7 +74,7 @@ Receiver 是一个小型载体而非领域对象的透明代理。需要 agent
### 注册表读取叠加一个精确 layer
-作用域感知的注册表使用 `ScopedLayers`,拥有一个即时创建的全局 aggregate 和按标识键惰性创建的 aggregate。读取解析全局 layer 和至多一个精确局部 layer;它不创建状态,也从不遍历父级链。注册可见性与 Cordis effect 所有权都从同一个上下文派生,而回收会等待具体 layer 的完整 aggregate 变空(见[决策](2026-07-12-scoped-layers-store.md))。
+作用域感知的注册表使用 `ScopedLayers`,拥有一个即时创建的全局 aggregate 和按标识键惰性创建的 aggregate。读取解析全局 layer 和至多一个精确局部 layer;它不创建状态,也从不遍历父级链。注册可见性与 Cordis effect 所有权都从同一个上下文派生,而回收会等待具体 layer 的完整 aggregate 变空(见[决策](2026-07-12-scoped-layers-store.zh.md))。
每个服务保留其领域规则。命名 command 和提示词视图使用共享的、保持插入顺序的 shadow 合并;工具保留更丰富的 resolver,因为限制会在加入局部工具前过滤全局工具,保留的 Code Mode transport 则单独插入。提示词变量和工具 guard 保持实时迭代,而工具提供方成员关系按每次 assembly 物化。Scope 提供存储生命周期和命名遮蔽,而非通用的注册表视图。
@@ -158,6 +160,8 @@ sequenceDiagram
此顺序让最终的 agent 和会话事件能使用匹配的作用域监听器,并使持久化观察者在最终刷新完成前保持附加。作用域 dispose 放在最后,因为注册撤销是外部可见的生命期边界。
+
+
## 会话追加:物化、验证、提交、通知
会话事件跨越持久化边界,因此追加操作拥有其数据。算法的其余部分使用一条已附加的注册表条目和一个提交点。
@@ -208,7 +212,7 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造
私有解析器应用当前展示模式、活跃的全局限制、精确的局部叠加和局部遮蔽。Schema、查找、执行、Code Mode SDK 生成和限制验证都使用该解析器或其限制前的全局名称视图。
-[subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md#tool-filtering-is-one-live-global-view-rule) 拥有用户可见的 allow/deny 语义。实现要求是一致性:被过滤掉的全局工具不能通过另一条查找路径仍可执行,局部遮蔽的定义就是被展示和执行的同一个定义。
+[subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md#tool-filtering-is-one-live-global-view-rule) 拥有用户可见的 allow/deny 语义。实现要求是一致性:被过滤掉的全局工具不能通过另一条查找路径仍可执行,局部遮蔽的定义就是被展示和执行的同一个定义。
`ToolRestriction` 接受 readonly 的 allow/deny 名称并将其编译为内部集合。多个限制取交集。公开的 `visible()` 和 `knownNames()` 方法是不必要的,因为只有注册表需要中间视图。
@@ -230,6 +234,8 @@ SystemPrompt 首先将全局加 agent 的段、变量和工具提供方解析为
Scope 直接解决了真正的隔离问题。结构化输出贡献注册在子级的精确作用域中,而 Code Mode 从同一个已解析的工具视图派生其传输和 SDK。第二套命名保护系统需要另一套所有权和碰撞规则来覆盖任意 schema 提供方(包括有意贡献重复名称的提供方),却不创建新的信任边界。
+
+
### 结构化输出仅提交权威结果
结构化输出将子作用域组合与两阶段执行提交相结合。子级在发布前注册其 `structured_output` 工具和指令;可信的 assembly 监听器可以变换这些普通贡献,并有责任在期望子级完成时保持协议。工具体验证候选值并按当前 `ToolExecution` 暂存,但成功捕获仅由不可变的 `tools/result` 观察决定。
@@ -242,6 +248,8 @@ Scope 直接解决了真正的隔离问题。结构化输出贡献注册在子
纯 Code Mode 的注册表贡献从原生 wire schema 中省略 `structured_output`,并通过生成的 SDK 暴露它。Assembly waterfall 可以有意改变该展示;执行仍然针对子作用域定义进行验证,监听器拥有其创建的任何替代模型可见路由的一致性。
+
+
### 三个执行边界有意设为单向
提示词组装有意是协作式的,但三个执行事实在其可扩展阶段之后需要单向结算:
@@ -266,7 +274,7 @@ subagent 启动有一次所有权转移。提供方拥有未发布资源,直
### 服务约定有一个取消通道
-`SubagentProvider.start()` 和 `SubagentRuntime.start()` 返回 `Promise`。Promise 会在后端跨过发布边界后兑现,因此调用方和 `subagent/start` 观察者从不需要第二个 `run.started` promise。提供方工作如果在发布前失败,`start()` 就会被拒绝;发布后的提示词、轮次、取消与基础设施结果会通过 `SubagentRun.result` 结算,且不会隐藏 child id,这也是[持久化目录决策](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)所要求的约定。
+`SubagentProvider.start()` 和 `SubagentRuntime.start()` 返回 `Promise`。Promise 会在后端跨过发布边界后兑现,因此调用方和 `subagent/start` 观察者从不需要第二个 `run.started` promise。提供方工作如果在发布前失败,`start()` 就会被拒绝;发布后的提示词、轮次、取消与基础设施结果会通过 `SubagentRun.result` 结算,且不会隐藏 child id,这也是[持久化目录决策](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md)所要求的约定。
`SubagentStartRequest.signal` 是必需的。中止它会在启动期间,以及已发布 run 的剩余就绪或轮次工作中请求取消。`SubagentRun.dispose()` 也请求取消并等待完全停稳。没有单独的公开 `run.cancel()` 通道。
@@ -292,6 +300,8 @@ Start 仅在 `initialize` 和 `newSession` 成功后才 resolve。Abort、spawn
Worker 和子进程桥接比同进程注册表需要更多状态,因为消息、进程死亡和清理可以独立结算。它们的状态围绕这些真实事实组织,而非重复的取消协议。
+
+
### 工作流子级是待定 start 或已发布记录
工作流宿主保持待定的提供方 start promise 和已发布的子级记录。子级仅在异步 `SubagentRuntime.start()` 兑现时才从待定变为已发布;被拒绝的 start 清理其部分提供方工作且不产生子级生命周期对。
@@ -308,7 +318,7 @@ Worker 边界仍然序列化请求和结果。宿主保留首个终端结果仲
### ACP 提示词结算不依赖更新投递
-[仅面向自动化的 ACP 桥接层](../simplification/2026-07-23-acp-automation-only-protocol.md)直接将一个进行中的提示词与其观察到的用户消息轮次关联。它不从日志水位线扫描,也不使用会话状态作为第二个调和预言机。
+[仅面向自动化的 ACP 桥接层](../simplification/2026-07-23-acp-automation-only-protocol.zh.md)直接将一个进行中的提示词与其观察到的用户消息轮次关联。它不从日志水位线扫描,也不使用会话状态作为第二个调和预言机。
即使已提交消息的更新无法送达客户端,会话事件监听器也会从匹配的 `turn/end` 结算关联。因此更新投递不能让会话永久处于进行中状态。ACP 创建由服务器分配 id 的全新会话,并拥有由此产生的每个 agent 句柄,直到连接拆除。
@@ -330,13 +340,13 @@ TypeScript 无法管控 JavaScript 强制转换、直接 Cordis dispatch、进
### 生成的产物使公开约定保持对齐
-事件目录、服务目录、生产者/消费方矩阵、配置目录、模块图、工具目录、type-equiv 块和作用域事件解析器映射都是从源码生成或受新鲜度门禁约束的。[TypeScript 语义门禁 Agent Note](../process/2026-07-14-typescript-program-backed-semantic-gates.md) 拥有 Program 构造、语义事件发现和解析器生成规则。
+事件目录、服务目录、生产者/消费方矩阵、配置目录、模块图、工具目录、type-equiv 块和作用域事件解析器映射都是从源码生成或受新鲜度门禁约束的。[TypeScript 语义门禁 Agent Note](../process/2026-07-14-typescript-program-backed-semantic-gates.zh.md) 拥有 Program 构造、语义事件发现和解析器生成规则。
行为测试固定了作用域路由和 dispose、最终写入注册表时的碰撞清理、发布回滚、有序完全停稳、持久化前/后提交行为、跨展示和执行的活跃工具过滤、协作式提示词组装、原生和 Code Mode 中的结构化输出提交、异步 subagent 启动和信号取消、worker 终端仲裁、ACP 结算和进程拆除。
## 曾考虑的替代方案
-[7 月 8 日 Agent Note](2026-07-08-agent-scope-contexts.md#alternatives-considered) 拥有公开扁平作用域约定的替代方案。此处的替代方案关注实现形态。
+[7 月 8 日 Agent Note](2026-07-08-agent-scope-contexts.zh.md#alternatives-considered) 拥有公开扁平作用域约定的替代方案。此处的替代方案关注实现形态。
### 使用透明代理作为作用域载体
@@ -389,4 +399,4 @@ Worker 消息、进程死亡和持久化输入确实跨越所有权和序列化
该设计信任同进程中的类型化插件。它不防御任意强制转换、有状态 getter、违反 readonly 约定的修改,或插件有意在支持的组合 API 之外使用环境服务访问。
-[安全与权限非目标](2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals)仍然是根本性的。这些机制证明注册组合、发布和生命期所有权;它们不证明隔离或父到子的非升权。
+[安全与权限非目标](2026-07-08-agent-scope-contexts.zh.md#security-and-authority-are-non-goals)仍然是根本性的。这些机制证明注册组合、发布和生命期所有权;它们不证明隔离或父到子的非升权。
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 86871f2bcd..205c3c4e39 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
@@ -3,4 +3,4 @@
# 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: 221711748007d5f909aec3fdfcf3919884d11cdb
-2026-07-12-scoped-layers-store.zh.md: 7f3ad11e23f02eeafe955efe08525bda8a9e9996
+2026-07-12-scoped-layers-store.zh.md: 97ad637b91a2b9b2cad9847d8b4cdbe23ff14a48
diff --git a/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.zh.md b/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.zh.md
index 7f3ad11e23..97ad637b91 100644
--- a/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.zh.md
@@ -6,7 +6,7 @@ Status: implemented
## 问题
-agent(智能体)作用域机制([决策](2026-07-08-agent-scope-contexts.md)、[运行时设计](2026-07-12-agent-scope-runtime-design.md))让支持作用域的注册表反复呈现同一种形态:一个全局注册层,加上一个与具体 agent 精确对应的层。七个注册门面都采用这一形态:`tools.register`、`tools.restrict` 和 `tools.guard`(位于 `dsh-tools`);`SystemPrompt.section`、`SystemPrompt.tools` 和 `SystemPrompt.variable`(位于 `dsh-system-prompt`);以及 `CommandRuntime.register`(位于 `dsh-commands`)。
+agent(智能体)作用域机制([决策](2026-07-08-agent-scope-contexts.zh.md)、[运行时设计](2026-07-12-agent-scope-runtime-design.zh.md))让支持作用域的注册表反复呈现同一种形态:一个全局注册层,加上一个与具体 agent 精确对应的层。七个注册门面都采用这一形态:`tools.register`、`tools.restrict` 和 `tools.guard`(位于 `dsh-tools`);`SystemPrompt.section`、`SystemPrompt.tools` 和 `SystemPrompt.variable`(位于 `dsh-system-prompt`);以及 `CommandRuntime.register`(位于 `dsh-commands`)。
如果没有共享原语,每个门面都要围绕自己的领域状态重复相同的生命周期编排:从调用方上下文导出可见性,按需创建专属容器,把属主绑定到同一个 Cordis fiber,先装入 undo 再通知观察者,原样返回 Cordis 的 disposer,并回收空的专属状态。各自分离的映射与集合类型也会让服务缺少一个表示某个 scope 完整贡献的对象。
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 3f4683f480..34523f2723 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
@@ -3,4 +3,4 @@
# 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: 78c8d6788006c503b532ff2bbddd30342415f0a4
-2026-07-14-provider-routed-llm-adapters.zh.md: 5e73cab5f1f2b1296c9a486d1c833e95bb5674a0
+2026-07-14-provider-routed-llm-adapters.zh.md: af8bc4fe27a50d47d7b49b51eada67afe889fc13
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 5e73cab5f1..af8bc4fe27 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
@@ -20,7 +20,7 @@ Status: implemented
`GenerateOptions` 与 `LlmCallConfig` 在 `model: string` 之外携带 `provider: string`,`AgentOptions` 则携带对应的可选创建字段。只有两个值都非空时,agent loop(智能体循环)请求才有效;两个值也都会写入请求头日志。`agent/request` 可以在任意步骤返回替换后的字段组合,因此会话可以切换提供方与模型,无需改变 Cordis 插件生命周期。
-`LlmRuntime` 按提供方注册和解析适配器。`registerAdapter(providers, adapter)` 在修改注册表前检查整个提供方列表,遇到重复项时返回 `DUPLICATE_ADAPTER`,并以一个 effect 为单位整体 dispose(资源释放)。模型 ID 不作为注册键;仍由选中的适配器负责验证或转发。后续的 [LLM 目录与 ACP 模型选择 Agent Note](2026-07-15-llm-model-catalog-and-acp-selection.md) 增加了建议性的 `listProviders()` / `listModels()` 发现接口,但不会把目录成员关系变成请求校验规则。
+`LlmRuntime` 按提供方注册和解析适配器。`registerAdapter(providers, adapter)` 在修改注册表前检查整个提供方列表,遇到重复项时返回 `DUPLICATE_ADAPTER`,并以一个 effect 为单位整体 dispose(资源释放)。模型 ID 不作为注册键;仍由选中的适配器负责验证或转发。后续的 [LLM 目录与 ACP 模型选择 Agent Note](2026-07-15-llm-model-catalog-and-acp-selection.zh.md) 增加了建议性的 `listProviders()` / `listModels()` 发现接口,但不会把目录成员关系变成请求校验规则。
在一个 Cordis 上下文中,一个提供方只能有一个适配器所有者。`dsh-llm-deepseek` 注册 `deepseek`;`dsh-llm-pi-ai` 也可以注册 `deepseek`,但同时加载两个所有者属于配置错误,不采用顺序规则或回退行为。若部署选择手写的 DeepSeek 实现,需从 pi-ai 配置中排除 `deepseek`;若部署选择 pi-ai 的 DeepSeek 实现,则不挂载 `dsh-llm-deepseek`。
@@ -40,11 +40,11 @@ pi-ai 的通用流选项不支持停止序列。若 Harness `stop` 选项已定
助手消息携带请求的 `provider` 和 `model`,以及可选的 JSON 可序列化适配器回放状态。成功的 `assistant/message` 会话事件记录这些字段,`deriveMessages()` 返回助手消息时也会包含它们。用户、系统、上下文与工具结果消息不携带助手路由字段。提供方/模型字段是 agent loop 的权威数据;适配器仅拥有其不透明回放状态 payload。
-成功的终止 `finish` 分片可以以 `ReplayEnvelope` 形式携带回放状态:不透明的响应级元数据,加上与发射块序列对齐的可选逐块条目。`BlockAssembler` 对内容与元数据只做一次保留/丢弃决定——max-token 组装丢弃工具调用时,数据同一位置的条目一并丢弃——因此 agent loop 附加到已组装助手消息模型来源中的状态始终描述存储的块,见 [max-token 回放状态对齐决定](../bug-fix/2026-08-15-max-token-replay-state-alignment.md)。agent loop 不公开响应改写钩子。错误或中止响应不会生成正常助手消息,因此不会进入后续模型历史。
+成功的终止 `finish` 分片可以以 `ReplayEnvelope` 形式携带回放状态:不透明的响应级元数据,加上与发射块序列对齐的可选逐块条目。`BlockAssembler` 对内容与元数据只做一次保留/丢弃决定——max-token 组装丢弃工具调用时,数据同一位置的条目一并丢弃——因此 agent loop 附加到已组装助手消息模型来源中的状态始终描述存储的块,见 [max-token 回放状态对齐决定](../bug-fix/2026-08-15-max-token-replay-state-alignment.zh.md)。agent loop 不公开响应改写钩子。错误或中止响应不会生成正常助手消息,因此不会进入后续模型历史。
pi-ai 回放状态用其成功 `AssistantMessage` 的带版本最小投影填充该结构:一个响应半区(源 API/提供方/模型、响应 ID/模型、停止原因),以及逐块的文本签名、thinking 签名和工具调用签名。它不会重复 Harness 内容块中已有的文本或工具参数,也不包含诊断信息、时间戳、用量或错误。后续请求中,只有历史提供方和目标提供方当前归同一个适配器实例所有时,`LlmRuntime` 才会把回放状态交给目标适配器。适配器在能够恢复历史响应时,将 Harness 记录的内容与回放状态组合,并负责所需的跨模型或跨提供方转换。持久化内容保持权威:适配器收到无法使用的回放状态——未知 kind 或版本、格式错误的元数据、或与内容不再匹配的块结构——会把该消息降级为提供方无关转换并带出诊断;其他适配器只能收到提供方无关的内容以及提供方/模型字段。
-该状态属于模型可见的回放输入,因此遵循现有的[请求可重建规则](2026-07-05-reconstructable-requests.md):它同时存在于终止 `finish` 分片和驱动派生的已组装 `assistant/message` 模型来源中。恢复和 fork 会原样保留该状态。压缩(compaction)遮蔽助手消息时,也会从活动 surface 中移除其回放状态;摘要属于普通的提供方无关内容。
+该状态属于模型可见的回放输入,因此遵循现有的[请求可重建规则](2026-07-05-reconstructable-requests.zh.md):它同时存在于终止 `finish` 分片和驱动派生的已组装 `assistant/message` 模型来源中。恢复和 fork 会原样保留该状态。压缩(compaction)遮蔽助手消息时,也会从活动 surface 中移除其回放状态;摘要属于普通的提供方无关内容。
### 在所有请求生产方中传播目标
diff --git a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml
index 458999f905..570b68263c 100644
--- a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml
@@ -3,4 +3,4 @@
# 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-agent-initiator-scope.md
2026-07-15-agent-initiator-scope.md: 63540c0ec6b29a10613e01f1ed9ced24e8f2d277
-2026-07-15-agent-initiator-scope.zh.md: a0e3638081d875adc2412191829ab84c8dcd697c
+2026-07-15-agent-initiator-scope.zh.md: 3ea893aa5f6992bf09965436c1db3144d2fae5ac
diff --git a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md
index a0e3638081..3ea893aa5f 100644
--- a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md
@@ -12,7 +12,7 @@ harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负
## 决策
-必需的 `ctx.agents` 服务使用 Node `AsyncLocalStorage` 携带发起 Agent。它直接存储同一个 `Agent`,不引入只有一个字段的帧;另一个私有运行标记只记录嵌套边界的谱系,供 teardown 记账使用,不携带身份。[核心数据目录](../../../../docs/subsystems/core.md#initiating-agent)标明了所携带的类型。
+必需的 `ctx.agents` 服务使用 Node `AsyncLocalStorage` 携带发起 Agent。它直接存储同一个 `Agent`,不引入只有一个字段的帧;另一个私有运行标记只记录嵌套边界的谱系,供 teardown 记账使用,不携带身份。[核心数据目录](../../../../docs/subsystems/core.zh.md#initiating-agent)标明了所携带的类型。
`currentInitiator()` 用于可选读取,`requireInitiator()` 抛出 `no initiating agent is active`,`withInitiator(agent, operation)` 保留操作返回的同步值或 Promise 本身。`withoutInitiator(operation)` 会建立清空边界,供不得继承 Agent 的工作使用。会话仍通过 `agent.session` 推导;轮次、步骤、工具调用、`signal`、模型、`cwd`、沙箱和授权继续由现有归属方管理。
@@ -28,7 +28,7 @@ harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负
宿主感知的传输层可以从 `ctx.agents.requireInitiator().session.id` 推导由部署方拥有的 `X-Harness-Session-Id` 等请求头;模型可见 schema 和参数中不包含该请求头。本决策不让现有生产 MCP 或 Web 传输层采用此请求头。测试替身传输层用于证明可信边界,而不会把宿主路由策略分配给现有的提供方无关 seam。
-本决策扩展 [Agent 注册作用域约定](2026-07-08-agent-scope-contexts.md)及其[运行时设计](2026-07-12-agent-scope-runtime-design.md),不会改变其中 `agent.ctx` 的静态含义。
+本决策扩展 [Agent 注册作用域约定](2026-07-08-agent-scope-contexts.zh.md)及其[运行时设计](2026-07-12-agent-scope-runtime-design.zh.md),不会改变其中 `agent.ctx` 的静态含义。
## 验证
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 69af08605c..c9fe4938a3 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
@@ -3,4 +3,4 @@
# 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: 3dddbe7e9fae74e4ce1ec1c8e93a40c3352b5a54
-2026-07-15-llm-model-catalog-and-acp-selection.zh.md: 145fd0bc379ff8132d09ec628b6381a18f755cfa
+2026-07-15-llm-model-catalog-and-acp-selection.zh.md: b93b333c6f25a779b1d307b6141bea9f5eb1e407
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 145fd0bc37..b93b333c6f 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
@@ -4,7 +4,7 @@ Status: implemented
[English](2026-07-15-llm-model-catalog-and-acp-selection.md) | 中文
-> 目录决策仍然有效。ACP(Agent Client Protocol)会话级模型选择已由 [ACP 作为仅面向自动化的协议](../simplification/2026-07-23-acp-automation-only-protocol.md)取代。
+> 目录决策仍然有效。ACP(Agent Client Protocol)会话级模型选择已由 [ACP 作为仅面向自动化的协议](../simplification/2026-07-23-acp-automation-only-protocol.zh.md)取代。
## 问题
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 aee9c76d55..277e0b097d 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
@@ -3,4 +3,4 @@
# 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: 90f9daf4b890bd53621916e492d4d82baaa3e8cc
-2026-07-15-lsp-capability-seam.zh.md: b85b478b25a375b04e1301d893e53198554641e3
+2026-07-15-lsp-capability-seam.zh.md: bdb5e812e94a4aec4402fe83ca9c818bfa00f9f9
diff --git a/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.zh.md b/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.zh.md
index b85b478b25..bdb5e812e9 100644
--- a/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.zh.md
@@ -79,7 +79,7 @@ interface LspService {
映射键规范化为带前导点的小写扩展名,并按 `filePath` 的最后一个扩展名选择;语言 id 仅用于文档同步。seam 中的位置和范围从零开始按 UTF-16 计数。`findReferences` 始终包含声明:提供方在内部执行该约束,本地映射设置 `context.includeDeclaration: true`,调用方不能配置。封闭结果联合将导航统一为位置,将 `hover` 统一为内容或 `null`;导航结果携带提供方的规范工作区 URI,使消费方在执行世界的命名空间内相对化文件 URI。seam 不公开协议类型、进程或文档控制,也不提供通用请求逃生口。
-`dsh-lsp-stdio` 负责服务器配置、JSON-RPC、进程与临时文档状态和协议转换。它通过 `ctx.fs` 读取,通过 `ctx.subprocess` 启动,只依赖二者的 Service Definition 包而非具体提供方;[可移植执行环境决策](2026-07-28-portable-execution-world-consumers.md)负责定义这种配对。服务器表的键是提供方 id。插件在注册前解析每个服务器的本地设置;如果后续映射无效或发生冲突,插件会撤销此前的注册,并为每个提供方保留独立进程池。`dsh-tool-lsp` 在运行时只注入 `tools`、`lsp` 和 `systemPrompt`,通过包内的 `sessionCwd(exec)` 辅助函数从 `exec.agent?.session.header.cwd` 取得工作区,其取值方式与文件系统工具一致,也不导入提供方。
+`dsh-lsp-stdio` 负责服务器配置、JSON-RPC、进程与临时文档状态和协议转换。它通过 `ctx.fs` 读取,通过 `ctx.subprocess` 启动,只依赖二者的 Service Definition 包而非具体提供方;[可移植执行环境决策](2026-07-28-portable-execution-world-consumers.zh.md)负责定义这种配对。服务器表的键是提供方 id。插件在注册前解析每个服务器的本地设置;如果后续映射无效或发生冲突,插件会撤销此前的注册,并为每个提供方保留独立进程池。`dsh-tool-lsp` 在运行时只注入 `tools`、`lsp` 和 `systemPrompt`,通过包内的 `sessionCwd(exec)` 辅助函数从 `exec.agent?.session.header.cwd` 取得工作区,其取值方式与文件系统工具一致,也不导入提供方。
## 面向模型的约定
diff --git a/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml
index 47e7156ab8..6ad1773f6f 100644
--- a/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml
@@ -3,4 +3,4 @@
# 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-replay-token-meter-service.md
2026-07-15-replay-token-meter-service.md: 10261189fca621b305b3ec6347b54471d81c8ba3
-2026-07-15-replay-token-meter-service.zh.md: 4e460b3c32630f205f447a16205bd4c9b1d89faf
+2026-07-15-replay-token-meter-service.zh.md: 370d3cf932d37702c5298bc79e9dc8f5a37835b9
diff --git a/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md
index 4e460b3c32..370d3cf932 100644
--- a/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md
@@ -16,7 +16,7 @@ Status: implemented
`@deepseek-ai/dsh-token-meter` 是 `packages/llm/` 下的单个具体包,并注册 `ctx.tokenMeter`。在第二种实现出现之前,它不会被拆成接口与后端。`TokenMeter` 本身公开 `measure(session, requestHeader?)` 与 `estimateMessage(message)`;消费方直接调用这个单例服务。
-服务没有配置。估算采用固定的每 token 四个字符启发式规则,并加上结构开销。服务不提供模型 profile、容量设置、密度设置、分词器后端或语言专用策略。对精确提供方/模型容量的查询由适配器单独负责,具体见[路由模型上下文与压缩策略 Agent Note](2026-07-20-routed-model-context-and-compaction-policy.md)。
+服务没有配置。估算采用固定的每 token 四个字符启发式规则,并加上结构开销。服务不提供模型 profile、容量设置、密度设置、分词器后端或语言专用策略。对精确提供方/模型容量的查询由适配器单独负责,具体见[路由模型上下文与压缩策略 Agent Note](2026-07-20-routed-model-context-and-compaction-policy.zh.md)。
### 逐会话回放折叠
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 342f2073ea..fcca895513 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
@@ -3,4 +3,4 @@
# 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: 86faad9929d3eb5b00e66bb1c46a2e35b135d954
-2026-07-16-explicit-turn-cancellation.zh.md: 5397b368628c196efc2b35c00855267246ac6e85
+2026-07-16-explicit-turn-cancellation.zh.md: acf56e0629668a227324045ffd0521619dc45f33
diff --git a/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.zh.md b/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.zh.md
index 5397b36862..acf56e0629 100644
--- a/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.zh.md
@@ -8,7 +8,7 @@ Status: implemented
取消是一种生命周期短于 Agent(智能体)驱动器的控制能力。自由文本字符串无法完整区分所有调用方,步骤级控制器也无法中断提示词提交、提示词组装、继续决策或轮次终止策略。持久化 `Error`、`AbortSignal.reason` 或后端私有对象还会向持久化回放暴露不稳定的运行时细节。
-[发起 Agent 作用域决策](2026-07-15-agent-initiator-scope.md)有意让 AsyncLocalStorage 只携带同一个 Agent。若把轮次、步骤或 signal 状态加入这个与驱动器同生命周期的边界,陈旧的异步后代就会看似仍对后续轮次拥有权限。因此,取消需要一个轮次归属方并显式传播,且不创建另一套环境上下文或公开的轮次包装层。
+[发起 Agent 作用域决策](2026-07-15-agent-initiator-scope.zh.md)有意让 AsyncLocalStorage 只携带同一个 Agent。若把轮次、步骤或 signal 状态加入这个与驱动器同生命周期的边界,陈旧的异步后代就会看似仍对后续轮次拥有权限。因此,取消需要一个轮次归属方并显式传播,且不创建另一套环境上下文或公开的轮次包装层。
## 决策
@@ -18,9 +18,9 @@ Agent 拥有仅用于运行时的 `AgentCancelCause` 联合类型 `{ kind: 'user
AgentLoop 为每个待启动轮次私有地持有一个 `TurnCancellation`。它在通知 `agent/status = running` 前安装该持有者,使其中唯一的 `AbortController` 持续覆盖 inbox 领取、`agent/pre-step`、提示词组装、每个步骤、模型与工具执行以及 `agent/turn-stopping`;随后在发布 `turn/end` 前立即清除所安装的那个持有者。因此,即使驱动器状态可能在持久化刷新结算前保持 `running`,终态事件观察者及其后的持久化刷新也无法取消已完成的轮次工作。所有参与的方法、事件和请求值都会收到同一个显式 signal;下一个轮次会收到全新的 signal。
-对于轮次被认领前已取消的排队工作,驱动器只保留一个不携带取消原因的运行前标记。实际生效的 `cancel()` 会先发出仅供观察的 `agent/cancel-requested` 通知并携带最终确定的类型化取消原因,然后才清除排队工作和 steering(中途引导)工作或中止持有者;通知失败不能阻止此次停止,空闲状态下调用则不发出任何通知。通知观察者同步加入队列的工作也会被这次清除,而稍后由 signal 中止观察者加入队列的工作会被锁存,并在被中止的活动收敛到空闲时执行——`disposed` 取消则将其停放([取消收敛窗口唤醒锁存](../bug-fix/2026-08-07-cancel-convergence-wake-latch.md))。若 `running` 监听器同步取消旧工作并发送替代提示词,驱动器会丢弃已中止的持有者,并为替代提示词创建全新的持有者。同一活跃持有者上的重复取消遵循首次请求优先,后续调用仍可清除新入队的待处理工作。
+对于轮次被认领前已取消的排队工作,驱动器只保留一个不携带取消原因的运行前标记。实际生效的 `cancel()` 会先发出仅供观察的 `agent/cancel-requested` 通知并携带最终确定的类型化取消原因,然后才清除排队工作和 steering(中途引导)工作或中止持有者;通知失败不能阻止此次停止,空闲状态下调用则不发出任何通知。通知观察者同步加入队列的工作也会被这次清除,而稍后由 signal 中止观察者加入队列的工作会被锁存,并在被中止的活动收敛到空闲时执行——`disposed` 取消则将其停放([取消收敛窗口唤醒锁存](../bug-fix/2026-08-07-cancel-convergence-wake-latch.zh.md))。若 `running` 监听器同步取消旧工作并发送替代提示词,驱动器会丢弃已中止的持有者,并为替代提示词创建全新的持有者。同一活跃持有者上的重复取消遵循首次请求优先,后续调用仍可清除新入队的待处理工作。
-显式事件签名传递单个 payload 对象:agent 作用域事件在 payload 中携带 `agent` 和 `signal`,`next` 位于最后;其余 API 保持 `signal` 紧邻 waterfall(瀑布式事件)的最终 `next` 之前。`PreStepContext` 与 `RequestFailureContext` 已退役,其字段并入 `agent/pre-step` 与 `agent/request-error` 的 payload([payload-object 事件](2026-08-06-agent-event-payload-objects.md))。进入 pre-step 时、请求配置、请求错误恢复、模型生成、工具执行、审批、轮次停止以及 subagent 或工作流请求都会收到当前 signal。钩子桥接器也必须提供 `RunHookOptions.signal`,使轮次取消能够到达 Bash 执行器终止进程组并等待其退出的边界。`SystemPrompt.assemble()` 在 `AssembleContext` 中携带 `signal?: AbortSignal`,因为该对象是显式请求值,也可表示轮次之外不携带 signal 的组装。监听器可以配合该 signal 取消,但不得保留它来控制其他轮次。
+显式事件签名传递单个 payload 对象:agent 作用域事件在 payload 中携带 `agent` 和 `signal`,`next` 位于最后;其余 API 保持 `signal` 紧邻 waterfall(瀑布式事件)的最终 `next` 之前。`PreStepContext` 与 `RequestFailureContext` 已退役,其字段并入 `agent/pre-step` 与 `agent/request-error` 的 payload([payload-object 事件](2026-08-06-agent-event-payload-objects.zh.md))。进入 pre-step 时、请求配置、请求错误恢复、模型生成、工具执行、审批、轮次停止以及 subagent 或工作流请求都会收到当前 signal。钩子桥接器也必须提供 `RunHookOptions.signal`,使轮次取消能够到达 Bash 执行器终止进程组并等待其退出的边界。`SystemPrompt.assemble()` 在 `AssembleContext` 中携带 `signal?: AbortSignal`,因为该对象是显式请求值,也可表示轮次之外不携带 signal 的组装。监听器可以配合该 signal 取消,但不得保留它来控制其他轮次。
`ctx.agents` 仍只携带发起 Agent。环境中的 Agent 并不代表存活、当前轮次或取消权限。cause 读取器是 loop 私有的,它直接陈述机器私有的 slot 不变量(只有 `cancel()` 会中止轮次控制器,且总是携带规范的冻结 cause),而不是对 reason 做结构化再校验;不存在从任意 signal 读取 cause 的公开辅助函数。并发 Agent 会同时隔离各自的发起方身份和轮次 signal;子驱动会遮蔽父发起方,而父请求 signal 仍通过 subagent seam 传递。
diff --git a/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.i18n.yaml
index e772ff9036..29807cfb7c 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.i18n.yaml
@@ -3,4 +3,4 @@
# 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-cooperative-tool-cancellation.md
2026-07-19-cooperative-tool-cancellation.md: 781202688a5cbcd7076ee694fc7dd9489683d8e8
-2026-07-19-cooperative-tool-cancellation.zh.md: a5ce2e671e92f4758ddeb3d3556d8af574467f7f
+2026-07-19-cooperative-tool-cancellation.zh.md: ec35734eef91c5c774d1be814b221e9fdb8f65fa
diff --git a/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.zh.md b/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.zh.md
index a5ce2e671e..ec35734eef 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.zh.md
@@ -42,7 +42,7 @@ Status: implemented
工具主体一旦启动,注册表就会等待它完成。取消通过融合信号到达工具主体,但注册表不会与其 promise 竞速或丢弃该 promise。协作式实现会停止自身工作或继续转发取消,并在所持有的工作完全停稳后完成;不协作的同进程实现可能让注册表无限期保持等待。进程、worker、网络和提供方层仍负责各自的终止机制。
-这项决策只要求工具调用边界携带取消信号。让工具主体可达的异步能力也必须接收信号,属于另一项迁移,见提议中的[工具可达能力 seam 中的必填取消](../../proposed/architecture/2026-07-19-required-cancellation-through-tool-capability-seams.md)。
+这项决策只要求工具调用边界携带取消信号。让工具主体可达的异步能力也必须接收信号,属于另一项迁移,见提议中的[工具可达能力 seam 中的必填取消](../../proposed/architecture/2026-07-19-required-cancellation-through-tool-capability-seams.zh.md)。
## 验证
@@ -62,7 +62,7 @@ Status: implemented
**禁止环绕包装层替换信号。** 不予采纳,因为截止时间和嵌套操作作用域需要词法派生信号。捕获并融合调用方信号既保留组合能力,也不允许切断调用方取消。
-**让工具 promise 与取消竞速。** 不予采纳,因为这种方式会在副作用仍可能存活时报告完成,违反[dispose(资源释放)必须完全停稳的规则](../../../../docs/defensive-patterns.md#dispose-must-reach-quiescence-not-just-request-it)。
+**让工具 promise 与取消竞速。** 不予采纳,因为这种方式会在副作用仍可能存活时报告完成,违反[dispose(资源释放)必须完全停稳的规则](../../../../docs/defensive-patterns.zh.md#dispose-must-reach-quiescence-not-just-request-it)。
## 后果
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 b864b41f76..1ed087f38c 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
@@ -3,4 +3,4 @@
# 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: 620803668e88f5a462ab2a75e6e916a85d433ed6
-2026-07-19-gui-layering-and-rpc-protocol.zh.md: 8145b6e5af149f79b45e03c80aeea90da90b3729
+2026-07-19-gui-layering-and-rpc-protocol.zh.md: 137cbce3e5dbc0be2736472f6e26d58112422697
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 8145b6e5af..137cbce3e5 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
@@ -4,7 +4,7 @@ Status: implemented
[English](2026-07-19-gui-layering-and-rpc-protocol.md) | 中文
-> 分工线:本篇 = 分层模型 + 通道无关的 RPC 协议;协议的 Web 实现由 HTTP 上行加 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.md)组成,浏览器对象层见 [Web 客户端架构笔记](2026-07-19-gui-web-client-architecture.md)。
+> 分工线:本篇 = 分层模型 + 通道无关的 RPC 协议;协议的 Web 实现由 HTTP 上行加 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.zh.md)组成,浏览器对象层见 [Web 客户端架构笔记](2026-07-19-gui-web-client-architecture.zh.md)。
## Problem
@@ -23,13 +23,13 @@ Status: implemented
目录按照如下分层:
- `packages/host/*`:包只提供 Host 侧能力(代表了以现在 Harness 实体插件系统为主体的 Node.js 代码核心工程),除此之外,还包含
- 统一后端协议(fetch、HTTP、流式接口等)定义和支持,见本篇「消息协议」起各节
-- `packages/client/*`:包只提供 Client 侧能力,每包单边不混。这里住三类包(两条轴归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有):
+- `packages/client/*`:包只提供 Client 侧能力,每包单边不混。这里住三类包(两条轴归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.zh.md) 所有):
- **纯库**(`ui-slots`、`ui-primitives`,外加内核包 `loader`):普通根入口包,静态打包进壳;两个客户端库播种进模块表。
- **静态到达 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-web-frontend`)是 vite 应用:`dsh-client-web` 导出的壳 API 之上的一层薄 `main.ts`。
- - `apps/cli`(`@deepseek-ai/dsh`)分发命令:`dsh web` = Host + webserver + 构建出的 `dsh-web-frontend` dist;`dsh --profile headless` = [直接使用核心 Agent/Session 的入口](2026-08-09-headless-direct-core-entry-point.md),不含 Host、HTTP 或浏览器层。
+ - `apps/cli`(`@deepseek-ai/dsh`)分发命令:`dsh web` = Host + webserver + 构建出的 `dsh-web-frontend` dist;`dsh --profile headless` = [直接使用核心 Agent/Session 的入口](2026-08-09-headless-direct-core-entry-point.zh.md),不含 Host、HTTP 或浏览器层。
- 将来的 Electron 应用经由 IPC fetch 载体复用同一套 web client 包。
```
@@ -50,9 +50,9 @@ harness core packages ──────────────────┘
- `runtime → apiproxy` 单向;apiproxy 仅依赖类型定义。
- client 侧包**永不 import** host 侧包的运行时(只吃 `/api`、`/client` 两个浏览器安全子路径)。
- `webserver` 不依赖 `runtime`:它提供 `{ fetch }` 特定实现 ——「webserver ← runtime」只是运行时注入关系,不是包依赖。
-- client 侧跨包 import 插件包一律走 `/client` 子路径,且插件包之间只限类型 import——跨插件值 import 在 tsdown 纯度门禁处即构建错误(值层面的协作走 cordis 服务;边规则归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有)。
+- client 侧跨包 import 插件包一律走 `/client` 子路径,且插件包之间只限类型 import——跨插件值 import 在 tsdown 纯度门禁处即构建错误(值层面的协作走 cordis 服务;边规则归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.zh.md) 所有)。
-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))。
+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.zh.md))。
协议侧:TS interface(`packages/host/apiproxy/src/api/`,零 Node 依赖,浏览器可 import);wire 消息统一为**双向模型**——每条逻辑消息按「谁发起 × request/response」分类(两轴四格,后文称四象限),与物理通道解耦;客户端统一继承 `AbstractApiClient`(协议不变量全在基类,平台差异只是 `doFetch` 传输切面)。
@@ -167,7 +167,7 @@ export type ResponseValue =
### 帧(server→client,具名 union)
-两条逻辑流:mux 流(`/api/events.mux`,全 session 聚合)与 host 流(`/api/events.host`,host 级事件)。浏览器通过每流一条下行 WebSocket 消费,进程内 fetch 载体以 SSE 保持同构;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.md)。帧示例一行:
+两条逻辑流:mux 流(`/api/events.mux`,全 session 聚合)与 host 流(`/api/events.host`,host 级事件)。浏览器通过每流一条下行 WebSocket 消费,进程内 fetch 载体以 SSE 保持同构;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.zh.md)。帧示例一行:
| 帧 type | 载荷 | 何时发 |
|---|---|---|
@@ -214,7 +214,7 @@ export type ResponseValue =
| 子类 | 所在包 | doFetch | 用途 |
|---|---|---|---|
| `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) |
+| `WebApiClient` | dsh-client-connection | `globalThis.fetch` 上行 + 每逻辑流一条同源 WebSocket 下行 | 浏览器客户端;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.zh.md) |
| `FixtureApiClient` | dsh-client-connection | 不用(协议层覆写) | 无 server 的 UI 开发(`?fixture`):覆写 `callUnary`/`openMux`/`openHost`/`respond` 虚方法,自己就是假 server(帧 rpcId 由它 mint,语义自洽) |
| IPC 桥子类(假想示例——尚无此形态) | Electron 壳 | IPC 序列化往返 | 只需换 doFetch,约定/基类零改 |
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 2d5aaa3a25..9ee365f446 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
@@ -3,4 +3,4 @@
# 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: 8b4f940299cbba78d403c34b1e5fc9740e44f2c2
-2026-07-19-gui-web-client-architecture.zh.md: a86f9d6f71bed0ce147ffee39bd921037d6c1c4c
+2026-07-19-gui-web-client-architecture.zh.md: 705b1337dd97ac37bd01bdcc5aa22484b7971908
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md
index a86f9d6f71..705b1337dd 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md
@@ -4,7 +4,7 @@ Status: implemented
[English](2026-07-19-gui-web-client-architecture.md) | 中文
-> 分工线:通道无关的分层模型与 RPC 协议(消息模型/类型体系/约定面/客户端基类)见 [分层与 RPC 协议笔记](2026-07-19-gui-layering-and-rpc-protocol.md);本篇 = 浏览器侧:client cordis 树如何装载、UI 插件如何经 slot 与服务组合、React-free 对象层如何以不可变快照供给 React。
+> 分工线:通道无关的分层模型与 RPC 协议(消息模型/类型体系/约定面/客户端基类)见 [分层与 RPC 协议笔记](2026-07-19-gui-layering-and-rpc-protocol.zh.md);本篇 = 浏览器侧:client cordis 树如何装载、UI 插件如何经 slot 与服务组合、React-free 对象层如何以不可变快照供给 React。
## Problem
@@ -30,21 +30,21 @@ Status: implemented
## client cordis 树与装载链
-装载链——两类包(普通包 vs dsh.client 插件)、模块系统/插件治理器之分、host 独家撰写的带修订号 entry 图之上的双阶段 boot、热重载——归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有。本篇赖以立足的事实:浏览器启动与 host 相同的 vendored `@cordisjs/plugin-loader`,由 client 模块系统(`ctx.modules`,`packages/client/modules`)填上其 `internal` 约定;凡带产品行为的单元都是 host 独家撰写的 `__DSH_BOOT__` 图里的 entry——每个生产插件包(含基础设施)都携带 `dsh.client` 声明、以 fetch 到达的 `./client` tsdown 闭包 bundle 供给,`immediately` 行的差别仅在 boot 第一阶段预取,而普通包(react 家族、cordis、尚未升格的库)保持打进壳、已播种、对图不可见;bundle 执行 `window.__ModuleLoader__.load({ id, factory })`,其 `require` 由 lazy CJS 模块表应答(种子词条 + 已登记工厂,首次 require 时物化并记忆化——跨插件值 import 是构建错误,协作走 cordis 服务);全局样式与 CSS Modules 都内联在其持有插件的 bundle 中,物化时注入为 `', '>')
+}
+
+function assertNever(row: never): never {
+ throw new Error(`webserver: unknown index injection row ${JSON.stringify(row)}`)
+}
+
+/** Render one row to markup with its placement. */
+function renderRow(row: IndexInjection): { placement: IndexInjectionPlacement; markup: string } {
+ switch (row.kind) {
+ case 'global': {
+ // `<` is escaped in JSON so a row-controlled string cannot break out of
+ // the script element.
+ const name = JSON.stringify(row.name).replaceAll('<', '\\u003c')
+ const value = row.value === undefined
+ ? 'undefined'
+ : JSON.stringify(row.value).replaceAll('<', '\\u003c')
+ return { placement: 'head', markup: `` }
+ }
+ case 'script':
+ return { placement: row.placement, markup: `` }
+ case 'script-src':
+ return { placement: row.placement, markup: `` }
+ case 'style':
+ return { placement: 'head', markup: `` }
+ case 'html':
+ return { placement: row.placement, markup: row.html }
+ default:
+ return assertNever(row)
+ }
+}
+
+/** Insert `markup` into `html` at `at`. */
+function splice(html: string, at: number, markup: string): string {
+ return `${html.slice(0, at)}${markup}${html.slice(at)}`
+}
+
+/**
+ * Render rows into an index.html body: head rows immediately after the
+ * opening head tag, body rows immediately after the opening body tag, each
+ * group in table order.
+ * @param html - the raw index.html body.
+ * @param rows - the collected injection table.
+ * @returns the html with every row rendered.
+ */
+export function renderIndexInjections(html: string, rows: readonly IndexInjection[]): string {
+ let head = ''
+ let body = ''
+ for (const row of rows) {
+ const rendered = renderRow(row)
+ if (rendered.placement === 'head') head += rendered.markup
+ else body += rendered.markup
+ }
+ let out = html
+ if (head !== '') {
+ const open = /]*)?>/i.exec(out)
+ // Headless fixture pages may lack ; prepending keeps the rows ahead
+ // of every document script.
+ out = open === null ? `${head}${out}` : splice(out, open.index + open[0].length, head)
+ }
+ if (body !== '') {
+ const open = /]*)?>/i.exec(out)
+ // Body-less fragments receive the rows at the end, where the HTML parser
+ // has already synthesized a body.
+ out = open === null ? `${out}${body}` : splice(out, open.index + open[0].length, body)
+ }
+ return out
+}
diff --git a/packages/host/webserver/tests/webserver.spec.ts b/packages/host/webserver/tests/webserver.spec.ts
index 2cbd285856..ffe5b4648d 100644
--- a/packages/host/webserver/tests/webserver.spec.ts
+++ b/packages/host/webserver/tests/webserver.spec.ts
@@ -15,7 +15,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
-import HttpServer from '../src/index.ts'
+import HttpServer, { renderIndexInjections } from '../src/index.ts'
let root: string | undefined
let context: Context | undefined
@@ -200,6 +200,54 @@ describe('real Loader composition', () => {
await expect(request(port, '/probe')).rejects.toThrow()
})
+ it('collects injection rows fresh per render and layers taps over the rendered rows', { timeout: 60_000 }, async () => {
+ const loaded = await loadComposition()
+ const server = loaded.webServer
+ let flag = 'dark'
+ loaded.on('webserver/index-inject', (table) => {
+ table.push(
+ { kind: 'script', placement: 'head', text: 'window.__Q__=1' },
+ { kind: 'script-src', placement: 'head', src: '/plugins/a.js?rev="1"&x=' },
+ { kind: 'global', name: '__DSH_BOOT__', value: { rev: '' } },
+ { kind: 'style', text: 'body{margin:0}' },
+ { kind: 'html', placement: 'head', html: '' },
+ { kind: 'script', placement: 'body', text: `window.__P__=${JSON.stringify(flag)}` },
+ )
+ })
+
+ const html = server.renderIndex('shell')
+ // Head rows land right after the opening head tag in table order; the body
+ // row lands right after the opening body tag.
+ const order = [
+ '',
+ '',
+ '',
+ 'globalThis["__DSH_BOOT__"] = {"rev":"\\u003c/script>\\u003cb>"}',
+ '',
+ '',
+ '',
+ '',
+ 'shell',
+ ].map(part => html.indexOf(part))
+ expect(order).toEqual([...order].sort((a, b) => a - b))
+ expect(order.every(at => at !== -1)).toBe(true)
+
+ // Fresh collection per render: the listener reads live state at emit time.
+ flag = 'light'
+ expect(server.renderIndex('')).toContain('window.__P__="light"')
+
+ // Raw taps still run, over the already-rendered rows.
+ const untap = server.tapIndex(h => h.replace('window.__Q__=1', 'window.__Q__=2'))
+ expect(server.renderIndex('')).toContain('window.__Q__=2')
+ untap()
+
+ // Tag-less fragments: head rows prepend, body rows append.
+ expect(renderIndexInjections('x', [
+ { kind: 'script', placement: 'head', text: 'H' },
+ { kind: 'script', placement: 'body', text: 'B' },
+ ])).toBe('x')
+ })
+
it('fails the fiber when the port is already taken (fail-loud at activation)', { timeout: 60_000 }, async () => {
const first = await loadComposition()
const takenPort = first.webServer.port
diff --git a/packages/identity/README.i18n.yaml b/packages/identity/README.i18n.yaml
index 653704a83c..f1f3b0c384 100644
--- a/packages/identity/README.i18n.yaml
+++ b/packages/identity/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/identity/README.md
README.md: ebbc7d937dfa793edf9a8617d86a1820e87866da
-README.zh.md: 3d29fc3dee7599e366cb91c09d6a73c8acddab96
+README.zh.md: cd5fa06a0e9c6c277292b5904a1617acaa8dcfc3
diff --git a/packages/identity/README.zh.md b/packages/identity/README.zh.md
index 3d29fc3dee..cd5fa06a0e 100644
--- a/packages/identity/README.zh.md
+++ b/packages/identity/README.zh.md
@@ -6,4 +6,4 @@
| 包 | 职责 | ctx key |
|---|---|---|
-| [`anonymous-user-id/`](anonymous-user-id/README.md) | 为遥测、反馈和 DeepSeek 请求持久化一个限定于 Harness home 的匿名关联 id | — |
+| [`anonymous-user-id/`](anonymous-user-id/README.zh.md) | 为遥测、反馈和 DeepSeek 请求持久化一个限定于 Harness home 的匿名关联 id | — |
diff --git a/packages/interaction/README.i18n.yaml b/packages/interaction/README.i18n.yaml
index 116db6b260..7086f59493 100644
--- a/packages/interaction/README.i18n.yaml
+++ b/packages/interaction/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/interaction/README.md
README.md: a7842e40aa708ee9ec159dd51b8a94a6b2b18539
-README.zh.md: ec329365331efd2769078078d1da4a8e1b3b57e0
+README.zh.md: 1a760484f887a459348612c867213d95e7257420
diff --git a/packages/interaction/README.zh.md b/packages/interaction/README.zh.md
index ec32936533..1a760484f8 100644
--- a/packages/interaction/README.zh.md
+++ b/packages/interaction/README.zh.md
@@ -6,12 +6,12 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`commands/`](commands/README.md) | 为交互式适配器注册并分派用户命令。 | `ctx.commands` |
-| [`user-approval/`](user-approval/README.md) | 协调一次性审批决策。 | `ctx.approval` |
-| [`permission/`](permission-presets/README.md) | 呈现并持久化面向用户的权限预设。 | `ctx.permissionPresets` |
-| [`user-questions/`](user-questions/README.md) | 定义与提供方无关的用户问答 seam。 | `ctx.userQuestions` |
-| [`tool-ask-user/`](tool-ask-user/README.md) | 向模型提供用户问题。 | (注册到 `ctx.tools`) |
+| [`commands/`](commands/README.zh.md) | 为交互式适配器注册并分派用户命令。 | `ctx.commands` |
+| [`user-approval/`](user-approval/README.zh.md) | 协调一次性审批决策。 | `ctx.approval` |
+| [`permission/`](permission-presets/README.zh.md) | 呈现并持久化面向用户的权限预设。 | `ctx.permissionPresets` |
+| [`user-questions/`](user-questions/README.zh.md) | 定义与提供方无关的用户问答 seam。 | `ctx.userQuestions` |
+| [`tool-ask-user/`](tool-ask-user/README.zh.md) | 向模型提供用户问题。 | (注册到 `ctx.tools`) |
-这些包通过现有的 agent 和会话约定集成,而不改变循环。交互式应用提供具体的命令、审批和提问适配器;自动化使用 [`acp/`](../acp/README.md),可运行的演示组合包位于 [`examples/`](../examples/README.md)。产品 [`dsh`](../../apps/cli/README.md) CLI(命令行界面)直接组合这些包。
+这些包通过现有的 agent 和会话约定集成,而不改变循环。交互式应用提供具体的命令、审批和提问适配器;自动化使用 [`acp/`](../acp/README.zh.md),可运行的演示组合包位于 [`examples/`](../examples/README.zh.md)。产品 [`dsh`](../../apps/cli/README.zh.md) CLI(命令行界面)直接组合这些包。
-子系统参考:[approval.md](../../docs/subsystems/approval.md)、[permission-presets.md](../../docs/subsystems/permission-presets.md)、[user-questions.md](../../docs/subsystems/user-questions.md)与 [commands.md](../../docs/subsystems/commands.md)。仅自动化的 ACP 传输是 [`acp/`](../acp/README.md),SDK 的 JSON-RPC 服务器端是 [`sdk/server`](../sdk/README.md),共享 bin 启动胶水是 [`boot/`](../boot/README.md)。
+子系统参考:[approval.md](../../docs/subsystems/approval.zh.md)、[permission-presets.md](../../docs/subsystems/permission-presets.zh.md)、[user-questions.md](../../docs/subsystems/user-questions.zh.md)与 [commands.md](../../docs/subsystems/commands.zh.md)。仅自动化的 ACP 传输是 [`acp/`](../acp/README.zh.md),SDK 的 JSON-RPC 服务器端是 [`sdk/server`](../sdk/README.zh.md),共享 bin 启动胶水是 [`boot/`](../boot/README.zh.md)。
diff --git a/packages/interaction/commands/README.i18n.yaml b/packages/interaction/commands/README.i18n.yaml
index fa8ce400f3..cde56d4ef9 100644
--- a/packages/interaction/commands/README.i18n.yaml
+++ b/packages/interaction/commands/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/interaction/commands/README.md
README.md: 4a4cb2a70b56ba1a18e9f4719541a50a9683510c
-README.zh.md: f89ccd1a5cc9189b2810026481d4c57855481111
+README.zh.md: d331e3e2f359572e4953831ea0c4dbe8d2fd64c3
diff --git a/packages/interaction/commands/README.zh.md b/packages/interaction/commands/README.zh.md
index f89ccd1a5c..d331e3e2f3 100644
--- a/packages/interaction/commands/README.zh.md
+++ b/packages/interaction/commands/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-由插件负责、供交互式 UI 适配器使用的面向用户命令注册表。[插件命令注册 Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md)定义了其边界与分发约定。
+由插件负责、供交互式 UI 适配器使用的面向用户命令注册表。[插件命令注册 Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.zh.md)定义了其边界与分发约定。
## 服务约定
@@ -24,7 +24,7 @@
#### 模型看到的内容
-注册表自身不会提交任何内容。已知斜杠命令在 UI 命令平面执行,其 `CommandResult` 文本不会作为用户消息提交。已交付的适配器会拒绝未知斜杠命令输入,而不是将其变成模型提示词。命令生产方可以显式使用接收命令的 `Agent`;例如,[`dsh-plan-mode`](../../plan/plan-mode/README.md#model-and-human-interactions)在选择 plan mode 后,会提交 `/plan [message]` 中的可选消息。图片附件遵循同一规则:执行器只负责把它们准入为持久化附件对象,是否以及如何成为模型可见的消息内容由声明接受的生产方决定。
+注册表自身不会提交任何内容。已知斜杠命令在 UI 命令平面执行,其 `CommandResult` 文本不会作为用户消息提交。已交付的适配器会拒绝未知斜杠命令输入,而不是将其变成模型提示词。命令生产方可以显式使用接收命令的 `Agent`;例如,[`dsh-plan-mode`](../../plan/plan-mode/README.zh.md#model-and-human-interactions)在选择 plan mode 后,会提交 `/plan [message]` 中的可选消息。图片附件遵循同一规则:执行器只负责把它们准入为持久化附件对象,是否以及如何成为模型可见的消息内容由声明接受的生产方决定。
#### Token 影响
diff --git a/packages/interaction/permission-presets/README.i18n.yaml b/packages/interaction/permission-presets/README.i18n.yaml
index d5b49532f4..d547571951 100644
--- a/packages/interaction/permission-presets/README.i18n.yaml
+++ b/packages/interaction/permission-presets/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/interaction/permission-presets/README.md
README.md: 7e993ece949aa54d0810d8a7434d3e74d86f4797
-README.zh.md: 0c107ddaaae37e158e2fcff205bd9585d4c4e6ae
+README.zh.md: 2946f340b2023f707d4d00c9b8161a71b4d05853
diff --git a/packages/interaction/permission-presets/README.zh.md b/packages/interaction/permission-presets/README.zh.md
index 0c107ddaaa..2946f340b2 100644
--- a/packages/interaction/permission-presets/README.zh.md
+++ b/packages/interaction/permission-presets/README.zh.md
@@ -8,7 +8,7 @@
该服务拥有 `permission` Settings namespace。其 `defaultPreset` 会初始化新建会话,以及 Web 明确确认为新会话复用目标、且权限来自默认值的 Workspace 空白会话:组合项使用 `Config.defaultPreset`;省略时,则推断与组合后的沙箱和审批默认值匹配的 preset。创建会话时会读取当前设置,并固定 `permission/preset`、`sandbox/mode` 和 `approval/policy`;preset 事实还会记录它来自默认值、显式选择还是旧旋钮推断。已提交的设置变更不会扫描或改写现有会话。当 Web workspace runtime 选中 cwd 匹配、属于该 Workspace 且未归档的空白会话时,host 会重新检查这些事实,并接纳这个确切会话(包括冷存储中的持久会话),而且只在它尚未开始轮次、最近选择来自默认值且有效旋钮仍匹配该选择时推进默认值。显式选择、由旧旋钮推断或没有来源标记的旧选择、独立变更的旋钮,以及普通 seed 恢复都会继续固定原权限。挂载服务时还会遍历所有已存活会话,因此 HMR(热模块替换)会固定插件缺席期间创建的所有会话。
-该服务要求存在具有约束能力的 `ctx.shell` 执行器和 `ctx.approval`。表中名为 `custom` 的条目会在加载时抛出异常。当组合默认值与任何 preset 都不匹配时,插件要求显式配置 `defaultPreset`;独立构造的零事件会话仍可能推导出 `custom`。详见[沙箱切换设计](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)。
+该服务要求存在具有约束能力的 `ctx.shell` 执行器和 `ctx.approval`。表中名为 `custom` 的条目会在加载时抛出异常。当组合默认值与任何 preset 都不匹配时,插件要求显式配置 `defaultPreset`;独立构造的零事件会话仍可能推导出 `custom`。详见[沙箱切换设计](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md)。
两个可选子功能在同一服务之上提供产品界面:`permissions` 会话投影单元(`src/types.ts` 声明该 key;单元以组合默认值为基础折叠三个全量值可调参数事件,并生成选择器视图,其中包含表内选项和仅作当前值的 `custom`)与 `/permission` 命令(不带参数调用时报告当前预设与表;预设参数经 `set` 切换)。每个子功能仅在其注册表(`ctx.sessionProjections` / `ctx.commands`)被组合时激活。
diff --git a/packages/interaction/tool-ask-user/README.i18n.yaml b/packages/interaction/tool-ask-user/README.i18n.yaml
index ed6cd6a35f..dd69a79586 100644
--- a/packages/interaction/tool-ask-user/README.i18n.yaml
+++ b/packages/interaction/tool-ask-user/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/interaction/tool-ask-user/README.md
README.md: bf2ba369fca36c462c6155ed2354ab3d0e930dd0
-README.zh.md: 671bea0132093b890de4ce27095fa60dac396276
+README.zh.md: 067235d84d70d2f3278fd39f5c01f130373d3ac2
diff --git a/packages/interaction/tool-ask-user/README.zh.md b/packages/interaction/tool-ask-user/README.zh.md
index 671bea0132..067235d84d 100644
--- a/packages/interaction/tool-ask-user/README.zh.md
+++ b/packages/interaction/tool-ask-user/README.zh.md
@@ -27,7 +27,7 @@
#### 模型看到的内容
-模型会看到生成的 [`ask_user_question` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-ask-user),其中包含问题 id、提示语、标题、选项和多选标志。
+模型会看到生成的 [`ask_user_question` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-ask-user),其中包含问题 id、提示语、标题、选项和多选标志。
#### Token 影响
diff --git a/packages/interaction/user-approval/README.i18n.yaml b/packages/interaction/user-approval/README.i18n.yaml
index 07843b5f58..ba340c5273 100644
--- a/packages/interaction/user-approval/README.i18n.yaml
+++ b/packages/interaction/user-approval/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/interaction/user-approval/README.md
README.md: 0cf5d458863194e29f8c84168a6f089baabbf3d2
-README.zh.md: ccd8641e30d8a78611eabf602fa46d2705f24487
+README.zh.md: a93f9c17c89ea50622e354eb7729547660e877e2
diff --git a/packages/interaction/user-approval/README.zh.md b/packages/interaction/user-approval/README.zh.md
index ccd8641e30..a93f9c17c8 100644
--- a/packages/interaction/user-approval/README.zh.md
+++ b/packages/interaction/user-approval/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-与通道无关的一次性审批 seam。`ctx.approval.request(req)` 返回 `allowed-once`、`rejected`、`cancelled` 或 `unavailable`;应答者缺失或失败时会以拒绝方式关闭,授权也只适用于所请求的操作。确切事件签名见 [approval.md](../../../docs/subsystems/approval.md#cordis-surface) 的生成区块。
+与通道无关的一次性审批 seam。`ctx.approval.request(req)` 返回 `allowed-once`、`rejected`、`cancelled` 或 `unavailable`;应答者缺失或失败时会以拒绝方式关闭,授权也只适用于所请求的操作。确切事件签名见 [approval.md](../../../docs/subsystems/approval.zh.md#cordis-surface) 的生成区块。
每个请求都必须属于一个尚未结束的 agent(智能体)轮次。服务会追加一对 `approval/asked` 与 `approval/decided` 审计记录,而模型只会看到由此产生且已写入日志的工具结果。已中止的请求会解析为 `cancelled`;如果审计记录的追加在提交前失败,Promise 会被拒绝,而不会返回一项未记录的决定。
@@ -10,7 +10,7 @@
`ApprovalPolicy` 为 `'ask'` 或 `'never'`。实际值取最后一条 `approval/policy` 事件,并回退到配置;`setApprovalPolicy()` 是写入路径。`'never'` 会在交互式分发之前拒绝请求。两种策略都会将各自完整的当前含义贡献给缓存安全的运行时上下文快照。
-工具流水线通过此 seam 路由 `ask` 决定,并在该 seam 缺失时以拒绝方式关闭;沙箱 bash 工具也会将它用于升权重试。ACP 自动化桥接层根据客户端的机器策略,回答其自有 agent 的调用。审计事件仍只写入日志,因此模型只会看到发起请求的消费方所返回的结果。详见[审批 seam Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-approval-seam.md)和[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)。
+工具流水线通过此 seam 路由 `ask` 决定,并在该 seam 缺失时以拒绝方式关闭;沙箱 bash 工具也会将它用于升权重试。ACP 自动化桥接层根据客户端的机器策略,回答其自有 agent 的调用。审计事件仍只写入日志,因此模型只会看到发起请求的消费方所返回的结果。详见[审批 seam Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md)和[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md)。
## 模型体验
diff --git a/packages/jobs/README.i18n.yaml b/packages/jobs/README.i18n.yaml
index 6c032dcbf0..121481d6ab 100644
--- a/packages/jobs/README.i18n.yaml
+++ b/packages/jobs/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/jobs/README.md
README.md: daad96ae561f8f943759332b6b1ea1e20fcaf40e
-README.zh.md: 4e733b1da09656c9d779c43a0ee77703e1c10bfa
+README.zh.md: 586f195f2603cd1b8e4cd5e23174841cb1a39910
diff --git a/packages/jobs/README.zh.md b/packages/jobs/README.zh.md
index 4e733b1da0..586f195f26 100644
--- a/packages/jobs/README.zh.md
+++ b/packages/jobs/README.zh.md
@@ -6,10 +6,10 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`jobs/`](jobs/README.md) | 定义任务注册表和生命周期约定 | `ctx.jobs` |
-| [`jobs-local/`](jobs-local/README.md) | 实现进程本地任务注册表 | 注册到 `ctx.jobs` |
-| [`tool-jobs/`](tool-jobs/README.md) | 向模型公开任务控制和完成通知 | 注册到 `ctx.tools` |
+| [`jobs/`](jobs/README.zh.md) | 定义任务注册表和生命周期约定 | `ctx.jobs` |
+| [`jobs-local/`](jobs-local/README.zh.md) | 实现进程本地任务注册表 | 注册到 `ctx.jobs` |
+| [`tool-jobs/`](tool-jobs/README.zh.md) | 向模型公开任务控制和完成通知 | 注册到 `ctx.tools` |
-参见[后台任务运行时](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)和[任务注册表](../../.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.md)决策。
+参见[后台任务运行时](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md)和[任务注册表](../../.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.zh.md)决策。
-子系统参考文档——id 方案、所有者隔离约定、快照——见 [docs/subsystems/jobs.md](../../docs/subsystems/jobs.md);设计见[后台任务运行时](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)与[任务注册表约定](../../.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.md)两篇 Agent Note。
+子系统参考文档——id 方案、所有者隔离约定、快照——见 [docs/subsystems/jobs.md](../../docs/subsystems/jobs.zh.md);设计见[后台任务运行时](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md)与[任务注册表约定](../../.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.zh.md)两篇 Agent Note。
diff --git a/packages/jobs/jobs-local/README.i18n.yaml b/packages/jobs/jobs-local/README.i18n.yaml
index 7d7f6d2c9b..8aa1a4ea4b 100644
--- a/packages/jobs/jobs-local/README.i18n.yaml
+++ b/packages/jobs/jobs-local/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/jobs/jobs-local/README.md
README.md: e55d9d1747e0dbb12d22f2528527312334f352e6
-README.zh.md: 78979d9518b1b588c689ce5184b33d79177a1ec0
+README.zh.md: 86fd3e8b9db9d0073ce5e6999f5a1fbce12059d2
diff --git a/packages/jobs/jobs-local/README.zh.md b/packages/jobs/jobs-local/README.zh.md
index 78979d9518..86fd3e8b9d 100644
--- a/packages/jobs/jobs-local/README.zh.md
+++ b/packages/jobs/jobs-local/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-[`@deepseek-ai/dsh-jobs`](../jobs/README.md) 注册表约定的进程本地实现:`LocalJobRegistry` 把每条记录保存在内存中,按 kind 签发 `-N` id,并且只交出全新快照,从不交出实时状态。作为插件加载后即注册为 `ctx.jobs`。
+[`@deepseek-ai/dsh-jobs`](../jobs/README.zh.md) 注册表约定的进程本地实现:`LocalJobRegistry` 把每条记录保存在内存中,按 kind 签发 `-N` id,并且只交出全新快照,从不交出实时状态。作为插件加载后即注册为 `ctx.jobs`。
## 准入
@@ -22,7 +22,7 @@
## 模型体验
-通过生产方插件和 [`dsh-tool-jobs`](../tool-jobs/README.md) 间接影响;它们会呈现 job id、输出、状态、取消和完成通知。
+通过生产方插件和 [`dsh-tool-jobs`](../tool-jobs/README.zh.md) 间接影响;它们会呈现 job id、输出、状态、取消和完成通知。
#### KV Cache 影响
diff --git a/packages/jobs/jobs/README.i18n.yaml b/packages/jobs/jobs/README.i18n.yaml
index 0473d974f8..8129e170ed 100644
--- a/packages/jobs/jobs/README.i18n.yaml
+++ b/packages/jobs/jobs/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/jobs/jobs/README.md
README.md: f4469286603f687be5789bd016ab462dba26e664
-README.zh.md: 30d8e4cb1e298bb4a3a0a3e65bb0858b2458a277
+README.zh.md: 6fd97aa3fe4cbbd555c2475f4132ec00c6a97390
diff --git a/packages/jobs/jobs/README.zh.md b/packages/jobs/jobs/README.zh.md
index 30d8e4cb1e..6fd97aa3fe 100644
--- a/packages/jobs/jobs/README.zh.md
+++ b/packages/jobs/jobs/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-后台任务注册表约定(`ctx.jobs`)。抽象的 `JobRegistry` 及其词汇类型在同一份约定下为长时间运行的生产方提供共享 id、owner 隔离、读取、取消、等待、通知和清理;进程局部注册表位于 [`dsh-jobs-local`](../jobs-local/README.md)。生产方插件使用其不透明 id namespace 扩展 `JobKindMap`。
+后台任务注册表约定(`ctx.jobs`)。抽象的 `JobRegistry` 及其词汇类型在同一份约定下为长时间运行的生产方提供共享 id、owner 隔离、读取、取消、等待、通知和清理;进程局部注册表位于 [`dsh-jobs-local`](../jobs-local/README.zh.md)。生产方插件使用其不透明 id namespace 扩展 `JobKindMap`。
## 服务约定
@@ -23,11 +23,11 @@
实现还必须兑现约定的生命周期语义:注册的存续期长于生产方 fiber 与控制器 fiber,owner 释放和服务释放会取消仍在运行的工作并等待守约的生产方,结算遵循首次结果优先(一条终止记录、一轮异常受到隔离的监听器通知,然后释放等待方)。
-参见[任务类型目录](../../../docs/subsystems/jobs.md)、[运行时 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)和 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.md)。
+参见[任务类型目录](../../../docs/subsystems/jobs.zh.md)、[运行时 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md)和 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.zh.md)。
## 模型体验
-通过生产方插件和 [`dsh-tool-jobs`](../tool-jobs/README.md) 间接影响;它们会渲染 job id、输出、状态、取消和完成通知。
+通过生产方插件和 [`dsh-tool-jobs`](../tool-jobs/README.zh.md) 间接影响;它们会渲染 job id、输出、状态、取消和完成通知。
#### KV Cache 影响
diff --git a/packages/jobs/tool-jobs/README.i18n.yaml b/packages/jobs/tool-jobs/README.i18n.yaml
index e1a51a2cbe..6b5cc133dc 100644
--- a/packages/jobs/tool-jobs/README.i18n.yaml
+++ b/packages/jobs/tool-jobs/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/jobs/tool-jobs/README.md
README.md: 8d6a00651f69258d764904ac99d000c2055982dc
-README.zh.md: 15903b6f263fe9d225c48bf567c46f587d23cb00
+README.zh.md: bd1045ecf6f2b7c9ed1f8a8cdfe4f965af418a37
diff --git a/packages/jobs/tool-jobs/README.zh.md b/packages/jobs/tool-jobs/README.zh.md
index 15903b6f26..bd1045ecf6 100644
--- a/packages/jobs/tool-jobs/README.zh.md
+++ b/packages/jobs/tool-jobs/README.zh.md
@@ -63,7 +63,7 @@ Track every background job id you start. You are notified in-session when a job
#### 模型看到的内容
-该工具集可见时,会看到生成的 [`job_output`、`job_list` 和 `job_kill` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-jobs)。
+该工具集可见时,会看到生成的 [`job_output`、`job_list` 和 `job_kill` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-jobs)。
#### Token 影响
diff --git a/packages/llm/README.i18n.yaml b/packages/llm/README.i18n.yaml
index a655518047..8dbce74a9d 100644
--- a/packages/llm/README.i18n.yaml
+++ b/packages/llm/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/llm/README.md
README.md: 15f90024abf3256560d67099af79be7d4e0ee310
-README.zh.md: 6be4123e2e5de5c41937c4b966580278c78e5a42
+README.zh.md: 31ccfd0c8d979f1dac1ec2c0d6a377c9277c3271
diff --git a/packages/llm/README.zh.md b/packages/llm/README.zh.md
index 6be4123e2e..31ccfd0c8d 100644
--- a/packages/llm/README.zh.md
+++ b/packages/llm/README.zh.md
@@ -6,12 +6,12 @@ LLM(大语言模型)seam 及其提供方适配器。`llm` 包同时承担 Se
| 包 | 职责 | ctx key |
|---|---|---|
-| [`llm/`](llm/README.md) | LLM 服务和共享流式词汇 | `ctx.llm` |
-| [`token-meter/`](token-meter/README.md) | 可感知回放的 token 测量 | `ctx.tokenMeter` |
-| [`llm-retry/`](llm-retry/README.md) | 提供方作用域的重试策略 | 监听 `agent/request-error` |
-| [`llm-deepseek/`](llm-deepseek/README.md) | 直接 DeepSeek 适配器 | 注册到 `ctx.llm` |
-| [`llm-pi-ai/`](llm-pi-ai/README.md) | 多提供方 pi-ai 适配器 | 注册到 `ctx.llm` |
+| [`llm/`](llm/README.zh.md) | LLM 服务和共享流式词汇 | `ctx.llm` |
+| [`token-meter/`](token-meter/README.zh.md) | 可感知回放的 token 测量 | `ctx.tokenMeter` |
+| [`llm-retry/`](llm-retry/README.zh.md) | 提供方作用域的重试策略 | 监听 `agent/request-error` |
+| [`llm-deepseek/`](llm-deepseek/README.zh.md) | 直接 DeepSeek 适配器 | 注册到 `ctx.llm` |
+| [`llm-pi-ai/`](llm-pi-ai/README.zh.md) | 多提供方 pi-ai 适配器 | 注册到 `ctx.llm` |
-适配器在 seam 上注册提供方路由;重试与 token 测量仍是独立消费方。子 README 负责路由、元数据、回放和提供方协议细节;[LLM 架构决策](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md)说明设计原理。
+适配器在 seam 上注册提供方路由;重试与 token 测量仍是独立消费方。子 README 负责路由、元数据、回放和提供方协议细节;[LLM 架构决策](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md)说明设计原理。
-子系统参考——消息与内容块、模型请求、`StreamChunk` 协议、适配器约定(adapter contract)——见 [docs/subsystems/llm-streaming.md](../../docs/subsystems/llm-streaming.md)(token 计量:[token-meter.md](../../docs/subsystems/token-meter.md));另见[孪生适配器](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md)、[回放 token 计量](../../.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md)与[按路由模型上下文](../../.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md) Agent Note。
+子系统参考——消息与内容块、模型请求、`StreamChunk` 协议、适配器约定(adapter contract)——见 [docs/subsystems/llm-streaming.md](../../docs/subsystems/llm-streaming.zh.md)(token 计量:[token-meter.md](../../docs/subsystems/token-meter.zh.md));另见[孪生适配器](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md)、[回放 token 计量](../../.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md)与[按路由模型上下文](../../.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.zh.md) Agent Note。
diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml
index a654c25dc6..6219042f95 100644
--- a/packages/llm/llm-deepseek/README.i18n.yaml
+++ b/packages/llm/llm-deepseek/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md
README.md: 1c007d34f329177603305da3858c63960e30a898
-README.zh.md: 181b30bd034ca27e983d4e7a54b03676fbaaba29
+README.zh.md: 5add3972037cc82fbacf8207476b63e40c7e631e
diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md
index 181b30bd03..5add397203 100644
--- a/packages/llm/llm-deepseek/README.zh.md
+++ b/packages/llm/llm-deepseek/README.zh.md
@@ -69,9 +69,9 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:
## 应用归因
-每个请求都携带 dsh-llm `attributionHeaders()` 的共享归因标头,即用于识别 harness 的必需 `User-Agent` 基线(见 [dsh-llm § 应用归因](../llm/README.md#app-attribution-attributionts))。在该适配器约定(adapter contract)下,直接 DeepSeek 请求与 OpenAI 兼容 gateway 请求都不会获得提供方特定应用归因标头;OpenRouter 应用归因暂缓到未来的显式 OpenRouter 适配器或模式。`GenerateOptions.purpose` 为 `compaction` 的请求(dsh-compaction-basic 的辅助摘要调用)还会携带 `x-deepseek-harness-compact: 1`,让宿主可以将压缩流量与会话请求分开。
+每个请求都携带 dsh-llm `attributionHeaders()` 的共享归因标头,即用于识别 harness 的必需 `User-Agent` 基线(见 [dsh-llm § 应用归因](../llm/README.zh.md#app-attribution-attributionts))。在该适配器约定(adapter contract)下,直接 DeepSeek 请求与 OpenAI 兼容 gateway 请求都不会获得提供方特定应用归因标头;OpenRouter 应用归因暂缓到未来的显式 OpenRouter 适配器或模式。`GenerateOptions.purpose` 为 `compaction` 的请求(dsh-compaction-basic 的辅助摘要调用)还会携带 `x-deepseek-harness-compact: 1`,让宿主可以将压缩流量与会话请求分开。
-DeepSeek 请求身份独立于应用归因。凭据解析成功后,每个提供方请求都会通过 `x-deepseek-harness-user-id` 携带来自 [`@deepseek-ai/dsh-anonymous-user-id`](../../identity/anonymous-user-id/README.md) 的稳定匿名 id;携带 `GenerateOptions.sessionId` 的请求还会通过 `x-deepseek-harness-session-id` 发送该确切值,缺少会话的直接调用则省略会话标头。两个标头都会发送至解析后的 `baseURL`(包括已配置的 gateway),且不会进入请求正文或模型可见内容。
+DeepSeek 请求身份独立于应用归因。凭据解析成功后,每个提供方请求都会通过 `x-deepseek-harness-user-id` 携带来自 [`@deepseek-ai/dsh-anonymous-user-id`](../../identity/anonymous-user-id/README.zh.md) 的稳定匿名 id;携带 `GenerateOptions.sessionId` 的请求还会通过 `x-deepseek-harness-session-id` 发送该确切值,缺少会话的直接调用则省略会话标头。两个标头都会发送至解析后的 `baseURL`(包括已配置的 gateway),且不会进入请求正文或模型可见内容。
## 协议格式说明
diff --git a/packages/llm/llm-deepseek/tests/adapter.e2e.ts b/packages/llm/llm-deepseek/tests/adapter.e2e.ts
index 19ce411406..a50606b568 100644
--- a/packages/llm/llm-deepseek/tests/adapter.e2e.ts
+++ b/packages/llm/llm-deepseek/tests/adapter.e2e.ts
@@ -72,7 +72,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', ()
try {
// JSON.stringify quotes the value: YAML is a JSON superset, so a real
// key survives whatever characters it happens to carry.
- await writeFile(join(dir, '.credentials.yaml'), `DEEPSEEK_API_KEY: ${JSON.stringify(key)}\n`, { mode: 0o600 })
+ await writeFile(join(dir, '.credentials.yaml'), `version: 1\nrefs:\n DEEPSEEK_API_KEY: ${JSON.stringify(key)}\n`, { mode: 0o600 })
// Scrub the ambient variable so only the credential seam can supply the
// key: this request proves the per-request resolution path end to end.
vi.stubEnv('DEEPSEEK_API_KEY', '')
diff --git a/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts b/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts
index be7d4e3688..d28c853c3a 100644
--- a/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts
+++ b/packages/llm/llm-deepseek/tests/dynamic-config.spec.ts
@@ -100,7 +100,7 @@ describe('request-level dynamic configuration', () => {
it('routes the next request with the freshly resolved base URL and credential', async () => {
vi.stubEnv('DEEPSEEK_API_KEY', '')
const dir = await home()
- await writeFile(join(dir, '.credentials.yaml'), 'DEEPSEEK_API_KEY: first-key\n', { mode: 0o600 })
+ await writeFile(join(dir, '.credentials.yaml'), 'version: 1\nrefs:\n DEEPSEEK_API_KEY: first-key\n', { mode: 0o600 })
const serverA = await mockServer([{ kind: 'sse', events: textEvents }])
const serverB = await mockServer([{ kind: 'sse', events: textEvents }])
const { ctx } = await boot(dir, { baseURL: serverA.url })
@@ -258,7 +258,7 @@ describe('request-level dynamic configuration', () => {
it('falls back to the composition entry when settings detach', async () => {
vi.stubEnv('DEEPSEEK_API_KEY', '')
const dir = await home()
- await writeFile(join(dir, '.credentials.yaml'), 'DEEPSEEK_API_KEY: steady-key\n', { mode: 0o600 })
+ await writeFile(join(dir, '.credentials.yaml'), 'version: 1\nrefs:\n DEEPSEEK_API_KEY: steady-key\n', { mode: 0o600 })
const serverA = await mockServer([{ kind: 'sse', events: textEvents }])
const serverB = await mockServer([{ kind: 'sse', events: textEvents }])
const { ctx, settingsFiber } = await boot(dir, { baseURL: serverA.url })
diff --git a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts
index 1398d36110..ec83345b61 100644
--- a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts
+++ b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts
@@ -53,7 +53,7 @@ async function loadComposition(
const credentialsPath = join(root, '.credentials.yaml')
if (options.withDynamic && fresh) {
await writeFile(settingsPath, '# personal settings\n')
- await writeFile(credentialsPath, 'DEEPSEEK_API_KEY: boot-key\n', { mode: 0o600 })
+ await writeFile(credentialsPath, 'version: 1\nrefs:\n DEEPSEEK_API_KEY: boot-key\n', { mode: 0o600 })
}
const configPath = join(root, 'cordis.yml')
@@ -124,7 +124,7 @@ describe('llm-deepseek real dynamic composition', () => {
await vi.waitFor(() => {
expect((ctx.get('settings')!.get(NS) as { baseURL?: string }).baseURL).toBe(serverB.url)
}, { timeout: 5000 })
- await writeFile(credentialsPath, 'DEEPSEEK_API_KEY: rotated-key\n', { mode: 0o600 })
+ await writeFile(credentialsPath, 'version: 1\nrefs:\n DEEPSEEK_API_KEY: rotated-key\n', { mode: 0o600 })
await vi.waitFor(async () => {
expect(await ctx.get('credentials')!.resolve(KEY_REF)).toEqual({ value: 'rotated-key', source: 'file' })
}, { timeout: 5000 })
diff --git a/packages/llm/llm-pi-ai/README.i18n.yaml b/packages/llm/llm-pi-ai/README.i18n.yaml
index 6ea69058b9..034382822f 100644
--- a/packages/llm/llm-pi-ai/README.i18n.yaml
+++ b/packages/llm/llm-pi-ai/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md
-README.md: 6d62aabf954a80120bacc9049a7498af269dd067
-README.zh.md: e7e832fa356c48bf14d7aebac7351b259b924214
+README.md: 19dbcfa90dbefbe322800c83da6d75c70c849f05
+README.zh.md: 334c6c3166f35f5dc7d7659b9de7cff404c6b56d
diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md
index 6d62aabf95..19dbcfa90d 100644
--- a/packages/llm/llm-pi-ai/README.md
+++ b/packages/llm/llm-pi-ai/README.md
@@ -140,7 +140,7 @@ Most listings disclose an id and nothing else; `context_window`/`context_length`
Each resolution produces one **immutable** snapshot — the profiles plus a `createModels()` collection holding the `Provider` each route built — and every operation captures a whole snapshot before its first `await`. A configuration change builds a *new* collection rather than mutating the one in use: `Models.streamSimple()` resolves its provider lazily, when the stream is first consumed, which is after the credential await, so a mutated collection would let a request that started under one configuration finish under another or fail on a provider that no longer exists. This is what makes the seam's per-step call freeze (`llm.prepareCall()`) hold end to end — switching models mid-reply takes effect on the next step, never inside the one in flight. Requests reach their provider through `Models.streamSimple()`. A catalog route that keeps its catalog protocol **reuses** the installed provider with its model list replaced, because that provider owns API implementations this package cannot reconstruct — Bedrock loads its Smithy module through a separate entry point — so rebuilding it from parts would silently narrow which providers work. Every other route is built by `createProvider()` over the protocol table behind `supportedProtocols()`, whose entries are the same factories pi-ai's own provider factories use.
-Credentials never enter that collection. The harness resolves a route's key through its own seam before the request reaches pi-ai and passes it as the request's `apiKey` option, which pi-ai treats as the highest-priority auth override; `Models` therefore holds no credential store, and the harness keeps its fail-loud reference semantics. A route naming no credential resolves as configured-but-keyless and leaves the requirement to the protocol, which is where it actually lives.
+A route's `apiKeyEnv` key still resolves through the harness seam before the request reaches pi-ai and rides as the request's `apiKey` option, which pi-ai treats as the highest-priority auth override — the fail-loud reference semantics stay the harness's. Beneath that override the collection carries this plugin's credential store and ambient auth context: a stored sign-in (an OAuth grant, or a key typed into pi-ai's own login prompt) authenticates its route through them and refreshes itself under the store's cross-process lock. The store addresses records as `llm-pi-ai/`, and a hand-declared route key outside that record grammar (uppercase, dots, underscores) reads as holding nothing stored rather than as an addressing error; such a route cannot be signed into — a record write for it refuses with `LlmError('UNSTORABLE_PROVIDER_ID')` — and authenticates through `apiKeyEnv` or ambient provider settings. A route naming no credential at all resolves as configured-but-keyless and leaves the requirement to the protocol, which is where it actually lives.
The selected model descriptor supplies the protocol implementation. This includes native API differences such as OpenAI models whose descriptor uses the Responses API rather than Chat Completions; the harness adapter does not hardcode endpoint selection by model name.
@@ -197,8 +197,8 @@ Recorded response content appends to the next request and does not invalidate it
## Known Limitations and Deferred Work
- **`maxRequestImageBytes` counts base64 image payload only** — text, tools, and JSON structure ride outside the bound, so it must sit below the gateway's request-body cap with headroom. Offload is decided at request conversion as a pure function of history and configuration and is not recorded as a session event; per-route capability metadata (image count, per-image size, total request size) driving admission and assembly together is deferred design work.
-- **A provider that authenticates through OAuth alone is not offered** — pi-ai resolves OAuth from a *stored* OAuth credential, and this adapter builds its `Models` collection with no credential store and runs no login flow, so every request on such a route fails `Provider is not configured` before it goes out. The configurable-provider directory withholds them; `openai-codex` is the only one the installed catalog ships. A route a settings document already names keeps its entry so a configuration surface can edit or delete it, and `apiKeyEnv` still authenticates it with that key — which for Codex is a token that expires with nothing here to refresh it.
-- **Provider-native discovery reads the process environment only** — a route naming no credential defers to the catalog provider's own resolution, which interrogates environment variables (`AZURE_OPENAI_API_KEY`, `AWS_PROFILE`, `AWS_ACCESS_KEY_ID`, and each provider's own set). It reads no local credential directory, so `~/.aws/credentials` without an exported `AWS_PROFILE` resolves as unconfigured, and a value held by the harness credential seam is invisible to it unless the process environment carries it too.
+- **A sign-in lives only in the process that started it** — an authorization attempt is not durable, so reloading the page mid-login abandons it and the human starts over. Signing out is `deleteRecord` on the stored record, which forgets it locally without telling the issuer.
+- **Provider-native discovery answers through this plugin's ambient context** — a route naming no credential defers to the catalog provider's own resolution, which asks for environment values (`AZURE_OPENAI_API_KEY`, `AWS_PROFILE`, and each provider's own set) and for local credential files. Both questions are answered here: the credential seam is consulted before the process environment, and file existence is checked against the host process's filesystem with `~` expanded. What it cannot do is *read* a credential file's contents — a provider that parses `~/.aws/credentials` itself does so directly, outside the seam.
- **Settings can add or override routes, not remove composition routes** — the user layer merges over the composition `base`, so deleting a `cordis.yml`-provided provider is a composition change; `replace` on the namespace only resets the user layer.
- **The layered merge has no delete for dict keys** — the settings seam merges the composition `base` and the user layer per key, recursively, so a `reasoningEfforts` level, `modelOverrides` entry, or `compat` field the base declares cannot be removed by the user layer, only overridden — and for `reasoningEfforts` absence *is* the meaning ("not offered"), so a base-declared level stays offered. This only triggers when a `cordis.yml` entry config declares per-model reasoning fields for the same model the user layer edits; the supported posture is to leave those to the settings document (the shipped composition mounts the adapter dormant), and a `models` list is an array replacing wholesale, which is the in-band escape.
- **`headers` can carry a credential the redactor never sees** — the profile's `headers` dict is plain strings, so `Authorization` or `api-key` set there is returned verbatim by a redacted `describe()` and rendered by any configuration UI. Store credentials as `apiKeyEnv` references; making the dict write-only is deferred with the rest of the [wire-boundary work](../llm/README.md#known-limitations-and-deferred-work).
diff --git a/packages/llm/llm-pi-ai/README.zh.md b/packages/llm/llm-pi-ai/README.zh.md
index e7e832fa35..334c6c3166 100644
--- a/packages/llm/llm-pi-ai/README.zh.md
+++ b/packages/llm/llm-pi-ai/README.zh.md
@@ -141,7 +141,7 @@ pi-ai 依据提供方 id 与 baseURL 决定每个请求的形状:系统提示
每次解析产出一份**不可变**快照——profiles 加上一个持有各路由所建 `Provider` 的 `createModels()` 集合——每个操作都在自己第一个 `await` 之前整体捕获一份快照。配置变化会构造**新**集合,而不是改动正在被使用的那个:`Models.streamSimple()` 是惰性的,它在流首次被消费时才解析 provider,而那已在 credential await 之后,因此改动共享集合会让一个在旧配置下开始的请求在新配置下结束,或者撞上一个已不存在的 provider。这正是 seam 的每步调用冻结(`llm.prepareCall()`)能贯通到底的原因——回复途中切换模型会在下一步生效,绝不会影响在途的那一步。请求经 `Models.streamSimple()` 抵达提供方。保持 catalog 协议不变的 catalog 路由会**复用**已安装提供方,只替换其模型列表,因为该提供方持有本包无法重建的 API 实现——Bedrock 经由独立入口加载其 Smithy 模块——从零件重建会静默收窄可用提供方的范围。其余路由都由 `createProvider()` 基于 `supportedProtocols()` 背后的协议表构造,表中条目正是 pi-ai 自己的提供方工厂所用的同一批 factory。
-凭据绝不进入该集合。harness 在请求抵达 pi-ai 之前经自身 seam 解析路由密钥,并作为请求的 `apiKey` 选项传入,而 pi-ai 将其视为优先级最高的 auth 覆盖;因此 `Models` 不持有任何凭据存储,harness 也保住了自己明确失败的引用语义。没有点名任何凭据的路由会解析为「已配置但无密钥」,把该要求留给协议——那才是它真正所在的位置。
+路由的 `apiKeyEnv` 密钥仍在请求抵达 pi-ai 之前经 harness 自身 seam 解析,并作为请求的 `apiKey` 选项传入——pi-ai 将其视为优先级最高的 auth 覆盖,harness 因此保住自己明确失败的引用语义。在该覆盖之下,集合携带本插件的凭据存储与 ambient auth context:已存储的登录(OAuth grant,或在 pi-ai 自己的登录提示里键入的密钥)经由它们为路由完成认证,并在存储的跨进程锁下自行刷新。存储以 `llm-pi-ai/` 为记录地址;手写路由键若落在记录文法之外(大写、点、下划线),读取时视为「没有存储任何东西」而不是寻址错误——这样的路由无法登录(对它的记录写入会以 `LlmError('UNSTORABLE_PROVIDER_ID')` 拒绝),只能经 `apiKeyEnv` 或提供方 ambient 设置认证。完全没有点名任何凭据的路由会解析为「已配置但无密钥」,把该要求留给协议——那才是它真正所在的位置。
所选模型 descriptor 提供协议实现。这包括原生 API 差异,例如 descriptor 使用 Responses API 而非 Chat Completions 的 OpenAI 模型;harness 适配器不会按模型名称硬编码端点选择。
@@ -159,7 +159,7 @@ pi-ai 依据提供方 id 与 baseURL 决定每个请求的形状:系统提示
## 应用归因
-每个请求都携带 dsh-llm `attributionHeaders()` 的共享归因标头,并通过 pi-ai `headers` 流选项合并。不会合成提供方特定应用归因标头。详见 [dsh-llm § 应用归因](../llm/README.md#app-attribution-attributionts)。
+每个请求都携带 dsh-llm `attributionHeaders()` 的共享归因标头,并通过 pi-ai `headers` 流选项合并。不会合成提供方特定应用归因标头。详见 [dsh-llm § 应用归因](../llm/README.zh.md#app-attribution-attributionts)。
## 依赖体量
@@ -198,11 +198,11 @@ pi-ai 事件会变为 harness 推理、文本、工具调用、usage 与 finish
## 已知限制与暂缓事项
- **`maxRequestImageBytes` 只统计 base64 图片载荷**:文本、工具与 JSON 结构不计入上限,因此该值必须低于网关请求体上限并留出余量。offload 在请求转换时决定,是历史与配置的纯函数,不记录为会话事件;由按路由能力元数据(图片数量、单图大小、请求总大小)同时驱动准入与组装的完整设计属于暂缓工作。
-- **仅以 OAuth 认证的提供方不予提供**:pi-ai 的 OAuth 只从*已存储*的 OAuth 凭据解析,而本适配器构造 `Models` 集合时不注入凭据存储、也不运行登录流程,因此这类路由的每个请求都会在发出之前以 `Provider is not configured` 失败。可配置提供方目录因此不列出它们;已安装 catalog 中只有 `openai-codex` 属于此类。settings 文档已经写过的路由仍保留目录条目,配置界面据此可以编辑或删除;`apiKeyEnv` 也仍能用该密钥完成认证——对 Codex 而言那是一个会过期、且这里没有任何环节会去刷新的 token。
-- **提供方自带的凭据发现只读进程环境**:不指定凭据的路由交由 catalog 提供方自行解析,而它探测的是环境变量(`AZURE_OPENAI_API_KEY`、`AWS_PROFILE`、`AWS_ACCESS_KEY_ID` 以及各提供方自己的那一组)。它不读任何本地凭据目录,因此只有 `~/.aws/credentials` 而未导出 `AWS_PROFILE` 会被解析为未配置;由 harness 凭据 seam 保管的值,除非进程环境里也有,否则对它不可见。
+- **一次登录只存活于发起它的进程中**:授权尝试不可持久,登录途中刷新页面会丢弃它,人需要重来。登出即对已存储记录执行 `deleteRecord`,它只在本地遗忘而不通知签发方。
+- **提供方自带的凭据发现经由本插件的 ambient context 作答**:不指定凭据的路由交由 catalog 提供方自行解析,它会询问环境值(`AZURE_OPENAI_API_KEY`、`AWS_PROFILE` 以及各提供方自己的那一组)与本地凭据文件是否存在。两类问题都在这里作答:先查凭据 seam 再查进程环境,文件存在性则按宿主进程的文件系统判断并展开 `~`。它做不到的是*读取*凭据文件的内容——自行解析 `~/.aws/credentials` 的提供方是直接读盘的,不经过 seam。
- **settings 能新增或覆盖路由,但不能移除组合路由**:用户层合并在组合 `base` 之上,因此删除 `cordis.yml` 提供的提供方属于组合变更;对该 namespace 执行 `replace` 只会重置用户层。
- **分层合并对字典键没有删除语义**:settings seam 把组合 `base` 与用户层按键递归合并,因此 base 声明的某个 `reasoningEfforts` 档位、`modelOverrides` 条目或 `compat` 字段,用户层只能覆盖、无法移除——而 `reasoningEfforts` 里缺席本身*就是*语义(「不提供」),于是 base 声明过的档位会一直被提供。只有 `cordis.yml` entry config 为用户层正在编辑的同一模型声明了按模型推理字段才会触发;受支持的姿态是把这些字段留给 settings 文档(shipped 组合以 dormant 方式挂载该适配器),且 `models` 列表是数组、整体替换,这是带内的解决办法。
-- **`headers` 可能承载一条脱敏器看不见的凭据**:profile 的 `headers` 是纯字符串字典,因此设在其中的 `Authorization` 或 `api-key` 会被脱敏后的 `describe()` 原样返回,并被任何配置 UI 渲染出来。请把凭据存为 `apiKeyEnv` 引用;把该字典整体改为只写与其余[协议边界工作](../llm/README.md#known-limitations-and-deferred-work)一并暂缓。
+- **`headers` 可能承载一条脱敏器看不见的凭据**:profile 的 `headers` 是纯字符串字典,因此设在其中的 `Authorization` 或 `api-key` 会被脱敏后的 `describe()` 原样返回,并被任何配置 UI 渲染出来。请把凭据存为 `apiKeyEnv` 引用;把该字典整体改为只写与其余[协议边界工作](../llm/README.zh.md#known-limitations-and-deferred-work)一并暂缓。
- **路由的 catalog 不会自我刷新**:catalog 就是 `settings.yaml` 所写的内容,因此模型列表的新鲜度只到最近一次编辑为止。这里没有任何环节会去问提供方它服务哪些模型;路由要多一个模型,得有人写进去。
- **每条路由只有一种协议格式**:`api` 作用于整条路由,因此混合协议的 catalog 路由(跨 Responses 与 Chat Completions 的 OpenAI 式 catalog)无法承载另一种协议的模型,向这类路由添加它未描述的模型必须点名 `api` 并把全部模型一起迁过去。把该提供方拆成两个路由键是变通办法。
- **模态声明不经验证,且多声明的后果超出本轮**:没有任何环节会去询问端点接受什么,因此声明了网关并不提供的 `image` 的模型不会在这里被拦下,而是由提供方在轮次中途拒绝。prompt 准入在构造请求之前就把用户消息持久化提交,于是被拒绝的图片留在会话日志里:该模型会不断重发它,而模型选择拒绝切换到任何纯文本模型。恢复途径是换一个确实支持图片的模型、fork 到图片之前,或开启新会话;发送失败时把尚未消费的图片消息从日志中回滚出去这件事已暂缓。
diff --git a/packages/llm/llm-pi-ai/package.json b/packages/llm/llm-pi-ai/package.json
index be02e941df..ab286f006f 100644
--- a/packages/llm/llm-pi-ai/package.json
+++ b/packages/llm/llm-pi-ai/package.json
@@ -33,6 +33,7 @@
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-attachment": "workspace:^",
+ "@deepseek-ai/dsh-authorization": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
@@ -47,6 +48,7 @@
},
"devDependencies": {
"@deepseek-ai/dsh-attachment": "workspace:^",
+ "@deepseek-ai/dsh-authorization": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
diff --git a/packages/llm/llm-pi-ai/src/adapter.ts b/packages/llm/llm-pi-ai/src/adapter.ts
index 5ecec593da..3c7ecd4a91 100644
--- a/packages/llm/llm-pi-ai/src/adapter.ts
+++ b/packages/llm/llm-pi-ai/src/adapter.ts
@@ -13,10 +13,15 @@
* way down: switching models mid-reply takes effect on the next step, never
* inside the one in flight.
*
- * Credentials stay outside that collection. The harness resolves a route's key
- * through its own seam and passes it as the request's `apiKey` option, which
- * pi-ai treats as the highest-priority auth override — so `Models` never holds
- * a credential store and the harness keeps its fail-loud reference semantics.
+ * A route naming a credential reference still resolves it through the harness
+ * seam and passes it as the request's `apiKey` option, which pi-ai treats as
+ * the highest-priority auth override — that is what keeps the fail-loud
+ * reference semantics. Everything that override does not cover reaches pi-ai
+ * through the collection's own auth: the credential store holds the records a
+ * login wrote and a refresh rotates, and the auth context answers the ambient
+ * questions a provider asks while resolving. Both are stable across snapshots,
+ * so a configuration change rebuilds the collection without forgetting who is
+ * signed in.
*
* @module dsh-llm-pi-ai/adapter
*/
@@ -24,6 +29,8 @@
import { createModels, getSupportedThinkingLevels } from '@earendil-works/pi-ai'
import type {
Api,
+ AuthContext,
+ CredentialStore,
Model,
Models,
ModelThinkingLevel,
@@ -74,6 +81,15 @@ export interface PiAiAdapterOptions {
* `MISSING_CREDENTIAL` rather than falling back.
*/
resolveApiKey: (provider: string, profile: ResolvedPiAiProviderProfile) => Promise
+ /**
+ * How every collection this adapter builds resolves auth the request-level
+ * `apiKey` override does not cover. Required rather than optional: a
+ * collection built without them gets pi-ai's in-memory default store, which
+ * is empty at every boot and discarded on every configuration change, so a
+ * route whose only method is a login would report itself unconfigured on
+ * every request no matter how often the human signed in.
+ */
+ auth: PiAiAuthInjection
/** Resolve the optional durable attachment service at request time. */
resolveAttachments?: () => AttachmentStore | undefined
/**
@@ -83,6 +99,14 @@ export interface PiAiAdapterOptions {
onReplayDegrade?: (detail: { provider: string; model: string; reason: string }) => void
}
+/** The two auth injectables a pi-ai collection is built with. */
+export interface PiAiAuthInjection {
+ /** Durable storage for credentials pi-ai itself writes: logins, and the refreshes it runs under its own lock. */
+ credentials: CredentialStore
+ /** Ambient lookups a provider performs while resolving its own auth. */
+ authContext: AuthContext
+}
+
/** Copy profile stream knobs into pi-ai's common option vocabulary. */
function profileOptions(
profile: ResolvedPiAiProviderProfile,
@@ -204,7 +228,7 @@ export class PiAiAdapter extends LlmAdapter {
private current(): PiAiSnapshot {
const profiles = this.config.profiles()
if (this.snapshot?.profiles === profiles) return this.snapshot
- const models: MutableModels = createModels()
+ const models: MutableModels = createModels(this.config.auth)
for (const profile of profiles.values()) models.setProvider(profile.piProvider)
this.snapshot = { profiles, models }
return this.snapshot
diff --git a/packages/llm/llm-pi-ai/src/auth.ts b/packages/llm/llm-pi-ai/src/auth.ts
new file mode 100644
index 0000000000..f729a77f85
--- /dev/null
+++ b/packages/llm/llm-pi-ai/src/auth.ts
@@ -0,0 +1,207 @@
+/**
+ * The three adapters between pi-ai's auth model and the harness credential
+ * plane. Every pi-ai-specific concept stays on this side of them: the harness
+ * seams they consume — `ctx.credentials` records and `ctx.authorization` flows —
+ * name nothing from this library, so another adapter family can arrive with a
+ * different auth model and share the same two seams.
+ *
+ * @module dsh-llm-pi-ai/auth
+ */
+
+import { homedir } from 'node:os'
+import { access } from 'node:fs/promises'
+import { resolve as resolvePath } from 'node:path'
+import type { AuthContext, Credential, CredentialInfo, CredentialStore } from '@earendil-works/pi-ai'
+import type { Context } from '@deepseek-ai/cordis'
+import {
+ credentialKey, credentialKeyId, credentialKeyScope, credentialRef, isCredentialKeySegment, isCredentialRefName,
+} from '@deepseek-ai/dsh-credentials'
+import type { CredentialKey, CredentialProvider, CredentialRecord } from '@deepseek-ai/dsh-credentials'
+import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
+import { LlmError } from '@deepseek-ai/dsh-llm'
+
+/**
+ * The record scope every credential this adapter family stores is written
+ * under. It is the plugin's registered name, which is what tells a later
+ * reader — a configuration UI, or a second adapter family serving the same
+ * provider name — that this plugin owns the format inside the record.
+ */
+export const RECORD_SCOPE = 'llm-pi-ai'
+
+/**
+ * The record address for one pi-ai provider id.
+ * @param providerId - pi-ai's own provider id, which is also the harness route key.
+ * @returns the scoped credential key this adapter family reads and writes.
+ */
+export function recordKeyFor(providerId: string): CredentialKey {
+ return credentialKey(RECORD_SCOPE, providerId)
+}
+
+/**
+ * Translate a stored record into the credential pi-ai expects.
+ *
+ * An `api-key` record is structural on both sides, so it is rebuilt field by
+ * field. A `grant` payload is pi-ai's own OAuth credential, stored verbatim:
+ * the seam treats it as opaque JSON precisely so a library that owns a token
+ * format keeps owning it, refresh fields and all.
+ * @param record - the stored record, or undefined when nothing is stored.
+ * @returns the pi-ai credential, or undefined for an absent record.
+ */
+function toPiCredential(record: CredentialRecord | undefined): Credential | undefined {
+ if (record === undefined) return undefined
+ if (record.kind === 'api-key') {
+ return {
+ type: 'api_key',
+ ...record.key === undefined ? {} : { key: record.key },
+ ...record.env === undefined ? {} : { env: { ...record.env } },
+ }
+ }
+ return record.payload as Credential
+}
+
+/**
+ * Translate a pi-ai credential into the record to store.
+ * @param credential - what a login or refresh produced.
+ * @returns the record to commit, in the union the credential seam stores.
+ */
+function toRecord(credential: Credential): CredentialRecord {
+ if (credential.type === 'api_key') {
+ return {
+ kind: 'api-key',
+ ...credential.key === undefined ? {} : { key: credential.key },
+ ...credential.env === undefined ? {} : { env: { ...credential.env } },
+ }
+ }
+ return { kind: 'grant', payload: credential }
+}
+
+/**
+ * The credential service, or the failure that names what is missing. Reads
+ * answer "nothing stored" without a service, because a composition with no
+ * credential plane genuinely holds no credential; writes refuse, because a
+ * login whose grant silently evaporated would report success and then fail
+ * every request.
+ * @param ctx - the plugin context.
+ * @returns the live service.
+ * @throws {LlmError} code `NO_CREDENTIAL_STORE` when none is mounted.
+ */
+function writableStore(ctx: Context): CredentialProvider {
+ const credentials = ctx.get('credentials')
+ if (credentials === undefined) {
+ throw new LlmError(
+ 'llm-pi-ai: this composition mounts no credentials service, so there is nowhere to store the'
+ + ' credential a sign-in produces; mount one (dsh-credentials-local) to sign in',
+ 'NO_CREDENTIAL_STORE',
+ )
+ }
+ return credentials
+}
+
+/**
+ * A pi-ai `CredentialStore` over the harness credential records.
+ *
+ * pi-ai runs OAuth refresh *inside* `modify()`, so this store's exclusion has
+ * to cover a network round trip rather than a file rename — which is why the
+ * record write path takes a wait limit of its own rather than the short one a
+ * local write would need.
+ *
+ * pi-ai asks this store about every provider in the collection, hand-declared
+ * routes included, and a route key is an arbitrary settings dict key while a
+ * record id is not. An id outside the record grammar can never have stored a
+ * record, so reads answer "nothing stored" and a delete has nothing to remove;
+ * only `modify` refuses it, because a write that cannot land must not report
+ * that it did.
+ * @param ctx - the plugin context carrying the optional `ctx.credentials`.
+ * @returns the store to hand `createModels()`.
+ */
+export function credentialStoreFrom(ctx: Context): CredentialStore {
+ return {
+ async read(providerId) {
+ const credentials = ctx.get('credentials')
+ if (credentials === undefined) return undefined
+ if (!isCredentialKeySegment(providerId)) return undefined
+ return toPiCredential(await credentials.readRecord(recordKeyFor(providerId)))
+ },
+ async list(): Promise {
+ const stored = await ctx.get('credentials')?.listRecords() ?? []
+ const mine: CredentialInfo[] = []
+ for (const entry of stored) {
+ // Records another plugin owns are not this collection's to report:
+ // their payloads are written in a format pi-ai never agreed to.
+ if (credentialKeyScope(entry.key) !== RECORD_SCOPE) continue
+ mine.push({
+ providerId: credentialKeyId(entry.key),
+ type: entry.kind === 'api-key' ? 'api_key' : 'oauth',
+ })
+ }
+ return mine
+ },
+ async modify(providerId, mutate) {
+ if (!isCredentialKeySegment(providerId)) {
+ throw new LlmError(
+ `llm-pi-ai: provider id "${providerId}" cannot address a stored credential record (a record id is a`
+ + ' lowercase hyphenated identifier); authenticate this route through apiKeyEnv instead of a stored'
+ + ' credential',
+ 'UNSTORABLE_PROVIDER_ID',
+ )
+ }
+ const stored = await writableStore(ctx).modifyRecord(recordKeyFor(providerId), async (current) => {
+ const next = await mutate(toPiCredential(current))
+ return next === undefined ? undefined : toRecord(next)
+ })
+ return toPiCredential(stored)
+ },
+ // `async` so a missing service reaches the caller as a rejection: pi-ai's
+ // store contract is promise-returning, and a synchronous throw would
+ // escape the `ModelsError` wrapper every other storage failure gets.
+ async delete(providerId) {
+ if (!isCredentialKeySegment(providerId)) return
+ await writableStore(ctx).deleteRecord(recordKeyFor(providerId))
+ },
+ }
+}
+
+/**
+ * A pi-ai `AuthContext` over the harness credential plane and the host
+ * filesystem.
+ *
+ * `env()` answers from the credential seam first, so a value a deployment
+ * stored through the harness is found by a provider's own ambient discovery —
+ * without this, that discovery reads only the process environment and a stored
+ * `AWS_ACCESS_KEY_ID` is invisible to it. `fileExists()` answers about the host
+ * process's own filesystem rather than the workspace `ctx.fs` seam, because the
+ * paths it is asked about (`~/.aws/credentials`, application-default
+ * credentials) are facts about where this process runs, not about the project
+ * under edit.
+ * @param ctx - the plugin context carrying the optional `ctx.credentials`.
+ * @returns the auth context to hand `createModels()`.
+ */
+export function authContextFrom(ctx: Context): AuthContext {
+ return {
+ async env(name) {
+ // pi-ai asks about arbitrary provider-declared names; one that is not a
+ // POSIX identifier can never have been stored as a reference, and asking
+ // the seam would throw instead of answering "not set".
+ if (isCredentialRefName(name)) {
+ const credentials = ctx.get('credentials')
+ const hit = await credentials?.resolve(credentialRef(name))
+ if (hit !== undefined) return hit.value
+ }
+ return launchEnvironmentOf(ctx).get(name)?.value
+ },
+ async fileExists(path) {
+ const expanded = path.startsWith('~/') || path === '~'
+ ? resolvePath(homedir(), path.slice(1).replace(/^\//, ''))
+ : path
+ try {
+ await access(expanded)
+ return true
+ } catch {
+ // Absent, unreadable, or a broken symlink — every one of which means
+ // this ambient credential source cannot be used, which is the only
+ // distinction the caller makes.
+ return false
+ }
+ },
+ }
+}
diff --git a/packages/llm/llm-pi-ai/src/catalog.ts b/packages/llm/llm-pi-ai/src/catalog.ts
index 229e881c98..1e9b126dd5 100644
--- a/packages/llm/llm-pi-ai/src/catalog.ts
+++ b/packages/llm/llm-pi-ai/src/catalog.ts
@@ -176,26 +176,6 @@ export function catalogProviderIds(): readonly string[] {
return getBuiltinProviders()
}
-/**
- * Whether the installed catalog provider for one route declares an api-key
- * method — the only authentication this adapter obtains on its own.
- *
- * A key is what the harness resolves through its own credential seam and hands
- * pi-ai per request. pi-ai's other method, OAuth, resolves from a *stored*
- * OAuth credential alone: `resolveProviderAuth` has no ambient path for it,
- * this adapter builds its `Models` collection with no credential store, and
- * nothing here runs a login flow. So a provider offering OAuth by itself
- * leaves nothing for this adapter to authenticate with, and the posture such a
- * provider invites — no key configured, credentials discovered by the provider
- * — fails every request with `Provider is not configured`.
- * @param provider - provider route key.
- * @returns whether the catalog provider takes an api key; false for a route
- * pi-ai does not ship, which the caller answers for separately.
- */
-export function catalogProviderTakesApiKey(provider: string): boolean {
- return catalogProvider(provider)?.auth.apiKey !== undefined
-}
-
/**
* The installed catalog models for one route, indexed by model id.
* @param provider - provider route key.
diff --git a/packages/llm/llm-pi-ai/src/index.ts b/packages/llm/llm-pi-ai/src/index.ts
index 1bbeec79db..846509f8c6 100644
--- a/packages/llm/llm-pi-ai/src/index.ts
+++ b/packages/llm/llm-pi-ai/src/index.ts
@@ -61,10 +61,12 @@ import { assertUsableApiKey, LlmError } from '@deepseek-ai/dsh-llm'
import type { AdapterRegistrationHandle, DirectoryRegistrationHandle, LlmConfigurableProvider } from '@deepseek-ai/dsh-llm'
import { deepEqualJson, installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
import { PiAiAdapter } from './adapter.ts'
-import { catalogProviderIds, catalogProviderTakesApiKey } from './catalog.ts'
+import { authContextFrom, credentialStoreFrom } from './auth.ts'
+import { catalogProviderIds } from './catalog.ts'
import { assertServiceable, Config, resolveProfiles } from './config.ts'
import type { ResolvedPiAiProviderProfile } from './config.ts'
import { discoverModels } from './discovery.ts'
+import { registerPiAiFlows } from './login.ts'
export { PiAiAdapter } from './adapter.ts'
export type { PiAiAdapterOptions } from './adapter.ts'
@@ -79,6 +81,7 @@ export type {
PiAiThinkingFormat,
ResolvedPiAiProviderProfile,
} from './config.ts'
+export { recordKeyFor } from './auth.ts'
export { supportedProtocols } from './provider.ts'
export const name = 'llm-pi-ai'
@@ -105,15 +108,10 @@ function registrationFacts(profiles: ReadonlyMap ctx.get('attachments'),
onReplayDegrade: ({ provider, model, reason }) => {
ctx.logger.warn(
@@ -208,6 +204,12 @@ export function apply(ctx: Context, config: Config): void {
)
},
})
+ // Independent of the route set: signing in is what makes a route worth
+ // adding, so the flows are offered before any profile names their provider.
+ // Scoped to the authorization seam rather than injected outright, because a
+ // composition without it (headless, ACP) simply has no surface to sign in
+ // from, while everything else this plugin does still works.
+ ctx.inject(['authorization'], (authorized) => { registerPiAiFlows(authorized, auth) })
// The full installed catalog is configurable from the moment the plugin
// mounts — dormant or not — so configuration surfaces can offer every
// pi-ai provider before any route exists. Hand-declared routes join it as
diff --git a/packages/llm/llm-pi-ai/src/login.ts b/packages/llm/llm-pi-ai/src/login.ts
new file mode 100644
index 0000000000..578489afb4
--- /dev/null
+++ b/packages/llm/llm-pi-ai/src/login.ts
@@ -0,0 +1,161 @@
+/**
+ * Authorization flows for the pi-ai providers that ship a login. This is the
+ * whole of the translation between the harness's neutral notice/prompt
+ * vocabulary and pi-ai's `AuthInteraction`; nothing above it knows which
+ * library ran the conversation.
+ *
+ * @module dsh-llm-pi-ai/login
+ */
+
+import { createModels } from '@earendil-works/pi-ai'
+import type { AuthEvent, AuthPrompt, AuthType, Provider } from '@earendil-works/pi-ai'
+import type { Context } from '@deepseek-ai/cordis'
+import type { AuthorizationMethod, AuthorizationPrompt, AuthorizationSession } from '@deepseek-ai/dsh-authorization'
+import { isCredentialKeySegment } from '@deepseek-ai/dsh-credentials'
+import { catalogProvider, catalogProviderIds } from './catalog.ts'
+import { recordKeyFor } from './auth.ts'
+import type { PiAiAuthInjection } from './adapter.ts'
+
+/**
+ * The login methods one catalog provider offers.
+ *
+ * A method appears only when pi-ai can actually run it: `oauth` always carries
+ * a `login`, while an api-key method has one only when the provider collects
+ * its key interactively — which every installed one currently does, so a key is
+ * typed into pi-ai's own prompt rather than into the settings form.
+ * @param provider - the installed catalog provider, if pi-ai ships one.
+ * @returns its methods, most preferred first; empty when it offers no login.
+ */
+function loginMethods(provider: Provider | undefined): AuthorizationMethod[] {
+ const methods: AuthorizationMethod[] = []
+ const oauth = provider?.auth.oauth
+ if (oauth !== undefined) methods.push({ id: 'oauth', label: oauth.loginLabel ?? oauth.name })
+ const apiKey = provider?.auth.apiKey
+ if (apiKey?.login !== undefined) methods.push({ id: 'api-key', label: apiKey.name })
+ return methods
+}
+
+/**
+ * Restate one pi-ai login event in the seam's vocabulary.
+ *
+ * A device-code grant is the one event carrying two things the human needs at
+ * once — where to go and what to type there — which is why the neutral notice
+ * has a `code` beside its `url` rather than folding the code into the message.
+ * @param event - what pi-ai reported.
+ * @param session - the attempt to report it to.
+ */
+function relay(event: AuthEvent, session: AuthorizationSession): void {
+ switch (event.type) {
+ case 'info': {
+ const link = event.links?.[0]
+ session.notify({ message: event.message, ...link === undefined ? {} : { url: link.url } })
+ return
+ }
+ case 'auth_url':
+ session.notify({
+ message: event.instructions ?? 'Open this page to continue signing in.',
+ url: event.url,
+ })
+ return
+ case 'device_code':
+ session.notify({
+ message: 'Enter this code on the verification page to finish signing in.',
+ url: event.verificationUri,
+ code: event.userCode,
+ })
+ return
+ case 'progress':
+ session.notify({ message: event.message })
+ return
+ default:
+ // pi-ai's event union is open to new members: a build that meets one it
+ // does not know still shows the human that something is happening rather
+ // than going silent mid-login.
+ session.notify({ message: 'Signing in…' })
+ }
+}
+
+/**
+ * Restate one pi-ai prompt in the seam's vocabulary.
+ *
+ * `manual_code` becomes a plain text question because the difference pi-ai
+ * draws — a code the human copies from a browser rather than a value they know
+ * — changes nothing a surface renders. Its own `signal` is carried through, and
+ * that is the part which matters: it is how a flow racing a typed code against
+ * a browser callback withdraws the losing question.
+ * @param prompt - what pi-ai asked.
+ * @returns the neutral prompt to put to the human.
+ */
+function restate(prompt: AuthPrompt): AuthorizationPrompt {
+ const signal = prompt.signal === undefined ? {} : { signal: prompt.signal }
+ switch (prompt.type) {
+ case 'select':
+ return { ...signal, kind: 'select', message: prompt.message, options: prompt.options }
+ case 'secret':
+ return {
+ ...signal,
+ kind: 'secret',
+ message: prompt.message,
+ ...prompt.placeholder === undefined ? {} : { placeholder: prompt.placeholder },
+ }
+ default:
+ return {
+ ...signal,
+ kind: 'text',
+ message: prompt.message,
+ ...prompt.placeholder === undefined ? {} : { placeholder: prompt.placeholder },
+ }
+ }
+}
+
+/**
+ * Register one authorization flow per installed provider that ships a login.
+ *
+ * Registration is unconditional on configuration: a provider has to be signed
+ * into before a route for it is worth adding, so the flow exists from the
+ * moment the plugin mounts rather than appearing once a profile does.
+ * @param ctx - the plugin context carrying `ctx.authorization`.
+ * @param auth - the injectables every collection here is built with.
+ */
+export function registerPiAiFlows(ctx: Context, auth: PiAiAuthInjection): void {
+ for (const providerId of catalogProviderIds()) {
+ const provider = catalogProvider(providerId)
+ const [first, ...rest] = loginMethods(provider)
+ /* v8 ignore next 3 -- every id here names an installed provider and every
+ installed provider ships a login, so nothing is skipped today; the guard
+ is what keeps that from becoming a crash if either stops being true. */
+ if (provider === undefined || first === undefined) continue
+ /* v8 ignore next 7 -- every installed catalog id today is a lowercase
+ hyphenated identifier; the guard keeps a future upstream id outside the
+ record grammar (dotted or uppercase, as vendor ids elsewhere already
+ are) from throwing in `recordKeyFor` and failing the whole mount. */
+ if (!isCredentialKeySegment(providerId)) {
+ ctx.logger.warn(
+ 'llm-pi-ai: catalog provider "%s" cannot address a credential record; its sign-in is not offered',
+ providerId)
+ continue
+ }
+ ctx.authorization.registerFlow({
+ key: recordKeyFor(providerId),
+ label: provider.name,
+ methods: [first, ...rest],
+ async run(session) {
+ // A collection of its own, holding only the provider being signed
+ // into: login is not serving requests, and the credential it produces
+ // lands in the shared store either way.
+ const models = createModels(auth)
+ models.setProvider(provider)
+ // Total over the two ids declared above, and the seam only ever hands
+ // back one a flow declared.
+ const type: AuthType = session.method === 'oauth' ? 'oauth' : 'api_key'
+ // pi-ai persists what the login returns through that same store, which
+ // is what makes it the single writer of this record.
+ await models.login(providerId, type, {
+ signal: session.signal,
+ notify: (event) => { relay(event, session) },
+ prompt: prompt => session.prompt(restate(prompt)),
+ })
+ },
+ })
+ }
+}
diff --git a/packages/llm/llm-pi-ai/tests/adapter.spec.ts b/packages/llm/llm-pi-ai/tests/adapter.spec.ts
index 2446b10286..a36a180f7d 100644
--- a/packages/llm/llm-pi-ai/tests/adapter.spec.ts
+++ b/packages/llm/llm-pi-ai/tests/adapter.spec.ts
@@ -13,6 +13,7 @@ import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import { getBuiltinModels } from '@earendil-works/pi-ai/providers/all'
import { DEFAULT_MAX_REQUEST_IMAGE_BYTES, resolveProfiles } from '../src/config.ts'
+import { memoryAuth } from './auth-double.ts'
import { assemble } from './assemble.ts'
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
@@ -47,6 +48,7 @@ function adapterOf(
return new PiAiAdapter({
profiles: () => resolveProfiles(providers),
resolveApiKey: () => Promise.resolve(apiKey),
+ auth: memoryAuth(),
})
}
diff --git a/packages/llm/llm-pi-ai/tests/auth-double.ts b/packages/llm/llm-pi-ai/tests/auth-double.ts
new file mode 100644
index 0000000000..a05c017d04
--- /dev/null
+++ b/packages/llm/llm-pi-ai/tests/auth-double.ts
@@ -0,0 +1,39 @@
+import type { Credential } from '@earendil-works/pi-ai'
+import type { PiAiAuthInjection } from '../src/adapter.ts'
+
+/**
+ * The auth injectables for tests that exercise streaming rather than
+ * authentication: an in-process credential store and an ambient context that
+ * finds nothing. A test needing real records builds the store over
+ * `ctx.credentials` instead, through `credentialStoreFrom`.
+ * @param seed - credentials to start with, by pi-ai provider id.
+ * @returns the injection to hand `PiAiAdapter`, with its store readable.
+ */
+export function memoryAuth(seed: Record = {}): PiAiAuthInjection & {
+ stored: Map
+} {
+ const stored = new Map(Object.entries(seed))
+ return {
+ stored,
+ credentials: {
+ read: id => Promise.resolve(stored.get(id)),
+ list: () => Promise.resolve([...stored].map(([providerId, credential]) => ({
+ providerId,
+ type: credential.type,
+ }))),
+ async modify(id, mutate) {
+ const next = await mutate(stored.get(id))
+ if (next !== undefined) stored.set(id, next)
+ return stored.get(id)
+ },
+ delete: (id) => {
+ stored.delete(id)
+ return Promise.resolve()
+ },
+ },
+ authContext: {
+ env: () => Promise.resolve(undefined),
+ fileExists: () => Promise.resolve(false),
+ },
+ }
+}
diff --git a/packages/llm/llm-pi-ai/tests/auth.spec.ts b/packages/llm/llm-pi-ai/tests/auth.spec.ts
new file mode 100644
index 0000000000..27a496a2c3
--- /dev/null
+++ b/packages/llm/llm-pi-ai/tests/auth.spec.ts
@@ -0,0 +1,172 @@
+import { mkdtemp, rm, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { Context } from '@deepseek-ai/cordis'
+import LocalCredentialProvider from '@deepseek-ai/dsh-credentials-local'
+import { credentialKey, credentialRef } from '@deepseek-ai/dsh-credentials'
+import { authContextFrom, credentialStoreFrom, recordKeyFor } from '../src/auth.ts'
+
+const CODEX = recordKeyFor('openai-codex')
+
+const dirs: string[] = []
+
+/** A context whose credential records live in a throwaway `$DSH_HOME`. */
+async function stored(): Promise {
+ const dir = await mkdtemp(join(tmpdir(), 'dsh-pi-auth-'))
+ dirs.push(dir)
+ const ctx = new Context()
+ await ctx.plugin(LocalCredentialProvider, { path: join(dir, '.credentials.yaml'), watch: false })
+ return ctx
+}
+
+afterEach(async () => {
+ vi.unstubAllEnvs()
+ await Promise.all(dirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
+})
+
+describe('pi-ai credential store over harness records', () => {
+ it('reads nothing for a provider with no record', async () => {
+ const store = credentialStoreFrom(await stored())
+
+ await expect(store.read('openai-codex')).resolves.toBeUndefined()
+ })
+
+ it('round-trips an api-key credential field by field', async () => {
+ const ctx = await stored()
+ const store = credentialStoreFrom(ctx)
+
+ await store.modify('cloudflare', () =>
+ Promise.resolve({ type: 'api_key', key: 'sk-live', env: { ACCOUNT_ID: 'acct-1' } }))
+
+ await expect(store.read('cloudflare'))
+ .resolves.toEqual({ type: 'api_key', key: 'sk-live', env: { ACCOUNT_ID: 'acct-1' } })
+ await expect(ctx.credentials.readRecord(recordKeyFor('cloudflare')))
+ .resolves.toEqual({ kind: 'api-key', key: 'sk-live', env: { ACCOUNT_ID: 'acct-1' } })
+ })
+
+ it('stores an api-key credential carrying neither a key nor env', async () => {
+ const store = credentialStoreFrom(await stored())
+
+ await store.modify('bedrock', () => Promise.resolve({ type: 'api_key' }))
+
+ await expect(store.read('bedrock')).resolves.toEqual({ type: 'api_key' })
+ })
+
+ it('keeps an OAuth credential verbatim, refresh fields and all', async () => {
+ const ctx = await stored()
+ const store = credentialStoreFrom(ctx)
+ const granted = { type: 'oauth' as const, access: 'at', refresh: 'rt', expires: 42, accountId: 'acc' }
+
+ await store.modify('openai-codex', () => Promise.resolve(granted))
+
+ await expect(store.read('openai-codex')).resolves.toEqual(granted)
+ await expect(ctx.credentials.readRecord(CODEX)).resolves.toEqual({ kind: 'grant', payload: granted })
+ })
+
+ it('shows the mutation the current credential and leaves it alone when declined', async () => {
+ const store = credentialStoreFrom(await stored())
+ await store.modify('openai-codex', () =>
+ Promise.resolve({ type: 'oauth', access: 'first', refresh: 'r', expires: 1 }))
+ const seen: unknown[] = []
+
+ const unchanged = await store.modify('openai-codex', (current) => {
+ seen.push(current)
+ return Promise.resolve(undefined)
+ })
+
+ expect(seen).toEqual([{ type: 'oauth', access: 'first', refresh: 'r', expires: 1 }])
+ expect(unchanged).toEqual({ type: 'oauth', access: 'first', refresh: 'r', expires: 1 })
+ })
+
+ it('lists only the records this adapter family owns', async () => {
+ const ctx = await stored()
+ const store = credentialStoreFrom(ctx)
+ await store.modify('openai-codex', () =>
+ Promise.resolve({ type: 'oauth', access: 'at', refresh: 'rt', expires: 1 }))
+ await store.modify('cloudflare', () => Promise.resolve({ type: 'api_key', key: 'k' }))
+ // Another plugin's record for a provider name this one also serves: its
+ // payload is written in a format pi-ai never agreed to.
+ await ctx.credentials.modifyRecord(credentialKey('llm-kimi', 'openai-codex'), () =>
+ Promise.resolve({ kind: 'grant', payload: { theirs: true } }))
+
+ await expect(store.list()).resolves.toEqual([
+ { providerId: 'openai-codex', type: 'oauth' },
+ { providerId: 'cloudflare', type: 'api_key' },
+ ])
+ })
+
+ it('forgets a credential on delete, and stays quiet when there was none', async () => {
+ const store = credentialStoreFrom(await stored())
+ await store.modify('openai-codex', () =>
+ Promise.resolve({ type: 'oauth', access: 'at', refresh: 'rt', expires: 1 }))
+
+ await store.delete('openai-codex')
+ await store.delete('openai-codex')
+
+ await expect(store.read('openai-codex')).resolves.toBeUndefined()
+ })
+
+ it('reads empty but refuses to write without a credentials service', async () => {
+ const store = credentialStoreFrom(new Context())
+
+ await expect(store.read('openai-codex')).resolves.toBeUndefined()
+ await expect(store.list()).resolves.toEqual([])
+ await expect(store.modify('openai-codex', () => Promise.resolve({ type: 'api_key', key: 'k' })))
+ .rejects.toThrow(/mounts no credentials service/)
+ await expect(store.delete('openai-codex')).rejects.toThrow(/mounts no credentials service/)
+ })
+
+ it('treats a provider id outside the record grammar as holding nothing', async () => {
+ const store = credentialStoreFrom(await stored())
+
+ // A hand-declared route key is an arbitrary settings dict key, and pi-ai
+ // reads it during auth resolution: the answer is "not signed in", never a
+ // thrown address error…
+ await expect(store.read('My_Proxy')).resolves.toBeUndefined()
+ // …nothing can ever be stored under it, so a logout has nothing to remove…
+ await expect(store.delete('My_Proxy')).resolves.toBeUndefined()
+ // …while a write that cannot land must refuse rather than report success.
+ await expect(store.modify('My_Proxy', () => Promise.resolve({ type: 'api_key', key: 'k' })))
+ .rejects.toThrow(/cannot address a stored credential record/)
+ })
+})
+
+describe('pi-ai ambient auth context', () => {
+ beforeEach(() => {
+ vi.stubEnv('PI_AUTH_AMBIENT', 'from-environment')
+ })
+
+ it('answers an environment name from the credential seam first', async () => {
+ const ctx = await stored()
+ await ctx.credentials.set(credentialRef('PI_AUTH_SEAM'), 'from-seam')
+
+ await expect(authContextFrom(ctx).env('PI_AUTH_SEAM')).resolves.toBe('from-seam')
+ })
+
+ it('falls back to the launch environment when nothing is stored', async () => {
+ await expect(authContextFrom(await stored()).env('PI_AUTH_AMBIENT')).resolves.toBe('from-environment')
+ })
+
+ it('answers "not set" for a name no reference could ever address', async () => {
+ // pi-ai asks about provider-declared names; one outside the reference
+ // grammar has no reference to miss, and must not throw.
+ await expect(authContextFrom(await stored()).env('not a var')).resolves.toBeUndefined()
+ })
+
+ it('answers about the host filesystem, expanding a leading ~', async () => {
+ const dir = await mkdtemp(join(tmpdir(), 'dsh-pi-home-'))
+ dirs.push(dir)
+ await writeFile(join(dir, 'creds'), 'x')
+ // Both spellings of "home": os.homedir() reads HOME on POSIX and
+ // USERPROFILE on Windows, and the expansion under test goes through it.
+ vi.stubEnv('HOME', dir)
+ vi.stubEnv('USERPROFILE', dir)
+ const context = authContextFrom(await stored())
+
+ await expect(context.fileExists('~/creds')).resolves.toBe(true)
+ await expect(context.fileExists('~/missing')).resolves.toBe(false)
+ await expect(context.fileExists(join(dir, 'creds'))).resolves.toBe(true)
+ await expect(context.fileExists('~')).resolves.toBe(true)
+ })
+})
diff --git a/packages/llm/llm-pi-ai/tests/catalog.spec.ts b/packages/llm/llm-pi-ai/tests/catalog.spec.ts
index 0322b81edf..99886cdbaf 100644
--- a/packages/llm/llm-pi-ai/tests/catalog.spec.ts
+++ b/packages/llm/llm-pi-ai/tests/catalog.spec.ts
@@ -15,6 +15,7 @@ import type { Api, Model, OpenAICompletionsCompat, Provider } from '@earendil-wo
import { resolveProfiles } from '../src/config.ts'
import { buildProvider, supportedProtocols } from '../src/provider.ts'
import { assemble } from './assemble.ts'
+import { memoryAuth } from './auth-double.ts'
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
const homes: string[] = []
@@ -1065,6 +1066,7 @@ describe('resolution snapshots', () => {
// Credential resolution is the real await inside a stream call, and the
// window a configuration change has to land in.
resolveApiKey: async () => { await held; return 'k' },
+ auth: memoryAuth(),
})
const chunks: StreamChunk[] = []
@@ -1093,7 +1095,11 @@ describe('resolution snapshots', () => {
const first = await mockServer([{ events: textEvents }])
const second = await mockServer([{ events: textEvents }])
let current = resolveProfiles({ deepseek: { baseURL: `${first.url}/v1` } })
- const adapter = new PiAiAdapter({ profiles: () => current, resolveApiKey: () => Promise.resolve('k') })
+ const adapter = new PiAiAdapter({
+ profiles: () => current,
+ resolveApiKey: () => Promise.resolve('k'),
+ auth: memoryAuth(),
+ })
const drain = async (): Promise => {
for await (const _chunk of adapter.stream({
provider: 'deepseek', model: 'deepseek-v4-flash', messages: [],
@@ -1160,30 +1166,22 @@ describe('configurable-provider directory', () => {
expect(ctx.llm.listConfigurableProviders()).toHaveLength(catalogOnly)
})
- it('withholds a catalog route this adapter cannot authenticate', async () => {
+ it('offers every installed catalog route, including one that only signs in', async () => {
const ctx = await harness({})
const offered = ctx.llm.listConfigurableProviders().map(entry => entry.provider)
// `openai-codex` is the one installed provider that authenticates through
- // OAuth alone. pi-ai resolves OAuth only from a *stored* credential, this
- // adapter constructs its collection with no credential store, and nothing
- // here runs a login flow — so every request on such a route fails with
- // `Provider is not configured` before it goes out. Offering it would put a
- // provider on the settings page that no amount of configuration can make
- // work.
- expect(offered).not.toContain('openai-codex')
- // A provider that offers OAuth *beside* an api-key method keeps its entry:
- // the key is a path this adapter can serve.
+ // OAuth alone. It is offered like any other because the collection now
+ // carries a durable credential store and a login flow writes into it, so
+ // the route has a posture that works rather than only one that fails.
+ expect(offered).toContain('openai-codex')
expect(offered).toContain('anthropic')
expect(offered).toContain('openai')
})
- it('still lists a withheld route a stored profile names, as a catalog route', async () => {
- // Withholding the offer must not strand a profile someone already stored:
- // the route keeps its entry so a configuration surface can edit or delete
- // it, and `declared` still answers catalog membership rather than the
- // offer, so the page does not mislabel it as a route this deployment
- // invented.
+ it('lists a route a stored profile names as a catalog route, not a declared one', async () => {
+ // `declared` answers catalog membership, so a profile stored against a
+ // route pi-ai ships is not mislabelled as one this deployment invented.
const ctx = await harness({ providers: { 'openai-codex': { apiKeyEnv: KEY_ENV } } })
expect(ctx.llm.listConfigurableProviders()).toContainEqual({
diff --git a/packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts b/packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts
index 1019354156..3decff3749 100644
--- a/packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts
+++ b/packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts
@@ -9,6 +9,7 @@ import { LocalCredentialProvider } from '@deepseek-ai/dsh-credentials-local'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { FileSettingsProvider } from '@deepseek-ai/dsh-settings-file'
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
+import AuthorizationService from '@deepseek-ai/dsh-authorization'
import { assemble } from './assemble.ts'
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
@@ -37,7 +38,11 @@ async function home(): Promise {
}
/** Real dynamic composition mirroring the deepseek twin's harness. */
-async function boot(dir: string, config: LlmPiAi.Config): Promise {
+async function boot(
+ dir: string,
+ config: LlmPiAi.Config,
+ options: { authorization?: boolean } = {},
+): Promise {
const ctx = new Context()
cleanups.push(async () => {
await ctx.fiber.dispose()
@@ -45,17 +50,38 @@ async function boot(dir: string, config: LlmPiAi.Config): Promise {
await ctx.plugin(LlmRuntime)
await ctx.plugin(FileSettingsProvider, { path: join(dir, 'settings.yaml'), watch: false })
await ctx.plugin(LocalCredentialProvider, { path: join(dir, '.credentials.yaml'), watch: false })
+ if (options.authorization === true) await ctx.plugin(AuthorizationService)
await ctx.plugin(LlmPiAi, config)
return ctx
}
+describe('login flows in a real composition', () => {
+ it('offers a sign-in for a provider no route names, once the seam is mounted', async () => {
+ const ctx = await boot(await home(), {}, { authorization: true })
+
+ // Zero routes configured: signing in is what makes a route worth adding,
+ // so the offer cannot wait for a profile to name the provider.
+ const codex = ctx.authorization.describe(LlmPiAi.recordKeyFor('openai-codex'))
+ expect(codex?.methods.map(method => method.id)).toEqual(['oauth'])
+ })
+
+ it('mounts without the seam, and simply offers no sign-in', async () => {
+ const ctx = await boot(await home(), {})
+
+ // A headless or ACP composition has no surface to sign in from; everything
+ // else this plugin does still works.
+ expect(ctx.get('authorization')).toBeUndefined()
+ expect(ctx.llm.listConfigurableProviders().length).toBeGreaterThan(0)
+ })
+})
+
describe('request-level dynamic profiles', () => {
it('mounts bare and dormant, then registers routes the moment settings supply providers', async () => {
vi.stubEnv('PI_DYNAMIC_KEY', '')
const dir = await home()
await writeFile(
join(dir, '.credentials.yaml'),
- 'PI_DYNAMIC_KEY: pk-from-settings\nPI_LIVE_KEY: live-key\nPI_OTHER_KEY: other\n',
+ 'version: 1\nrefs:\n PI_DYNAMIC_KEY: pk-from-settings\n PI_LIVE_KEY: live-key\n PI_OTHER_KEY: other\n',
{ mode: 0o600 },
)
const server = await mockServer([{ events: textEvents }])
@@ -93,7 +119,7 @@ describe('request-level dynamic profiles', () => {
const dir = await home()
await writeFile(
join(dir, '.credentials.yaml'),
- 'PI_LIVE_KEY: live-key\nPI_OTHER_KEY: other\n',
+ 'version: 1\nrefs:\n PI_LIVE_KEY: live-key\n PI_OTHER_KEY: other\n',
{ mode: 0o600 },
)
const server = await mockServer([{ events: textEvents }])
@@ -122,7 +148,7 @@ describe('request-level dynamic profiles', () => {
it('rotates the per-request credential referenced by apiKeyEnv', async () => {
vi.stubEnv('PI_DYNAMIC_KEY', '')
const dir = await home()
- await writeFile(join(dir, '.credentials.yaml'), 'PI_DYNAMIC_KEY: pk-one\n', { mode: 0o600 })
+ await writeFile(join(dir, '.credentials.yaml'), 'version: 1\nrefs:\n PI_DYNAMIC_KEY: pk-one\n', { mode: 0o600 })
const server = await mockServer([{ events: textEvents }, { events: textEvents }])
const ctx = await boot(dir, {
providers: { deepseek: { apiKeyEnv: 'PI_DYNAMIC_KEY', baseURL: server.url } },
@@ -173,7 +199,7 @@ describe('request-level dynamic profiles', () => {
const dir = await home()
await writeFile(
join(dir, '.credentials.yaml'),
- 'PI_LIVE_KEY: live-key\nPI_OTHER_KEY: other\n',
+ 'version: 1\nrefs:\n PI_LIVE_KEY: live-key\n PI_OTHER_KEY: other\n',
{ mode: 0o600 },
)
const server = await mockServer([{ events: textEvents }, { events: textEvents }])
diff --git a/packages/llm/llm-pi-ai/tests/loader-composition.spec.ts b/packages/llm/llm-pi-ai/tests/loader-composition.spec.ts
index a4e89424f6..6ca02f281a 100644
--- a/packages/llm/llm-pi-ai/tests/loader-composition.spec.ts
+++ b/packages/llm/llm-pi-ai/tests/loader-composition.spec.ts
@@ -49,7 +49,7 @@ async function loadComposition(): Promise<{ ctx: Context; settingsPath: string }
root = await mkdtemp(join(tmpdir(), 'dsh-pi-composition-'))
const settingsPath = join(root, 'settings.yaml')
await writeFile(settingsPath, '# personal settings\n')
- await writeFile(join(root, '.credentials.yaml'), 'PI_COMPOSITION_KEY: key-from-store\n', { mode: 0o600 })
+ await writeFile(join(root, '.credentials.yaml'), 'version: 1\nrefs:\n PI_COMPOSITION_KEY: key-from-store\n', { mode: 0o600 })
const configPath = join(root, 'cordis.yml')
await writeFile(configPath, [
diff --git a/packages/llm/llm-pi-ai/tests/login.spec.ts b/packages/llm/llm-pi-ai/tests/login.spec.ts
new file mode 100644
index 0000000000..beb65ce188
--- /dev/null
+++ b/packages/llm/llm-pi-ai/tests/login.spec.ts
@@ -0,0 +1,198 @@
+import { mkdtemp, rm } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { Context } from '@deepseek-ai/cordis'
+import AuthorizationService from '@deepseek-ai/dsh-authorization'
+import type { AuthorizationInteraction, AuthorizationNotice, AuthorizationPrompt } from '@deepseek-ai/dsh-authorization'
+import LocalCredentialProvider from '@deepseek-ai/dsh-credentials-local'
+import type { CredentialKey } from '@deepseek-ai/dsh-credentials'
+import type { AuthEvent, AuthInteraction, AuthPrompt, AuthType, Credential } from '@earendil-works/pi-ai'
+
+const login = vi.hoisted(() => vi.fn())
+
+// The whole of what this module does with pi-ai is run one provider's login
+// against a collection built with the harness store, so the collection is the
+// boundary worth observing; a real login would open a browser.
+vi.mock('@earendil-works/pi-ai', async importOriginal => ({
+ ...await importOriginal(),
+ createModels: () => ({ setProvider: () => {}, login }),
+}))
+
+const { credentialStoreFrom, authContextFrom, recordKeyFor } = await import('../src/auth.ts')
+const { registerPiAiFlows } = await import('../src/login.ts')
+
+const CODEX = recordKeyFor('openai-codex')
+const dirs: string[] = []
+
+/** A context with the record store, the seam, and every pi-ai login flow. */
+async function harness(): Promise {
+ const dir = await mkdtemp(join(tmpdir(), 'dsh-pi-login-'))
+ dirs.push(dir)
+ const ctx = new Context()
+ await ctx.plugin(LocalCredentialProvider, { path: join(dir, '.credentials.yaml'), watch: false })
+ await ctx.plugin(AuthorizationService)
+ registerPiAiFlows(ctx, { credentials: credentialStoreFrom(ctx), authContext: authContextFrom(ctx) })
+ return ctx
+}
+
+/** An interaction recording everything a flow says, answering every question. */
+function surface(answer = 'typed'): AuthorizationInteraction & {
+ notices: AuthorizationNotice[]
+ prompts: AuthorizationPrompt[]
+} {
+ const notices: AuthorizationNotice[] = []
+ const prompts: AuthorizationPrompt[] = []
+ return {
+ notices,
+ prompts,
+ notify: (notice) => { notices.push(notice) },
+ prompt: (prompt) => {
+ prompts.push(prompt)
+ return Promise.resolve(answer)
+ },
+ }
+}
+
+/** Drive one attempt, letting the mocked login talk back through `converse`. */
+async function attempt(
+ ctx: Context,
+ converse: (interaction: AuthInteraction) => Promise,
+ request: { key?: CredentialKey; method?: string } = {},
+): Promise> {
+ const ui = surface()
+ login.mockImplementation(async (providerId: string, _type: AuthType, interaction: AuthInteraction) => {
+ await converse(interaction)
+ const granted: Credential = { type: 'oauth', access: 'at', refresh: 'rt', expires: 1 }
+ await credentialStoreFrom(ctx).modify(providerId, () => Promise.resolve(granted))
+ return granted
+ })
+ await expect(ctx.authorization.begin({
+ key: request.key ?? CODEX,
+ interaction: ui,
+ ...request.method === undefined ? {} : { method: request.method },
+ })).resolves.toEqual({ status: 'authorized' })
+ return ui
+}
+
+afterEach(async () => {
+ login.mockReset()
+ await Promise.all(dirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
+})
+
+describe('pi-ai login flows', () => {
+ it('offers one flow per installed provider, with the methods that provider ships', async () => {
+ const ctx = await harness()
+ const offered = ctx.authorization.list()
+
+ // The OAuth-only provider is exactly the case this exists for: nothing
+ // else could ever configure it.
+ expect(offered.find(entry => entry.key === CODEX)?.methods)
+ .toEqual([{ id: 'oauth', label: expect.stringContaining('ChatGPT') as string }])
+ // A provider offering both keeps both, the subscription login first.
+ expect(offered.find(entry => entry.key === recordKeyFor('anthropic'))?.methods.map(one => one.id))
+ .toEqual(['oauth', 'api-key'])
+ // A key-only provider still gets a flow, because pi-ai collects the key
+ // through its own prompt rather than leaving it to the settings form.
+ expect(offered.find(entry => entry.key === recordKeyFor('deepseek'))?.methods.map(one => one.id))
+ .toEqual(['api-key'])
+ })
+
+ it('runs the pi-ai auth type the chosen method names', async () => {
+ const ctx = await harness()
+
+ await attempt(ctx, () => Promise.resolve())
+ expect(login).toHaveBeenLastCalledWith('openai-codex', 'oauth', expect.anything())
+
+ await attempt(ctx, () => Promise.resolve(), { key: recordKeyFor('anthropic'), method: 'api-key' })
+ expect(login).toHaveBeenLastCalledWith('anthropic', 'api_key', expect.anything())
+ })
+
+ it('commits what the login produced, where the adapter reads it back', async () => {
+ const ctx = await harness()
+
+ await attempt(ctx, () => Promise.resolve())
+
+ await expect(ctx.credentials.readRecord(CODEX)).resolves.toEqual({
+ kind: 'grant',
+ payload: { type: 'oauth', access: 'at', refresh: 'rt', expires: 1 },
+ })
+ })
+
+ it('restates every pi-ai login event in the neutral vocabulary', async () => {
+ const ctx = await harness()
+ const events: AuthEvent[] = [
+ { type: 'info', message: 'Read this first', links: [{ url: 'https://help.example' }] },
+ { type: 'info', message: 'Nothing to open' },
+ { type: 'auth_url', url: 'https://auth.example/start', instructions: 'Approve in the tab' },
+ { type: 'auth_url', url: 'https://auth.example/plain' },
+ { type: 'device_code', userCode: 'WXYZ-1234', verificationUri: 'https://device.example' },
+ { type: 'progress', message: 'Exchanging the code' },
+ // pi-ai's event union is open; an unrecognised member must still show
+ // the human that something is happening.
+ { type: 'quantum-handshake' } as unknown as AuthEvent,
+ ]
+
+ const ui = await attempt(ctx, (interaction) => {
+ for (const event of events) interaction.notify(event)
+ return Promise.resolve()
+ })
+
+ expect(ui.notices).toEqual([
+ { message: 'Read this first', url: 'https://help.example' },
+ { message: 'Nothing to open' },
+ { message: 'Approve in the tab', url: 'https://auth.example/start' },
+ { message: 'Open this page to continue signing in.', url: 'https://auth.example/plain' },
+ {
+ message: 'Enter this code on the verification page to finish signing in.',
+ url: 'https://device.example',
+ code: 'WXYZ-1234',
+ },
+ { message: 'Exchanging the code' },
+ { message: 'Signing in…' },
+ ])
+ })
+
+ it('restates every pi-ai prompt, carrying the per-prompt withdrawal signal', async () => {
+ const ctx = await harness()
+ const withdraw = new AbortController()
+ const prompts: AuthPrompt[] = [
+ { type: 'text', message: 'Your workspace', placeholder: 'acme' },
+ { type: 'secret', message: 'Paste the key' },
+ { type: 'secret', message: 'Paste the token', placeholder: 'sk-…' },
+ { type: 'select', message: 'Which account?', options: [{ id: 'a', label: 'Work' }] },
+ // The manual-code question a browser callback can win the race against.
+ { type: 'manual_code', message: 'Paste the code', signal: withdraw.signal },
+ ]
+
+ const ui = await attempt(ctx, async (interaction) => {
+ for (const prompt of prompts) await interaction.prompt(prompt)
+ })
+
+ expect(ui.prompts).toEqual([
+ { kind: 'text', message: 'Your workspace', placeholder: 'acme' },
+ { kind: 'secret', message: 'Paste the key' },
+ { kind: 'secret', message: 'Paste the token', placeholder: 'sk-…' },
+ { kind: 'select', message: 'Which account?', options: [{ id: 'a', label: 'Work' }] },
+ { kind: 'text', message: 'Paste the code', signal: withdraw.signal },
+ ])
+ })
+
+ it('hands the flow the attempt-wide cancellation signal', async () => {
+ const ctx = await harness()
+ let seen: AbortSignal | undefined
+ const controller = new AbortController()
+ login.mockImplementation((_id: string, _type: AuthType, interaction: AuthInteraction) => {
+ seen = interaction.signal
+ controller.abort()
+ return new Promise(() => {})
+ })
+
+ await expect(ctx.authorization.begin({
+ key: CODEX,
+ interaction: surface(),
+ signal: controller.signal,
+ })).resolves.toEqual({ status: 'cancelled' })
+ expect(seen?.aborted).toBe(true)
+ })
+})
diff --git a/packages/llm/llm-pi-ai/tests/sdk-options.spec.ts b/packages/llm/llm-pi-ai/tests/sdk-options.spec.ts
index 317b803f0c..2f409c6d9d 100644
--- a/packages/llm/llm-pi-ai/tests/sdk-options.spec.ts
+++ b/packages/llm/llm-pi-ai/tests/sdk-options.spec.ts
@@ -13,6 +13,7 @@ vi.mock('@earendil-works/pi-ai/api/openai-completions.lazy', () => ({
import { PiAiAdapter } from '../src/adapter.ts'
import { resolveProfiles } from '../src/config.ts'
+import { memoryAuth } from './auth-double.ts'
afterEach(() => { streamSimple.mockReset() })
@@ -27,6 +28,7 @@ function gatewayAdapter(): PiAiAdapter {
},
}),
resolveApiKey: () => Promise.resolve('test-key'),
+ auth: memoryAuth(),
})
}
diff --git a/packages/llm/llm-pi-ai/tsconfig.json b/packages/llm/llm-pi-ai/tsconfig.json
index 6f0b821d36..172dbe8a6e 100644
--- a/packages/llm/llm-pi-ai/tsconfig.json
+++ b/packages/llm/llm-pi-ai/tsconfig.json
@@ -29,6 +29,9 @@
{
"path": "../../credentials/credentials"
},
+ {
+ "path": "../../credentials/authorization"
+ },
{
"path": "../../settings/settings"
},
diff --git a/packages/llm/llm/README.i18n.yaml b/packages/llm/llm/README.i18n.yaml
index 0700b14362..2621de89af 100644
--- a/packages/llm/llm/README.i18n.yaml
+++ b/packages/llm/llm/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/llm/llm/README.md
README.md: e6b3c4924ad4e7cf115abdbfb38d22d0f524e377
-README.zh.md: 51f034af632c19ca91c1d2f91a7c9c22714870e0
+README.zh.md: 91f1c6ede2b800671b35a90d02a23a6d79e96e0e
diff --git a/packages/llm/llm/README.zh.md b/packages/llm/llm/README.zh.md
index 51f034af63..91f1c6ede2 100644
--- a/packages/llm/llm/README.zh.md
+++ b/packages/llm/llm/README.zh.md
@@ -63,9 +63,11 @@
`LlmCallConfig` 记录一个会话的模型请求所使用的提供方、模型、由适配器定义的可选推理强度,以及采样参数(`provider`、`model`、`reasoningEffort`、`temperature`、`maxTokens`、`stop`,分别与同名 `GenerateOptions` 字段一一对应)。它是作为请求标头一部分记录在会话日志中的每会话状态(见 dsh-session `request/header` 事件),绝不是可静默调整的每次调用旋钮:`agent/request` waterfall 会提议替换,`prepareCall()` 在轮次 signal 控制下校验它并填入适配器默认值,loop 随后记录生效值以及标明哪些字段由适配器默认值填入的标记,再使用已准备调用中与注册绑定的流。下一次提议会省略带标记的默认值,使变更后的路由解析自身的值;未带标记的显式字段会保留。`callConfigEquals(a, b)` 是逐字段真实变更检测器;`deepFreeze(value)` 是 loop 使用的请求所有权辅助函数:每个构造完成的请求都会在分发前深度冻结;`llm/stream` 监听器和适配器只能读取,绝不能改写。`markAgentLoopRequest()` 将该精确对象标记为由进程本地 agent loop 创建,`isAgentLoopRequest()` 让观测方可以将其与同样可能冻结并关联会话、但独立记录的辅助调用区分。`GenerateOptions.purpose` 会对已记录的辅助压缩和会话标题调用进行分类,使适配器可以按调用目的应用不同的传输策略,而不改变普通会话请求。
+
+
### 应用归因(`attribution.ts`)
-每个产品适配器都会在提供方 HTTP 请求上发送应用身份。`attributionHeaders(identity?)` 构建标准 `User-Agent`,默认为公开 `APP_IDENTITY`;白标部署可以替换它,但不能抑制它。适配器会直接验证 wire 标头,或通过自身库 hook 验证。详见 [归因 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md)。
+每个产品适配器都会在提供方 HTTP 请求上发送应用身份。`attributionHeaders(identity?)` 构建标准 `User-Agent`,默认为公开 `APP_IDENTITY`;白标部署可以替换它,但不能抑制它。适配器会直接验证 wire 标头,或通过自身库 hook 验证。详见 [归因 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md)。
### API 密钥校验(`api-key.ts`)
@@ -85,7 +87,7 @@
### 真实适配器
-两个适配器使用不同内部机制实现 `LlmAdapter`:[`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) 针对 `deepseek-official` 路由使用直接 fetch 加 `eventsource-parser` SSE(Server-Sent Events)分帧,[`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) 则通过 `@earendil-works/pi-ai` 动态解析已配置提供方/模型对。两者都遵循 `types.ts` 中的 `StreamChunk` 约定:usage 先于 finish,工具参数保持原始字符串。适配器实现在内部可以抛出异常或发出失败 finish;`LlmRuntime` 会将两者都暴露为终止失败 finish。适配器理由见[双 LLM 适配器](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md),服务边界见[终止失败决策](../../../.agents/notes/implemented/architecture/2026-07-29-terminal-llm-stream-failures.md)。
+两个适配器使用不同内部机制实现 `LlmAdapter`:[`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) 针对 `deepseek-official` 路由使用直接 fetch 加 `eventsource-parser` SSE(Server-Sent Events)分帧,[`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) 则通过 `@earendil-works/pi-ai` 动态解析已配置提供方/模型对。两者都遵循 `types.ts` 中的 `StreamChunk` 约定:usage 先于 finish,工具参数保持原始字符串。适配器实现在内部可以抛出异常或发出失败 finish;`LlmRuntime` 会将两者都暴露为终止失败 finish。适配器理由见[双 LLM 适配器](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md),服务边界见[终止失败决策](../../../.agents/notes/implemented/architecture/2026-07-29-terminal-llm-stream-failures.zh.md)。
## 模型体验
@@ -95,6 +97,8 @@
透传;注册表保留已组装请求前缀,cache 复用与路由边界属于所选适配器和提供方。
+
+
## 已知限制与暂缓事项
- **本服务不执行重试、缓存或速率限制**:提供方注册会存储重试策略,但 `llm/stream` 仍是单次尝试调用包装层。agent loop 会将已验证模型请求失败单独提供给 `agent/request-error`,其默认行为是保留原始失败;`@deepseek-ai/dsh-llm-retry` 是共享示例主干加载的可选执行器。
diff --git a/packages/llm/token-meter/README.i18n.yaml b/packages/llm/token-meter/README.i18n.yaml
index 44db6530cd..98b96e634d 100644
--- a/packages/llm/token-meter/README.i18n.yaml
+++ b/packages/llm/token-meter/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/llm/token-meter/README.md
README.md: a2deab11a31285ba598b8864d3a734ecf7c56620
-README.zh.md: e0c3a8aa123ca0acc374277cd92b415e0b0b3cb5
+README.zh.md: d14cded74691f88db7267ea470f536db85a39218
diff --git a/packages/llm/token-meter/README.zh.md b/packages/llm/token-meter/README.zh.md
index e0c3a8aa12..d14cded746 100644
--- a/packages/llm/token-meter/README.zh.md
+++ b/packages/llm/token-meter/README.zh.md
@@ -41,7 +41,7 @@ fold 跟踪完整请求标头快照、步骤边界、表层追加与替换、成
这是刻意的选择。占用率百分比是面向用户的参考数字,既不是计费记录,也不是门控输入:harness 中没有任何环节依据它做决策,压缩改为直接读取 `measure()`。UI 用测得的压力除以为所选模型单独解析出的容量来计算占用率。
-[Agent Note](../../../.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md) 记录了否决「让这对值保持原子」方案的那次对比。需要同一边界精确数字的消费方应在自己的请求边界调用 `measure()`,而不是读取该投影。
+[Agent Note](../../../.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md) 记录了否决「让这对值保持原子」方案的那次对比。需要同一边界精确数字的消费方应在自己的请求边界调用 `measure()`,而不是读取该投影。
## 组合
diff --git a/packages/lsp/README.i18n.yaml b/packages/lsp/README.i18n.yaml
index 195967e3a2..2670636cb5 100644
--- a/packages/lsp/README.i18n.yaml
+++ b/packages/lsp/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/lsp/README.md
README.md: 414311fd3f537634651daf6f313baf33daede26b
-README.zh.md: 8791b0ea9c01ed637586cee5c131690a73263a6a
+README.zh.md: 8128bd410daeaa12186191a2dccddddc2a0e5b37
diff --git a/packages/lsp/README.zh.md b/packages/lsp/README.zh.md
index 8791b0ea9c..8128bd410d 100644
--- a/packages/lsp/README.zh.md
+++ b/packages/lsp/README.zh.md
@@ -12,6 +12,6 @@
Service Definition 位于 `lsp/lsp/`。该 seam 恰好公开四种语义操作:`goToDefinition`、`findReferences`、`goToImplementation`、`hover`,且不提供通用 JSON-RPC 逃生口;因此,替换提供方不会改变模型请求导航的方式,也不会让协议载荷或未经评审的修改进入模型约定。提供方注册的是**能力**而非工具;`tool-lsp` 是面向模型的名称、schema、提示词指引和呈现的唯一 owner。
-设计原理见 [LSP 能力 seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md),其中也解释了文档为何在每次查询时临时打开、stdio 主机为何使用共享的文件系统/子进程执行环境,以及扩展名归属为何在同一运行时内互斥。
+设计原理见 [LSP 能力 seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.zh.md),其中也解释了文档为何在每次查询时临时打开、stdio 主机为何使用共享的文件系统/子进程执行环境,以及扩展名归属为何在同一运行时内互斥。
-子系统参考——操作、坐标、请求/结果、`LspError`——见 [docs/subsystems/lsp.md](../../docs/subsystems/lsp.md);设计依据见 [LSP 能力 seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md)。
+子系统参考——操作、坐标、请求/结果、`LspError`——见 [docs/subsystems/lsp.md](../../docs/subsystems/lsp.zh.md);设计依据见 [LSP 能力 seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.zh.md)。
diff --git a/packages/lsp/lsp/README.i18n.yaml b/packages/lsp/lsp/README.i18n.yaml
index 1beb022f57..0fb8bcab4e 100644
--- a/packages/lsp/lsp/README.i18n.yaml
+++ b/packages/lsp/lsp/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/lsp/lsp/README.md
README.md: cf32f9cc60a346def48d9720bf0695db36553b0e
-README.zh.md: 6f19d1f649eb1483782b6ebb4c82f05da7a42a80
+README.zh.md: eba25a7494f8c624b0e9d0aa5b5c7dd484d1f0f9
diff --git a/packages/lsp/lsp/README.zh.md b/packages/lsp/lsp/README.zh.md
index 6f19d1f649..eba25a7494 100644
--- a/packages/lsp/lsp/README.zh.md
+++ b/packages/lsp/lsp/README.zh.md
@@ -39,6 +39,6 @@
## 已知限制与暂缓事项
-- **同一运行时内扩展名归属互斥**:两个提供方不能同时声明 `.ts`,即使 language id 不同;重叠会使注册失败。预期扩展是在注册之上增加部署配置的 selector;它可以放宽互斥保留,而无需把提供方选择加入模型输入(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md))。
+- **同一运行时内扩展名归属互斥**:两个提供方不能同时声明 `.ts`,即使 language id 不同;重叠会使注册失败。预期扩展是在注册之上增加部署配置的 selector;它可以放宽互斥保留,而无需把提供方选择加入模型输入(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.zh.md))。
- **仅四种操作**:symbol 与 call hierarchy 暂缓(它们需要不同 schema);diagnostics 需要独立的新鲜度/累积规则;修改操作(rename、code action、formatting)需要独立工具,并集成预览、权限和写入策略。
- **没有观测表层**:可用性只能通过运行 `query()` 并按抛出的 `LspError` code 路由来观测;没有提供方变更事件或能力状态查询。
diff --git a/packages/lsp/tool-lsp/README.i18n.yaml b/packages/lsp/tool-lsp/README.i18n.yaml
index 2eb5cb2f17..8287d4b40b 100644
--- a/packages/lsp/tool-lsp/README.i18n.yaml
+++ b/packages/lsp/tool-lsp/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/lsp/tool-lsp/README.md
README.md: e0fd5738cf114314a8483125d20142d5f1ffac40
-README.zh.md: 8c908c1e8766ab47e9cefbc9d1f72f05e8c2709f
+README.zh.md: 71d6a4b5479b2da187b78a388d617afe7214e78c
diff --git a/packages/lsp/tool-lsp/README.zh.md b/packages/lsp/tool-lsp/README.zh.md
index 8c908c1e87..71d6a4b547 100644
--- a/packages/lsp/tool-lsp/README.zh.md
+++ b/packages/lsp/tool-lsp/README.zh.md
@@ -46,7 +46,7 @@ Use search/read for ordinary navigation. Use lsp when textual matches are ambigu
#### 模型看到的内容
-模型会看到生成的 [`lsp` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-lsp)。
+模型会看到生成的 [`lsp` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-lsp)。
#### Token 影响
@@ -86,5 +86,5 @@ Use search/read for ordinary navigation. Use lsp when textual matches are ambigu
## 已知限制与暂缓事项
-- **UTF-16 光标坐标**:列坐标与协议精确一致,但模型难以在非 BMP 字符周围计数;未落在符号上的位置可能返回空结果,因此提示词解释了该约定,但不鼓励广泛使用 LSP(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md))。
+- **UTF-16 光标坐标**:列坐标与协议精确一致,但模型难以在非 BMP 字符周围计数;未落在符号上的位置可能返回空结果,因此提示词解释了该约定,但不鼓励广泛使用 LSP(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.zh.md))。
- **不承诺跨服务器完整性**:受支持的服务器仍可能根据索引就绪情况返回空或部分结果;该工具不承诺跨语言或服务器的完整性。
diff --git a/packages/mcp/README.i18n.yaml b/packages/mcp/README.i18n.yaml
index 3ce6f13ac3..93fa4aa4aa 100644
--- a/packages/mcp/README.i18n.yaml
+++ b/packages/mcp/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/mcp/README.md
README.md: cc3440f6cb2e507277e6141393ccf39fc7022d7d
-README.zh.md: 0a74f3e9016be95f76104303396dc690888d645d
+README.zh.md: 9cdc01b80dce63e06bb36fe92cfaf62ed5adce51
diff --git a/packages/mcp/README.zh.md b/packages/mcp/README.zh.md
index 0a74f3e901..9cdc01b80d 100644
--- a/packages/mcp/README.zh.md
+++ b/packages/mcp/README.zh.md
@@ -6,4 +6,4 @@
| 包 | 职责 |
|---|---|
-| [`mcp-client/`](mcp-client/README.md) | MCP 客户端桥接,将外部服务器工具注册到 `ctx.tools` |
+| [`mcp-client/`](mcp-client/README.zh.md) | MCP 客户端桥接,将外部服务器工具注册到 `ctx.tools` |
diff --git a/packages/plan/README.i18n.yaml b/packages/plan/README.i18n.yaml
index c2a60d7d2d..51a2fb09eb 100644
--- a/packages/plan/README.i18n.yaml
+++ b/packages/plan/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/plan/README.md
README.md: 535a2b02e297d7c3bc68538fa1d03226089372e0
-README.zh.md: 0ee1c658ee0a7863eb16b99d956e882f320a3717
+README.zh.md: 288daf7a7b19a14372ab9396363bf558f1bcc240
diff --git a/packages/plan/README.zh.md b/packages/plan/README.zh.md
index 0ee1c658ee..288daf7a7b 100644
--- a/packages/plan/README.zh.md
+++ b/packages/plan/README.zh.md
@@ -6,8 +6,8 @@ Plan mode 是按 agent(智能体)记录的协作状态,而不是通用模
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`plan-mode/`](plan-mode/README.md) | 负责 plan mode 状态、指引、命令和评审流程 | `ctx.planMode` |
+| [`plan-mode/`](plan-mode/README.zh.md) | 负责 plan mode 状态、指引、命令和评审流程 | `ctx.planMode` |
-[plan 专用协作状态](../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md)决策记录了该家族的设计。
+[plan 专用协作状态](../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.zh.md)决策记录了该家族的设计。
-子系统参考——`plan/mode` 折叠、步骤边界刷写、配置、退出工具——见 [docs/subsystems/plan.md](../../docs/subsystems/plan.md);设计见[计划专属协作状态](../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md)。
+子系统参考——`plan/mode` 折叠、步骤边界刷写、配置、退出工具——见 [docs/subsystems/plan.md](../../docs/subsystems/plan.zh.md);设计见[计划专属协作状态](../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.zh.md)。
diff --git a/packages/plan/plan-mode/README.i18n.yaml b/packages/plan/plan-mode/README.i18n.yaml
index c12c5c248f..b1c7671e8b 100644
--- a/packages/plan/plan-mode/README.i18n.yaml
+++ b/packages/plan/plan-mode/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/plan/plan-mode/README.md
README.md: 3eabe2cb3f04b434b7f908f7beca869f1022a59e
-README.zh.md: f7d6a1f8e9f5ba95f8aad9457f3dde5fc415fdcf
+README.zh.md: b3548da880af5d0cacfebba4486294d9d5085626
diff --git a/packages/plan/plan-mode/README.zh.md b/packages/plan/plan-mode/README.zh.md
index f7d6a1f8e9..b3548da880 100644
--- a/packages/plan/plan-mode/README.zh.md
+++ b/packages/plan/plan-mode/README.zh.md
@@ -10,6 +10,8 @@
`ctx.planMode.set(agent, active)` 会在 agent 空闲时立即追加独立的 `plan/mode` 事件,因为下一个提示词之前不会运行轮内 pre-step。agent 运行时,该方法会保留待生效选择,直到下一个被接受的轮内 pre-step。返回值区分 `committed`、`queued`、表示反转的 `cancelled` 和 `noop`。`get(agent)` 返回 `{ active, pending? }`,将用于组装当前步骤的日志状态与用户的轮中选择分开。初始与续步 pre-step 都会应用待生效选择;同一步骤的请求恢复重试会复用已冻结的 assembly,并将该选择保留到下一个被接受的轮内 pre-step。当最后记录的请求头描述了另一状态时,用户选择的变更会贡献一条插件来源的 `user/message` 通知(两条提交路径皆然)。
+
+
## 模型与人类交互
激活时,`plan:policy` 会渲染已配置的 `section`。插件始终注册 `exit_plan_mode`,使工具 schema 在转换期间保持稳定;其 execute 路径只接受已激活的 plan mode,且只有通过 `ctx.userQuestions` 获得用户明确批准后才退出。
@@ -22,7 +24,7 @@ Web 客户端使用该插件提供的 `/plan` 命令;其他入口可以直接
## 会话投影
-当组合挂载 `ctx.sessionProjections`([`@deepseek-ai/dsh-session-projection`](../../session/session-projection/README.md))时,本包会在一个注入的子插件中注册 `plan` 投影单元。名为 `plan` 且携带已记录 `args` 的 `command/run` 记录会开始一个候选目标(`off` → 未激活,其余 → 激活);与它配对的 `command/done` 保留成功选择并丢弃错误选择;`plan/mode` 提交已记录状态并清除已保留的选择。其他任何事件都返回同一个状态引用。`view` 推导 `{ active, pending }`,其中 `pending` 仅在未结算或已成功的选择与已记录状态不同时为 true。该值仍完全由日志回放得出,因此 host 重启、其他标签页和冷读都能仅凭日志恢复它,被拒绝的带图 `/plan off` 也不会留下待退出状态。key 由 `src/types.ts` 通过声明合并加入 `SessionProjectionMap`:host 消费方经 `./types` 获取,client 聚合经 `./client` 获取。框架负责驱动该单元,载体通过历史尾页和 `session/projection` 推送帧提供其值。未挂载注册表的组合不受影响。
+当组合挂载 `ctx.sessionProjections`([`@deepseek-ai/dsh-session-projection`](../../session/session-projection/README.zh.md))时,本包会在一个注入的子插件中注册 `plan` 投影单元。名为 `plan` 且携带已记录 `args` 的 `command/run` 记录会开始一个候选目标(`off` → 未激活,其余 → 激活);与它配对的 `command/done` 保留成功选择并丢弃错误选择;`plan/mode` 提交已记录状态并清除已保留的选择。其他任何事件都返回同一个状态引用。`view` 推导 `{ active, pending }`,其中 `pending` 仅在未结算或已成功的选择与已记录状态不同时为 true。该值仍完全由日志回放得出,因此 host 重启、其他标签页和冷读都能仅凭日志恢复它,被拒绝的带图 `/plan off` 也不会留下待退出状态。key 由 `src/types.ts` 通过声明合并加入 `SessionProjectionMap`:host 消费方经 `./types` 获取,client 聚合经 `./client` 获取。框架负责驱动该单元,载体通过历史尾页和 `session/projection` 推送帧提供其值。未挂载注册表的组合不受影响。
## 配置
@@ -37,7 +39,7 @@ Web 客户端使用该插件提供的 `/plan` 命令;其他入口可以直接
`section` 必填且非空。出现未知键时,插件会加载失败。该包不接受任意命名的 mode、工具过滤器、沙箱设置或批准策略。
-设计:[plan 专用协作状态](../../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md)。
+设计:[plan 专用协作状态](../../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.zh.md)。
## 模型体验
@@ -79,7 +81,7 @@ You are in plan mode. Explore and design before presenting the complete plan thr
#### 模型所见内容
-[`exit_plan_mode` schema](../../../docs/tool-catalog.md#deepseek-aidsh-plan-mode) 在两种状态下均可用;在 plan mode 外执行会失败,而 plan mode 内经批准的评审会返回规范的 `{ approved: true }` 值,并渲染既有的确认文本。拒绝仍是携带评审反馈的失败调用,放弃审阅则是一次指明用户接手的失败调用。
+[`exit_plan_mode` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-plan-mode) 在两种状态下均可用;在 plan mode 外执行会失败,而 plan mode 内经批准的评审会返回规范的 `{ approved: true }` 值,并渲染既有的确认文本。拒绝仍是携带评审反馈的失败调用,放弃审阅则是一次指明用户接手的失败调用。
#### Token 影响
@@ -89,6 +91,8 @@ You are in plan mode. Explore and design before presenting the complete plan thr
mode 转换不改变工具目录;plan 参数与评审结果按常规方式扩展对话。
+
+
## 已知限制与暂缓事项
- Plan mode 只进行引导,而不强制执行;需要强制限制的部署必须分别配置沙箱与批准控制。
diff --git a/packages/preset/README.i18n.yaml b/packages/preset/README.i18n.yaml
index b8d5dd62d9..bf3583068a 100644
--- a/packages/preset/README.i18n.yaml
+++ b/packages/preset/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/preset/README.md
README.md: 5e7805eaa303b78c4d8a1a8972e1d031ee304fae
-README.zh.md: 9afb0a4f585fc445977f81839695f15cf8b21327
+README.zh.md: 217d807f40abc0908fb0c67fa0c94dc4d0806592
diff --git a/packages/preset/README.zh.md b/packages/preset/README.zh.md
index 9afb0a4f58..217d807f40 100644
--- a/packages/preset/README.zh.md
+++ b/packages/preset/README.zh.md
@@ -13,4 +13,4 @@
本组假定的组装划分是:注册表与跨会话设施是进程单例,留在宿主组装中;preset 只承载单个 agent 对它们的贡献。若 preset 中某一行发布了进程级全局服务,挂载时即被拒绝,而不是留到与下一个会话相撞。
-设计详见 [按会话组装 agent preset 的 Agent Note](../../.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md)。
+设计详见 [按会话组装 agent preset 的 Agent Note](../../.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.zh.md)。
diff --git a/packages/preset/agent-presets/README.i18n.yaml b/packages/preset/agent-presets/README.i18n.yaml
index f3fef85bbd..25ebbe1b15 100644
--- a/packages/preset/agent-presets/README.i18n.yaml
+++ b/packages/preset/agent-presets/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/preset/agent-presets/README.md
README.md: 2438d751f000297cc4ede7b2a34736a62cc39dda
-README.zh.md: c8ab8e167d624f639f6657d4c0ade7205b23821b
+README.zh.md: a786afbc3790595edbd64ec16686717ef3d8140f
diff --git a/packages/preset/agent-presets/README.zh.md b/packages/preset/agent-presets/README.zh.md
index c8ab8e167d..a786afbc37 100644
--- a/packages/preset/agent-presets/README.zh.md
+++ b/packages/preset/agent-presets/README.zh.md
@@ -4,7 +4,7 @@
按 preset 组装 agent(智能体)。**preset** 是一个目录,其中放置一份 `agent.cordis.yml`;roster 在整个进程内只把它挂载一次(常驻 scope),命名它的每个会话通过把自己 agent 的 scope key 认父到该挂载(`dsh-scope` 的父链)来加入。挂载的工具、提示词段落与投影单元只存在一份,覆盖所有已加入的 agent——其插件本就按 Session/Agent 分键存状态,会话在共享实例内互不串扰——而完全没有 agent 的宿主读取方(冷读记录)也能按 preset id 解析到同一份常驻注册。
-其机制是两条 seam。entry 上下文沿原型链连到子树被挂载时所在的上下文,而 [`dsh-tools`](../../core/tools/README.md) 与 [`dsh-system-prompt`](../../core/system-prompt/README.md) 本就按调用方上下文的 scope 分层归档注册——因此常驻挂载的贡献落在 **preset 的分层**里。把它们送达每个会话的是 `dsh-scope` 的父链:agent 的视图按 `agent → preset → global` 解析(近者遮蔽远者),挂载的监听器对认父到它的每个 agent 放行,而兄弟 preset 的监听器保持失聪。
+其机制是两条 seam。entry 上下文沿原型链连到子树被挂载时所在的上下文,而 [`dsh-tools`](../../core/tools/README.zh.md) 与 [`dsh-system-prompt`](../../core/system-prompt/README.zh.md) 本就按调用方上下文的 scope 分层归档注册——因此常驻挂载的贡献落在 **preset 的分层**里。把它们送达每个会话的是 `dsh-scope` 的父链:agent 的视图按 `agent → preset → global` 解析(近者遮蔽远者),挂载的监听器对认父到它的每个 agent 放行,而兄弟 preset 的监听器保持失聪。
## 服务:`AgentPresets`(ctx 键:`agentPresets`)
@@ -36,7 +36,7 @@ subagent 的子 agent 通过 `composeFrom()` 加入其父方的常驻组装,
按 id 重新挂载父方的 preset 与认父有两处差别,且两处都要紧。父方启动后被编辑过的组装文件会把与父方历史所产出时**不同**的一个代际交给子 agent;而此后被删除的 preset 会让子 agent 直接失败,尽管其父方仍在正常运行。认父还是同步的,这正是进程内 subagent 驱动能够使用它的前提——它们在同步的创建窗口里组装子 agent。
-子 agent 会把所加入的 id 记在自己的持久化 header 上(见 [`dsh-subagent`](../../subagent/subagent/README.md)),因此冷读子 agent 的历史时重建的是它实际运行过的组装,而不是部署默认值。
+子 agent 会把所加入的 id 记在自己的持久化 header 上(见 [`dsh-subagent`](../../subagent/subagent/README.zh.md)),因此冷读子 agent 的历史时重建的是它实际运行过的组装,而不是部署默认值。
### 会话实际运行的是哪个 preset
@@ -48,7 +48,7 @@ subagent 的子 agent 通过 `composeFrom()` 加入其父方的常驻组装,
`recompose()` 先卸载已装入的子树、再装入新的,因为两份组装无法共存——它们会把相同的工具名注册进同一个层。挂载失败会恢复先前的组装,而不是让 agent 一无所有;未知 id 则在任何东西被拆除之前就被拒绝。
-"仅限尚未产出任何内容的 agent"是一条产品规则而非机制约束:在对话进行中调换工具,会留下新组装无法执行的、已被记录的工具调用。该规则由网关在传输层执行([`dsh-apiproxy`](../../host/apiproxy/README.md) 返回 `agent-preset-locked`),因为会话历史在那里才拿得到。
+"仅限尚未产出任何内容的 agent"是一条产品规则而非机制约束:在对话进行中调换工具,会留下新组装无法执行的、已被记录的工具调用。该规则由网关在传输层执行([`dsh-apiproxy`](../../host/apiproxy/README.zh.md) 返回 `agent-preset-locked`),因为会话历史在那里才拿得到。
## 创作
@@ -93,7 +93,7 @@ description: 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agen
### 可写根目录属于本包,随附根目录属于 app
-`/.agent-presets` 是个人自有 preset 的所在,正如 `/skills` 是其自有 skill 的所在([`dsh-skill-filesystem`](../../skill/skill-filesystem/README.md)),因此 roster 自行推导它,而不等某个部署记得配置——一个什么都没配的启动器同样能发现并创作 preset。它追加在全部已配置根目录**之后**,从而保持靠前的根目录赢得重复 id:随附的 `standard` 仍然遮蔽一个占用该名字的家目录目录,而 `copy()` 会拒绝该 id,不会落下一个无人解析得到的 preset。
+`/.agent-presets` 是个人自有 preset 的所在,正如 `/skills` 是其自有 skill 的所在([`dsh-skill-filesystem`](../../skill/skill-filesystem/README.zh.md)),因此 roster 自行推导它,而不等某个部署记得配置——一个什么都没配的启动器同样能发现并创作 preset。它追加在全部已配置根目录**之后**,从而保持靠前的根目录赢得重复 id:随附的 `standard` 仍然遮蔽一个占用该名字的家目录目录,而 `copy()` 会拒绝该 id,不会落下一个无人解析得到的 preset。
根目录在服务构造时解析一次。若根目录集合在一次 `list()` 与依据其答案执行的 `copy()` 之间发生变化,写入的将是调用方从未见过的目录。
diff --git a/packages/preset/persona/README.i18n.yaml b/packages/preset/persona/README.i18n.yaml
index 0323741aed..560a90256a 100644
--- a/packages/preset/persona/README.i18n.yaml
+++ b/packages/preset/persona/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/preset/persona/README.md
README.md: 978d01ddbd52095bc2486d27d08fde2d4a8fa79b
-README.zh.md: d36af6c028f1258d044608535c6da4eaf2fb92e8
+README.zh.md: a6e682711c0d6bba742cd1bb192fb9f38bf32cd0
diff --git a/packages/preset/persona/README.zh.md b/packages/preset/persona/README.zh.md
index d36af6c028..a6e682711c 100644
--- a/packages/preset/persona/README.zh.md
+++ b/packages/preset/persona/README.zh.md
@@ -4,7 +4,7 @@
把 agent(智能体)人设做成一个可组装的行:它既可以遮蔽部署级人设,也可以拥有完整系统提示词。
-[`dsh-system-prompt`](../../core/system-prompt/README.md) 以自身配置持有部署级人设,并且无条件注册该段落,因此一个进程只有一份。[agent preset](../agent-presets/README.md) 无法自行挂载提示词注册表——若没有属于自己的行,preset 能改变 agent 的工具,却永远改不了它的身份。本包就是那一行。
+[`dsh-system-prompt`](../../core/system-prompt/README.zh.md) 以自身配置持有部署级人设,并且无条件注册该段落,因此一个进程只有一份。[agent preset](../agent-presets/README.zh.md) 无法自行挂载提示词注册表——若没有属于自己的行,preset 能改变 agent 的工具,却永远改不了它的身份。本包就是那一行。
## 仅限 scope 内使用
diff --git a/packages/sandbox/README.i18n.yaml b/packages/sandbox/README.i18n.yaml
index ab79d37170..cc93658129 100644
--- a/packages/sandbox/README.i18n.yaml
+++ b/packages/sandbox/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/sandbox/README.md
README.md: 61127118c895cce728ab0b4eed387f51054a83f1
-README.zh.md: 984a677aad6ab97579abf861ea8b99f149c07d08
+README.zh.md: c5b8f12c544e2b6b2c7ce1ec3fc63d274df18609
diff --git a/packages/sandbox/README.zh.md b/packages/sandbox/README.zh.md
index 984a677aad..c5b8f12c54 100644
--- a/packages/sandbox/README.zh.md
+++ b/packages/sandbox/README.zh.md
@@ -6,10 +6,10 @@
| 包 | 职责 | ctx key |
|---|---|---|
-| [`sandbox/`](sandbox/README.md) | 定义进程沙箱服务和共享升权词汇 | `ctx.sandbox` |
-| [`sandbox-local/`](sandbox-local/README.md) | 提供本地平台限制后端 | 注册到 `ctx.sandbox` |
-| [`sandbox-policy/`](sandbox-policy/README.md) | 解析持久的逐会话沙箱策略 | `ctx.sandboxPolicy` |
+| [`sandbox/`](sandbox/README.zh.md) | 定义进程沙箱服务和共享升权词汇 | `ctx.sandbox` |
+| [`sandbox-local/`](sandbox-local/README.zh.md) | 提供本地平台限制后端 | 注册到 `ctx.sandbox` |
+| [`sandbox-policy/`](sandbox-policy/README.zh.md) | 解析持久的逐会话沙箱策略 | `ctx.sandboxPolicy` |
-[沙箱决策](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)记录了能力边界,[文件系统集成决策](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)记录了跨家族策略的使用方式。
+[沙箱决策](../../.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md)记录了能力边界,[文件系统集成决策](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.zh.md)记录了跨家族策略的使用方式。
-子系统参考——模式与强制执行、按调用策略、包装 argv 方言、故障关闭错误——见 [docs/subsystems/sandbox.md](../../docs/subsystems/sandbox.md);边界与跨家族阶段见[沙箱](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)与[跨家族 fs 沙箱](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md) Agent Note。
+子系统参考——模式与强制执行、按调用策略、包装 argv 方言、故障关闭错误——见 [docs/subsystems/sandbox.md](../../docs/subsystems/sandbox.zh.md);边界与跨家族阶段见[沙箱](../../.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md)与[跨家族 fs 沙箱](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.zh.md) Agent Note。
diff --git a/packages/sandbox/sandbox-local/README.i18n.yaml b/packages/sandbox/sandbox-local/README.i18n.yaml
index fc7291c28f..8c63ddc717 100644
--- a/packages/sandbox/sandbox-local/README.i18n.yaml
+++ b/packages/sandbox/sandbox-local/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/sandbox/sandbox-local/README.md
README.md: 7f96ebd725280cc5b767c40dcf52e9353aaafe79
-README.zh.md: ce2dc2c2ce070d225ba6bd6aaf5fb5709f6061aa
+README.zh.md: 8cb14128ae4b97c3089d72bb6126bc4fd835ac31
diff --git a/packages/sandbox/sandbox-local/README.zh.md b/packages/sandbox/sandbox-local/README.zh.md
index ce2dc2c2ce..8cb14128ae 100644
--- a/packages/sandbox/sandbox-local/README.zh.md
+++ b/packages/sandbox/sandbox-local/README.zh.md
@@ -8,9 +8,9 @@
不受支持的平台和不可用 runner 会以 `SANDBOX_UNAVAILABLE` 拒绝执行;执行绝不会静默回退为不受限制。每次包装都携带结构化 runner 失败规则,使消费方能够区分损坏的沙箱与命令失败。
-策略逐调用传入;提供方只存储机制与缓存的 runner 结论。每次包装都会报告强制执行完整度,以及后端专用的拒绝签名和 runner 失败规则。Landlock 只有在退出码为 125,且仅排除完全匹配的部分强制执行通知后仍存在一行 `landlock-run:` 致命诊断时,才判定 runner 失败;携带该通知的子进程即使以 1、2 或 125 退出,也仍按子进程结果处理。Bubblewrap 和 Seatbelt 仍仅依据签名,因为两者的公开约定均未保留 launcher 失败状态。消费方会直接 spawn 返回的 argv,因此 runner 缺失或不可执行属于带外 spawn 失败,而成功启动的子进程以 126 或 127 退出时仍按普通结果处理。`runnerCommand` 会跳过探测,并要求为自定义 runner 自身的致命方言提供一个或多个非空、单行、不区分大小写的 `runnerFailureSignatures` 条目。由于其机制未知,它会同时携带两种 Linux 拒绝方言。`probeTimeoutMs` 限定功能探测的时长。[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) 负责说明选择与失败语义。
+策略逐调用传入;提供方只存储机制与缓存的 runner 结论。每次包装都会报告强制执行完整度,以及后端专用的拒绝签名和 runner 失败规则。Landlock 只有在退出码为 125,且仅排除完全匹配的部分强制执行通知后仍存在一行 `landlock-run:` 致命诊断时,才判定 runner 失败;携带该通知的子进程即使以 1、2 或 125 退出,也仍按子进程结果处理。Bubblewrap 和 Seatbelt 仍仅依据签名,因为两者的公开约定均未保留 launcher 失败状态。消费方会直接 spawn 返回的 argv,因此 runner 缺失或不可执行属于带外 spawn 失败,而成功启动的子进程以 126 或 127 退出时仍按普通结果处理。`runnerCommand` 会跳过探测,并要求为自定义 runner 自身的致命方言提供一个或多个非空、单行、不区分大小写的 `runnerFailureSignatures` 条目。由于其机制未知,它会同时携带两种 Linux 拒绝方言。`probeTimeoutMs` 限定功能探测的时长。[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md) 负责说明选择与失败语义。
-bwrap profile 将只读宿主根目录、全新的 `/dev` 与使用私有 PID 命名空间的 `/proc` 组合起来。命令可管理后代进程,但看不到宿主进程;隐藏宿主的 `/proc/` 条目,可以防止 `root`、`fd` 等魔法链接绕过其挂载约束。`workspace-write` 另加临时的 `/tmp` 与可写工作区绑定挂载。[私有 PID Agent Note](../../../.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.md)记录该边界。
+bwrap profile 将只读宿主根目录、全新的 `/dev` 与使用私有 PID 命名空间的 `/proc` 组合起来。命令可管理后代进程,但看不到宿主进程;隐藏宿主的 `/proc/` 条目,可以防止 `root`、`fd` 等魔法链接绕过其挂载约束。`workspace-write` 另加临时的 `/tmp` 与可写工作区绑定挂载。[私有 PID Agent Note](../../../.agents/notes/implemented/bug-fix/2026-08-06-bwrap-private-pid-namespace.zh.md)记录该边界。
Seatbelt profile 默认允许,但带 `(deny file-write*)` 和写入 allow-list,因此恰好约束相应模式承诺的文件操作:`read-only` 只授予 `/dev/null` 字面路径;`workspace-write` 另加工作区根目录、`/tmp` 和逐用户 darwin 临时目录(`os.tmpdir()`,即平台供 mkstemp 家族工具使用的真实临时区域)。每个根目录都经过规范化,因为 Seatbelt 匹配解析后的路径(`/tmp` 就是 `/private/tmp`)。Apple 将 `sandbox-exec` CLI(命令行界面)标为 deprecated,但所有 macOS 系统仍会提供它;若情况发生变化,功能探测会使执行被拒绝。
@@ -27,7 +27,7 @@ Windows 档为每个工作区保留一个确定性写入 SID 和常驻 ACE,但
## 模型体验
-通过 [`dsh-bash-sandbox`](../../shell/bash-sandbox/README.md) 和 [`dsh-tool-bash`](../../shell/tool-bash/README.md) 间接影响;它们渲染该提供方的强制执行与拒绝事实,而 [`dsh-sandbox`](../sandbox/README.md) seam 负责定义 `SANDBOX_UNAVAILABLE` 文本,runner 选择与 profile 则不进入上下文。
+通过 [`dsh-bash-sandbox`](../../shell/bash-sandbox/README.zh.md) 和 [`dsh-tool-bash`](../../shell/tool-bash/README.zh.md) 间接影响;它们渲染该提供方的强制执行与拒绝事实,而 [`dsh-sandbox`](../sandbox/README.zh.md) seam 负责定义 `SANDBOX_UNAVAILABLE` 文本,runner 选择与 profile 则不进入上下文。
#### KV Cache 影响
diff --git a/packages/sandbox/sandbox-policy/README.i18n.yaml b/packages/sandbox/sandbox-policy/README.i18n.yaml
index f12249398a..31ef984c5e 100644
--- a/packages/sandbox/sandbox-policy/README.i18n.yaml
+++ b/packages/sandbox/sandbox-policy/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/sandbox/sandbox-policy/README.md
README.md: c85420019b490d311360f310c04212ac6cd737d1
-README.zh.md: 58354adf55a55951cad1ea9adb88aea2457c7f7b
+README.zh.md: 46e1deea680122622e58490064dcc4ea6699d1d3
diff --git a/packages/sandbox/sandbox-policy/README.zh.md b/packages/sandbox/sandbox-policy/README.zh.md
index 58354adf55..46e1deea68 100644
--- a/packages/sandbox/sandbox-policy/README.zh.md
+++ b/packages/sandbox/sandbox-policy/README.zh.md
@@ -2,11 +2,11 @@
[English](README.md) | 中文
-沙箱策略解析的唯一归属位置:部署默认 [`SandboxMode`](../sandbox/README.md) 与回退根目录,加上每个会话的持久模式覆盖和不可变工作区根目录。每项负责强制执行的能力在每次调用时都会收到一项解析完成的模式与根目录策略;模型在每次请求前会收到当前策略,而不会另收一份能力清单。
+沙箱策略解析的唯一归属位置:部署默认 [`SandboxMode`](../sandbox/README.zh.md) 与回退根目录,加上每个会话的持久模式覆盖和不可变工作区根目录。每项负责强制执行的能力在每次调用时都会收到一项解析完成的模式与根目录策略;模型在每次请求前会收到当前策略,而不会另收一份能力清单。
## 为何需要共享归属位置
-文件系统工具、一次性 bash 命令和终端会话可以用不同组合强制执行同一套模式词汇。如果各自解析 `mode` + `workspaceRoot`,就可能漂移成分裂世界,正是[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)所警告的情况。每个强制执行后端都会消费归属方解析出的完整策略,而当前上下文只说明该策略对于任何受 DSH 文件沙箱强制执行的可用操作有何含义。[跨家族 fs 沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)记录了共享策略决策。
+文件系统工具、一次性 bash 命令和终端会话可以用不同组合强制执行同一套模式词汇。如果各自解析 `mode` + `workspaceRoot`,就可能漂移成分裂世界,正是[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md)所警告的情况。每个强制执行后端都会消费归属方解析出的完整策略,而当前上下文只说明该策略对于任何受 DSH 文件沙箱强制执行的可用操作有何含义。[跨家族 fs 沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.zh.md)记录了共享策略决策。
## 配置
diff --git a/packages/sandbox/sandbox-windows-acl/README.i18n.yaml b/packages/sandbox/sandbox-windows-acl/README.i18n.yaml
index a394957f49..ef0522e661 100644
--- a/packages/sandbox/sandbox-windows-acl/README.i18n.yaml
+++ b/packages/sandbox/sandbox-windows-acl/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/sandbox/sandbox-windows-acl/README.md
README.md: a78f334342196ec1848a4a360e5c60b375a28057
-README.zh.md: 8962653b69b23b92fe763e4fcc90bf45911865f7
+README.zh.md: dc7f4d4c080d4e2204103afb8b1a2defcfb306c7
diff --git a/packages/sandbox/sandbox-windows-acl/README.zh.md b/packages/sandbox/sandbox-windows-acl/README.zh.md
index 8962653b69..dc7f4d4c08 100644
--- a/packages/sandbox/sandbox-windows-acl/README.zh.md
+++ b/packages/sandbox/sandbox-windows-acl/README.zh.md
@@ -6,7 +6,7 @@
一句话机制:把调用者令牌复制为 `WRITE_RESTRICTED` 受限令牌,其 restricting SIDs 携带彼此独立的工作区能力与私有临时目录能力。工作区 SID 由规范工作区路径确定性派生(`workspaceWriteSid`),因此工作区根目录 ACE 每台机器每个工作区只物化一次,之后每次会话、调用或重启都命中精确 ACE 跳过。每个活跃的会话/工作区对则获得一个随机临时目录,以及一个从该路径派生的 SID(`tempWriteSid`),因此各会话共享预期的工作区权限,却不会继承彼此的临时目录权限。此后 Windows 只在「调用者正常权限」与「restricting SID 交集」同时允许时才放行写入。这些 SID 是主要白名单,在系统其余位置不授予任何权限;但该检查还会继承**其他** restricting SID 的环境写 ACE(保活组登录 SID + Everyone),而 NTFS ACL 属于文件对象而非路径。Everyone 与硬链接边界正是该档报告部分而非完整强制执行的原因。
-直接构建在原生 ACL 机制上是记录在案的设计选择:它实现两种隔离模式,且不背负被否决的容器方案的问题——见[设计笔记](../../../.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md)([mxc](https://github.com/microsoft/mxc/blob/main/docs/process-container/os-version-support.md) 要求 Windows 11 24H2 的 OS 下限,且任意路径读取需要整体改写宿主 DACL;AppContainer 根本无法任意路径读取)。
+直接构建在原生 ACL 机制上是记录在案的设计选择:它实现两种隔离模式,且不背负被否决的容器方案的问题——见[设计笔记](../../../.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md)([mxc](https://github.com/microsoft/mxc/blob/main/docs/process-container/os-version-support.md) 要求 Windows 11 24H2 的 OS 下限,且任意路径读取需要整体改写宿主 DACL;AppContainer 根本无法任意路径读取)。
## 用法
@@ -86,7 +86,7 @@ koffi 结构体定义在模块加载时对照探针断言其大小,因此头
## 模型体验
-间接地通过 [`dsh-bash-sandbox`](../../shell/bash-sandbox/README.md)、[`dsh-pwsh-sandbox`](../../shell/pwsh-sandbox/README.md) 及其工具呈现:它们渲染此后端的部分强制执行与拒绝事实(工具层通过 `denialSignatures` 分类的受限 stderr),而 [`dsh-sandbox`](../sandbox/README.md) seam 拥有 `SANDBOX_UNAVAILABLE` 文本与 runner 选择。
+间接地通过 [`dsh-bash-sandbox`](../../shell/bash-sandbox/README.zh.md)、[`dsh-pwsh-sandbox`](../../shell/pwsh-sandbox/README.zh.md) 及其工具呈现:它们渲染此后端的部分强制执行与拒绝事实(工具层通过 `denialSignatures` 分类的受限 stderr),而 [`dsh-sandbox`](../sandbox/README.zh.md) seam 拥有 `SANDBOX_UNAVAILABLE` 文本与 runner 选择。
#### KV Cache 影响
diff --git a/packages/sandbox/sandbox/README.i18n.yaml b/packages/sandbox/sandbox/README.i18n.yaml
index 7e7ed765b2..d92c0552ea 100644
--- a/packages/sandbox/sandbox/README.i18n.yaml
+++ b/packages/sandbox/sandbox/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/sandbox/sandbox/README.md
README.md: 3ebbffd623eee51dd6fcb588eec374e6d09be2f0
-README.zh.md: bde83955a85cbe2d60c521e3677b5b305a29ecb1
+README.zh.md: b9215b59d5ed5314275e91261c5541be1e5586fe
diff --git a/packages/sandbox/sandbox/README.zh.md b/packages/sandbox/sandbox/README.zh.md
index bde83955a8..b9215b59d5 100644
--- a/packages/sandbox/sandbox/README.zh.md
+++ b/packages/sandbox/sandbox/README.zh.md
@@ -2,23 +2,25 @@
[English](README.md) | 中文
-进程沙箱 Service Definition。负责定义 `ctx.sandbox` 服务约定([`SandboxProvider`](src/index.ts))与 harness 共享的限制词汇:`SandboxMode`(`read-only`/`workspace-write`/`danger-full-access`,仅限文件操作)、`SandboxEnforcement`(`full`/`partial`,针对每种内核 ABI)、`SandboxExecutionPolicy`(每次调用的完整模式及工作区根目录)、`SandboxPolicy`(其中受限制的子集),以及故障时拒绝放行的 `SANDBOX_UNAVAILABLE` 错误。作为[能力 seam 拆分](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)中的 Service Definition 角色,它只依赖 cordis(及 harness 错误基类),绝不依赖后端。
+进程沙箱 Service Definition。负责定义 `ctx.sandbox` 服务约定([`SandboxProvider`](src/index.ts))与 harness 共享的限制词汇:`SandboxMode`(`read-only`/`workspace-write`/`danger-full-access`,仅限文件操作)、`SandboxEnforcement`(`full`/`partial`,针对每种内核 ABI)、`SandboxExecutionPolicy`(每次调用的完整模式及工作区根目录)、`SandboxPolicy`(其中受限制的子集),以及故障时拒绝放行的 `SANDBOX_UNAVAILABLE` 错误。作为[能力 seam 拆分](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)中的 Service Definition 角色,它只依赖 cordis(及 harness 错误基类),绝不依赖后端。
-用一句话概括约定:`ctx.sandbox.confine(argv, policy)` 返回用于 spawn、应当取代调用方原始 argv 的 argv。返回值经过包装,使进程及其派生的所有进程都在限制下运行;还会附带所选后端达到的强制执行完整度、拒绝方言(`denialSignatures`)和结构化 runner 失败证据(`runnerFailureRules`)。没有可用后端时,它会抛出异常,绝不会原样传递 argv 使其不受限制地运行。[核心类型目录](../../../docs/subsystems/sandbox.md#wrapped-argv-and-classification-dialects)负责定义分类器的精确结构。
+用一句话概括约定:`ctx.sandbox.confine(argv, policy)` 返回用于 spawn、应当取代调用方原始 argv 的 argv。返回值经过包装,使进程及其派生的所有进程都在限制下运行;还会附带所选后端达到的强制执行完整度、拒绝方言(`denialSignatures`)和结构化 runner 失败证据(`runnerFailureRules`)。没有可用后端时,它会抛出异常,绝不会原样传递 argv 使其不受限制地运行。[核心类型目录](../../../docs/subsystems/sandbox.zh.md#wrapped-argv-and-classification-dialects)负责定义分类器的精确结构。
策略随调用传递,而不属于提供方:两个消费方可以同时按不同策略施加限制(bash 使用 `read-only`,而受限制的子 agent(智能体)保持其状态目录可写);获批的升权重试只是使用更宽策略发起的新调用。
-**只支持与宿主共享文件系统和内核的限制。** 后端与宿主共享文件系统和内核(`bwrap`、Landlock、Seatbelt);`workspaceRoot` 指向文件系统规范化后的真实主机目录。系统先解析工作区所指的目录,再做词法规范化,因此包含 `symlink/..` 的有效 cwd 会授权 `chdir` 实际到达的目录,而非无关的词法父目录。容器、microVM 与远程执行器都不是该 seam 的后端:它们会以环境一致的分组替换整个能力 seam 的 Service Provider(`ctx.shell`、`ctx.fs`)。边界及其设计理由见[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)。
+**只支持与宿主共享文件系统和内核的限制。** 后端与宿主共享文件系统和内核(`bwrap`、Landlock、Seatbelt);`workspaceRoot` 指向文件系统规范化后的真实主机目录。系统先解析工作区所指的目录,再做词法规范化,因此包含 `symlink/..` 的有效 cwd 会授权 `chdir` 实际到达的目录,而非无关的词法父目录。容器、microVM 与远程执行器都不是该 seam 的后端:它们会以环境一致的分组替换整个能力 seam 的 Service Provider(`ctx.shell`、`ctx.fs`)。边界及其设计理由见[沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md)。
实现:[`@deepseek-ai/dsh-sandbox-local`](../sandbox-local/)(Linux:`bwrap`,否则使用相应平台的 Landlock launcher;macOS:`sandbox-exec`/Seatbelt)。消费方:[`@deepseek-ai/dsh-bash-sandbox`](../../shell/bash-sandbox/)(包装 `['bash', '-c', command]`)。
## 模型体验
+
+
### 间接的限制错误
#### 模型看到的内容
-通过 [`dsh-bash-sandbox`](../../shell/bash-sandbox/README.md) 和 [`dsh-tool-bash`](../../shell/tool-bash/README.md),无法强制执行所请求模式时会产生错误码 `SANDBOX_UNAVAILABLE` 及以下精确错误。执行期 runner 失败会追加 ` Runner failure: `。
+通过 [`dsh-bash-sandbox`](../../shell/bash-sandbox/README.zh.md) 和 [`dsh-tool-bash`](../../shell/tool-bash/README.zh.md),无法强制执行所请求模式时会产生错误码 `SANDBOX_UNAVAILABLE` 及以下精确错误。执行期 runner 失败会追加 ` Runner failure: `。
##### 精确错误
diff --git a/packages/schedule/README.i18n.yaml b/packages/schedule/README.i18n.yaml
index 4d04edf194..90aa318ed0 100644
--- a/packages/schedule/README.i18n.yaml
+++ b/packages/schedule/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/schedule/README.md
README.md: 2fac190cc19f40e5d5e87acacaddce970e4b8474
-README.zh.md: 3db72e7e6650e38ab51fd75087d95cb2faf121d4
+README.zh.md: af4cd96dcb5f095a7ea83556e5f73eca532d0dee
diff --git a/packages/schedule/README.zh.md b/packages/schedule/README.zh.md
index 3db72e7e66..af4cd96dcb 100644
--- a/packages/schedule/README.zh.md
+++ b/packages/schedule/README.zh.md
@@ -10,4 +10,4 @@ Schedule 家族负责管理提醒,其持久状态保存在原 Session 日志
本包有意不公开 Schedule service 或可变数据库。工具与 runtime 向 Session stream 追加事件;到期工作通过 Agent 的普通 follow-up 队列进入同一对话。
-有关持久记录、转换、视图与交付约定,请参阅[仅限 Session 内的 Schedule](../../docs/subsystems/schedule.md)。
+有关持久记录、转换、视图与交付约定,请参阅[仅限 Session 内的 Schedule](../../docs/subsystems/schedule.zh.md)。
diff --git a/packages/schedule/schedule/README.i18n.yaml b/packages/schedule/schedule/README.i18n.yaml
index 76577697c4..8dc406becf 100644
--- a/packages/schedule/schedule/README.i18n.yaml
+++ b/packages/schedule/schedule/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/schedule/schedule/README.md
README.md: f56c689198b7e17a85f43a1ecb6e5f1527da6a91
-README.zh.md: 75425c77d2b43c6511c400a55bb5f8f898f978cc
+README.zh.md: 7180565370d83a7207c73aa9a2539219974dace0
diff --git a/packages/schedule/schedule/README.zh.md b/packages/schedule/schedule/README.zh.md
index 75425c77d2..7180565370 100644
--- a/packages/schedule/schedule/README.zh.md
+++ b/packages/schedule/schedule/README.zh.md
@@ -26,7 +26,7 @@ Schedule 负责确定性的日历规范化。落在夏令时缺口内的本地
## 管理工具
-生成的[工具目录](../../../docs/tool-catalog.md)负责 `schedule_create`、`schedule_list` 和 `schedule_delete` 的参数与输出 schema。虽然模型输入使用 `after_seconds` 和 `time_zone`,但其规范值中的记录字段使用 camelCase。
+生成的[工具目录](../../../docs/tool-catalog.zh.md)负责 `schedule_create`、`schedule_list` 和 `schedule_delete` 的参数与输出 schema。虽然模型输入使用 `after_seconds` 和 `time_zone`,但其规范值中的记录字段使用 camelCase。
一条 Agent-scoped 队列会将每项已接纳的管理事务与 live owner 的到期事务从 preflight 到任何 post-append barrier 全程串行化。`schedule_create` 要求 `after_seconds`、`at` 与 `every_seconds` 有且只有一项;它会在进入队列前验证只依赖输入形状的失败,随后执行检查点、分配永不复用的 id、追加 create,再次执行检查点。`schedule_list` 按创建顺序返回活动记录,其中包含 `state: "scheduled" | "overdue"` 与 `deliveryMode: "session-local"`。`schedule_delete` 会在进入队列前拒绝空 id 或前后带空白的 id,并只为活动 id 追加事件;未知或已终结的 id 会在 preflight 后返回 `{ id, deleted: false, code: "schedule_not_found" }`。
diff --git a/packages/sdk/README.i18n.yaml b/packages/sdk/README.i18n.yaml
index c9fc6e2600..b78b628c94 100644
--- a/packages/sdk/README.i18n.yaml
+++ b/packages/sdk/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/sdk/README.md
README.md: 052ac933defae8766e99d79b79bc4cdc6c6d74db
-README.zh.md: 364f752fcbed06cae8b0675cae852be2760bd3f4
+README.zh.md: e420c5178de7233e9939147e6b674d83e29902b8
diff --git a/packages/sdk/README.zh.md b/packages/sdk/README.zh.md
index 364f752fcb..e420c5178d 100644
--- a/packages/sdk/README.zh.md
+++ b/packages/sdk/README.zh.md
@@ -2,10 +2,10 @@
[English](README.md) | 中文
-本组包含用于从另一进程驱动 Harness 运行时的协议栈。调用方提供运行时可执行文件及其 `cordis.yml`;本组不创建、配置、构建或启动开发者项目。[TypeScript SDK 决策](../../.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md)负责客户端约定,[工具链移除](../../.agents/notes/implemented/simplification/2026-08-11-remove-sdk-project-toolchain.md)负责产品边界。
+本组包含用于从另一进程驱动 Harness 运行时的协议栈。调用方提供运行时可执行文件及其 `cordis.yml`;本组不创建、配置、构建或启动开发者项目。[TypeScript SDK 决策](../../.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md)负责客户端约定,[工具链移除](../../.agents/notes/implemented/simplification/2026-08-11-remove-sdk-project-toolchain.zh.md)负责产品边界。
| 包 | 职责 |
|---|---|
-| [`protocol/`](protocol/README.md) | 定义 SDK 运行时通信协议 |
-| [`client/`](client/README.md) | 通过 TypeScript 客户端 API 驱动 Harness 运行时 |
-| [`server/`](server/README.md) | 通过 stdio JSON-RPC 为进程外 SDK 客户端提供服务 |
+| [`protocol/`](protocol/README.zh.md) | 定义 SDK 运行时通信协议 |
+| [`client/`](client/README.zh.md) | 通过 TypeScript 客户端 API 驱动 Harness 运行时 |
+| [`server/`](server/README.zh.md) | 通过 stdio JSON-RPC 为进程外 SDK 客户端提供服务 |
diff --git a/packages/sdk/client/README.i18n.yaml b/packages/sdk/client/README.i18n.yaml
index da2067692b..ab49e47455 100644
--- a/packages/sdk/client/README.i18n.yaml
+++ b/packages/sdk/client/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/sdk/client/README.md
README.md: b33457875f81d11d09bab2e5aa5ce730e233c78a
-README.zh.md: 271f07ffb0f97abe005971962beb517acfdc05a4
+README.zh.md: b89e56629fefe572394751bc1bee38aaba6f3300
diff --git a/packages/sdk/client/README.zh.md b/packages/sdk/client/README.zh.md
index 271f07ffb0..b89e56629f 100644
--- a/packages/sdk/client/README.zh.md
+++ b/packages/sdk/client/README.zh.md
@@ -2,9 +2,9 @@
[English](README.md) | 中文
-以子进程方式驱动 DeepSeek Harness 运行时、走 stdio JSON-RPC 的 TypeScript 客户端 SDK——[Python SDK](../../../python/README.md)(`deepseek-harness`)的设计孪生,共享同一个运行时对端、协议与分层:`DeepSeekHarness` 是高层自有运行 API,`HarnessClient` 是低层协议客户端。包(package)根枚举消费方接口:两层客户端、面向调用方的类型和 `JsonRpcResponseError`;源模块、规范化辅助函数与订阅投递机制不供消费方导入。纯库:不在任何 Cordis 上下文注册;它所 spawn 的运行时进程是一个完整 harness,其组成由自己的 `cordis.yml` 决定。
+以子进程方式驱动 DeepSeek Harness 运行时、走 stdio JSON-RPC 的 TypeScript 客户端 SDK——[Python SDK](../../../python/README.zh.md)(`deepseek-harness`)的设计孪生,共享同一个运行时对端、协议与分层:`DeepSeekHarness` 是高层自有运行 API,`HarnessClient` 是低层协议客户端。包(package)根枚举消费方接口:两层客户端、面向调用方的类型和 `JsonRpcResponseError`;源模块、规范化辅助函数与订阅投递机制不供消费方导入。纯库:不在任何 Cordis 上下文注册;它所 spawn 的运行时进程是一个完整 harness,其组成由自己的 `cordis.yml` 决定。
-与 Python SDK 不同,启动规格完全显式(`command`/`args`):本包面向仓库近旁的 TypeScript 消费方,包括 [`dsh-subagent-dsh-sdk`](../../subagent/subagent-dsh-sdk/README.md) 后端和自动化;它们知道自己要启动哪个运行时。捆绑运行时解析(寻找打包可执行文件)仍归 Python 发行版负责。
+与 Python SDK 不同,启动规格完全显式(`command`/`args`):本包面向仓库近旁的 TypeScript 消费方,包括 [`dsh-subagent-dsh-sdk`](../../subagent/subagent-dsh-sdk/README.zh.md) 后端和自动化;它们知道自己要启动哪个运行时。捆绑运行时解析(寻找打包可执行文件)仍归 Python 发行版负责。
## DeepSeekHarness
@@ -29,7 +29,7 @@ console.log(result.finalResponse)
自有运行 API 之下的协议客户端:显式 `start()`/`initialize()`/`prompt()`/`request()`/`close()`,外加通知订阅。`prompt()` 在运行时接受排队消息后立即返回该消息的 ID,绝不等待 agent 活动。`subscribe(filter?)` 返回 `NotificationSubscription`(可等待的 `next()`、非阻塞 `tryNext()`、异步迭代);`subscribeSessionTree(id)` 把范围限定到一个会话及从 `subagent.started` 血缘边发现的后代——运行时对上下文内每个会话都发通知,范围限定在客户端完成,与 Python SDK 完全一致。本包导出有明确类型的错误:`JsonRpcResponseError`(协议错误响应,保留 code/data)、`RequestTimeoutError`(配置的时限已到)、`SdkProtocolError`(响应超出文档化协议)、`TransportClosedError`(运行时已消失——消息携带退出码与有界 stderr 尾部)。
-`close()` 先请求协议 `shutdown`(受 `shutdownTimeoutMs` 约束,默认 1000 毫秒),然后走 stdin-EOF → SIGTERM → SIGKILL 阶梯(`disposeEofGraceMs` 默认 6000,`disposeGraceMs` 默认 3000)直到进程真正退出。该阶梯为本客户端私有:它运行在任何 harness 上下文之外,无法搭乘 [`dsh-subprocess`](../../subprocess/README.md) 服务——即该 seam 所记录的 SDK 托管传输例外。幂等,已关闭的客户端拒绝复用。
+`close()` 先请求协议 `shutdown`(受 `shutdownTimeoutMs` 约束,默认 1000 毫秒),然后走 stdin-EOF → SIGTERM → SIGKILL 阶梯(`disposeEofGraceMs` 默认 6000,`disposeGraceMs` 默认 3000)直到进程真正退出。该阶梯为本客户端私有:它运行在任何 harness 上下文之外,无法搭乘 [`dsh-subprocess`](../../subprocess/README.zh.md) 服务——即该 seam 所记录的 SDK 托管传输例外。幂等,已关闭的客户端拒绝复用。
`HarnessClientOptions.env` 给定时整体替换子进程环境(`undefined` 原样继承父进程环境);凭据策略归调用方——`dsh-subprocess` 的 `scrubbedParentEnv` 是面向隔离启动的共享擦除基底。
@@ -44,6 +44,6 @@ console.log(result.finalResponse)
## 已知限制与暂缓事项
- **无捆绑运行时解析**——调用方显式指定运行时可执行文件;打包可执行文件的发现留在 Python 侧,直到出现 TypeScript 发行版消费方。
-- **无轮次中取消**——协议层没有提示词取消方法;放弃轮次意味着关闭运行时(见协议的 [已知限制](../protocol/README.md))。
+- **无轮次中取消**——协议层没有提示词取消方法;放弃轮次意味着关闭运行时(见协议的 [已知限制](../protocol/README.zh.md))。
- **没有逐提示词结果或取消**——低层 `prompt()` 只返回入队回执;高层 `run()` 负责从回执收集到 idle,放弃该过程意味着关闭运行时。
- **客户端→服务端通知与服务端→客户端请求**在协议两端都未实现;传输层为未来审批流保留了承载能力。
diff --git a/packages/sdk/protocol/README.i18n.yaml b/packages/sdk/protocol/README.i18n.yaml
index 262a608875..63a7e665ac 100644
--- a/packages/sdk/protocol/README.i18n.yaml
+++ b/packages/sdk/protocol/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/sdk/protocol/README.md
README.md: 9024f9ca34a5467aff1b83cb9cd864c1ec06e56b
-README.zh.md: ffd67552a770561320f919eec95679681fadbe0d
+README.zh.md: 28372bf3dcd57a817225a2769ef969d6f0c10936
diff --git a/packages/sdk/protocol/README.zh.md b/packages/sdk/protocol/README.zh.md
index ffd67552a7..28372bf3dc 100644
--- a/packages/sdk/protocol/README.zh.md
+++ b/packages/sdk/protocol/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-DeepSeek Harness SDK 运行时的共享协议格式(wire format):一个按换行分帧的 JSON-RPC 2.0 传输类,加上协议两端共同使用的具名请求、结果与通知类型。包根枚举协议消费方接口;源模块不支持深层导入。服务端是 [`dsh-sdk-jsonrpc-server`](../server/README.md) 插件;客户端是 [`dsh-sdk-client`](../client/README.md)(TypeScript)与 [Python SDK](../../../python/README.md)(后者复现这些结构但不导入它们)。纯库——无插件、无 Config、无注册。
+DeepSeek Harness SDK 运行时的共享协议格式(wire format):一个按换行分帧的 JSON-RPC 2.0 传输类,加上协议两端共同使用的具名请求、结果与通知类型。包根枚举协议消费方接口;源模块不支持深层导入。服务端是 [`dsh-sdk-jsonrpc-server`](../server/README.zh.md) 插件;客户端是 [`dsh-sdk-client`](../client/README.zh.md)(TypeScript)与 [Python SDK](../../../python/README.zh.md)(后者复现这些结构但不导入它们)。纯库——无插件、无 Config、无注册。
## 传输
@@ -26,7 +26,7 @@ DeepSeek Harness SDK 运行时的共享协议格式(wire format):一个按
## 模型体验
-无,因为此包定义面向客户端的协议格式;模型可见接口属于组合在对外服务入口 [`dsh-sdk-jsonrpc-server`](../server/README.md) 后方的运行时插件。
+无,因为此包定义面向客户端的协议格式;模型可见接口属于组合在对外服务入口 [`dsh-sdk-jsonrpc-server`](../server/README.zh.md) 后方的运行时插件。
#### KV Cache 影响
@@ -35,5 +35,5 @@ DeepSeek Harness SDK 运行时的共享协议格式(wire format):一个按
## 已知限制与暂缓事项
- **无协议版本协商**——握手只携带 `serverInfo.version`(`0.0.1`,客户端不校验);处于预发布阶段,无兼容承诺。
-- **无取消与会话关闭方法**——客户端放弃轮次的方式是关闭运行时进程;见 [`dsh-sdk-jsonrpc-server` README](../server/README.md)。
+- **无取消与会话关闭方法**——客户端放弃轮次的方式是关闭运行时进程;见 [`dsh-sdk-jsonrpc-server` README](../server/README.zh.md)。
- **server→client 请求是未使用的功能**——传输层支持,但服务器从不发送;Python SDK 的应答接口为未来审批流程预留。
diff --git a/packages/sdk/server/README.i18n.yaml b/packages/sdk/server/README.i18n.yaml
index 4f277446d3..bd7b0b22c4 100644
--- a/packages/sdk/server/README.i18n.yaml
+++ b/packages/sdk/server/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/sdk/server/README.md
README.md: 29ad5840b9d70c9c22ecd387730ba21ce89cbe07
-README.zh.md: f5afb255c83fa62fa4c2891cf725ffbf4c93e77d
+README.zh.md: c5e170beb70e3cd887865d653f49f0810bc07d48
diff --git a/packages/sdk/server/README.zh.md b/packages/sdk/server/README.zh.md
index f5afb255c8..c5e170beb7 100644
--- a/packages/sdk/server/README.zh.md
+++ b/packages/sdk/server/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-`jsonrpc` 插件通过 stdio 提供以换行符分隔的 JSON-RPC,使进程外 SDK 客户端能够驱动 harness agent(智能体)。[`HarnessSdkJsonRpcServer`](src/server.ts) 负责协议方法和通知;传输与具名协议类型位于 [`dsh-sdk-protocol`](../protocol/README.md),与客户端 SDK 共享;[`jsonrpc-demo`](../../examples/jsonrpc-demo/README.md) 提供外围的 `cordis.yml` 应用。
+`jsonrpc` 插件通过 stdio 提供以换行符分隔的 JSON-RPC,使进程外 SDK 客户端能够驱动 harness agent(智能体)。[`HarnessSdkJsonRpcServer`](src/server.ts) 负责协议方法和通知;传输与具名协议类型位于 [`dsh-sdk-protocol`](../protocol/README.zh.md),与客户端 SDK 共享;[`jsonrpc-demo`](../../examples/jsonrpc-demo/README.zh.md) 提供外围的 `cordis.yml` 应用。
## 组装
diff --git a/packages/session-query/README.i18n.yaml b/packages/session-query/README.i18n.yaml
index 544fef197b..ba11f33c04 100644
--- a/packages/session-query/README.i18n.yaml
+++ b/packages/session-query/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session-query/README.md
README.md: b955a7f6e19f5a49e48e564523cc343e32816dd2
-README.zh.md: d1e6c645050f4e58f9409e328552b5f9209bc3a5
+README.zh.md: ebf3bd5c5f18e00b0beec4855fb99907019664ed
diff --git a/packages/session-query/README.zh.md b/packages/session-query/README.zh.md
index d1e6c64505..ebf3bd5c5f 100644
--- a/packages/session-query/README.zh.md
+++ b/packages/session-query/README.zh.md
@@ -6,9 +6,9 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`session-query/`](session-query/README.md) | 定义可信读取、关系查询和搜索操作 | `ctx.sessionQuery` |
-| [`session-query-sqlite/`](session-query-sqlite/README.md) | 使用 SQLite 全文搜索实现会话查询 | `ctx.sessionQuery` |
-| [`session-log-export/`](session-log-export/README.md) | 在 Host ZIP 端点之上增加 Web `/export` 命令、共享浏览器下载状态和结果弹窗 | `ctx.sessionLogDownload` |
-| [`tool-session-query/`](tool-session-query/README.md) | 向模型公开经过工作区授权的会话查询 | 注册到 `ctx.tools` |
+| [`session-query/`](session-query/README.zh.md) | 定义可信读取、关系查询和搜索操作 | `ctx.sessionQuery` |
+| [`session-query-sqlite/`](session-query-sqlite/README.zh.md) | 使用 SQLite 全文搜索实现会话查询 | `ctx.sessionQuery` |
+| [`session-log-export/`](session-log-export/README.zh.md) | 在 Host ZIP 端点之上增加 Web `/export` 命令、共享浏览器下载状态和结果弹窗 | `ctx.sessionLogDownload` |
+| [`tool-session-query/`](tool-session-query/README.zh.md) | 向模型公开经过工作区授权的会话查询 | 注册到 `ctx.tools` |
-子系统参考——逻辑记录、有界读取、追踪、筛选器、结果页——见 [docs/subsystems/session-query.md](../../docs/subsystems/session-query.md)。
+子系统参考——逻辑记录、有界读取、追踪、筛选器、结果页——见 [docs/subsystems/session-query.md](../../docs/subsystems/session-query.zh.md)。
diff --git a/packages/session-query/session-log-export/README.i18n.yaml b/packages/session-query/session-log-export/README.i18n.yaml
index d333d5ed90..0d158164ee 100644
--- a/packages/session-query/session-log-export/README.i18n.yaml
+++ b/packages/session-query/session-log-export/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session-query/session-log-export/README.md
README.md: f3625338e85a38ab279c5fa4700a7a9e1124cfb3
-README.zh.md: 5a2fa86d126f6bda808d3036bddc0e26fd310690
+README.zh.md: 101f0bb41c7296916b72a82186dae74b1b576e7f
diff --git a/packages/session-query/session-log-export/README.zh.md b/packages/session-query/session-log-export/README.zh.md
index 5a2fa86d12..101f0bb41c 100644
--- a/packages/session-query/session-log-export/README.zh.md
+++ b/packages/session-query/session-log-export/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-Web Session 日志下载控制,使用 `dsh-host-apiproxy` 拥有的 Host 流式 ZIP 端点。Host 半包注册 `/export`;浏览器半包在 Session Header 中提供 111×32 的 `Session log` 操作,以及一个供该按钮与斜杠命令共用的下载控制器和弹窗。ZIP 生成、原始 JSONL/zstd 读取、子 Session、附件、背压和 HTTP 错误语义仍由 [ApiProxy 下载实现](../../host/apiproxy/README.md)负责。
+Web Session 日志下载控制,使用 `dsh-host-apiproxy` 拥有的 Host 流式 ZIP 端点。Host 半包注册 `/export`;浏览器半包在 Session Header 中提供 111×32 的 `Session log` 操作,以及一个供该按钮与斜杠命令共用的下载控制器和弹窗。ZIP 生成、原始 JSONL/zstd 读取、子 Session、附件、背压和 HTTP 错误语义仍由 [ApiProxy 下载实现](../../host/apiproxy/README.zh.md)负责。
## 命令约定
diff --git a/packages/session-query/session-query/README.i18n.yaml b/packages/session-query/session-query/README.i18n.yaml
index 783b6d9e6f..b668d26d4b 100644
--- a/packages/session-query/session-query/README.i18n.yaml
+++ b/packages/session-query/session-query/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session-query/session-query/README.md
README.md: 02640bee4b3eb7f6877756a2ce84b26f698c9f58
-README.zh.md: 794c0e6e86606d92ed82ec286844647487df3295
+README.zh.md: dbac518b2c295825459475dba5a9c6c38f53afc8
diff --git a/packages/session-query/session-query/README.zh.md b/packages/session-query/session-query/README.zh.md
index 794c0e6e86..dbac518b2c 100644
--- a/packages/session-query/session-query/README.zh.md
+++ b/packages/session-query/session-query/README.zh.md
@@ -29,7 +29,7 @@
`SessionQueryEngine.searchSessions(request, exec?)` 按匹配最强的事件对逻辑语料库分组;`searchEvents(request, exec?)` 搜索一个逻辑会话。这两个是服务仅有的抽象方法。两者都返回分页结果,其延续信息是由服务持有的带品牌 `SessionSearchCursor`;接受可选取消,并在不使用提供方专用数值分数的情况下提供摘录。事件搜索分页结果还携带来自与命中相同索引世代的克隆目标 header,使授权消费方可将策略绑定到此次载荷观察。搜索请求只接受事件元数据过滤器,因为字面文本过滤使用上文所述扫描路径。
-该包没有提供方协调器、回退实现或独立具体插件。具体服务后端继承已实现的读取、过滤和跟踪,同时负责全文观察、对账、排名、游标世代和查询执行;第一个实现是 [`@deepseek-ai/dsh-session-query-sqlite`](../session-query-sqlite/README.md)。
+该包没有提供方协调器、回退实现或独立具体插件。具体服务后端继承已实现的读取、过滤和跟踪,同时负责全文观察、对账、排名、游标世代和查询执行;第一个实现是 [`@deepseek-ai/dsh-session-query-sqlite`](../session-query-sqlite/README.zh.md)。
`SessionQueryError.code` 是一个封闭联合,覆盖请求验证、缺失目标、格式错误的表层、来源冲突、持久化/索引失败、取消,以及无效或陈旧游标;精确字面值在 [`src/config.ts`](src/config.ts) 中定义。
@@ -53,4 +53,4 @@
## 已知限制与暂缓事项
- **无调用方授权**:这是上下文范围内的可信基础设施;未来的模型工具或 UI 必须限制调用方可检查的会话。
-- **无注册表或面向模型工具**:尚未提供提取器和搜索提供方注册表、递归遍历所引用的源事件的能力,以及面向模型的工具。[跟踪决策](../../../.agents/notes/implemented/feature/2026-07-13-session-query-tracing.md) 负责关系语义;SQLite 归属和 tokenizer 决策位于[已实现搜索记录](../../../.agents/notes/implemented/feature/2026-07-10-sqlite-session-query-provider.md)。
+- **无注册表或面向模型工具**:尚未提供提取器和搜索提供方注册表、递归遍历所引用的源事件的能力,以及面向模型的工具。[跟踪决策](../../../.agents/notes/implemented/feature/2026-07-13-session-query-tracing.zh.md) 负责关系语义;SQLite 归属和 tokenizer 决策位于[已实现搜索记录](../../../.agents/notes/implemented/feature/2026-07-10-sqlite-session-query-provider.zh.md)。
diff --git a/packages/session-query/tool-session-query/README.i18n.yaml b/packages/session-query/tool-session-query/README.i18n.yaml
index 4bc40ec6a2..33ba5b3460 100644
--- a/packages/session-query/tool-session-query/README.i18n.yaml
+++ b/packages/session-query/tool-session-query/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session-query/tool-session-query/README.md
README.md: d973daf1124c4be05f7335b18661d431d45be39f
-README.zh.md: 18ff18520ba8bb9d3d3dba5091d6281a0a47b78a
+README.zh.md: 71c1c74addf8cb2b29773933a36f3368b1c81bf5
diff --git a/packages/session-query/tool-session-query/README.zh.md b/packages/session-query/tool-session-query/README.zh.md
index 18ff18520b..71c1c74add 100644
--- a/packages/session-query/tool-session-query/README.zh.md
+++ b/packages/session-query/tool-session-query/README.zh.md
@@ -45,7 +45,7 @@ Use session_search to find relevant work from prior sessions, or session_event_s
#### 模型看到的内容
-模型会看到生成的 [`session_search`、`session_event_search`、`session_trace`、`session_event_trace` 和 `session_event_read` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-session-query)。搜索过滤器会增加固定 schema token,而游标、工作区路径、输出分页和模型可控结果上限仍不存在。
+模型会看到生成的 [`session_search`、`session_event_search`、`session_trace`、`session_event_trace` 和 `session_event_read` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-session-query)。搜索过滤器会增加固定 schema token,而游标、工作区路径、输出分页和模型可控结果上限仍不存在。
#### Token 影响
diff --git a/packages/session/README.i18n.yaml b/packages/session/README.i18n.yaml
index fc393e9bd6..69cbe17e10 100644
--- a/packages/session/README.i18n.yaml
+++ b/packages/session/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/README.md
README.md: 78fbc0ab6f0a3b9a77c9e3b1a538dd227c532296
-README.zh.md: 0aedc9b75c5452cdf9c81777cbbdcd1f60386011
+README.zh.md: e1c04337931a42a1751edb2d3d616b4b53d2af1b
diff --git a/packages/session/README.zh.md b/packages/session/README.zh.md
index 0aedc9b75c..e1c0433793 100644
--- a/packages/session/README.zh.md
+++ b/packages/session/README.zh.md
@@ -10,12 +10,12 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`session-persistence/`](session-persistence/README.md) | 定义持久化服务和共享写入协调机制 | `ctx.sessionPersistence` |
-| [`session-checkpoint-policy/`](session-checkpoint-policy/README.md) | 应用语义持久性检查点 | 包装 `ctx.llm` 和 `ctx.tools` |
-| [`session-persistence-jsonl/`](session-persistence-jsonl/README.md) | 将会话持久化到 JSONL 文件 | 注册到 `ctx.sessionPersistence` |
-| [`session-persistence-sqlite/`](session-persistence-sqlite/README.md) | 使用物理分片打包行的可选 SQLite 后端 | 注册到 `ctx.sessionPersistence` |
+| [`session-persistence/`](session-persistence/README.zh.md) | 定义持久化服务和共享写入协调机制 | `ctx.sessionPersistence` |
+| [`session-checkpoint-policy/`](session-checkpoint-policy/README.zh.md) | 应用语义持久性检查点 | 包装 `ctx.llm` 和 `ctx.tools` |
+| [`session-persistence-jsonl/`](session-persistence-jsonl/README.zh.md) | 将会话持久化到 JSONL 文件 | 注册到 `ctx.sessionPersistence` |
+| [`session-persistence-sqlite/`](session-persistence-sqlite/README.zh.md) | 使用物理分片打包行的可选 SQLite 后端 | 注册到 `ctx.sessionPersistence` |
-[会话持久化决策](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)记录了持久化设计。
+[会话持久化决策](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md)记录了持久化设计。
## 投影
@@ -23,9 +23,9 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`session-projection/`](session-projection/README.md) | 定义并驱动会话投影单元 | `ctx.sessionProjections` |
-| [`session-projection-cache/`](session-projection-cache/README.md) | 持久化并恢复投影检查点 | `ctx.sessionProjectionCache` |
-| [`session-stats/`](session-stats/README.md) | 提供全日志会话计数与墙钟时间(`sessionStats` 单元) | 注册到 `ctx.sessionProjections` |
+| [`session-projection/`](session-projection/README.zh.md) | 定义并驱动会话投影单元 | `ctx.sessionProjections` |
+| [`session-projection-cache/`](session-projection-cache/README.zh.md) | 持久化并恢复投影检查点 | `ctx.sessionProjectionCache` |
+| [`session-stats/`](session-stats/README.zh.md) | 提供全日志会话计数与墙钟时间(`sessionStats` 单元) | 注册到 `ctx.sessionProjections` |
## 标题
@@ -33,20 +33,20 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`session-title/`](session-title/README.md) | 负责标题状态、回退行为、提供方注册与刷新 | `ctx.sessionTitle` |
-| [`session-title-llm/`](session-title-llm/README.md) | 提供共享的模型标题生成能力 | — |
-| [`session-title-first-prompt-llm/`](session-title-first-prompt-llm/README.md) | 根据第一条合格的人类消息生成会话标题 | 注册到 `ctx.sessionTitle` |
-| [`session-title-all-prompts-llm/`](session-title-all-prompts-llm/README.md) | 根据所有合格的人类消息生成会话标题 | 注册到 `ctx.sessionTitle` |
+| [`session-title/`](session-title/README.zh.md) | 负责标题状态、回退行为、提供方注册与刷新 | `ctx.sessionTitle` |
+| [`session-title-llm/`](session-title-llm/README.zh.md) | 提供共享的模型标题生成能力 | — |
+| [`session-title-first-prompt-llm/`](session-title-first-prompt-llm/README.zh.md) | 根据第一条合格的人类消息生成会话标题 | 注册到 `ctx.sessionTitle` |
+| [`session-title-all-prompts-llm/`](session-title-all-prompts-llm/README.zh.md) | 根据所有合格的人类消息生成会话标题 | 注册到 `ctx.sessionTitle` |
部署可以注册一个模型驱动提供方;未注册时,服务仍保留确定性回退机制。
## 遥测
-将会话活动投影为外发遥测,并将投递委派给配置的上报后端。[遥测决策](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)记录上报边界;[模式决策](../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md)记录即时、反馈门控与禁用投递。
+将会话活动投影为外发遥测,并将投递委派给配置的上报后端。[遥测决策](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md)记录上报边界;[模式决策](../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.zh.md)记录即时、反馈门控与禁用投递。
| 包 | 职责 |
|---|---|
-| [`session-telemetry/`](session-telemetry/README.md) | 定义捕获、脱敏、投影,以及实时或按需后端投递。 |
-| [`session-telemetry-otel/`](session-telemetry-otel/README.md) | 通过 OpenTelemetry 日志以 `FULL`、`FEEDBACK_ONLY` 或 `DISABLED` 模式投递遥测。 |
+| [`session-telemetry/`](session-telemetry/README.zh.md) | 定义捕获、脱敏、投影,以及实时或按需后端投递。 |
+| [`session-telemetry-otel/`](session-telemetry-otel/README.zh.md) | 通过 OpenTelemetry 日志以 `FULL`、`FEEDBACK_ONLY` 或 `DISABLED` 模式投递遥测。 |
-子系统参考:[persistence.md](../../docs/subsystems/persistence.md)、[session-projection.md](../../docs/subsystems/session-projection.md)、[session-title.md](../../docs/subsystems/session-title.md) 与 [session-telemetry.md](../../docs/subsystems/session-telemetry.md)。同一时间只允许一个标题提供方注册;demo 主干挂载回退服务,两个模型提供方都留在默认组合之外。
+子系统参考:[persistence.md](../../docs/subsystems/persistence.zh.md)、[session-projection.md](../../docs/subsystems/session-projection.zh.md)、[session-title.md](../../docs/subsystems/session-title.zh.md) 与 [session-telemetry.md](../../docs/subsystems/session-telemetry.zh.md)。同一时间只允许一个标题提供方注册;demo 主干挂载回退服务,两个模型提供方都留在默认组合之外。
diff --git a/packages/session/session-persistence-jsonl/README.i18n.yaml b/packages/session/session-persistence-jsonl/README.i18n.yaml
index 2184f491c1..f9ff2ed252 100644
--- a/packages/session/session-persistence-jsonl/README.i18n.yaml
+++ b/packages/session/session-persistence-jsonl/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/session-persistence-jsonl/README.md
README.md: 4cff3215cdb083d2fdb7c4a8f1b60e8c4028ba84
-README.zh.md: 38b41cb9f7fa0f144923feced06152a185254c0a
+README.zh.md: 1675f14a6b9fd2fedf50cca0484aff68ab06f975
diff --git a/packages/session/session-persistence-jsonl/README.zh.md b/packages/session/session-persistence-jsonl/README.zh.md
index 38b41cb9f7..1675f14a6b 100644
--- a/packages/session/session-persistence-jsonl/README.zh.md
+++ b/packages/session/session-persistence-jsonl/README.zh.md
@@ -16,7 +16,7 @@ JSONL 持久会话存储后端:`SessionPersistence` 的一个具体实现(`d
- 第一个逻辑行是不可变的 `SessionHeader`,标记为 `{ type: 'session', version, id, cwd?, createdAt, parentSession?, seedLength?, origin?, delegationDepth, agentPreset? }`。`delegationDepth` 在磁盘上必需,顶层会话为 `0`;缺失或无效值会拒绝日志。`agentPreset` 必须持久化,因为它决定了被恢复会话的工具与提示词——恢复成另一套组装,就会回放模型已无法据以行动的历史。后续每个逻辑行是一条存储记录;`assistant/chunk` 事件绝不丢弃,且 `seq` 在解码日志中保持连续(`events[i].seq === i`)。
- 存储记录是原样 `SessionEvent` JSON,或在 `packChunks` 已启用且连续段符合条件时写入的**打包分片行**(`text-chunks` / `reasoning-chunks` / `tool-call-chunks`;像 header 的 `session` 一样不带斜杠,因此行 tag 不会与事件类型混淆):一行保存至少 3 个连续同 block `assistant/chunk` delta 事件,`seq0`/`time0` 和各成员的 `dt` 间隔精确重建每个成员的 `seq`/`time`。无损 codec 位于 `@deepseek-ai/dsh-session`(`packChunkRuns`/`decodeStorageRecord`),并使用精确形态 allowlist:任何未识别内容原样存储。读取与布局无关:`load` 始终解码行,因此打包、非打包和混合文件加载结果一致。
-- 项目目录保留规范化 cwd 的可读形式,便于导航,并限制在文件系统组件上限内。分隔符替换和截断刻意有损,因此规范化相同的 cwd 字符串共享项目目录;会话 id 仍选择不同会话目录。在不区分大小写的文件系统上,只有文件系统规范化将两种写法解析到同一 transcript(文本记录)时,身份验证才接受备选路径写法。配置根仍由部署控制:可以是项目本地、共享、临时或集中式。[项目会话目录决策](../../../.agents/notes/implemented/architecture/2026-07-24-project-session-directories.md) 记录这项取舍。
+- 项目目录保留规范化 cwd 的可读形式,便于导航,并限制在文件系统组件上限内。分隔符替换和截断刻意有损,因此规范化相同的 cwd 字符串共享项目目录;会话 id 仍选择不同会话目录。在不区分大小写的文件系统上,只有文件系统规范化将两种写法解析到同一 transcript(文本记录)时,身份验证才接受备选路径写法。配置根仍由部署控制:可以是项目本地、共享、临时或集中式。[项目会话目录决策](../../../.agents/notes/implemented/architecture/2026-07-24-project-session-directories.zh.md) 记录这项取舍。
- 会话 id 是未验证的带品牌类型的字符串,因此在使用前单射转义为一个安全路径段(无遍历、无冲突)。结果目录保留给其他会话自有产物;发现只读取固定 transcript 文件名。
## 配置
@@ -33,7 +33,7 @@ JSONL 持久会话存储后端:`SessionPersistence` 的一个具体实现(`d
## 物理编码
-默认产物是独立 [Zstandard frame](../../../.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.md) 的标准拼接:一个仅包含 header 行的带 checksum frame,后跟每个持久 append 批次一个带 checksum frame。后端使用 Node 内置 Zstandard API 和默认压缩级别,不提供级别开关。列表只读取并验证 header frame。`compression: 'none'` 在原始表示中保留相同逻辑行。
+默认产物是独立 [Zstandard frame](../../../.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.zh.md) 的标准拼接:一个仅包含 header 行的带 checksum frame,后跟每个持久 append 批次一个带 checksum frame。后端使用 Node 内置 Zstandard API 和默认压缩级别,不提供级别开关。列表只读取并验证 header frame。`compression: 'none'` 在原始表示中保留相同逻辑行。
一个根只属于一种编码。启动发现和定向查找会拒绝相反 suffix,错误会命名不兼容产物,并指示调用方选择匹配 mode 或独立根。平铺 `/.jsonl*` 产物也会被拒绝,而不是忽略。不提供迁移、混合根回退或双写。
@@ -42,7 +42,7 @@ JSONL 持久会话存储后端:`SessionPersistence` 的一个具体实现(`d
- **绑定存储身份。** 查找要求可读项目目录中只有一个匹配会话目录,然后验证 header id 等于请求 id,且 header id/cwd 派生所选 transcript 路径。列表应用同一路径检查,并拒绝重复 id。身份失败发生在修复或 append 前。
- **延迟实体化。**`create(meta)` 不写入;第一次 `append` 将编码 header 和第一批写入临时文件并执行 `fsync`。POSIX 通过硬链接无覆盖发布,并对父目录 `fsync`。Windows 通过 `MoveFileExW(..., MOVEFILE_WRITE_THROUGH)` 无覆盖发布,并通过同一 write-through pattern 创建缺失目录。已创建但从未 append 的会话不留下磁盘内容,不在 `list` 中。
- **仅追加。** 已 flush 事件绝不重写。后续原始批次 append 行;压缩批次 append 一个 frame。两条路径都执行 `fsync`,并在捕获到写入或同步失败时回滚到之前字节长度。
-- **崩溃恢复:保留有效尾部工作。**`load` 验证每个完整压缩 frame,并扫描解压 JSONL。最后 frame 结构不完整时,读取器保留其完整解码记录,从 frame 开头截断,并使用共享[持久化约定](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md) 需要的合成工具、步骤和轮次 closer 重新编码这些记录。原始 mode 从第一个不完整行截断。已经存在却没有完整 header frame 的压缩工件、完整 frame 中的 checksum/解压失败,或位于最后已提交的 `turn/end` 处或之前的缺陷都属于损坏,会被拒绝。
+- **崩溃恢复:保留有效尾部工作。**`load` 验证每个完整压缩 frame,并扫描解压 JSONL。最后 frame 结构不完整时,读取器保留其完整解码记录,从 frame 开头截断,并使用共享[持久化约定](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md) 需要的合成工具、步骤和轮次 closer 重新编码这些记录。原始 mode 从第一个不完整行截断。已经存在却没有完整 header frame 的压缩工件、完整 frame 中的 checksum/解压失败,或位于最后已提交的 `turn/end` 处或之前的缺陷都属于损坏,会被拒绝。
- **非修改式检查。**`inspect()` 返回不可变、平衡的逻辑视图,并可在内存中合成恢复 closer,但不会截断不完整尾部或更改轻量修订。
- **连续 seq。**`append` 拒绝第一个 `seq` 不继续已存储日志的批次,并拒绝无法 JSON 序列化的 `event.data`,同时命名违规事件类型。
- **轻量修订。**`listSnapshots(signal?)` 使用 device、inode、size 和纳秒时间戳标识日志,避免解析完整日志;该标识会在 append、修复、替换或存储变更后改变。完整前缀读取要求读取字节前后的身份一致,`readStoredRevision()` 使用同一身份校验保留的 preparation,而不加载日志。快照列表通过产物发现原样转发该信号,并在每个 `stat` 前后检查取消;由于文件系统 `stat` 不可中断,取消会等待活动调用完成,然后在不启动另一次调用的情况下拒绝。
diff --git a/packages/session/session-persistence/README.i18n.yaml b/packages/session/session-persistence/README.i18n.yaml
index 74b808b05e..d5623dca3f 100644
--- a/packages/session/session-persistence/README.i18n.yaml
+++ b/packages/session/session-persistence/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/session-persistence/README.md
README.md: 335f40e7439fc4a49fa49bb4541f101250468d1b
-README.zh.md: 5a50f051a2c53a5f27b009df11ad46f5952a32ac
+README.zh.md: 7cda3f271398fca8279fcdfa92801aa219e9e8ed
diff --git a/packages/session/session-persistence/README.zh.md b/packages/session/session-persistence/README.zh.md
index 5a50f051a2..7cda3f2713 100644
--- a/packages/session/session-persistence/README.zh.md
+++ b/packages/session/session-persistence/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-会话持久化是一项能力 seam。抽象的 `SessionPersistence` 服务(`ctx.sessionPersistence`)是其 Service Definition。它要求持久化后端持久存储、重新加载和列出会话,但不规定具体存储实现。该 seam 采用与 `dsh-shell` 相同的角色划分(见[能力 seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)):本包负责 Service Definition,同级包负责 Service Provider,Consumer 注入该服务。
+会话持久化是一项能力 seam。抽象的 `SessionPersistence` 服务(`ctx.sessionPersistence`)是其 Service Definition。它要求持久化后端持久存储、重新加载和列出会话,但不规定具体存储实现。该 seam 采用与 `dsh-shell` 相同的角色划分(见[能力 seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)):本包负责 Service Definition,同级包负责 Service Provider,Consumer 注入该服务。
持久化单元就是现有 `SessionEvent`(事件溯源模型:日志是唯一真源),因此不存在另一套并行的「持久消息」类型。不属于可回放对话状态的元数据(格式版本、cwd、血缘、种子边界、origin、委托深度)作为 `SessionHeader` 单独传输,该类型归 `dsh-session` 所有,并在此重新导出。
@@ -31,13 +31,13 @@
## 写入协调器
-`PersistenceCoordinator` 负责每 id 状态和串行化、每个活动会话各自的有界写入 controller、延迟实体化、崩溃尾部修复、会话接管和完全停稳的 dispose。第一方后端组合一个协调器,实现小型 `PersistenceBackend` 存储钩子接口,并委托其有状态方法。因此 JSONL 和 SQLite 共享生命周期正确性,同时保留不同存储原语;见[协调器 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)、[flush controller 简化](../../../.agents/notes/implemented/simplification/2026-07-23-collapse-persistence-flush-state.md)和[有界批处理决策](../../../.agents/notes/implemented/architecture/2026-08-08-bounded-session-persistence-write-batching.md)。
+`PersistenceCoordinator` 负责每 id 状态和串行化、每个活动会话各自的有界写入 controller、延迟实体化、崩溃尾部修复、会话接管和完全停稳的 dispose。第一方后端组合一个协调器,实现小型 `PersistenceBackend` 存储钩子接口,并委托其有状态方法。因此 JSONL 和 SQLite 共享生命周期正确性,同时保留不同存储原语;见[协调器 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.zh.md)、[flush controller 简化](../../../.agents/notes/implemented/simplification/2026-07-23-collapse-persistence-flush-state.zh.md)和[有界批处理决策](../../../.agents/notes/implemented/architecture/2026-08-08-bounded-session-persistence-write-batching.zh.md)。
每个 `session/event` 将事件复制到会话 controller。第一个待处理事件会开启固定批处理窗口;后续事件会加入该批次,但不会重置截止时间。配置的 `writeBatchMaxDelayMs` 只限制这段有意等待,而不限制事件循环、初始化、串行化操作或后端延迟。写入期间接纳的事件会形成一个新的有界批次。`session/flush` 会取消等待,并作为共享的完全停稳屏障,排空屏障运行期间接纳的事件。后台写入失败只记录一次日志,保留顺序不变的批次,并暂停自动重试;新事件会开启新的固定窗口,而显式 flush 或后端拆卸会立即重试,并在失败再次发生时向调用方暴露失败。
崩溃修复只适用于冷状态。对于已有活动会话的 id,`load(id)` 为权威内存日志制作快照,等待该快照持久,并只在平衡时返回;活动会话中开放的轮次会被拒绝,而不会收到合成中断 closer。对于冷 id,检查只读取、验证、冻结并构造一次未发布 Session;只有来源修订值仍然是当前值时,重复检查才会复用该对象图。`prepare(id)` 在修复前执行相同校验,预留该 Session 本身,提交任何待处理的撕裂尾部或中断轮次修复,并将其返回用于发布。HMR(热模块替换)接管通过 `loadStored` 读取,应用协调器 cwd 检查,并绝不关闭活动轮次。
-后端读取会在验证当前记录前,转换同一格式版本中明确受支持的旧记录。消息标识机制引入前的消息会获得确定性的 id `legacy-message::`;工具结果的内容替换会继承其目标导入后的 id。react-loop 引入前的 `turn/start` 会移除过时的 trigger,已移除的 steering(中途引导)事件 `steering/message` 会转换为同一条带标识的 `user/message`;旧版 `turn/end` 会映射终止原因,但不会虚构旧记录中没有记载的调用方。协调器对 `load`、`inspect`、`readFrom`、无所有者状态的认领和 HMR 前缀接管使用同一份转换后视图。存储仍然仅追加:读取不会重写旧记录,此后追加的事件使用当前格式。这些是[消息标识机制引入前的消息](../../../.agents/notes/implemented/bug-fix/2026-07-28-load-pre-identity-session-messages.md)与 [react-loop 引入前会话](../../../.agents/notes/implemented/bug-fix/2026-08-04-load-pre-react-loop-sessions.md)决策所规定的范围受限的导入例外,并不构成通用的 v0 迁移承诺。
+后端读取会在验证当前记录前,转换同一格式版本中明确受支持的旧记录。消息标识机制引入前的消息会获得确定性的 id `legacy-message::`;工具结果的内容替换会继承其目标导入后的 id。react-loop 引入前的 `turn/start` 会移除过时的 trigger,已移除的 steering(中途引导)事件 `steering/message` 会转换为同一条带标识的 `user/message`;旧版 `turn/end` 会映射终止原因,但不会虚构旧记录中没有记载的调用方。协调器对 `load`、`inspect`、`readFrom`、无所有者状态的认领和 HMR 前缀接管使用同一份转换后视图。存储仍然仅追加:读取不会重写旧记录,此后追加的事件使用当前格式。这些是[消息标识机制引入前的消息](../../../.agents/notes/implemented/bug-fix/2026-07-28-load-pre-identity-session-messages.zh.md)与 [react-loop 引入前会话](../../../.agents/notes/implemented/bug-fix/2026-08-04-load-pre-react-loop-sessions.zh.md)决策所规定的范围受限的导入例外,并不构成通用的 v0 迁移承诺。
活动会话发出 `session/disposed` 时,协调器等待其 controller,以串行方式执行最终 drain,然后释放该精确 `Session` 对象拥有的状态。失败退役会将 controller 保留在活动会话 map 中,使后端拆卸可重试。后端拆卸先停止事件接纳,flush 每个剩余 controller,等待每 id 操作,最后才关闭存储句柄。
@@ -56,7 +56,7 @@
| `list(signal?)` | 列出全部已存储元数据,并遵循可选的取消信号。 |
| `close?()` | 可选生命周期拆卸(例如关闭 db 句柄),在 dispose drain 后等待其完成。 |
-协调器断言已存储 id,并在修复或活动会话接管前比较已存储/活动会话 cwd。其 `inspect()` 路径取得新鲜后端值的所有权,只验证和冻结一次,并在不调用 `commitRepair` 的情况下最多保留配置数量的未发布 Session。只有保留源的修订值仍等于 `readStoredRevision` 时,系统才会复用或修复它;否则协调器会重新读取。该新鲜性校验不会增加跨进程写入排他。持久日志在一次读取与复核往返内保持不变时,修订值重试才能收敛;持续的外部写入可能延迟 `load`、`inspect` 或 `prepare`。`tornMarker` 完全不透明:协调器只测试 `!== undefined`,并将其原样往返给 `commitRepair`,绝不检查值(JSONL 后端使用待截断字节偏移,SQLite 后端使用待删除 seq)。第三方后端可以不用协调器直接实现抽象服务,但必须提供相同的非修改式检查和可信轻量快照修订。详见[写入协调器 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)。
+协调器断言已存储 id,并在修复或活动会话接管前比较已存储/活动会话 cwd。其 `inspect()` 路径取得新鲜后端值的所有权,只验证和冻结一次,并在不调用 `commitRepair` 的情况下最多保留配置数量的未发布 Session。只有保留源的修订值仍等于 `readStoredRevision` 时,系统才会复用或修复它;否则协调器会重新读取。该新鲜性校验不会增加跨进程写入排他。持久日志在一次读取与复核往返内保持不变时,修订值重试才能收敛;持续的外部写入可能延迟 `load`、`inspect` 或 `prepare`。`tornMarker` 完全不透明:协调器只测试 `!== undefined`,并将其原样往返给 `commitRepair`,绝不检查值(JSONL 后端使用待截断字节偏移,SQLite 后端使用待删除 seq)。第三方后端可以不用协调器直接实现抽象服务,但必须提供相同的非修改式检查和可信轻量快照修订。详见[写入协调器 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.zh.md)。
## 元数据与位置类型
diff --git a/packages/session/session-projection-cache/README.i18n.yaml b/packages/session/session-projection-cache/README.i18n.yaml
index 4100b166ea..609f146f24 100644
--- a/packages/session/session-projection-cache/README.i18n.yaml
+++ b/packages/session/session-projection-cache/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/session-projection-cache/README.md
README.md: 33908578a5127f2b6bb78ed7467833aaaa2cf085
-README.zh.md: 9760cf3cf8382bda6866e679f1d884990a09f0cf
+README.zh.md: 0ca410f91562360d85faadf4cf64cb61ac467482
diff --git a/packages/session/session-projection-cache/README.zh.md b/packages/session/session-projection-cache/README.zh.md
index 9760cf3cf8..0ca410f915 100644
--- a/packages/session/session-projection-cache/README.zh.md
+++ b/packages/session/session-projection-cache/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-持久投影缓存(`ctx.sessionProjectionCache`):把每个投影单元的状态保存为检查点,基于域数据形态(domain data form)每会话一条记录(`session_projcache` 域——出厂 JSON 后端将其落在配置的存储根目录下、`workspace.json` 旁边)。设计权威:[session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md)(persisted projection cache 一节)。
+持久投影缓存(`ctx.sessionProjectionCache`):把每个投影单元的状态保存为检查点,基于域数据形态(domain data form)每会话一条记录(`session_projcache` 域——出厂 JSON 后端将其落在配置的存储根目录下、`workspace.json` 旁边)。设计权威:[session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md)(persisted projection cache 一节)。
一条存储行 `(key → {ver, seq, val})` 是折叠捷径,绝不是权威:可能陈旧(`seq` 精确说明陈旧到哪),但绝不会错。实现据此承诺:
diff --git a/packages/session/session-projection/README.i18n.yaml b/packages/session/session-projection/README.i18n.yaml
index 73d292ee2b..2d6a5a01f9 100644
--- a/packages/session/session-projection/README.i18n.yaml
+++ b/packages/session/session-projection/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/session-projection/README.md
README.md: f77b7f1279161d4ca5787c519b522454c2ef6f1e
-README.zh.md: a0b185be1219610fcc898011bb9a6c5790b08a1e
+README.zh.md: 57944258f99ba3e08784724780bd3d410d3d61e3
diff --git a/packages/session/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md
index a0b185be12..57944258f9 100644
--- a/packages/session/session-projection/README.zh.md
+++ b/packages/session/session-projection/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-会话投影 Service Definition 与驱动注册表。它拥有 `ctx.sessionProjections`:该注册表把已提交的会话事件折叠进每个已注册投影,并向载体提供完整的最终值,目前包括 api-proxy 历史尾页和 `session/projection` 推送帧。领域注册的只是纯数学;驱动权归框架。[session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md) 记录了设计理由。
+会话投影 Service Definition 与驱动注册表。它拥有 `ctx.sessionProjections`:该注册表把已提交的会话事件折叠进每个已注册投影,并向载体提供完整的最终值,目前包括 api-proxy 历史尾页和 `session/projection` 推送帧。领域注册的只是纯数学;驱动权归框架。[session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md) 记录了设计理由。
## 服务:`SessionProjectionRegistry`(ctx 键:`sessionProjections`)
diff --git a/packages/session/session-telemetry-otel/README.i18n.yaml b/packages/session/session-telemetry-otel/README.i18n.yaml
index c119707a56..31fa16233c 100644
--- a/packages/session/session-telemetry-otel/README.i18n.yaml
+++ b/packages/session/session-telemetry-otel/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/session-telemetry-otel/README.md
README.md: 351690b5941bfe33cfbbf591269683f706701978
-README.zh.md: e7031ff971cc8c9f2d1ce436513fbef1b9f27149
+README.zh.md: ae33e9df046e87d0c9726b8710525270cdd324e2
diff --git a/packages/session/session-telemetry-otel/README.zh.md b/packages/session/session-telemetry-otel/README.zh.md
index e7031ff971..ae33e9df04 100644
--- a/packages/session/session-telemetry-otel/README.zh.md
+++ b/packages/session/session-telemetry-otel/README.zh.md
@@ -29,13 +29,13 @@
上传授权采用显式许可,且为 fail-closed。通过直接构造传入未知模式时,会在读取传输配置前失败。只有 `FULL` 接受对 `ctx.sessionTelemetry.emit()` 的直接调用。`FEEDBACK_ONLY` 向其按需协调器提供私有后端能力,并且仅在 `feedback/record` 对象已经存储于 `session.events[event.seq]` 且对象身份完全相同时,才将其视为同意;独立发出的总线值会被忽略。即使存在导出器选项,`DISABLED` 也绝不会构造 SDK 流水线。
-已挂载的服务通过 seam 的 [`SessionTelemetrySharingStatus`](../session-telemetry/README.md#the-sharing-disclosure) `sharing` 属性披露解析后的模式(`full` / `feedback-only` / `disabled`),因此 `/feedback` 的确认文本可以报告会话是否以及如何被共享。该披露在构造函数中设置,与采集相互独立:即使 `DISABLED` 也会披露 `disabled`。
+已挂载的服务通过 seam 的 [`SessionTelemetrySharingStatus`](../session-telemetry/README.zh.md#the-sharing-disclosure) `sharing` 属性披露解析后的模式(`full` / `feedback-only` / `disabled`),因此 `/feedback` 的确认文本可以报告会话是否以及如何被共享。该披露在构造函数中设置,与采集相互独立:即使 `DISABLED` 也会披露 `disabled`。
`exporter.url` 在 `FULL` 与 `FEEDBACK_ONLY` 中必填,无默认值,且必须能解析为 `http(s)`;在 `DISABLED` 中可省略且不使用。在上传模式中,`shutdownTimeoutMillis` 是由 DSH 管理的有限正数外层截止时间,默认值为 3000 ms;`processor.maxExportBatchSize` 不是正整数时也会在插件加载时失败,因为 SDK 会接受该值,随后却在关闭时挂起。两个 SDK 配置块都整体透传(passthrough):`OTLPExporterNodeConfigBase` 的每个字段(`headers`、`timeoutMillis`、`compression`、`keepAlive` 等)都会到达导出器;批处理、导出节奏(`scheduledDelayMillis`)、重试、队列上限,以及持续失败下的丢失策略,都是通过 `processor` 调节的 SDK 行为。该后端不实现 `flush()`:常规 flush 由批处理器负责。关闭期间,OTel 会先等待 `exporter.forceFlush()`,再等待受处理器 `exportTimeoutMillis` 限制的完成 promise;如果该传输 promise 始终不结算,本包会在 `shutdownTimeoutMillis` 到期时放弃等待,通过协调器记录已隔离的关闭失败,并让应用继续拆卸。该截止时间无法取消 SDK 传输,因此届时仍待处理的记录可能在进程退出时丢失。
## 哪些数据会离开本机
-在上传模式中,记录携带完整的 `event.data`,内容以 seam 的 `sessionTelemetry/record` waterfall(瀑布式事件)返回的结果为准:用户与 assistant 消息内容、工具参数与工具结果(命令输出、文件内容)、完整的系统提示词与工具 schema(`request/header`)、todo 文本、压缩(compaction)摘要、钩子的 `stderrSummary`、反馈文本,以及会话 `cwd`(一个本地路径)。seam 不带任何脱敏规则:未挂载 `sessionTelemetry/record` 监听器时,导出的就是捕获原样的副本,因此向可信边界之外导出的部署方要挂载自己的规则(见 [seam README](../session-telemetry/README.md#the-redact-waterfall))。`FULL` 在追加时运行脱敏;`FEEDBACK_ONLY` 不保留遥测副本,而是在反馈触发权威日志回放时运行当时挂载的规则。无论如何,提供方凭据都不会出现:适配器的 API key 是构造函数参数而非会话事件,因此它们在结构上就不存在于日志中,也就不存在于遥测中。`DISABLED` 不会构造 SDK 流水线,也不会将任何捕获内容交给后端。
+在上传模式中,记录携带完整的 `event.data`,内容以 seam 的 `sessionTelemetry/record` waterfall(瀑布式事件)返回的结果为准:用户与 assistant 消息内容、工具参数与工具结果(命令输出、文件内容)、完整的系统提示词与工具 schema(`request/header`)、todo 文本、压缩(compaction)摘要、钩子的 `stderrSummary`、反馈文本,以及会话 `cwd`(一个本地路径)。seam 不带任何脱敏规则:未挂载 `sessionTelemetry/record` 监听器时,导出的就是捕获原样的副本,因此向可信边界之外导出的部署方要挂载自己的规则(见 [seam README](../session-telemetry/README.zh.md#the-redact-waterfall))。`FULL` 在追加时运行脱敏;`FEEDBACK_ONLY` 不保留遥测副本,而是在反馈触发权威日志回放时运行当时挂载的规则。无论如何,提供方凭据都不会出现:适配器的 API key 是构造函数参数而非会话事件,因此它们在结构上就不存在于日志中,也就不存在于遥测中。`DISABLED` 不会构造 SDK 流水线,也不会将任何捕获内容交给后端。
## 字段映射
diff --git a/packages/session/session-telemetry/README.i18n.yaml b/packages/session/session-telemetry/README.i18n.yaml
index efeccd9894..99cd673d4d 100644
--- a/packages/session/session-telemetry/README.i18n.yaml
+++ b/packages/session/session-telemetry/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/session-telemetry/README.md
README.md: e4ebee1324bc1228d6d97d891159685b05f94e4f
-README.zh.md: 963d5f6d1c5c0f60f5bf14956f3b490d60bd6574
+README.zh.md: 718f07db3fb8d8f8a21236dcb917a33ed3e0cad8
diff --git a/packages/session/session-telemetry/README.zh.md b/packages/session/session-telemetry/README.zh.md
index 963d5f6d1c..718f07db3f 100644
--- a/packages/session/session-telemetry/README.zh.md
+++ b/packages/session/session-telemetry/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-遥测(telemetry)Service Definition 声明 `SessionTelemetrySink` 后端约定,捕获协调器把会话记录传给实现该约定的任意上报 SDK 后端。捕获侧可跟随实时会话事件,也可按需回放权威会话日志前缀。本包调用 `emit()` 后就停止处理:批处理、重试、排队与丢失策略都属于后端自身的 SDK,本包既不规定也不包装。设计依据与被否决的替代方案见[复活 Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)、[反馈门控投递](../../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md)与[无缓冲反馈回放](../../../.agents/notes/implemented/simplification/2026-08-06-buffer-free-feedback-telemetry.md)。
+遥测(telemetry)Service Definition 声明 `SessionTelemetrySink` 后端约定,捕获协调器把会话记录传给实现该约定的任意上报 SDK 后端。捕获侧可跟随实时会话事件,也可按需回放权威会话日志前缀。本包调用 `emit()` 后就停止处理:批处理、重试、排队与丢失策略都属于后端自身的 SDK,本包既不规定也不包装。设计依据与被否决的替代方案见[复活 Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md)、[反馈门控投递](../../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.zh.md)与[无缓冲反馈回放](../../../.agents/notes/implemented/simplification/2026-08-06-buffer-free-feedback-telemetry.zh.md)。
## 后端约定
@@ -20,6 +20,8 @@
在 `live` 模式中,协调器的全部注册都经由组合方 fiber 的 effect 完成:`session/created`(收养:记录 header,并经投影从构造边界起回读日志;来自 fork 或恢复的构造函数种子绝不会在 firehose 上再次发出,也绝不会再次导出)、`session/event`(投影、深拷贝、脱敏,再交接;零 I/O)、`session/flush`(转发可选的 `flush()` 提示并返回 void;循环所等待的并行任务绝不能等待遥测)、`session/disposed`(在会话自身的终止边缘捕获该会话的 `shutdown` 运维记录,然后将其退役)、`agent/error`(唯一的实时总线转发;会话事件词汇有意不包含运维错误记录)、一个 dispose effect(捕获每个仍存活会话的 shutdown,再等待后端的 `shutdown()`;失败只发出警告而不抛出),以及对 `ctx.sessions.list()` 的收养扫描(热重载不会重放 `session/created`)。在 `on-demand` 模式中,协调器只注册 dispose effect:`captureSession()` 读取权威日志,直至可选的序列号边界(含边界);flush 提示与运维事件留在本地。
+
+
## 脱敏 waterfall(瀑布式事件)
每条记录在投影后立即经过 `sessionTelemetry/record` waterfall,这是 Service Definition 的脱敏扩展点。本包自身不带任何规则:最内层的 `next()` 原样透传记录,因此未挂载监听器时,记录以捕获时的原样到达后端;导出数据能干净到什么程度,恰恰取决于部署方挂载了什么规则。监听器通过变换 `next()` 的返回值来堆叠;不调用 `next()` 就返回,即替换其下方的全部逻辑;抛出异常的监听器会在协调器的隔离范围内以 fail-closed 方式拦下这一条记录。实时捕获在追加时运行 waterfall;按需捕获则在回放权威日志时使用当时挂载的规则运行 waterfall。脱敏只作用于外发副本;权威会话日志永不改写。
@@ -46,6 +48,6 @@
## 已知限制与暂缓事项
-- **尽力而为的投递**:游标标记的是已交接而非已投递;在重载窗口内被拆除的会话无法重新收养;崩溃时留在后端队列中的内容会丢失。持久化 outbox(spool、每 sink 游标、at-least-once)推迟到有部署方提出明确的崩溃丢失要求时再实现;见[复活 Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)。
+- **尽力而为的投递**:游标标记的是已交接而非已投递;在重载窗口内被拆除的会话无法重新收养;崩溃时留在后端队列中的内容会丢失。持久化 outbox(spool、每 sink 游标、at-least-once)推迟到有部署方提出明确的崩溃丢失要求时再实现;见[复活 Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md)。
- **不内置脱敏规则**:未挂载 `sessionTelemetry/record` 监听器时,记录以捕获时的原样离开进程,包括文件内容或命令输出中内嵌的任何凭据;向共享 collector 导出的部署方自行负责其规则集。
- **按需脱敏使用当前状态**:未捕获的事件只存在于权威会话日志中。后续的 `captureSession()` 会使用当时挂载的策略,深拷贝并脱敏其当前值;不存在捕获时的遥测快照或持久化的捕获前 spool。
diff --git a/packages/session/session-title-all-prompts-llm/README.i18n.yaml b/packages/session/session-title-all-prompts-llm/README.i18n.yaml
index 2d337c5a7c..06c98ab145 100644
--- a/packages/session/session-title-all-prompts-llm/README.i18n.yaml
+++ b/packages/session/session-title-all-prompts-llm/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/session-title-all-prompts-llm/README.md
README.md: cdda77f4bd92b927a156db093d690062ea10d531
-README.zh.md: 25af029ce1002ecffcd40e453213899d8f0b8f31
+README.zh.md: 8cf028ecc545f7ea2a494f0c9554d13e48b3adbf
diff --git a/packages/session/session-title-all-prompts-llm/README.zh.md b/packages/session/session-title-all-prompts-llm/README.zh.md
index 25af029ce1..8cf028ecc5 100644
--- a/packages/session/session-title-all-prompts-llm/README.zh.md
+++ b/packages/session/session-title-all-prompts-llm/README.zh.md
@@ -4,7 +4,7 @@
可选的 `ctx.sessionTitle` 提供方,通过 `ctx.llm` 总结所有符合条件的用户消息。它注册 `all-prompts` 节奏,并在每条新用户提示词后启动新 revision,同时使用预置历史与子会话提示词。较新的 revision 会中止并取代旧工作;即使提供方忽略取消,也无法提交陈旧输出。
-该插件使用完整且必填的[共享 LLM(大语言模型)配置](../session-title-llm/README.md#configuration)。同时省略 `provider` 与 `model` 时,会继承每个当前已记录主请求的确切路由;也可以同时设置二者,使标题生成使用独立路由。如果最终封装的聚合提示词超过 `maxInputBytes`,请求会失败而不是截断历史;自动使用时会发出警告并保留先前标题。
+该插件使用完整且必填的[共享 LLM(大语言模型)配置](../session-title-llm/README.zh.md#configuration)。同时省略 `provider` 与 `model` 时,会继承每个当前已记录主请求的确切路由;也可以同时设置二者,使标题生成使用独立路由。如果最终封装的聚合提示词超过 `maxInputBytes`,请求会失败而不是截断历史;自动使用时会发出警告并保留先前标题。
## 模型体验
diff --git a/packages/session/session-title-first-prompt-llm/README.i18n.yaml b/packages/session/session-title-first-prompt-llm/README.i18n.yaml
index cb3350a925..b5a4ed3952 100644
--- a/packages/session/session-title-first-prompt-llm/README.i18n.yaml
+++ b/packages/session/session-title-first-prompt-llm/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/session-title-first-prompt-llm/README.md
README.md: fef0651e1c4ba0032e6a1854590d5ac023bd71d8
-README.zh.md: 377779467a091b2c68d91a8be1903702a0808d10
+README.zh.md: 19998df3ab799fe77e18e207468bce92faf03ec8
diff --git a/packages/session/session-title-first-prompt-llm/README.zh.md b/packages/session/session-title-first-prompt-llm/README.zh.md
index 377779467a..19998df3ab 100644
--- a/packages/session/session-title-first-prompt-llm/README.zh.md
+++ b/packages/session/session-title-first-prompt-llm/README.zh.md
@@ -4,7 +4,7 @@
可选的 `ctx.sessionTitle` 提供方,通过 `ctx.llm` 总结第一条符合条件的用户消息。它注册 `first-prompt` 节奏,只在全新非 fork 会话首次创建回退时自动运行,并将结果归因于该消息的确切 seq。自动失败会保留回退,之后只能通过 `ctx.sessionTitle.refresh()` 重试。
-该插件使用完整且必填的[共享 LLM(大语言模型)配置](../session-title-llm/README.md#configuration)。同时省略 `provider` 与 `model` 时,会继承当前已记录主请求的确切路由;也可以同时设置二者,使标题生成使用独立路由。
+该插件使用完整且必填的[共享 LLM(大语言模型)配置](../session-title-llm/README.zh.md#configuration)。同时省略 `provider` 与 `model` 时,会继承当前已记录主请求的确切路由;也可以同时设置二者,使标题生成使用独立路由。
## 模型体验
diff --git a/packages/session/session-title-llm/README.i18n.yaml b/packages/session/session-title-llm/README.i18n.yaml
index 6d84c3559c..d75451c2b9 100644
--- a/packages/session/session-title-llm/README.i18n.yaml
+++ b/packages/session/session-title-llm/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/session-title-llm/README.md
README.md: db03205cabf9f723ee9bf6ad122026d75cb92113
-README.zh.md: f325cbd9344c15e91f3193204c27c40d2e3b62a6
+README.zh.md: 7e3a7d30022cd2dec4b59d98b90b9f02e5d6e9b3
diff --git a/packages/session/session-title-llm/README.zh.md b/packages/session/session-title-llm/README.zh.md
index f325cbd934..7e3a7d3002 100644
--- a/packages/session/session-title-llm/README.zh.md
+++ b/packages/session/session-title-llm/README.zh.md
@@ -12,6 +12,8 @@
路由与输入验证完成后,辅助模块会在模型分发前直接通过 `Session` 追加仅写入日志的 `session/title-llm-request` 事件。它包含标题提供方 id、确切来源 seq、路由、系统提示词、消息列表,以及该调用使用的输出 token 上限。持久化会立即观察到该记录;追加不需要标题专属标记、类型断言、结算队列或刷写。分发的请求封套会深度冻结,携带 `purpose: 'session-title'`,且有意不包含 dsh-agent-loop 的进程本地请求身份。拦截器会与记录保持一致,而循环专用重建观察者不会把它与对话请求头比较。DeepSeek 适配器会根据该用途禁用思考,使少量输出预算全部用于可见标题文本;其他适配器负责自身用途专用行为。后续模型失败会保留请求记录;从未成为可分发请求的验证失败不会创建记录。该事件始终位于派生模型历史之外。
+
+
## 配置
除成对的路由覆盖项外,每个字段都必填;库不提供默认值。
diff --git a/packages/session/session-title/README.i18n.yaml b/packages/session/session-title/README.i18n.yaml
index bbe1cfc651..e28149f030 100644
--- a/packages/session/session-title/README.i18n.yaml
+++ b/packages/session/session-title/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/session-title/README.md
README.md: e923bd9700180214f235c4971d4b020565bfee43
-README.zh.md: 8e84ac27d9f1509c8530b838eeb5eb2216d87199
+README.zh.md: 6b1eef269dea65c66c1e9a70ce9469c66d192e84
diff --git a/packages/session/session-title/README.zh.md b/packages/session/session-title/README.zh.md
index 8e84ac27d9..6b1eef269d 100644
--- a/packages/session/session-title/README.zh.md
+++ b/packages/session/session-title/README.zh.md
@@ -31,7 +31,7 @@ fork 出的会话会原样继承种子中的标题事件。首消息节奏不会
提供方会提供带品牌类型的稳定 id、自动模式(`first-prompt` 或 `all-prompts`)和 `generate(request)`。请求携带活跃会话、截至一次固定修订的所有符合条件消息、可用时当前已记录的主请求路由,以及取消信号。结果包含非空标题、该请求中互不重复且有序的来源消息 seq,以及生成该标题时使用的可选提供方/模型路由。服务会在结果持久保存前进行规范化和验证。
-参见[会话标题数据结构](../../../docs/subsystems/session-title.md)与[已实现决策](../../../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md)。
+参见[会话标题数据结构](../../../docs/subsystems/session-title.zh.md)与[已实现决策](../../../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.zh.md)。
## 模型体验
diff --git a/packages/settings/README.i18n.yaml b/packages/settings/README.i18n.yaml
index 0e9a8956bc..3523bfd4c7 100644
--- a/packages/settings/README.i18n.yaml
+++ b/packages/settings/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/settings/README.md
README.md: 9aad9391160ba6232d2de5005010ea99d8760abe
-README.zh.md: 1ffc4f5184ac044d9605b256e9b57dcceecfa562
+README.zh.md: 30b9fab712dd2802e41f03fbf715b5f541519f8e
diff --git a/packages/settings/README.zh.md b/packages/settings/README.zh.md
index 1ffc4f5184..30b9fab712 100644
--- a/packages/settings/README.zh.md
+++ b/packages/settings/README.zh.md
@@ -6,7 +6,7 @@
| 包 | 职责 | ctx key |
|---|---|---|
-| [`settings/`](settings/README.md) | 定义命名空间注册、分层解析与提交 | `ctx.settings` |
-| [`settings-file/`](settings-file/README.md) | 在本地文件中存储设置并观察外部编辑 | 注册到 `ctx.settings` |
+| [`settings/`](settings/README.zh.md) | 定义命名空间注册、分层解析与提交 | `ctx.settings` |
+| [`settings-file/`](settings-file/README.zh.md) | 在本地文件中存储设置并观察外部编辑 | 注册到 `ctx.settings` |
-子系统参考——命名空间、owner scope、解析顺序、热提交——见 [docs/subsystems/settings.md](../../docs/subsystems/settings.md)。
+子系统参考——命名空间、owner scope、解析顺序、热提交——见 [docs/subsystems/settings.md](../../docs/subsystems/settings.zh.md)。
diff --git a/packages/shell/README.i18n.yaml b/packages/shell/README.i18n.yaml
index 65372e643b..3f17b799d3 100644
--- a/packages/shell/README.i18n.yaml
+++ b/packages/shell/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/shell/README.md
README.md: f075db4fbe57c6052fea89b30e35126117f75a7c
-README.zh.md: 2fdd92aac8d768024b6b4a9d6bd96bb6f5dafb05
+README.zh.md: 21425afbaf237887134ed29152e46ad699c9f639
diff --git a/packages/shell/README.zh.md b/packages/shell/README.zh.md
index 2fdd92aac8..21425afbaf 100644
--- a/packages/shell/README.zh.md
+++ b/packages/shell/README.zh.md
@@ -6,14 +6,14 @@
| 包 | 职责 | ctx key |
|---|---|---|
-| [`shell/`](shell/README.md) | 定义 Service Provider 与 Consumer 共享的执行器约定。 | `ctx.shell` |
-| [`bash-local/`](bash-local/README.md) | 通过本地 [`subprocess`](../subprocess/README.md) 服务执行命令。 | (注册 `ctx.shell`) |
-| [`bash-sandbox/`](bash-sandbox/README.md) | 在本地执行前应用已配置的 [`sandbox`](../sandbox/README.md) 后端。 | (注册 `ctx.shell`) |
-| [`pwsh-local/`](pwsh-local/README.md) | 采用 Windows 特有的进程行为执行 PowerShell 命令。 | (注册 `ctx.shell`) |
-| [`shell-env/`](shell-env/README.md) | 提供 shell 工具共享的托管 `DSH_*` 环境。 | `ctx.shellEnv` |
-| [`tool-bash/`](tool-bash/README.md) | 向模型公开 Bash 执行和后台任务集成。 | (注册到 `ctx.tools`) |
-| [`tool-pwsh/`](tool-pwsh/README.md) | 向模型公开 PowerShell 执行。 | (注册到 `ctx.tools`) |
+| [`shell/`](shell/README.zh.md) | 定义 Service Provider 与 Consumer 共享的执行器约定。 | `ctx.shell` |
+| [`bash-local/`](bash-local/README.zh.md) | 通过本地 [`subprocess`](../subprocess/README.zh.md) 服务执行命令。 | (注册 `ctx.shell`) |
+| [`bash-sandbox/`](bash-sandbox/README.zh.md) | 在本地执行前应用已配置的 [`sandbox`](../sandbox/README.zh.md) 后端。 | (注册 `ctx.shell`) |
+| [`pwsh-local/`](pwsh-local/README.zh.md) | 采用 Windows 特有的进程行为执行 PowerShell 命令。 | (注册 `ctx.shell`) |
+| [`shell-env/`](shell-env/README.zh.md) | 提供 shell 工具共享的托管 `DSH_*` 环境。 | `ctx.shellEnv` |
+| [`tool-bash/`](tool-bash/README.zh.md) | 向模型公开 Bash 执行和后台任务集成。 | (注册到 `ctx.tools`) |
+| [`tool-pwsh/`](tool-pwsh/README.zh.md) | 向模型公开 PowerShell 执行。 | (注册到 `ctx.tools`) |
叶节点 `cordis.yml` 选择一个执行器实现和所需的面向模型工具。沙箱化组合还会选择一个 `ctx.sandbox` 提供方;[ACP(Agent Client Protocol)示例](../../examples/acp-agent/)展示一套完整接线。
-子系统参考——请求/spec 词汇、结果、后台进程、服务与事件——见 [docs/subsystems/shell.md](../../docs/subsystems/shell.md)。
+子系统参考——请求/spec 词汇、结果、后台进程、服务与事件——见 [docs/subsystems/shell.md](../../docs/subsystems/shell.zh.md)。
diff --git a/packages/shell/bash-local/README.i18n.yaml b/packages/shell/bash-local/README.i18n.yaml
index 9e8a0eaf85..82a7387566 100644
--- a/packages/shell/bash-local/README.i18n.yaml
+++ b/packages/shell/bash-local/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/shell/bash-local/README.md
README.md: 5e62ea24676f3bedf32b1b48f8618d703d5fb462
-README.zh.md: 6f236f3dd78bd444eb57443ba068a0271ac5d8df
+README.zh.md: 92f842a777aebec2e2cb6a8c54966e46202531f1
diff --git a/packages/shell/bash-local/README.zh.md b/packages/shell/bash-local/README.zh.md
index 6f236f3dd7..92f842a777 100644
--- a/packages/shell/bash-local/README.zh.md
+++ b/packages/shell/bash-local/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-`@deepseek-ai/dsh-shell` 执行器 seam 的本地 Service Provider,构建在 [`@deepseek-ai/dsh-subprocess`](../../subprocess/subprocess/README.md) 服务之上:`LocalBashExecutor` 每次调用都通过 `ctx.subprocess` 把 `bash -c ` 作为受管进程组 spawn,并负责所有 Bash 层职责(命令默认值补全与上限、超时与取消分类、适合模型的终端环境,以及后台读取时面向模型的 stdout/stderr 合并)。以 spill 文件兜底的有界输出、凭据清除、kill 升级和 dispose(资源释放)等进程组机制则由 subprocess 服务负责。
+`@deepseek-ai/dsh-shell` 执行器 seam 的本地 Service Provider,构建在 [`@deepseek-ai/dsh-subprocess`](../../subprocess/subprocess/README.zh.md) 服务之上:`LocalBashExecutor` 每次调用都通过 `ctx.subprocess` 把 `bash -c ` 作为受管进程组 spawn,并负责所有 Bash 层职责(命令默认值补全与上限、超时与取消分类、适合模型的终端环境,以及后台读取时面向模型的 stdout/stderr 合并)。以 spill 文件兜底的有界输出、凭据清除、kill 升级和 dispose(资源释放)等进程组机制则由 subprocess 服务负责。
包根目录导出默认与具名的 `LocalBashExecutor` 插件及其 `Config`。
@@ -23,11 +23,11 @@
## 行为
- **每次调用都 spawn,不保留 shell 状态**:每次调用都启动新的非登录 `bash -c`,且不读取 rc 文件。
-- **组装条目是一层,而不是最终值**:当组装中存在 settings 提供方时,本执行器以上面的条目为 base 注册该能力的 [`bash` 命名空间](../shell/README.md),因此 `settings.yaml` 中的用户段会叠加其上,下一条命令即按新预算运行。schema 无法判定的值(正有限、`graceMs` 的定时器上界)会在写入时被拒绝,运行中的执行器保持它最后一份可用的段;没有提供方、或提供方脱离之后,运行的就是组装条目。
-- **在受管进程组之上应用配置预算**:`resolve()` 从配置补全 `workdir`/`timeoutMs`/`stdoutMaxBytes`,每次 spawn 都向服务传入显式的字节上限、spill 上限与 `graceMs`。该宽限期须为正有限值,且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md),这样 Node 就能用一个定时器表示它。进程组终止、退出后管道排空、尾部保留与有界 spill 文件是 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 的机制。前台 `ShellExecRequest.stdoutMaxBytes` 可为某个受信任调用方提高单次 stdout 捕获预算;stderr 和后台运行仍使用 `maxOutputBytes`。
-- **超时与取消分类**:`run()` 通过同一个 deadline 把经配置钳位的超时与调用方的信号融合;只有执行器自身的超时报告 `timedOut`,上游取消报告 `aborted`,自身因信号终止的命令两者皆不报告(见[超时库 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md))。
-- **适合模型的终端环境**:`NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` 防止分页器与 ANSI 颜色破坏结果。这些值作为普通 env 合并,遵循服务的凭据清除与 `DSH_*` 通道规则;调用方的显式条目依旧优先。详见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md) 与 [受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
-- **后台进程**:`start()` 会立即返回活动的 `ShellProcess` 句柄且不应用超时;`readOutput()` 把基于偏移量的 stdout/stderr 读取合并为一条消费式增量,并在存在 stderr 时将其置于 `[stderr]` 标记下。运行中的进程属于 subprocess 服务,可在执行器重载后存活,并在服务 dispose 时被终止且等待退出。job id、所有权、轮询和通知属于通用 [`ctx.jobs` 运行时](../../jobs/jobs/README.md),工具层会在其中注册该句柄。
+- **组装条目是一层,而不是最终值**:当组装中存在 settings 提供方时,本执行器以上面的条目为 base 注册该能力的 [`bash` 命名空间](../shell/README.zh.md),因此 `settings.yaml` 中的用户段会叠加其上,下一条命令即按新预算运行。schema 无法判定的值(正有限、`graceMs` 的定时器上界)会在写入时被拒绝,运行中的执行器保持它最后一份可用的段;没有提供方、或提供方脱离之后,运行的就是组装条目。
+- **在受管进程组之上应用配置预算**:`resolve()` 从配置补全 `workdir`/`timeoutMs`/`stdoutMaxBytes`,每次 spawn 都向服务传入显式的字节上限、spill 上限与 `graceMs`。该宽限期须为正有限值,且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.zh.md),这样 Node 就能用一个定时器表示它。进程组终止、退出后管道排空、尾部保留与有界 spill 文件是 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.zh.md) 的机制。前台 `ShellExecRequest.stdoutMaxBytes` 可为某个受信任调用方提高单次 stdout 捕获预算;stderr 和后台运行仍使用 `maxOutputBytes`。
+- **超时与取消分类**:`run()` 通过同一个 deadline 把经配置钳位的超时与调用方的信号融合;只有执行器自身的超时报告 `timedOut`,上游取消报告 `aborted`,自身因信号终止的命令两者皆不报告(见[超时库 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md))。
+- **适合模型的终端环境**:`NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` 防止分页器与 ANSI 颜色破坏结果。这些值作为普通 env 合并,遵循服务的凭据清除与 `DSH_*` 通道规则;调用方的显式条目依旧优先。详见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.zh.md) 与 [受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.zh.md)。
+- **后台进程**:`start()` 会立即返回活动的 `ShellProcess` 句柄且不应用超时;`readOutput()` 把基于偏移量的 stdout/stderr 读取合并为一条消费式增量,并在存在 stderr 时将其置于 `[stderr]` 标记下。运行中的进程属于 subprocess 服务,可在执行器重载后存活,并在服务 dispose 时被终止且等待退出。job id、所有权、轮询和通知属于通用 [`ctx.jobs` 运行时](../../jobs/jobs/README.zh.md),工具层会在其中注册该句柄。
## 模型体验
@@ -39,9 +39,9 @@
## 已知限制与暂缓事项
-- **自身不提供隔离**:此执行器始终以 harness 进程的权限运行命令;需要隔离的部署可以组合 [`dsh-bash-sandbox`](../bash-sandbox/README.md),每次调用的 allow/deny/ask 策略则属于 `tools/pre-execute`。
+- **自身不提供隔离**:此执行器始终以 harness 进程的权限运行命令;需要隔离的部署可以组合 [`dsh-bash-sandbox`](../bash-sandbox/README.zh.md),每次调用的 allow/deny/ask 策略则属于 `tools/pre-execute`。
- **没有持久 shell 或 PTY**:每次调用都启动新的非登录 `bash -c`;仅持久化 cwd 与交互式终端会话均继续暂缓,直到真实工作流需要它们。
- **仅支持 POSIX**:`bash` 二进制已硬编码,底层服务的进程组语义也是 POSIX 的;不支持 Windows。
- **后台 spawn 失败提示只交付一次**:subprocess 服务不会为从未真正运行的进程缓冲任何输出,因此执行器把 `spawn failed: …` 注入恰好一个 `readOutput()` 增量;丢弃了该增量的读取方无法再恢复它。
-凭据清除启发式规则与 spill 保留的注意事项随 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 记录;这些机制归它所有。
+凭据清除启发式规则与 spill 保留的注意事项随 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.zh.md) 记录;这些机制归它所有。
diff --git a/packages/shell/bash-sandbox/README.i18n.yaml b/packages/shell/bash-sandbox/README.i18n.yaml
index 787ee4d2f1..ce3625a642 100644
--- a/packages/shell/bash-sandbox/README.i18n.yaml
+++ b/packages/shell/bash-sandbox/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/shell/bash-sandbox/README.md
README.md: dd0f6dd80394ee8bf0349f706e8fd77cd3c14189
-README.zh.md: 6670058e3b8d14f72bf0133d40a433b82b17e4fa
+README.zh.md: 00b146ea88ffb449c7eaca1de6185657592fe774
diff --git a/packages/shell/bash-sandbox/README.zh.md b/packages/shell/bash-sandbox/README.zh.md
index 6670058e3b..00b146ea88 100644
--- a/packages/shell/bash-sandbox/README.zh.md
+++ b/packages/shell/bash-sandbox/README.zh.md
@@ -42,7 +42,7 @@
#### 模型看到的内容
-基线是生成的 [`dsh-tool-bash` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash)。通过公布表明启用隔离的 `sandboxMode` 能力,此后端会为 `bash` 增加 `sandbox_permissions`,其 enum 为 `workspace-write` | `danger-full-access`,并增加 `justification`。策略归属方会另行贡献当前且不区分具体能力的 `sandbox:policy` 上下文。
+基线是生成的 [`dsh-tool-bash` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-bash)。通过公布表明启用隔离的 `sandboxMode` 能力,此后端会为 `bash` 增加 `sandbox_permissions`,其 enum 为 `workspace-write` | `danger-full-access`,并增加 `justification`。策略归属方会另行贡献当前且不区分具体能力的 `sandbox:policy` 上下文。
#### Token 影响
@@ -70,7 +70,7 @@
#### 模型看到的内容
-如果没有 runner 能强制执行受限模式,前台调用会传播 [`SANDBOX_UNAVAILABLE` 错误](../../sandbox/sandbox/README.md#confinement-error-indirectly);该错误由 `dsh-sandbox` 定义。判定为 runner 失败的 spawn 错误会以原始 spawn 错误作为详细信息;如果拒绝没有通过 `ENOENT`/`EACCES` 的 `path` 或 `syscall` 证据指明 `argv[0]`,它仍是普通的命令启动错误。已结算的 runner 失败则以匹配到的致命 stderr 行作为详细信息,并保留原始 stderr 收集结果。如果追加了 `Runner failure: `,它就是权威诊断;前面的后端安装文本只是通用的 `SANDBOX_UNAVAILABLE` 前缀。
+如果没有 runner 能强制执行受限模式,前台调用会传播 [`SANDBOX_UNAVAILABLE` 错误](../../sandbox/sandbox/README.zh.md#confinement-error-indirectly);该错误由 `dsh-sandbox` 定义。判定为 runner 失败的 spawn 错误会以原始 spawn 错误作为详细信息;如果拒绝没有通过 `ENOENT`/`EACCES` 的 `path` 或 `syscall` 证据指明 `argv[0]`,它仍是普通的命令启动错误。已结算的 runner 失败则以匹配到的致命 stderr 行作为详细信息,并保留原始 stderr 收集结果。如果追加了 `Runner failure: `,它就是权威诊断;前面的后端安装文本只是通用的 `SANDBOX_UNAVAILABLE` 前缀。
#### Token 影响
diff --git a/packages/shell/pwsh-local/README.i18n.yaml b/packages/shell/pwsh-local/README.i18n.yaml
index c9ce9f1d6b..a7712c1df6 100644
--- a/packages/shell/pwsh-local/README.i18n.yaml
+++ b/packages/shell/pwsh-local/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/shell/pwsh-local/README.md
README.md: 2eccc59b919d1f729eef52a42581f4da0f1d9e60
-README.zh.md: a03f80343711a1471bc96017e23476a3253ec46c
+README.zh.md: b7072ebd9be4e148cedd4edd6c20b12536814856
diff --git a/packages/shell/pwsh-local/README.zh.md b/packages/shell/pwsh-local/README.zh.md
index a03f803437..b7072ebd9b 100644
--- a/packages/shell/pwsh-local/README.zh.md
+++ b/packages/shell/pwsh-local/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-`@deepseek-ai/dsh-shell` 执行器 seam 的本地 PowerShell Service Provider,基于 [`@deepseek-ai/dsh-subprocess`](../../subprocess/subprocess/README.md) 服务:`PwshLocalExecutor` 每次调用以受管进程的方式通过 `ctx.subprocess` spawn `pwsh -NoLogo -NoProfile -NonInteractive -Command `,并负责所有 PowerShell 相关事项——可执行文件解析、命令默认化与上限、超时/取消分类、面向模型的终端环境,以及后台读取的 stdout/stderr 合并。进程组机制(有界 spill 输出、凭据清理、终止升级、dispose(资源释放))属于 subprocess 服务。
+`@deepseek-ai/dsh-shell` 执行器 seam 的本地 PowerShell Service Provider,基于 [`@deepseek-ai/dsh-subprocess`](../../subprocess/subprocess/README.zh.md) 服务:`PwshLocalExecutor` 每次调用以受管进程的方式通过 `ctx.subprocess` spawn `pwsh -NoLogo -NoProfile -NonInteractive -Command `,并负责所有 PowerShell 相关事项——可执行文件解析、命令默认化与上限、超时/取消分类、面向模型的终端环境,以及后台读取的 stdout/stderr 合并。进程组机制(有界 spill 输出、凭据清理、终止升级、dispose(资源释放))属于 subprocess 服务。
命令字符串作为单个 argv 元素传给 `-Command`:由 PowerShell 自己解析文本,不存在中间 shell,因此没有需要转义的 shell 引号层(这里不存在与 `bash -c` 字符串域对应的层)。原生 Win32 路径(`C:\...`)原样通过。
@@ -28,13 +28,13 @@
这是 `dsh-bash-local` 的 Windows 对应实现,有意逐次调用保持语义一致:
- **每次调用新建进程,无 shell 状态**——每次调用都是全新的非交互 `pwsh -Command`(确定性;不加载 profile 文件)。`-NoLogo -NoProfile -NonInteractive` 关闭启动横幅、profile 加载与会干扰工具输出的提示符。
-- **组装条目是一层,而不是最终值**——当组装中存在 settings 提供方时,本执行器以上面的条目为 base 注册该能力的 [`bash` 命名空间](../shell/README.md),因此 `settings.yaml` 中的用户段会叠加其上,下一条命令即按新预算运行。该命名空间与 POSIX 家族共用,因为一个宿主只组装一个 `ctx.shell` 提供方;在任一平台写下的文档在另一平台仍能解析。schema 无法判定的值(正有限、`graceMs` 的定时器上界)会在写入时被拒绝,运行中的执行器保持它最后一份可用的段。
+- **组装条目是一层,而不是最终值**——当组装中存在 settings 提供方时,本执行器以上面的条目为 base 注册该能力的 [`bash` 命名空间](../shell/README.zh.md),因此 `settings.yaml` 中的用户段会叠加其上,下一条命令即按新预算运行。该命名空间与 POSIX 家族共用,因为一个宿主只组装一个 `ctx.shell` 提供方;在任一平台写下的文档在另一平台仍能解析。schema 无法判定的值(正有限、`graceMs` 的定时器上界)会在写入时被拒绝,运行中的执行器保持它最后一份可用的段。
- **UTF-8 输出固定**——每条命令都先以 UTF-8 设置 `[Console]::OutputEncoding` 与 `$OutputEncoding`,因此 Windows PowerShell 5.1 兜底(或任何控制台代码页非 UTF-8 的主机)不会破坏非 ASCII 输出:subprocess 收集器以 UTF-8 解码字节。输入编码保持宿主默认;pwsh 7 默认为 UTF-8,不受影响。
- **可执行文件解析**——`resolvePwshPath` 优先显式 `pwshPath`,然后在 Windows 上依次探测 PowerShell 7 安装位置、每个 PATH 条目(Microsoft Store 安装;剥离两端引号)以及作为遗留兜底的 Windows PowerShell 5.1,逐一用 lstat 探测检查(接受真实文件或链接形态的重解析点:Store 的 app execution alias 对其目标 stat 会因 ACL 失败,但 lstat 能看到别名本身);其他平台回退为通过 PATH 解析的裸 `pwsh`。解析是 `(configured, env, platform)` 的纯函数;它在构造时执行,此后仅当存储的 `pwshPath` 与当前可执行文件所依据的值不同才再次执行,因此无关的设置变更绝不会重新探测文件系统。
-- **受管进程组之上的配置预算**——`resolve()` 从配置填充 `workdir`/`timeoutMs`/`stdoutMaxBytes`,每次 spawn 都向服务提供显式字节上限、spill 上限与 `graceMs`。该宽限期须为正有限值,且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md),这样 Node 就能用一个定时器表示它。进程树终止(Windows 用 taskkill,POSIX 用进程组信号)、退出后管道排空宽限、保尾截断与有界 spill 文件是 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 的机制。前台 `ShellExecRequest.stdoutMaxBytes` 可为单个受信调用方提高 stdout 捕获预算;stderr 与后台运行仍使用 `maxOutputBytes`。
-- **超时与取消分类**——`run()` 通过一个 deadline 融合按配置上限截取的超时与调用方信号;只有执行器自身超时报告 `timedOut`,上游取消报告 `aborted`,自我终止的命令两者都不报告(见 [timeout 库 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md))。Windows 将强制终止报告为退出码 1 且无信号,因此带信号标记的事实(`signal`、`killed` 状态)在那里仅限 POSIX;超时/取消分类与平台无关。
+- **受管进程组之上的配置预算**——`resolve()` 从配置填充 `workdir`/`timeoutMs`/`stdoutMaxBytes`,每次 spawn 都向服务提供显式字节上限、spill 上限与 `graceMs`。该宽限期须为正有限值,且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.zh.md),这样 Node 就能用一个定时器表示它。进程树终止(Windows 用 taskkill,POSIX 用进程组信号)、退出后管道排空宽限、保尾截断与有界 spill 文件是 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.zh.md) 的机制。前台 `ShellExecRequest.stdoutMaxBytes` 可为单个受信调用方提高 stdout 捕获预算;stderr 与后台运行仍使用 `maxOutputBytes`。
+- **超时与取消分类**——`run()` 通过一个 deadline 融合按配置上限截取的超时与调用方信号;只有执行器自身超时报告 `timedOut`,上游取消报告 `aborted`,自我终止的命令两者都不报告(见 [timeout 库 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md))。Windows 将强制终止报告为退出码 1 且无信号,因此带信号标记的事实(`signal`、`killed` 状态)在那里仅限 POSIX;超时/取消分类与平台无关。
- **面向模型的终端环境**——`NO_COLOR=1 PAGER=cat GIT_PAGER=cat`(没有 `TERM=dumb`:那是 POSIX 概念;现代 PowerShell 渲染器遵循 `NO_COLOR`),作为普通 env 在服务的凭据清理与 `DSH_*` 通道规则之下合并;显式调用方条目仍然优先。
-- **后台进程**——`start()` 立即返回存活的 `ShellProcess` 句柄,不设超时;句柄的 `readOutput()` 把服务基于偏移的 stdout/stderr 读取合并为一条按分段标记、通过消费游标推进的增量。仍在运行的进程属于 subprocess 服务,因此它跨执行器重载存活,并随服务 dispose(被终止并 join)。一切任务相关职责(job id、所有权、轮询、通知)都在通用 [`ctx.jobs` 运行时](../../jobs/jobs/README.md) 中,由工具层把句柄注册进去——本执行器从不接触会话或注册表。
+- **后台进程**——`start()` 立即返回存活的 `ShellProcess` 句柄,不设超时;句柄的 `readOutput()` 把服务基于偏移的 stdout/stderr 读取合并为一条按分段标记、通过消费游标推进的增量。仍在运行的进程属于 subprocess 服务,因此它跨执行器重载存活,并随服务 dispose(被终止并 join)。一切任务相关职责(job id、所有权、轮询、通知)都在通用 [`ctx.jobs` 运行时](../../jobs/jobs/README.zh.md) 中,由工具层把句柄注册进去——本执行器从不接触会话或注册表。
## 模型体验
@@ -54,4 +54,4 @@
- **编码 preamble 位于命令之前**——PowerShell 要求 `param(...)`、`#requires` 与 `using namespace`/`using assembly` 语句位于脚本最顶部,因此以其中一种开头的命令无法在 UTF-8 输出 preamble 下运行。`param(...)` 脚本可包进 `& { … }`(param 块可以合法地位于脚本块开头);`using` 语句与 `#requires` 在命令内没有变通办法(`#requires` 在 `-Command` 中无论位置如何都不生效)——此类脚本请改从文件运行。
- **Windows PowerShell 5.1 下的非 ASCII stdin 可能被错误解码**——preamble 只固定输出编码;`[Console]::InputEncoding` 保持主机默认,因为在重定向 stdin 下设置它会抛出异常。pwsh 7 默认 UTF-8,不受影响。
-清理启发式与 spill 保留的注意事项见 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md),相关机制由其负责。
+清理启发式与 spill 保留的注意事项见 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.zh.md),相关机制由其负责。
diff --git a/packages/shell/pwsh-sandbox/README.i18n.yaml b/packages/shell/pwsh-sandbox/README.i18n.yaml
index 27d1e0d7c6..3a64425236 100644
--- a/packages/shell/pwsh-sandbox/README.i18n.yaml
+++ b/packages/shell/pwsh-sandbox/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/shell/pwsh-sandbox/README.md
README.md: 533b89b533084843a17153f3f3da39f21d271b04
-README.zh.md: 370cc6fd4a3961d6f79c5c8829ac5c42e093c2a5
+README.zh.md: 8607554d10562bc519f379030ccd9518cb63b3b6
diff --git a/packages/shell/pwsh-sandbox/README.zh.md b/packages/shell/pwsh-sandbox/README.zh.md
index 370cc6fd4a..8607554d10 100644
--- a/packages/shell/pwsh-sandbox/README.zh.md
+++ b/packages/shell/pwsh-sandbox/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-沙盒消费型的 [`ctx.shell` 执行器 seam](../shell/) 的 PowerShell 实现:每条命令以 `pwsh -NoLogo -NoProfile -NonInteractive -Command ` 运行,**经 `ctx.sandbox` 隔离**,选定模式、强制完整性、拒绝事实都盖在每次结算的结果上。它是 [`@deepseek-ai/dsh-bash-sandbox`](../bash-sandbox/) 的 pwsh 孪生,按 [pwsh 执行器与工具决策](../../../.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.md) 逐调用镜像——隔离实体本身是平台无关的:Windows 上沙盒 seam 解析到 ACL 受限令牌 runner 链([`@deepseek-ai/dsh-sandbox-windows-acl`](../../sandbox/sandbox-windows-acl/)),Linux/macOS 上解析到 bwrap/Landlock/Seatbelt。
+沙盒消费型的 [`ctx.shell` 执行器 seam](../shell/) 的 PowerShell 实现:每条命令以 `pwsh -NoLogo -NoProfile -NonInteractive -Command ` 运行,**经 `ctx.sandbox` 隔离**,选定模式、强制完整性、拒绝事实都盖在每次结算的结果上。它是 [`@deepseek-ai/dsh-bash-sandbox`](../bash-sandbox/) 的 pwsh 孪生,按 [pwsh 执行器与工具决策](../../../.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.zh.md) 逐调用镜像——隔离实体本身是平台无关的:Windows 上沙盒 seam 解析到 ACL 受限令牌 runner 链([`@deepseek-ai/dsh-sandbox-windows-acl`](../../sandbox/sandbox-windows-acl/)),Linux/macOS 上解析到 bwrap/Landlock/Seatbelt。
执行器继承 [`@deepseek-ai/dsh-pwsh-local`](../pwsh-local/) 的进程机制,并消费其 argv 级 seam(`argv()` / `runArgv()` / `startArgv()` / `onProcessDone()`)把精确的 pwsh 调用经 provider 包装。沙盒策略(模式 + 工作区根目录)不是本包的配置:每次调用由 `ctx.sandboxPolicy` 随行(工具层传调用会话解析后的策略;直接调用回退到部署策略)。
diff --git a/packages/shell/shell-env/README.i18n.yaml b/packages/shell/shell-env/README.i18n.yaml
index 44eff1b922..a8eb16c5b9 100644
--- a/packages/shell/shell-env/README.i18n.yaml
+++ b/packages/shell/shell-env/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/shell/shell-env/README.md
README.md: b056289bd58e960e9fd36ee84ed20579e7bde42e
-README.zh.md: 3ced6d63525db8a15f9202311c9ca9e32335cab8
+README.zh.md: 76d1af497b603d4df41fa1b65eaeddd38c0ae9b3
diff --git a/packages/shell/shell-env/README.zh.md b/packages/shell/shell-env/README.zh.md
index 3ced6d6352..76d1af497b 100644
--- a/packages/shell/shell-env/README.zh.md
+++ b/packages/shell/shell-env/README.zh.md
@@ -17,7 +17,7 @@
## Managed environment
-每次前台与后台模型 shell 调用都会收到一份新收集的受信任 `DSH_*` 环境。`DSH_HOME` 是由 [`@deepseek-ai/dsh-home-paths`](../../util/home-paths/README.md) 解析的 Harness 主目录绝对路径(`dshHome` 配置,然后环境变量 `$DSH_HOME`,然后 `~/.dsh`),`DSH_SHELL=1` 标识受管理的子进程。带 agent(智能体)的调用额外收到 `DSH_SESSION_ID=agent.session.header.id`;当活动的持久化 seam 定位到 JSONL 工件时,它们还会收到 `DSH_SESSION_JSONL=<绝对目标路径>`。JSONL 路径只是位置提示:首次 flush 之前它可能不存在,也不一定包含当前缓冲中的轮次,并且它不是授权凭据。
+每次前台与后台模型 shell 调用都会收到一份新收集的受信任 `DSH_*` 环境。`DSH_HOME` 是由 [`@deepseek-ai/dsh-home-paths`](../../util/home-paths/README.zh.md) 解析的 Harness 主目录绝对路径(`dshHome` 配置,然后环境变量 `$DSH_HOME`,然后 `~/.dsh`),`DSH_SHELL=1` 标识受管理的子进程。带 agent(智能体)的调用额外收到 `DSH_SESSION_ID=agent.session.header.id`;当活动的持久化 seam 定位到 JSONL 工件时,它们还会收到 `DSH_SESSION_JSONL=<绝对目标路径>`。JSONL 路径只是位置提示:首次 flush 之前它可能不存在,也不一定包含当前缓冲中的轮次,并且它不是授权凭据。
`ctx.shellEnv` 负责收集。其他插件可以注册一个受 effect 作用域约束的 contributor,带有稳定名称、已声明的键/描述以及 `resolve(execution: ToolExecution)`;重复所有权与未声明的运行时键会响亮失败,而 `list()` 只枚举声明、不执行 provider。Harness 内置键保留 `DSH_HOME`、`DSH_SHELL` 与 `DSH_SESSION_ID`;本插件的持久化翻译器通过读取与后端无关的 `sessionPersistence.locate()` seam 拥有 `DSH_SESSION_JSONL`。
diff --git a/packages/shell/shell/README.i18n.yaml b/packages/shell/shell/README.i18n.yaml
index a4e67f3760..5317414e55 100644
--- a/packages/shell/shell/README.i18n.yaml
+++ b/packages/shell/shell/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/shell/shell/README.md
README.md: a15980ead797a73c18dd6d8825a4b38cf845df60
-README.zh.md: 87a7dffdfb28fcf8a58e6254bf450e513dd458f4
+README.zh.md: 008b19835697d5d9514cf5472696852cfc684acd
diff --git a/packages/shell/shell/README.zh.md b/packages/shell/shell/README.zh.md
index 87a7dffdfb..008b198356 100644
--- a/packages/shell/shell/README.zh.md
+++ b/packages/shell/shell/README.zh.md
@@ -13,7 +13,7 @@
| `@deepseek-ai/dsh-bash-sandbox` | Service Provider:沿用 `dsh-bash-local` 的机制,但通过 [`ctx.sandbox`](../../sandbox/sandbox/) 限制每次 spawn,并将拒绝报告为结果事实 |
| `@deepseek-ai/dsh-tool-bash` | 基于 `ctx.shell`、面向模型的工具 schema |
-该拆分是一个标准的能力 seam([capability-seams Agent Note](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)):`dsh-bash-sandbox` 是位于同一 Service Definition 之后的沙箱执行器——Consumer 检测其 `sandboxMode` 能力并添加升权字段,无需导入提供方——容器化或远程执行器也可以同样接入。
+该拆分是一个标准的能力 seam([capability-seams Agent Note](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)):`dsh-bash-sandbox` 是位于同一 Service Definition 之后的沙箱执行器——Consumer 检测其 `sandboxMode` 能力并添加升权字段,无需导入提供方——容器化或远程执行器也可以同样接入。
## 服务 API(`ctx.shell`)
@@ -33,9 +33,9 @@
`ShellExecRequest`(command、workdir?、timeoutMs?、stdoutMaxBytes?、signal?、stdin?、env?、dshEnv?、sandboxPolicy?)在执行前解析为 `ShellExecSpec`(command、workdir、timeoutMs、stdoutMaxBytes、signal?、stdin?、env?、dshEnv?、sandboxPolicy)。`stdoutMaxBytes` 是受信任前台运行的捕获预算,用于必须解析完整有界 stdout 的消费方;面向模型的 bash 工具不公开该字段。`sandboxPolicy` 在请求上可选,在已解析 spec 上必填但可为 null:它携带完整的每次调用模式与工作区根目录。沙箱工具路径通过 `ctx.sandboxPolicy` 从调用会话解析它;沙箱执行器的直接调用方回退到部署策略,非沙箱执行器则携带该字段但不作限制。
-每会话沙箱模式覆盖词汇(`'sandbox/mode'` 事件、`effectiveSandboxMode(events)` fold 以及 `setSandboxMode(session, mode)` 写入路径)不位于此处。它是所有强制执行家族共享的策略状态,属于 [`@deepseek-ai/dsh-sandbox-policy`](../../sandbox/sandbox-policy/)。`run()` 返回 `ShellRunResult`;`start()` 返回 `ShellProcess`,其增量读取与终止方法由 `dsh-tool-bash` 适配为通用任务注册。沙箱执行器会在前台结果与已结算进程句柄上标记 `ShellSandboxInfo`。详见 `src/types.ts` 与 [subsystems/shell.md](../../../docs/subsystems/shell.md)。
+每会话沙箱模式覆盖词汇(`'sandbox/mode'` 事件、`effectiveSandboxMode(events)` fold 以及 `setSandboxMode(session, mode)` 写入路径)不位于此处。它是所有强制执行家族共享的策略状态,属于 [`@deepseek-ai/dsh-sandbox-policy`](../../sandbox/sandbox-policy/)。`run()` 返回 `ShellRunResult`;`start()` 返回 `ShellProcess`,其增量读取与终止方法由 `dsh-tool-bash` 适配为通用任务注册。沙箱执行器会在前台结果与已结算进程句柄上标记 `ShellSandboxInfo`。详见 `src/types.ts` 与 [subsystems/shell.md](../../../docs/subsystems/shell.zh.md)。
-`stdin` 与普通 `env` 由同进程插件(hooks 桥接、原生插件)设置,用于向 hook 命令提供其 JSON payload 和 `CLAUDE_PROJECT_DIR`/`CLAUDE_PLUGIN_ROOT` 值。`dshEnv` 是受类型限制、仅允许受管 key 的独立受信任 overlay;导出的 `DSH_ENV_PREFIX` 是该 namespace、其 `DshEnvironmentKey` 模板类型、执行器清理、注册表验证、派生内置名称与模型指引的统一来源。模型 bash 使用 `ctx.shellEnv` 收集的当前快照。实现会移除继承的受管 key,再在普通 `env` 之后合并 `dshEnv`,因此省略的当前事实不会回退到陈旧环境状态,`env` 条目也无法顶掉受管值。面向模型的工具不将这三者中的任何一个公开为参数。这三者在已解析 spec 上仍然可选;缺失表示没有输入/overlay。详见 [bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md) 与 [会话环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
+`stdin` 与普通 `env` 由同进程插件(hooks 桥接、原生插件)设置,用于向 hook 命令提供其 JSON payload 和 `CLAUDE_PROJECT_DIR`/`CLAUDE_PLUGIN_ROOT` 值。`dshEnv` 是受类型限制、仅允许受管 key 的独立受信任 overlay;导出的 `DSH_ENV_PREFIX` 是该 namespace、其 `DshEnvironmentKey` 模板类型、执行器清理、注册表验证、派生内置名称与模型指引的统一来源。模型 bash 使用 `ctx.shellEnv` 收集的当前快照。实现会移除继承的受管 key,再在普通 `env` 之后合并 `dshEnv`,因此省略的当前事实不会回退到陈旧环境状态,`env` 条目也无法顶掉受管值。面向模型的工具不将这三者中的任何一个公开为参数。这三者在已解析 spec 上仍然可选;缺失表示没有输入/overlay。详见 [bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.zh.md) 与 [会话环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.zh.md)。
导出的 `parseExitStatus`(连同 `ParsedExitStatus`)是 shell 工具共享渲染约定的另一半:`dsh-tool-bash` 的 `renderResult` 与 `dsh-tool-pwsh` 的 `renderPwshResult` 追加的 `[exit code: N]`/`[killed by signal: X]` marker 的逆解析。两个工具的 `presentResult` 都用它把渲染文本拆成 terminal 卡的输出正文与其退出状态 pill;它放在 Service Definition 中,两个工具便永远不会在 marker 约定上漂移。
@@ -50,4 +50,4 @@
## 已知限制与暂缓事项
- **没有交互式输入词汇**:`stdin` 只会在 spawn 时写入一次并关闭;seam 不提供向运行中任务继续输入的通道,也没有 PTY 会话概念。
-- **前台超时始终由执行器负责**:seam 上由调用方负责 deadline 的模式已由 [工具调用超时策略 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md) 明确暂缓。
+- **前台超时始终由执行器负责**:seam 上由调用方负责 deadline 的模式已由 [工具调用超时策略 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md) 明确暂缓。
diff --git a/packages/shell/tool-bash-persistent/README.i18n.yaml b/packages/shell/tool-bash-persistent/README.i18n.yaml
index d81a95be5f..e512e891ce 100644
--- a/packages/shell/tool-bash-persistent/README.i18n.yaml
+++ b/packages/shell/tool-bash-persistent/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/shell/tool-bash-persistent/README.md
README.md: 606920d087b42344f34b70103e167b0046d3cfd5
-README.zh.md: dd88db87cb617fb2f4d35ada9550d0142e32e979
+README.zh.md: 85652bac23be02bd2c72753070734d91fb43ee98
diff --git a/packages/shell/tool-bash-persistent/README.zh.md b/packages/shell/tool-bash-persistent/README.zh.md
index dd88db87cb..85652bac23 100644
--- a/packages/shell/tool-bash-persistent/README.zh.md
+++ b/packages/shell/tool-bash-persistent/README.zh.md
@@ -19,7 +19,7 @@
#### 模型所见
-生成的 [`bash` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash-persistent),其中包含配置的 `description`。本插件不贡献独立系统提示词段;persona 与环境指导由部署负责。
+生成的 [`bash` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-bash-persistent),其中包含配置的 `description`。本插件不贡献独立系统提示词段;persona 与环境指导由部署负责。
#### Token 影响
diff --git a/packages/shell/tool-bash/README.i18n.yaml b/packages/shell/tool-bash/README.i18n.yaml
index ccdd7fe9cd..f3acda2aca 100644
--- a/packages/shell/tool-bash/README.i18n.yaml
+++ b/packages/shell/tool-bash/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/shell/tool-bash/README.md
README.md: 21749bf0a1cd3cdc46257fb2a02ba6b4ab1f5ee6
-README.zh.md: e79190ced58abbd73d85127f3a4362f4e6b292cd
+README.zh.md: 860db1a4c347d1aefe04f70e14a65e4b511ff7f6
diff --git a/packages/shell/tool-bash/README.zh.md b/packages/shell/tool-bash/README.zh.md
index e79190ced5..860db1a4c3 100644
--- a/packages/shell/tool-bash/README.zh.md
+++ b/packages/shell/tool-bash/README.zh.md
@@ -4,7 +4,7 @@
模型侧 `bash` 工具,注册在 `ctx.shell` 执行器 seam 上。前台执行始终位于该 seam 之后;后台进程句柄会注册到通用 `ctx.jobs` 运行时,并通过 `job_output`、`job_list` 和 `job_kill` 控制;这些工具由 `@deepseek-ai/dsh-tool-jobs` 提供。
-需要加载执行器 Service Provider(例如 `@deepseek-ai/dsh-bash-local`)与 [`@deepseek-ai/dsh-shell-env`](../shell-env/README.md) 注册表;在每个注入服务就绪之前,插件会保持等待状态(`inject: ['tools', 'bash', 'systemPrompt', 'bashEnv']`)。工具约定是 bash 方言——请挂载能解析 bash 的执行器。
+需要加载执行器 Service Provider(例如 `@deepseek-ai/dsh-bash-local`)与 [`@deepseek-ai/dsh-shell-env`](../shell-env/README.zh.md) 注册表;在每个注入服务就绪之前,插件会保持等待状态(`inject: ['tools', 'bash', 'systemPrompt', 'bashEnv']`)。工具约定是 bash 方言——请挂载能解析 bash 的执行器。
包根只公开 Cordis 插件约定(`name`、`inject`、`Config`、`apply`);结果渲染和后台进程适配仍保留在包内部。
@@ -28,7 +28,7 @@
### 托管 shell 环境
-每次模型发起的前台或后台 bash 调用都会通过共享的 [`dsh-shell-env`](../shell-env/README.md) 注册表收到新收集的一组可信 `DSH_*` 环境变量:`DSH_HOME`(Harness home 绝对路径)、`DSH_SHELL=1`、agent 的 `DSH_SESSION_ID`,以及当活跃持久化后端能定位时的 `DSH_SESSION_JSONL`。注册表约定——贡献方注册、重复键/未声明键的显式报错机制、内置项保留与贡献方示例——载于该包的 README。快照通过专用的 `ShellExecRequest.dshEnv` 通道传递;本地执行器会先删除继承的所有 `DSH_*` 再合并,因此嵌套 harness 和并发的父/子 agent 不会泄漏陈旧身份,且绝不修改 `process.env`。工具说明只教授通用 `$DSH_*` 约定,不会点名持久化专用变量,也不会添加永久的系统提示词段落。
+每次模型发起的前台或后台 bash 调用都会通过共享的 [`dsh-shell-env`](../shell-env/README.zh.md) 注册表收到新收集的一组可信 `DSH_*` 环境变量:`DSH_HOME`(Harness home 绝对路径)、`DSH_SHELL=1`、agent 的 `DSH_SESSION_ID`,以及当活跃持久化后端能定位时的 `DSH_SESSION_JSONL`。注册表约定——贡献方注册、重复键/未声明键的显式报错机制、内置项保留与贡献方示例——载于该包的 README。快照通过专用的 `ShellExecRequest.dshEnv` 通道传递;本地执行器会先删除继承的所有 `DSH_*` 再合并,因此嵌套 harness 和并发的父/子 agent 不会泄漏陈旧身份,且绝不修改 `process.env`。工具说明只教授通用 `$DSH_*` 约定,不会点名持久化专用变量,也不会添加永久的系统提示词段落。
结果文本依次包含 stdout、可选的 `[stderr]` 段落和适用的沙箱拒绝、超时、信号、退出代码及截断标记。超时与最终退出状态分别报告;非零退出仍是由模型解释的结果,不会成为 `isError`。截断结果会链接安全的完整 spill 文件,或报告文件不可用。只有 spawn 错误和中止等基础设施故障才会产生 `isError`。
@@ -42,17 +42,17 @@
## 工具仅使用具名参数构建请求
-`ShellExecRequest` 携带可选的 `stdoutMaxBytes`、`stdin`、普通 `env` 和托管 `dshEnv`,供可信进程内插件及此工具的环境注册表使用。模型侧工具不公开 `stdoutMaxBytes`、`stdin` 或 `env`:它使用具名的命令/工作目录/超时/信号/沙箱字段,加上从注册表收集的 `dshEnv` 来构建请求。额外模型键会被忽略,无法替换托管值。Shell 语法可以提供等价的命令级行为,而本地执行器会清除环境中的凭据和陈旧 `DSH_*` 值。参见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md)。
+`ShellExecRequest` 携带可选的 `stdoutMaxBytes`、`stdin`、普通 `env` 和托管 `dshEnv`,供可信进程内插件及此工具的环境注册表使用。模型侧工具不公开 `stdoutMaxBytes`、`stdin` 或 `env`:它使用具名的命令/工作目录/超时/信号/沙箱字段,加上从注册表收集的 `dshEnv` 来构建请求。额外模型键会被忽略,无法替换托管值。Shell 语法可以提供等价的命令级行为,而本地执行器会清除环境中的凭据和陈旧 `DSH_*` 值。参见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.zh.md)。
## 权限与升权
除非启用沙箱的执行器([`dsh-bash-sandbox`](../bash-sandbox/))限制命令,否则命令以执行器的完整权限运行。仅拒绝型沙箱会把拒绝作为结果事实报告,并在此渲染为拒绝标记;逐调用的允许/拒绝/询问策略由 `tools/pre-execute` waterfall(瀑布式事件)负责(参见 docs/architecture.md)。
-需要升权的 bash 调用会在执行前解析 `ctx.approval`。`allowed-once` 只对该次调用应用请求模式;审批被拒、取消、不可用或缺少审批上下文时,命令完全不会执行,并返回不同的错误。发生真实拒绝后,模型可以在同一轮次中使用满足需要的最窄模式和理由重试同一命令一次;审批提示本身就是征求同意的步骤。升权绝不能预先推测,禁用或拒绝审批即为最终结果。其理由见 [沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)。
+需要升权的 bash 调用会在执行前解析 `ctx.approval`。`allowed-once` 只对该次调用应用请求模式;审批被拒、取消、不可用或缺少审批上下文时,命令完全不会执行,并返回不同的错误。发生真实拒绝后,模型可以在同一轮次中使用满足需要的最窄模式和理由重试同一命令一次;审批提示本身就是征求同意的步骤。升权绝不能预先推测,禁用或拒绝审批即为最终结果。其理由见 [沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md)。
## 逐会话模式切换
-对于启用沙箱的执行器,每次调用依次按单次升权、会话覆盖、执行器默认值解析模式。未启用沙箱以及没有 agent 的调用不携带会话覆盖。策略归属方贡献当前且不区分具体能力的常驻模式;拒绝结果仍负责特定于该操作的有效模式与重试引导。参见 [`dsh-shell` 折叠计算](../shell/README.md)和[沙箱切换约定](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)。
+对于启用沙箱的执行器,每次调用依次按单次升权、会话覆盖、执行器默认值解析模式。未启用沙箱以及没有 agent 的调用不携带会话覆盖。策略归属方贡献当前且不区分具体能力的常驻模式;拒绝结果仍负责特定于该操作的有效模式与重试引导。参见 [`dsh-shell` 折叠计算](../shell/README.zh.md)和[沙箱切换约定](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md)。
## 模型体验
@@ -80,7 +80,7 @@ Check the [exit code: N] marker on every bash result; investigate failures befor
#### 模型看到的内容
-模型会看到生成的 [`bash` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash)。仅当此生产方启用 `run_in_background` 时,该字段才会出现;仅当已挂载执行器声明支持沙箱时,`sandbox_permissions` 和 `justification` 才会出现。Agent 作用域的工具限制可以移除该 agent 的定义。
+模型会看到生成的 [`bash` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-bash)。仅当此生产方启用 `run_in_background` 时,该字段才会出现;仅当已挂载执行器声明支持沙箱时,`sandbox_permissions` 和 `justification` 才会出现。Agent 作用域的工具限制可以移除该 agent 的定义。
#### Token 影响
@@ -94,7 +94,7 @@ Check the [exit code: N] marker on every bash result; investigate failures befor
#### 模型看到的内容
-renderer 先输出依数据而定的 stdout 尾部,再输出可选的 `[stderr]` 和 stderr 尾部。没有输出时,它会精确输出 `(no output)`。条件行精确为 `[output truncated; full output: ]`、`[sandbox: file access denied under mode]`、`[timed out after ms]`、`[killed by signal: ]` 和 `[exit code: ]`;沙箱升权与 runner 故障行原文列于 [`dsh-bash-sandbox`](../bash-sandbox/README.md)。
+renderer 先输出依数据而定的 stdout 尾部,再输出可选的 `[stderr]` 和 stderr 尾部。没有输出时,它会精确输出 `(no output)`。条件行精确为 `[output truncated; full output: ]`、`[sandbox: file access denied under mode]`、`[timed out after ms]`、`[killed by signal: ]` 和 `[exit code: ]`;沙箱升权与 runner 故障行原文列于 [`dsh-bash-sandbox`](../bash-sandbox/README.zh.md)。
#### Token 影响
@@ -108,7 +108,7 @@ renderer 先输出依数据而定的 stdout 尾部,再输出可选的 `[stderr
#### 模型看到的内容
-启动会精确返回 `started background job `。此生产方会向通用任务运行时提供增量进程输出、可选的 `[some output was dropped from memory; full output: ]`、沙箱事实,以及 `exit code: ` 或 `signal: ` 等终止详情。[`dsh-tool-jobs`](../../jobs/tool-jobs/README.md) 负责模型可见的状态行、完成通知、列表和取消响应。
+启动会精确返回 `started background job `。此生产方会向通用任务运行时提供增量进程输出、可选的 `[some output was dropped from memory; full output: ]`、沙箱事实,以及 `exit code: ` 或 `signal: ` 等终止详情。[`dsh-tool-jobs`](../../jobs/tool-jobs/README.zh.md) 负责模型可见的状态行、完成通知、列表和取消响应。
#### Token 影响
@@ -135,5 +135,5 @@ renderer 先输出依数据而定的 stdout 尾部,再输出可选的 `[stderr
## 已知限制与延期工作
- **回放退出状态 pill 从结果文本解析**:如果输出最后一行恰好精确为 `[exit code: N]` / `[killed by signal: …]`,会话回放将显示错误的 pill,并且该行会从卡片正文中丢失,因为解析会把它当作自己消耗的标记;这是仅影响展示的已知残留问题。
-- **`bash` 工具不采用 `timeout-policy` 预算**:根据[工具调用 timeout-policy Agent Note](../../../.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md),它保留由执行器持有的 `BASH_TIMEOUT` 路径。
+- **`bash` 工具不采用 `timeout-policy` 预算**:根据[工具调用 timeout-policy Agent Note](../../../.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md),它保留由执行器持有的 `BASH_TIMEOUT` 路径。
- **后台进程没有执行器超时**:工作不再需要时,调用方必须使用 `job_kill`,或依赖持有者/服务的 dispose。
diff --git a/packages/shell/tool-pwsh-persistent/README.i18n.yaml b/packages/shell/tool-pwsh-persistent/README.i18n.yaml
index cc1175ca0e..cad7c52a6d 100644
--- a/packages/shell/tool-pwsh-persistent/README.i18n.yaml
+++ b/packages/shell/tool-pwsh-persistent/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/shell/tool-pwsh-persistent/README.md
README.md: a57c940801606c2eef450f0e54fecb434c62a406
-README.zh.md: 4bd7ecdad08e504daa3ff6283f8b77ac9428385f
+README.zh.md: 75f6babfaffc79120f96441bdd5e08895b8e452f
diff --git a/packages/shell/tool-pwsh-persistent/README.zh.md b/packages/shell/tool-pwsh-persistent/README.zh.md
index 4bd7ecdad0..75f6babfaf 100644
--- a/packages/shell/tool-pwsh-persistent/README.zh.md
+++ b/packages/shell/tool-pwsh-persistent/README.zh.md
@@ -19,7 +19,7 @@
#### 模型看到什么
-生成的 [`pwsh` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-pwsh-persistent),含配置的 `description`。本插件不贡献独立的 system-prompt 段落;persona 与环境指引由部署方负责。
+生成的 [`pwsh` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-pwsh-persistent),含配置的 `description`。本插件不贡献独立的 system-prompt 段落;persona 与环境指引由部署方负责。
#### Token 影响
diff --git a/packages/shell/tool-pwsh/README.i18n.yaml b/packages/shell/tool-pwsh/README.i18n.yaml
index 589769709c..c5dcb85005 100644
--- a/packages/shell/tool-pwsh/README.i18n.yaml
+++ b/packages/shell/tool-pwsh/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/shell/tool-pwsh/README.md
README.md: e862fcf0ca85d0ecb0a5fe6cff3ee3c7a8153716
-README.zh.md: e03a980acfe05583721a1f084cbf53545c076126
+README.zh.md: 0a66b119ac2bf0386c0686e9e22ddfe1c3bb87b1
diff --git a/packages/shell/tool-pwsh/README.zh.md b/packages/shell/tool-pwsh/README.zh.md
index e03a980acf..0a66b119ac 100644
--- a/packages/shell/tool-pwsh/README.zh.md
+++ b/packages/shell/tool-pwsh/README.zh.md
@@ -66,7 +66,7 @@ Non-zero exits are reported as `[exit code: N]` markers; investigate failures be
#### 模型看到的内容
-模型看到生成的 [`pwsh` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-pwsh)。按 agent 作用域的工具限制可以移除该 agent 的定义。
+模型看到生成的 [`pwsh` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-pwsh)。按 agent 作用域的工具限制可以移除该 agent 的定义。
#### Token 影响
@@ -120,7 +120,7 @@ ack 是固定短行;任务输出按读取有界。
## 已知限制与暂缓事项
-- **Windows 沙箱下的语言模式与 named-pipe 捕获** — 在 [Windows ACL 沙箱](../../sandbox/sandbox-windows-acl/README.md) 下,read-only pwsh 会以 ConstrainedLanguage 启动,因为临时目录写入被拒绝,导致 PowerShell 的 AppLocker 探针失败并按 fail-closed 处理:`Add-Type`、非核心 .NET 静态调用(`[System.IO.*]::`、`[math]::`)、COM 对象与反射都会以“only core types”错误失败,且该模式无法从内部解除。workspace-write 的私有临时目录使探针得以完成,因此除非主机策略另有规定,否则它保持 FullLanguage。两种受限模式都拒绝 named-pipe 打开,因此受限命令内的管道 stdio spawn 以 EPERM 失败。工具描述把这两个约定教给模型;后端 README 负责完整的限制说明。
-- **无持久 shell** — 每次调用都启动全新的 `pwsh -Command`;持久 shell 对应物是 [`@deepseek-ai/dsh-tool-pwsh-persistent`](../tool-pwsh-persistent/README.md),它在 Windows(ConPTY)以及装有 pwsh 的 POSIX 主机上跨调用保持一个 owner 作用域的 pwsh 存活。
+- **Windows 沙箱下的语言模式与 named-pipe 捕获** — 在 [Windows ACL 沙箱](../../sandbox/sandbox-windows-acl/README.zh.md) 下,read-only pwsh 会以 ConstrainedLanguage 启动,因为临时目录写入被拒绝,导致 PowerShell 的 AppLocker 探针失败并按 fail-closed 处理:`Add-Type`、非核心 .NET 静态调用(`[System.IO.*]::`、`[math]::`)、COM 对象与反射都会以“only core types”错误失败,且该模式无法从内部解除。workspace-write 的私有临时目录使探针得以完成,因此除非主机策略另有规定,否则它保持 FullLanguage。两种受限模式都拒绝 named-pipe 打开,因此受限命令内的管道 stdio spawn 以 EPERM 失败。工具描述把这两个约定教给模型;后端 README 负责完整的限制说明。
+- **无持久 shell** — 每次调用都启动全新的 `pwsh -Command`;持久 shell 对应物是 [`@deepseek-ai/dsh-tool-pwsh-persistent`](../tool-pwsh-persistent/README.zh.md),它在 Windows(ConPTY)以及装有 pwsh 的 POSIX 主机上跨调用保持一个 owner 作用域的 pwsh 存活。
- **PowerShell 方言约定** — 模型必须写 PowerShell(原生路径、`$env:` 变量),而不是 bash;没有方言翻译。
- **会话 cwd 身份不做规范化** — workdir 基座直接取会话头 cwd 原值,不同于 bash 工具经 sandbox-root 规范化的身份。在隔离执行器下,策略的工作区根**会**被规范化(由共享的策略服务完成),因此当原始会话 cwd 与其规范化形态不同时,workdir 与隔离根可能不一致——这一 parity 差距留待共享 shell 工具基座提取时解决。
diff --git a/packages/skill/README.i18n.yaml b/packages/skill/README.i18n.yaml
index e935ba1c20..4e2383be6c 100644
--- a/packages/skill/README.i18n.yaml
+++ b/packages/skill/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/skill/README.md
README.md: a9fc04cbd4e81ddcf4b9cfebb4a4d8b9e8888227
-README.zh.md: 84bfa69e133962e27ecb792d6b989ca446eee787
+README.zh.md: 4b5f4e4ffdef0ea45797a5861840e13ec4405ab5
diff --git a/packages/skill/README.zh.md b/packages/skill/README.zh.md
index 84bfa69e13..4b5f4e4ffd 100644
--- a/packages/skill/README.zh.md
+++ b/packages/skill/README.zh.md
@@ -6,11 +6,11 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`skill/`](skill/README.md) | 定义 skill 提供方注册和查找 | `ctx.skills` |
-| [`skill-badge/`](skill-badge/README.md) | 贡献可选的内置 dsh 徽章 skill | 注册到 `ctx.skills` |
-| [`skill-filesystem/`](skill-filesystem/README.md) | 从本地文件系统发现 skill | 注册到 `ctx.skills` |
-| [`tool-skill/`](tool-skill/README.md) | 发布 skill 目录和面向模型的 loader | 注册到 `ctx.tools` |
+| [`skill/`](skill/README.zh.md) | 定义 skill 提供方注册和查找 | `ctx.skills` |
+| [`skill-badge/`](skill-badge/README.zh.md) | 贡献可选的内置 dsh 徽章 skill | 注册到 `ctx.skills` |
+| [`skill-filesystem/`](skill-filesystem/README.zh.md) | 从本地文件系统发现 skill | 注册到 `ctx.skills` |
+| [`tool-skill/`](tool-skill/README.zh.md) | 发布 skill 目录和面向模型的 loader | 注册到 `ctx.tools` |
该能力位于核心控制主干之外,可以使用本地、嵌入式或远程提供方,而无需更改面向模型的约定。
-子系统参考——发现优先级、目录快照、`skill` 加载器——见 [docs/subsystems/skills.md](../../docs/subsystems/skills.md)。
+子系统参考——发现优先级、目录快照、`skill` 加载器——见 [docs/subsystems/skills.md](../../docs/subsystems/skills.zh.md)。
diff --git a/packages/skill/skill-filesystem/README.i18n.yaml b/packages/skill/skill-filesystem/README.i18n.yaml
index ea3b9892b8..ea8f3ec05e 100644
--- a/packages/skill/skill-filesystem/README.i18n.yaml
+++ b/packages/skill/skill-filesystem/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/skill/skill-filesystem/README.md
README.md: 33fb550a9ca804c4f0f380232c2bfd94cc58a67e
-README.zh.md: 4c09111fc2d31bf5467b57f90f65cf68e298da09
+README.zh.md: c1d24f590c7fc525fbf15356f7a8bee9c4d1308b
diff --git a/packages/skill/skill-filesystem/README.zh.md b/packages/skill/skill-filesystem/README.zh.md
index 4c09111fc2..c1d24f590c 100644
--- a/packages/skill/skill-filesystem/README.zh.md
+++ b/packages/skill/skill-filesystem/README.zh.md
@@ -16,7 +16,7 @@
|---|---|---|
| `providerName` | `filesystem` | 在 `ctx.skills` 上注册该提供方时使用的唯一名称。 |
| `includeDefaultRoots` | `true` | 在 `customSkillDirs` 周围包含项目根和用户根;设为 false 时仅使用隔离的自定义根。 |
-| `dshHome` | `$DSH_HOME` 或 `~/.dsh` | 由 [`@deepseek-ai/dsh-home-paths`](../../util/home-paths/README.md) 解析的 DeepSeek Harness 配置根目录;扫描该目录下的 `skills`。 |
+| `dshHome` | `$DSH_HOME` 或 `~/.dsh` | 由 [`@deepseek-ai/dsh-home-paths`](../../util/home-paths/README.zh.md) 解析的 DeepSeek Harness 配置根目录;扫描该目录下的 `skills`。 |
| `agentsHome` | `$DSH_AGENTS_HOME` 或 `~/.agents` | 为兼容 skill 扫描的共享 agent(智能体)配置根目录。 |
| `customSkillDirs` | `[]` | 在项目根目录之后、用户根目录之前扫描的其他本地 skill 根目录。 |
| `watch` | `true` | 监视宿主本地根,并在目录成员或 frontmatter 可能发生变化时使本地提供方失效。 |
diff --git a/packages/skill/tool-skill/README.i18n.yaml b/packages/skill/tool-skill/README.i18n.yaml
index 918a10d01b..b1c6702524 100644
--- a/packages/skill/tool-skill/README.i18n.yaml
+++ b/packages/skill/tool-skill/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/skill/tool-skill/README.md
README.md: 4cfd8f22b3cfaf22b6f9f6cdc48f0db994858906
-README.zh.md: 9651e6d3209af77d3a2bbabb1bf0ec93647d3e90
+README.zh.md: 4b4fc6382a7f3e00592b6ebbf0b4887748cddf70
diff --git a/packages/skill/tool-skill/README.zh.md b/packages/skill/tool-skill/README.zh.md
index 9651e6d320..4b4fc6382a 100644
--- a/packages/skill/tool-skill/README.zh.md
+++ b/packages/skill/tool-skill/README.zh.md
@@ -14,7 +14,7 @@
如果最初没有模型可调用 skill,则省略目录;如果该 agent(智能体)的工具视图排除了随附的 `skill` 工具,或解析出同名的作用域内遮蔽项,也会省略目录。身份比对针对本插件所注册的那个定义,而非按自身名字回查,因此本插件既可全局挂载,也可挂在单个 agent 的组装内——在后者中 `register()` 只注册到该 agent 的层中。可见性变更参与 digest 计算,使提示词指引、模型可见 schema 和可执行分派保持对齐。
-`catalogDescriptionMaxLength` 控制规范化后的目录描述,渲染时会对其执行 XML 转义。其默认值是 `500`,且必须是不小于 `3` 的整数,以便为截断省略号保留空间。[skill 目录热刷新 Agent Note](../../../.agents/notes/implemented/feature/2026-07-27-skill-catalog-hot-refresh.md) 负责定义持久初始目录和替换目录的生命周期。
+`catalogDescriptionMaxLength` 控制规范化后的目录描述,渲染时会对其执行 XML 转义。其默认值是 `500`,且必须是不小于 `3` 的整数,以便为截断省略号保留空间。[skill 目录热刷新 Agent Note](../../../.agents/notes/implemented/feature/2026-07-27-skill-catalog-hot-refresh.zh.md) 负责定义持久初始目录和替换目录的生命周期。
## 工具:`skill`
@@ -65,7 +65,7 @@ A user may also invoke a skill directly; its block then appears
#### 模型看到的内容
-模型会看到生成的 [`skill` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-skill)。
+模型会看到生成的 [`skill` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-skill)。
#### Token 影响
diff --git a/packages/spill/README.i18n.yaml b/packages/spill/README.i18n.yaml
index 1d1ace946c..4dfc9aac96 100644
--- a/packages/spill/README.i18n.yaml
+++ b/packages/spill/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/spill/README.md
README.md: 74fd4837cb23767754174df168f2f4ba71c2b938
-README.zh.md: 2417bb793dca1cb10ead6208cb6ad896c18cdf03
+README.zh.md: 9a1f3495220142f7a34f806548c5ca5cbfab8734
diff --git a/packages/spill/README.zh.md b/packages/spill/README.zh.md
index 2417bb793d..9a1f349522 100644
--- a/packages/spill/README.zh.md
+++ b/packages/spill/README.zh.md
@@ -6,10 +6,10 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`spill/`](spill/README.md) | 定义 spill 存储 | `ctx.spillStore` |
-| [`spill-local/`](spill-local/README.md) | 在会话范围的本地文件中存储 spill 文本 | 注册到 `ctx.spillStore` |
-| [`spill-policy/`](spill-policy/README.md) | 应用执行后 spill 策略 | 监听 `ctx.tools` |
+| [`spill/`](spill/README.zh.md) | 定义 spill 存储 | `ctx.spillStore` |
+| [`spill-local/`](spill-local/README.zh.md) | 在会话范围的本地文件中存储 spill 文本 | 注册到 `ctx.spillStore` |
+| [`spill-policy/`](spill-policy/README.zh.md) | 应用执行后 spill 策略 | 监听 `ctx.tools` |
-参见[工具输出 spill 决策](../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md),其中记录了存储、保留和工具自有输出处理之间的边界。
+参见[工具输出 spill 决策](../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md),其中记录了存储、保留和工具自有输出处理之间的边界。
-子系统参考——`SaveTextSpill`、所有者/来源、品牌化定位符——见 [docs/subsystems/spill.md](../../docs/subsystems/spill.md);依据见[工具输出 spill Agent Note](../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md)。
+子系统参考——`SaveTextSpill`、所有者/来源、品牌化定位符——见 [docs/subsystems/spill.md](../../docs/subsystems/spill.zh.md);依据见[工具输出 spill Agent Note](../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md)。
diff --git a/packages/spill/spill-local/README.i18n.yaml b/packages/spill/spill-local/README.i18n.yaml
index 0a0ab3823d..37659c3aac 100644
--- a/packages/spill/spill-local/README.i18n.yaml
+++ b/packages/spill/spill-local/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/spill/spill-local/README.md
README.md: 2270a65d9270e1549a9e48d6a36b821e48c29070
-README.zh.md: 907a2b5c98f3ee07e0b896b8827e6a00fa1827df
+README.zh.md: b3e4999d8f2d982ef01637199299f79d06e12c4b
diff --git a/packages/spill/spill-local/README.zh.md b/packages/spill/spill-local/README.zh.md
index 907a2b5c98..b3e4999d8f 100644
--- a/packages/spill/spill-local/README.zh.md
+++ b/packages/spill/spill-local/README.zh.md
@@ -18,7 +18,7 @@
|---|---|---|
| `root` | 私有 0700 临时目录 | spill 文件的根目录。设置后可将这些文件保存在已知位置。 |
-`saveText` 在发生真实存储故障(权限、ENOSPC)时返回拒绝;spill 策略会按尽力而为原则处理该拒绝,并保留内联结果。词汇见 seam README,设计见[工具输出 spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md)。
+`saveText` 在发生真实存储故障(权限、ENOSPC)时返回拒绝;spill 策略会按尽力而为原则处理该拒绝,并保留内联结果。词汇见 seam README,设计见[工具输出 spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md)。
## 模型体验
diff --git a/packages/spill/spill-policy/README.i18n.yaml b/packages/spill/spill-policy/README.i18n.yaml
index f98bf76167..e6fe69d9e3 100644
--- a/packages/spill/spill-policy/README.i18n.yaml
+++ b/packages/spill/spill-policy/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/spill/spill-policy/README.md
README.md: 715d10bee6d32b0753e0f6afcc18d83fe0261a60
-README.zh.md: 89635717e82f078055f6594b2aa8a49715435060
+README.zh.md: 11fe4aada6a5138dd1f7bb537cd8ad1463d72b05
diff --git a/packages/spill/spill-policy/README.zh.md b/packages/spill/spill-policy/README.zh.md
index 89635717e8..11fe4aada6 100644
--- a/packages/spill/spill-policy/README.zh.md
+++ b/packages/spill/spill-policy/README.zh.md
@@ -30,11 +30,11 @@
**尽力而为**:没有会话所有者、没有 `ctx.spillStore` 后端,或 `saveText` 返回拒绝 ⇒ 策略记录警告并返回原始结果。spill 失败绝不会将成功调用变为 `isError`,也不会隐藏内联结果。成功替换时只会更改 `content`;规范的程序化值保持不变。
-**dispatch-log 分支:**注册在 `tools/code-dispatch-log` 上的第二个监听器,把同一套上限、替换流水线与尽力而为的回退应用到每个 `run_code` 子调用结果的持久化副本上(产物标签为 `dispatch`,按子调用 id 归档)。程序的值不受影响,因为它早已完整跨过 worker 边界;`read` 子调用同样设界:日志副本不是模型上下文,因此不会发生 read-again 循环,而 `read` 恰恰是最容易产生巨型日志的工具([原理](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md))。
+**dispatch-log 分支:**注册在 `tools/code-dispatch-log` 上的第二个监听器,把同一套上限、替换流水线与尽力而为的回退应用到每个 `run_code` 子调用结果的持久化副本上(产物标签为 `dispatch`,按子调用 id 归档)。程序的值不受影响,因为它早已完整跨过 worker 边界;`read` 子调用同样设界:日志副本不是模型上下文,因此不会发生 read-again 循环,而 `read` 恰恰是最容易产生巨型日志的工具([原理](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.zh.md))。
## 范围
-该策略只能看到最终格式化的呈现结果,看不到工具的内部资源或规范值。如果提供方已经截断内容(例如 `web-fetch-http.maxBodyChars`),spill 产物保存的是工具返回的完整格式化结果,而非完整原始源。提供方/资源上限仍然是必需的,并且与该策略相互独立。`glob`/`grep` 负责对项级呈现结果执行 spill,因为渲染前仍然存在完整的已获取值;bash 流负责在获取时 spill。通用策略预先注册自己的 waterfall(瀑布式事件)监听器,然后再委托,因此无论插件加载顺序如何,普通工具自身的异步投影都会在通用字节限制之前完成。详见[工具输出 spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md)。
+该策略只能看到最终格式化的呈现结果,看不到工具的内部资源或规范值。如果提供方已经截断内容(例如 `web-fetch-http.maxBodyChars`),spill 产物保存的是工具返回的完整格式化结果,而非完整原始源。提供方/资源上限仍然是必需的,并且与该策略相互独立。`glob`/`grep` 负责对项级呈现结果执行 spill,因为渲染前仍然存在完整的已获取值;bash 流负责在获取时 spill。通用策略预先注册自己的 waterfall(瀑布式事件)监听器,然后再委托,因此无论插件加载顺序如何,普通工具自身的异步投影都会在通用字节限制之前完成。详见[工具输出 spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md)。
## 模型体验
diff --git a/packages/spill/spill/README.i18n.yaml b/packages/spill/spill/README.i18n.yaml
index 0a84c591d5..695457a9af 100644
--- a/packages/spill/spill/README.i18n.yaml
+++ b/packages/spill/spill/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/spill/spill/README.md
README.md: f36dccfa1e6f1f21e80c0b0aa97ee150a0f5fd45
-README.zh.md: 268e390923e30150a8d36b719b7dad75827f99fe
+README.zh.md: 64f0a99bd1cdb10af2bbe0007de802d8ce658b7a
diff --git a/packages/spill/spill/README.zh.md b/packages/spill/spill/README.zh.md
index 268e390923..64f0a99bd1 100644
--- a/packages/spill/spill/README.zh.md
+++ b/packages/spill/spill/README.zh.md
@@ -26,7 +26,7 @@
`SaveTextSpill`(owner、source、suggestedName、content)是请求;`SpillRef`(locator、bytes、retrievalHint)是结果。`SpillLocator` 是[带品牌类型](../../util/brand)的值,并以不透明字符串的形式呈现给模型;对 `dsh-spill-local` 而言它是本地路径,但未来的后端可以返回 URI、键或命令 token,无需修改策略/工具消费方。`SpillOwner.sessionId` 是保存时存储命名空间:fork 后的会话会从种子日志继承现有定位信息,无需复制或更改其归属;fork 后新产生的 spill 使用子会话 id。`SpillSource` 记录产生该 spill 的 `toolName`、`callId` 和 `label`,供后端命名和检查使用,不用于访问控制。完整约定见 `src/types.ts`。
-设计原理见[工具输出 spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md),其中说明了为什么创建操作应由运行时 spill seam 而非面向模型的 `write` 工具承担。
+设计原理见[工具输出 spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md),其中说明了为什么创建操作应由运行时 spill seam 而非面向模型的 `write` 工具承担。
## 模型体验
diff --git a/packages/storage/README.i18n.yaml b/packages/storage/README.i18n.yaml
index 2d6db691b7..d0c1293124 100644
--- a/packages/storage/README.i18n.yaml
+++ b/packages/storage/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/storage/README.md
README.md: dd9eb5ac97529c75df5986c56b714331dca0b6f1
-README.zh.md: a69414826b3ffd613c432836ea506b664ceb8da5
+README.zh.md: 615212e46b358c55792e2cbc550797c7b0c8ca91
diff --git a/packages/storage/README.zh.md b/packages/storage/README.zh.md
index a69414826b..615212e46b 100644
--- a/packages/storage/README.zh.md
+++ b/packages/storage/README.zh.md
@@ -6,11 +6,11 @@
| 包 | 职责 | ctx key |
|---|---|---|
-| [`storage/`](storage/README.md) | 将已注册后端与类型化数据形式连接起来 | `ctx.storage` |
-| [`storage-json/`](storage-json/README.md) | 在 JSON 文件中存储数据 | 注册后端 `json` |
-| [`storage-sqlite/`](storage-sqlite/README.md) | 在 SQLite 中存储数据 | 注册后端 `sqlite` |
-| [`storage-domain/`](storage-domain/README.md) | 提供经过验证的领域记录存储 | `ctx.storageDomain` |
+| [`storage/`](storage/README.zh.md) | 将已注册后端与类型化数据形式连接起来 | `ctx.storage` |
+| [`storage-json/`](storage-json/README.zh.md) | 在 JSON 文件中存储数据 | 注册后端 `json` |
+| [`storage-sqlite/`](storage-sqlite/README.zh.md) | 在 SQLite 中存储数据 | 注册后端 `sqlite` |
+| [`storage-domain/`](storage-domain/README.zh.md) | 提供经过验证的领域记录存储 | `ctx.storageDomain` |
-消费方使用数据形式,而不是直接访问后端。[领域存储决策](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md)记录了该家族的设计。
+消费方使用数据形式,而不是直接访问后端。[领域存储决策](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md)记录了该家族的设计。
-子系统参考——后端约定、`StorageForms`、`DomainSpec`/`Domain`、`domain/changed`——见 [docs/subsystems/storage.md](../../docs/subsystems/storage.md)。
+子系统参考——后端约定、`StorageForms`、`DomainSpec`/`Domain`、`domain/changed`——见 [docs/subsystems/storage.md](../../docs/subsystems/storage.zh.md)。
diff --git a/packages/storage/storage-domain/README.i18n.yaml b/packages/storage/storage-domain/README.i18n.yaml
index 80ccac9a5f..9c4c1c8b4c 100644
--- a/packages/storage/storage-domain/README.i18n.yaml
+++ b/packages/storage/storage-domain/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/storage/storage-domain/README.md
-README.md: 7c058ab557cfb128f54c15e715685d59f6720696
+README.md: 59f5590867d35169ad7636075da524ebbf162489
README.zh.md: 10eb33711e147bc5ac6b7770a196bfea4fe66306
diff --git a/packages/storage/storage-domain/README.md b/packages/storage/storage-domain/README.md
index 7c058ab557..59f5590867 100644
--- a/packages/storage/storage-domain/README.md
+++ b/packages/storage/storage-domain/README.md
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
Domain data form for the DeepSeek Harness storage hub: exposes the injectable `ctx.storageDomain` service and the matching `ctx.storage.domain` projection after every configured backend is registered. A domain is declared once with `defineDomain` (zod record schemas, `z.infer`-derived types), opened through `DomainFacility.open`, and served from authoritative in-memory state — reads are synchronous, writes serialize on one per-domain chain, reach durability on the routed backend first, then update memory and emit `domain/changed`. The opening consumer owns the handle's lifecycle and releases it with `Domain.close()` (idempotent; typically its own `ctx.effect` disposer); domains still open when the plugin unmounts are closed by the facility.
-Design rationale, open semantics, and the storage/domain layer split live in the [Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md).
+Design rationale, open semantics, and the storage/domain layer split live in the [Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md).
## Configuration
diff --git a/packages/storage/storage-json/README.i18n.yaml b/packages/storage/storage-json/README.i18n.yaml
index 0ca8f2d918..109bafd8f5 100644
--- a/packages/storage/storage-json/README.i18n.yaml
+++ b/packages/storage/storage-json/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/storage/storage-json/README.md
-README.md: c3417846a70f8a227a19a5abf834156d530e3597
-README.zh.md: e66c711dca838620eb9c90a3edacd2639970505e
+README.md: 3a079da3f516af4a3f7bc13dcf0115589fa42ff3
+README.zh.md: f9b2ffb7444893a27a2da1d3dcd324c73452cc92
diff --git a/packages/storage/storage-json/README.md b/packages/storage/storage-json/README.md
index c3417846a7..3a079da3f5 100644
--- a/packages/storage/storage-json/README.md
+++ b/packages/storage/storage-json/README.md
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
-JSON backend for the [storage hub](../storage/README.md): one human-readable `.json` file per unit under a configured root, registered as backend `json`. Design: [domain KV storage Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md).
+JSON backend for the [storage hub](../storage/README.md): one human-readable `.json` file per unit under a configured root, registered as backend `json`. Design: [domain KV storage Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md).
## Model
diff --git a/packages/storage/storage-json/README.zh.md b/packages/storage/storage-json/README.zh.md
index e66c711dca..f9b2ffb744 100644
--- a/packages/storage/storage-json/README.zh.md
+++ b/packages/storage/storage-json/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-[存储中心](../storage/README.md)的 JSON 后端:配置根目录下每个单元使用一个人类可读的 `.json` 文件,注册为后端 `json`。设计见[领域 KV 存储 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md)。
+[存储中心](../storage/README.zh.md)的 JSON 后端:配置根目录下每个单元使用一个人类可读的 `.json` 文件,注册为后端 `json`。设计见[领域 KV 存储 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md)。
## 模型
diff --git a/packages/storage/storage-sqlite/README.i18n.yaml b/packages/storage/storage-sqlite/README.i18n.yaml
index 2215e835e2..b1b8b1def0 100644
--- a/packages/storage/storage-sqlite/README.i18n.yaml
+++ b/packages/storage/storage-sqlite/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/storage/storage-sqlite/README.md
-README.md: 48aae14aa8613886d69652c39194878c190259cf
-README.zh.md: d25591ac67a96bce2ea74b2686e2fbb8375af959
+README.md: 654ae004986240ae6c7c8f74f80e45089270a0ab
+README.zh.md: 9f31c55a3281a02ece793d2810480de4e9d8f339
diff --git a/packages/storage/storage-sqlite/README.md b/packages/storage/storage-sqlite/README.md
index 48aae14aa8..654ae00498 100644
--- a/packages/storage/storage-sqlite/README.md
+++ b/packages/storage/storage-sqlite/README.md
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
-SQLite backend for the [storage hub](../storage/README.md): registers as backend `sqlite`, serving the `kv` facet over one `node:sqlite` database file (or `:memory:`). Design and trade-offs: [domain KV storage Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md).
+SQLite backend for the [storage hub](../storage/README.md): registers as backend `sqlite`, serving the `kv` facet over one `node:sqlite` database file (or `:memory:`). Design and trade-offs: [domain KV storage Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md).
## Storage model
diff --git a/packages/storage/storage-sqlite/README.zh.md b/packages/storage/storage-sqlite/README.zh.md
index d25591ac67..9f31c55a32 100644
--- a/packages/storage/storage-sqlite/README.zh.md
+++ b/packages/storage/storage-sqlite/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-[存储中心](../storage/README.md)的 SQLite 后端:注册为后端 `sqlite`,通过一个数据库提供 `kv` facet;该数据库由 `node:sqlite` 操作,可以是单个文件,也可以是 `:memory:`。设计与取舍见[领域 KV 存储 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md)。
+[存储中心](../storage/README.zh.md)的 SQLite 后端:注册为后端 `sqlite`,通过一个数据库提供 `kv` facet;该数据库由 `node:sqlite` 操作,可以是单个文件,也可以是 `:memory:`。设计与取舍见[领域 KV 存储 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md)。
## 存储模型
diff --git a/packages/storage/storage/README.i18n.yaml b/packages/storage/storage/README.i18n.yaml
index 09fa051bac..74b71a0f88 100644
--- a/packages/storage/storage/README.i18n.yaml
+++ b/packages/storage/storage/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/storage/storage/README.md
README.md: bf827220afe0d8b1cbc53b3a12e6d0034004e1d2
-README.zh.md: 86b6994d74a727275e0bcaea2a83bd51f93b6ca1
+README.zh.md: fb52c96e38c17b76ef0e6b70a79177911a088fb7
diff --git a/packages/storage/storage/README.zh.md b/packages/storage/storage/README.zh.md
index 86b6994d74..fb52c96e38 100644
--- a/packages/storage/storage/README.zh.md
+++ b/packages/storage/storage/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-非会话数据的存储中心(`ctx.storage`):具名后端注册表加已挂载的数据形式设施。中心自身不执行 IO:后端拥有介质,数据形式拥有语义。[存储家族概述](../README.md)列出了这些包;[领域 KV 存储 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md)记录了设计理由。
+非会话数据的存储中心(`ctx.storage`):具名后端注册表加已挂载的数据形式设施。中心自身不执行 IO:后端拥有介质,数据形式拥有语义。[存储家族概述](../README.zh.md)列出了这些包;[领域 KV 存储 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md)记录了设计理由。
## 结构
diff --git a/packages/subagent/README.i18n.yaml b/packages/subagent/README.i18n.yaml
index 6b69f7ba0c..fdff53c59c 100644
--- a/packages/subagent/README.i18n.yaml
+++ b/packages/subagent/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/README.md
README.md: 1b3ccb6afba226e0b6bcf9aacd0c9fa752fdfdbb
-README.zh.md: a98874baf8d235758dbe370272ecf230cbfb112c
+README.zh.md: 245b187b71b06f0c291966fb30a65941ed18aefb
diff --git a/packages/subagent/README.zh.md b/packages/subagent/README.zh.md
index a98874baf8..245b187b71 100644
--- a/packages/subagent/README.zh.md
+++ b/packages/subagent/README.zh.md
@@ -6,20 +6,20 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`subagent/`](subagent/README.md) | 定义提供方注册、委派和继续执行 | `ctx.subagents` |
-| [`subagent-inprocess/`](subagent-in-process-driver/README.md) | 提供共享的进程内运行驱动器 | 无 |
-| [`subagent-spawn-in-process/`](subagent-spawn-in-process/README.md) | 启动全新的进程内子 agent | 注册到 `ctx.subagents` |
-| [`subagent-fork-in-process/`](subagent-fork-in-process/README.md) | 从父 agent 已完成的历史记录启动进程内子 agent | 注册到 `ctx.subagents` |
-| [`subagent-acp/`](subagent-acp/README.md) | 通过 ACP(Agent Client Protocol)启动进程外子 agent | 注册到 `ctx.subagents` |
-| [`subagent-codex/`](subagent-codex/README.md) | 启动真实的 Codex app-server 子 agent | 注册到 `ctx.subagents` |
-| [`subagent-claude-code/`](subagent-claude-code/README.md) | 通过官方 Claude Agent SDK 启动真实的 Claude Code 子 agent | 注册到 `ctx.subagents` |
-| [`subagent-dsh-sdk/`](subagent-dsh-sdk/README.md) | 通过 TypeScript SDK 启动进程外 Harness 子 agent | 注册到 `ctx.subagents` |
-| [`tool-subagent/`](tool-subagent/README.md) | 向模型公开委派操作 | 注册到 `ctx.tools` |
-| [`tool-subagent-control/`](tool-subagent-control/README.md) | 向模型公开子级消息发送和列举操作 | 注册到 `ctx.tools` |
-| [`tool-subagent-report/`](tool-subagent-report/README.md) | 提供从子级到父级的报告通道 | 注册到子级作用域 |
+| [`subagent/`](subagent/README.zh.md) | 定义提供方注册、委派和继续执行 | `ctx.subagents` |
+| [`subagent-inprocess/`](subagent-in-process-driver/README.zh.md) | 提供共享的进程内运行驱动器 | 无 |
+| [`subagent-spawn-in-process/`](subagent-spawn-in-process/README.zh.md) | 启动全新的进程内子 agent | 注册到 `ctx.subagents` |
+| [`subagent-fork-in-process/`](subagent-fork-in-process/README.zh.md) | 从父 agent 已完成的历史记录启动进程内子 agent | 注册到 `ctx.subagents` |
+| [`subagent-acp/`](subagent-acp/README.zh.md) | 通过 ACP(Agent Client Protocol)启动进程外子 agent | 注册到 `ctx.subagents` |
+| [`subagent-codex/`](subagent-codex/README.zh.md) | 启动真实的 Codex app-server 子 agent | 注册到 `ctx.subagents` |
+| [`subagent-claude-code/`](subagent-claude-code/README.zh.md) | 通过官方 Claude Agent SDK 启动真实的 Claude Code 子 agent | 注册到 `ctx.subagents` |
+| [`subagent-dsh-sdk/`](subagent-dsh-sdk/README.zh.md) | 通过 TypeScript SDK 启动进程外 Harness 子 agent | 注册到 `ctx.subagents` |
+| [`tool-subagent/`](tool-subagent/README.zh.md) | 向模型公开委派操作 | 注册到 `ctx.tools` |
+| [`tool-subagent-control/`](tool-subagent-control/README.zh.md) | 向模型公开子级消息发送和列举操作 | 注册到 `ctx.tools` |
+| [`tool-subagent-report/`](tool-subagent-report/README.zh.md) | 提供从子级到父级的报告通道 | 注册到子级作用域 |
Codex 与 Claude Code 包是彼此独立的可选 Profile Bundle。使用 `dsh plugin --profile add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code` 安装其中一个或两个包,再重启该 Profile;每个包只注册自己的休眠 Host provider。要授予工具,请复制一份完整 Agent Preset,删除各对应工具行的 `disabled`,再启动新 Session。移除其中一个包后,下一次 Profile 启动只会撤回对应 provider 及其私有运行时闭包。
-参见有关[能力家族](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可继续执行的子级](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)和[控制工具](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)的决策。
+参见有关[能力家族](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md)、[可继续执行的子级](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md)和[控制工具](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.zh.md)的决策。
-子系统参考——启动请求、结果、实时运行、提供方约定、可续跑后台子 agent——见 [docs/subsystems/subagent.md](../../docs/subsystems/subagent.md);设计依据见 [subagent 能力 seam](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可续跑后台 subagent](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)与[合并 subagent 控制服务](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md) Agent Note。
+子系统参考——启动请求、结果、实时运行、提供方约定、可续跑后台子 agent——见 [docs/subsystems/subagent.md](../../docs/subsystems/subagent.zh.md);设计依据见 [subagent 能力 seam](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md)、[可续跑后台 subagent](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md)与[合并 subagent 控制服务](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.zh.md) Agent Note。
diff --git a/packages/subagent/subagent-acp/README.i18n.yaml b/packages/subagent/subagent-acp/README.i18n.yaml
index 61b76620ae..4aaf1b7552 100644
--- a/packages/subagent/subagent-acp/README.i18n.yaml
+++ b/packages/subagent/subagent-acp/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-acp/README.md
README.md: 3bccddbca021bed1f8bf5766b9575f3bd7441669
-README.zh.md: 38c882f240dcdd02213acecf266f5a2424b37cce
+README.zh.md: 7ae89ece0ce4282ad5b9a20142a2ba9b111f6d88
diff --git a/packages/subagent/subagent-acp/README.zh.md b/packages/subagent/subagent-acp/README.zh.md
index 38c882f240..7ae89ece0c 100644
--- a/packages/subagent/subagent-acp/README.zh.md
+++ b/packages/subagent/subagent-acp/README.zh.md
@@ -30,8 +30,8 @@ ACP 不声明任何启动时能力,因为当前进程无法强制执行远程
| `cwd` | 父会话 cwd | 子进程及其 ACP 会话的工作目录覆盖值;不得为空。相对值会在加载时以 harness 启动目录为基准解析,结果必须指向 harness 可以进入的目录。 |
| `permission` | `reject` | 自动回答权限请求:拒绝,或选择第一个 `allow_once` 或 `allow_always` 选项。 |
| `env` | `{}` | 显式子进程环境,叠加到已清理凭据的父进程环境之上。 |
-| `disposeEofGraceMs` | `6000` | stdin EOF 之后、平台终止之前的宽限时间须为正值,且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md)。 |
-| `disposeGraceMs` | `3000` | POSIX 在 SIGTERM 后、SIGKILL 前的宽限时间(Windows 直接强制终止),须为正值且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md)。 |
+| `disposeEofGraceMs` | `6000` | stdin EOF 之后、平台终止之前的宽限时间须为正值,且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.zh.md)。 |
+| `disposeGraceMs` | `3000` | POSIX 在 SIGTERM 后、SIGKILL 前的宽限时间(Windows 直接强制终止),须为正值且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.zh.md)。 |
```yaml
- id: subagent-acp
@@ -57,9 +57,9 @@ ACP 不声明任何启动时能力,因为当前进程无法强制执行远程
## 进程边界
-子进程经由 [`dsh-subprocess`](../../subprocess/subprocess/README.md) seam spawn:共享的凭据清除先移除疑似凭据的环境变量和环境中已有的 `DSH_*` 名称,显式 `config.env` 值在清除之后合并(有意转发的 `DEEPSEEK_API_KEY` 会保留下来,`DSH_PERMISSION_MODE` 这类 `DSH_*` 部署事实也以同样的方式到达子进程——清除只丢弃其陈旧的同名环境值),stderr 会继承到父进程自身的流,dispose 则先应用本插件的 EOF 时间窗,再由子进程责任方执行 SIGTERM→SIGKILL 升级并等待整棵进程树退出。ACP 协议格式(wire format)是真正的序列化边界;同进程 subagent 值不会为防御目的而克隆。
+子进程经由 [`dsh-subprocess`](../../subprocess/subprocess/README.zh.md) seam spawn:共享的凭据清除先移除疑似凭据的环境变量和环境中已有的 `DSH_*` 名称,显式 `config.env` 值在清除之后合并(有意转发的 `DEEPSEEK_API_KEY` 会保留下来,`DSH_PERMISSION_MODE` 这类 `DSH_*` 部署事实也以同样的方式到达子进程——清除只丢弃其陈旧的同名环境值),stderr 会继承到父进程自身的流,dispose 则先应用本插件的 EOF 时间窗,再由子进程责任方执行 SIGTERM→SIGKILL 升级并等待整棵进程树退出。ACP 协议格式(wire format)是真正的序列化边界;同进程 subagent 值不会为防御目的而克隆。
-本包没有默认导出。否则 Cordis loader 的解包会隐藏具名 `inject` 元数据;见[事故复盘(postmortem)0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md)。
+本包没有默认导出。否则 Cordis loader 的解包会隐藏具名 `inject` 元数据;见[事故复盘(postmortem)0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.zh.md)。
## 模型体验
@@ -93,7 +93,7 @@ ACP 不声明任何启动时能力,因为当前进程无法强制执行远程
## 已知限制与暂缓事项
-- **每次运行使用全新进程**:持久进程池属于后续优化(见 [seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md))。
+- **每次运行使用全新进程**:持久进程池属于后续优化(见 [seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md))。
- **仅支持本地工作区**:解析后的 cwd 是交给同一台机器上子进程的本地路径;远程 ACP agent 的工作区映射需要独立的后端能力,此处尚未设计这种能力。
- **不支持可选启动时能力**:该提供方无法在远程进程内应用本地 harness 的 `outputSchema`、深度上限、工具过滤器或 persona,因此不会声明这些能力;服务会拒绝需要它们的请求。
- **只收集已提交的 `agent_message_chunk` 文本**:自动化服务器把推理(reasoning)、工具活动、计划和其他 trace 数据保留在子 agent 会话日志中,不通过 ACP 发出。
diff --git a/packages/subagent/subagent-claude-code/README.i18n.yaml b/packages/subagent/subagent-claude-code/README.i18n.yaml
index fbe903740e..ff129c60e2 100644
--- a/packages/subagent/subagent-claude-code/README.i18n.yaml
+++ b/packages/subagent/subagent-claude-code/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-claude-code/README.md
README.md: 0260d9c82dee82541e5b60ff7fe2c323cf9b7331
-README.zh.md: 0a301ff53589112afe3f7f3d42d334fbebd04083
+README.zh.md: 484241f1ca23f2b8ee843b6f412d0f779832d400
diff --git a/packages/subagent/subagent-claude-code/README.zh.md b/packages/subagent/subagent-claude-code/README.zh.md
index 0a301ff535..484241f1ca 100644
--- a/packages/subagent/subagent-claude-code/README.zh.md
+++ b/packages/subagent/subagent-claude-code/README.zh.md
@@ -2,11 +2,11 @@
[English](README.md) | 中文
-本包(package)注册由 Profile 命名、默认名称为 `claude-code` 的 Claude Code subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中调用官方 Claude Agent SDK,让锁定版本的 SDK 选择随包安装的平台 CLI,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定返回严格的最终答案或独立的安全失败诊断。
+本包(package)注册由 Profile 命名、默认名称为 `claude-code` 的 Claude Code subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中调用官方 Claude Agent SDK,让锁定版本的 SDK 选择随包安装的平台 CLI,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.zh.md) 结果约定返回严格的最终答案或独立的安全失败诊断。
## 启动与所有权
-`start(request)` 只接受非空的文本块序列,并根据父会话确定子级 cwd。它会创建一个私有 `AbortController`,调用官方 SDK 的 `query()`,并仅在 SDK 的 `spawnClaudeCodeProcess` 钩子已经提供由 [`dsh-subprocess`](../../subprocess/subprocess/README.md) 管理的活动 CLI 句柄后发布此次运行。若在发布前发生失败或取消,它会关闭 query、终止所有已取得的进程树并等待其退出,然后拒绝 `start()` 调用。
+`start(request)` 只接受非空的文本块序列,并根据父会话确定子级 cwd。它会创建一个私有 `AbortController`,调用官方 SDK 的 `query()`,并仅在 SDK 的 `spawnClaudeCodeProcess` 钩子已经提供由 [`dsh-subprocess`](../../subprocess/subprocess/README.zh.md) 管理的活动 CLI 句柄后发布此次运行。若在发布前发生失败或取消,它会关闭 query、终止所有已取得的进程树并等待其退出,然后拒绝 `start()` 调用。
SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK 消息流,而且只接受满足以下条件的 `result` 消息:其 `subtype: "success"`、`is_error: false` 且 `result` 非空白,之后迭代器还须正常结束。所有失败仍映射为 `error`:Agent SDK 0.3.220 的四种错误子类型保留准确类别;标记为错误或内容空白的成功消息成为 `invalid-success`;缺失结果成为 `missing-result`;未分类的 query 失败成为 `unknown`;CLI 提前退出成为 `process-exit`。诊断还会注明当前 `query-start`、`query-run`、`process` 或 `teardown` 阶段,并分别保留已观测到的退出码与信号。该提供方不会产生 `max-tokens` 或 `refusal`。
@@ -29,7 +29,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
| `providerName` | `claude-code` | `ctx.subagents` 中的非空注册名称;每个已挂载实例都需要唯一值。 |
| `env` | `{}` | 显式指定的 SDK/CLI 环境,叠加在由共享机制清除凭证后的父环境之上。 |
| `permissionMode` | `dontAsk` | 为该提供方实例的每次运行固定原生非交互权限策略。 |
-| `disposeGraceMs` | `3000` | 共享进程树责任方各终止层级之间的宽限期,单位为毫秒且须为正有限值,并不得大于仓库共享的 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md);随后资源释放会等待整棵进程树退出。 |
+| `disposeGraceMs` | `3000` | 共享进程树责任方各终止层级之间的宽限期,单位为毫秒且须为正有限值,并不得大于仓库共享的 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.zh.md);随后资源释放会等待整棵进程树退出。 |
| `permissionMode` 值 | 原生行为 |
|---|---|
diff --git a/packages/subagent/subagent-codex/README.i18n.yaml b/packages/subagent/subagent-codex/README.i18n.yaml
index da32c1e25a..0f8ed31eab 100644
--- a/packages/subagent/subagent-codex/README.i18n.yaml
+++ b/packages/subagent/subagent-codex/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-codex/README.md
README.md: 975f353b9f1bc6fab61a4c0eb40ebaf50c436623
-README.zh.md: c51f35d9e419e621b5a2cc3101de6bd5e56aa145
+README.zh.md: 2ea256afb3bb8fdfe57fd555b6db10522a709a56
diff --git a/packages/subagent/subagent-codex/README.zh.md b/packages/subagent/subagent-codex/README.zh.md
index c51f35d9e4..2ea256afb3 100644
--- a/packages/subagent/subagent-codex/README.zh.md
+++ b/packages/subagent/subagent-codex/README.zh.md
@@ -2,11 +2,11 @@
[English](README.md) | 中文
-本包注册由 Profile 命名、默认名称为 `codex` 的 Codex subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中使用 `app-server --stdio` 启动官方包内 Codex wrapper,创建一个临时 Codex 线程,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定返回选定的最终答案或独立的安全失败诊断。
+本包注册由 Profile 命名、默认名称为 `codex` 的 Codex subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中使用 `app-server --stdio` 启动官方包内 Codex wrapper,创建一个临时 Codex 线程,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.zh.md) 结果约定返回选定的最终答案或独立的安全失败诊断。
## 启动与所有权
-`start(request)` 只接受非空的文本块序列,并根据父会话确定子级 cwd。随后,它通过 [`dsh-subprocess`](../../subprocess/subprocess/README.md) spawn 固定命令,依次执行 `initialize` → `initialized`,把 Profile 选择的模式映射为官方 `thread/start` approval/reviewer/sandbox 字段并与 `{ cwd, ephemeral: true }` 一起发送,且仅在 Codex 返回有效的临时线程后才发布此次运行。若在发布前发生失败或取消,它会关闭通信链路、终止受管进程树并等待其退出,然后拒绝 `start()` 调用。非取消拒绝只公开固定的 `initialize` 或 `thread-start` 阶段及已经观测到的进程结果;原始产品与 Host 错误只保留在内部 cause 链中。
+`start(request)` 只接受非空的文本块序列,并根据父会话确定子级 cwd。随后,它通过 [`dsh-subprocess`](../../subprocess/subprocess/README.zh.md) spawn 固定命令,依次执行 `initialize` → `initialized`,把 Profile 选择的模式映射为官方 `thread/start` approval/reviewer/sandbox 字段并与 `{ cwd, ephemeral: true }` 一起发送,且仅在 Codex 返回有效的临时线程后才发布此次运行。若在发布前发生失败或取消,它会关闭通信链路、终止受管进程树并等待其退出,然后拒绝 `start()` 调用。非取消拒绝只公开固定的 `initialize` 或 `thread-start` 阶段及已经观测到的进程结果;原始产品与 Host 错误只保留在内部 cause 链中。
已发布的 `run.result` 恰好启动一个轮次。它只接受与此次运行的线程和轮次匹配的通知,随后等待权威的终止通知 `turn/completed`。以最后一条 `phase: "final_answer"` 的 `agentMessage` 为准;若 Codex 没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退。过程说明绝不会取代上述任一答案;成功完成的轮次若没有非空白答案,结果也会判为错误。
@@ -27,7 +27,7 @@
| `providerName` | `codex` | `ctx.subagents` 中的非空注册名称;每个已挂载实例都需要唯一值。 |
| `env` | `{}` | 显式指定的子进程环境,叠加在由子进程 seam 清除凭证后的父环境之上。 |
| `permissionMode` | `never` | 为该提供方实例的每个线程固定原生非交互审批与沙箱模式。 |
-| `disposeGraceMs` | `3000` | 共享进程树责任方各终止层级之间的宽限期,单位为毫秒且须为正有限值,并不得大于仓库共享的 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md);随后资源释放会等待整棵进程树退出。 |
+| `disposeGraceMs` | `3000` | 共享进程树责任方各终止层级之间的宽限期,单位为毫秒且须为正有限值,并不得大于仓库共享的 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.zh.md);随后资源释放会等待整棵进程树退出。 |
| `permissionMode` 值 | `thread/start` 字段 | 原生行为 |
|---|---|---|
diff --git a/packages/subagent/subagent-dsh-sdk/README.i18n.yaml b/packages/subagent/subagent-dsh-sdk/README.i18n.yaml
index dc98bdc655..69ee09db1a 100644
--- a/packages/subagent/subagent-dsh-sdk/README.i18n.yaml
+++ b/packages/subagent/subagent-dsh-sdk/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-dsh-sdk/README.md
README.md: 8d20b44dacd773345986e2a2fc0e6aca9470bf2c
-README.zh.md: 4c9117cad610276ca9d12c914a44dcbaecbbf26e
+README.zh.md: 3937724c28a693e3e74cca2e1c3f1ae61766a14e
diff --git a/packages/subagent/subagent-dsh-sdk/README.zh.md b/packages/subagent/subagent-dsh-sdk/README.zh.md
index 4c9117cad6..3937724c28 100644
--- a/packages/subagent/subagent-dsh-sdk/README.zh.md
+++ b/packages/subagent/subagent-dsh-sdk/README.zh.md
@@ -2,13 +2,13 @@
[English](README.md) | 中文
-SDK 提供方会在全新的子进程中把每个 subagent 作为完整的 DeepSeek Harness 运行时运行,并经由 [TypeScript SDK 客户端](../../sdk/client/README.md) 通过 stdio JSON-RPC 驱动。它是 [`subagent-acp`](../subagent-acp/README.md) 之外的第二个进程外后端,差异在协议格式(wire format)和子进程约定:ACP(Agent Client Protocol)后端能驱动任何 Agent Client Protocol agent(智能体);本后端专门驱动 harness SDK 运行时(`dsh-jsonrpc-agent` bin 或打包后的可执行文件),因此子进程是一个完整的对等 harness,拥有由 `cordis.yml` 决定的组合、会话持久化、模型路由和工具。
+SDK 提供方会在全新的子进程中把每个 subagent 作为完整的 DeepSeek Harness 运行时运行,并经由 [TypeScript SDK 客户端](../../sdk/client/README.zh.md) 通过 stdio JSON-RPC 驱动。它是 [`subagent-acp`](../subagent-acp/README.zh.md) 之外的第二个进程外后端,差异在协议格式(wire format)和子进程约定:ACP(Agent Client Protocol)后端能驱动任何 Agent Client Protocol agent(智能体);本后端专门驱动 harness SDK 运行时(`dsh-jsonrpc-agent` bin 或打包后的可执行文件),因此子进程是一个完整的对等 harness,拥有由 `cordis.yml` 决定的组合、会话持久化、模型路由和工具。
## 启动与所有权
`start(request)` 先解析子进程工作目录,通过 `DeepSeekHarness` spawn 运行时,并在履行前完成 `initialize` 握手(携带配置的 `provider`/`model` 路由及可选的 `maxTokens` 输出上限)。因此,履行意味着子运行时已就绪、所有权已移交给调用方。spawn、握手或发布前取消失败时,只会在子进程被回收后拒绝;工作目录解析失败则会在尚未 spawn 任何内容时拒绝。
-工作目录的解析与 ACP 后端完全一致,并使用 seam 共享的进程外辅助工具([`dsh-subagent`](../subagent/README.md)):设置了 `cwd` 覆盖值时使用该值(加载时校验一次),否则使用发起委派的父会话 cwd,绝不使用服务器进程自身的 cwd。解析出的路径同时成为子进程 cwd 和其 SDK 会话的工作区 cwd。
+工作目录的解析与 ACP 后端完全一致,并使用 seam 共享的进程外辅助工具([`dsh-subagent`](../subagent/README.zh.md)):设置了 `cwd` 覆盖值时使用该值(加载时校验一次),否则使用发起委派的父会话 cwd,绝不使用服务器进程自身的 cwd。解析出的路径同时成为子进程 cwd 和其 SDK 会话的工作区 cwd。
返回的 run id 在父级命名空间中生成;子运行时的会话 id 只存在于子进程内部。发布后,提供方拥有一段 SDK 活动,并从子会话事件中读取答案:最后一条完整且非空的 `assistant/message`(记录 usage 的空内容消息会被跳过);若没有这类消息,则取累积的 `text-delta` 流。取消或发生错误后,部分输出仍然可用。
@@ -29,7 +29,7 @@ Provider 不宣告任何启动期能力(`outputSchema`/`depthLimit`/`toolFilte
| `providerName` | `dsh-sdk` | `ctx.subagents` 上的注册名。 |
| `command` | 必填 | 每次运行时 spawn 的可执行文件(子运行时 bin 或打包后的可执行文件)。 |
| `args` | `[]` | 命令参数(通常是子进程的 `cordis.yml` 路径)。 |
-| `cwd` | 父会话 cwd | 工作目录覆盖;校验规则与 [`subagent-acp`](../subagent-acp/README.md) 相同。 |
+| `cwd` | 父会话 cwd | 工作目录覆盖;校验规则与 [`subagent-acp`](../subagent-acp/README.zh.md) 相同。 |
| `provider` | `deepseek-official` | 写入子进程 `initialize` 的提供方路由。 |
| `model` | `deepseek-v4-flash` | 写入子进程 `initialize` 的模型。 |
| `maxTokens` | 适配器/提供方路由默认值 | 写入子进程 `initialize` 的单次请求输出 token 上限;对子运行时的根 agent 及其进程内后代生效。 |
@@ -55,9 +55,9 @@ Provider 不宣告任何启动期能力(`outputSchema`/`depthLimit`/`toolFilte
## 进程边界
-子进程环境以 [`dsh-subprocess`](../../subprocess/README.md) seam 的 `scrubbedParentEnv()` 为基础,先移除疑似凭据和名称为 `DSH_*` 的环境变量,再合并显式 `config.env` 值。子进程由 SDK 客户端 spawn,而不是经由 `ctx.subprocess` spawn(这是 subprocess README 中记录的 SDK 托管传输例外),因此本后端会自行执行环境清理。JSON-RPC 协议格式才是真正的序列化边界。
+子进程环境以 [`dsh-subprocess`](../../subprocess/README.zh.md) seam 的 `scrubbedParentEnv()` 为基础,先移除疑似凭据和名称为 `DSH_*` 的环境变量,再合并显式 `config.env` 值。子进程由 SDK 客户端 spawn,而不是经由 `ctx.subprocess` spawn(这是 subprocess README 中记录的 SDK 托管传输例外),因此本后端会自行执行环境清理。JSON-RPC 协议格式才是真正的序列化边界。
-本包没有默认导出。否则 Cordis loader 解包会隐藏具名 `inject` 元数据;见[事故复盘(postmortem)0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md)。
+本包没有默认导出。否则 Cordis loader 解包会隐藏具名 `inject` 元数据;见[事故复盘(postmortem)0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.zh.md)。
## 模型体验
diff --git a/packages/subagent/subagent-fork-in-process/README.i18n.yaml b/packages/subagent/subagent-fork-in-process/README.i18n.yaml
index b3c3bd3995..9cd2361d52 100644
--- a/packages/subagent/subagent-fork-in-process/README.i18n.yaml
+++ b/packages/subagent/subagent-fork-in-process/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-fork-in-process/README.md
README.md: 74c27ff10c76aa711ed3e954e806c00a27aacfa5
-README.zh.md: bca890769bab1f898abd0b71fc8aa321b66e93c8
+README.zh.md: 43e7ef489b33d52b674420d08f7edf8c89fb0e42
diff --git a/packages/subagent/subagent-fork-in-process/README.zh.md b/packages/subagent/subagent-fork-in-process/README.zh.md
index bca890769b..43e7ef489b 100644
--- a/packages/subagent/subagent-fork-in-process/README.zh.md
+++ b/packages/subagent/subagent-fork-in-process/README.zh.md
@@ -14,7 +14,7 @@ subagent 启动时,父 agent 当前的工具调用轮次仍未结束:其日
## 启动与能力
-`start(request)` 将已完成轮次的初始内容传给 [`startInProcessRun`](../subagent-in-process-driver/README.md),并等待子 agent 发布。共享驱动器负责取消、深度、定制、结果读取和 dispose(资源释放)。
+`start(request)` 将已完成轮次的初始内容传给 [`startInProcessRun`](../subagent-in-process-driver/README.zh.md),并等待子 agent 发布。共享驱动器负责取消、深度、定制、结果读取和 dispose(资源释放)。
fork 声明 `{ outputSchema: true, depthLimit: true, toolFilter: true, persona: true }`,与 spawn 相同。
@@ -23,7 +23,7 @@ fork 声明 `{ outputSchema: true, depthLimit: true, toolFilter: true, persona:
| 键 | 含义 |
|---|---|
| `providerName` | `ctx.subagents` 上的注册表名称(默认 `fork`)。 |
-运行生命周期、模型继承与深度跟踪均为共享行为,见 [`dsh-subagent-spawn-in-process`](../subagent-spawn-in-process/README.md)。
+运行生命周期、模型继承与深度跟踪均为共享行为,见 [`dsh-subagent-spawn-in-process`](../subagent-spawn-in-process/README.zh.md)。
## 模型体验
@@ -39,7 +39,7 @@ fork 会把保留的已完成历史复制到独立的子 agent 请求中;随
#### KV Cache 影响
-在提供方和模型相同的前提下,子 agent 可以复用继承的逐字节相同前缀。persona、工具过滤、生成 SDK 或路由变化可能在继承历史之前使复用失效;后续子 agent 历史仅追加。因此随附组合把本提供方绑定为 `backgroundMode: one-shot`:可继续子 agent 还会额外携带作用域局部的 `report` 工具及其提示词 section,而这些增量位于继承历史之前,会使继承历史整体失效(见 [fork 保持 one-shot 的 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md))。
+在提供方和模型相同的前提下,子 agent 可以复用继承的逐字节相同前缀。persona、工具过滤、生成 SDK 或路由变化可能在继承历史之前使复用失效;后续子 agent 历史仅追加。因此随附组合把本提供方绑定为 `backgroundMode: one-shot`:可继续子 agent 还会额外携带作用域局部的 `report` 工具及其提示词 section,而这些增量位于继承历史之前,会使继承历史整体失效(见 [fork 保持 one-shot 的 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md))。
### 父 agent 工具结果(间接)
@@ -58,4 +58,4 @@ fork 会把保留的已完成历史复制到独立的子 agent 请求中;随
## 已知限制与暂缓事项
- **初始内容是一次性快照**:子 agent 只能看到 fork 时父 agent 已完成的轮次,看不到父 agent 此后记录的任何内容;不会实时共享上下文。
-- **没有任何随附组合会创建可继续的 fork 子 agent**:`prepareContinuable` 仍然实现完好,seam 也接受它,但每份随附的 `cordis.yml` 都在 fork 委派工具上设置 `backgroundMode: one-shot`,因此该提供方的可继续路径没有生产调用方。重新开放它需要子 agent 的系统提示词与工具 schema 与父 agent 逐字节一致,而这一点目前被 [`report` 返回通道](../tool-subagent-report/README.md)阻止。理由与重新开放条件见 [fork 保持 one-shot 的 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md)。
+- **没有任何随附组合会创建可继续的 fork 子 agent**:`prepareContinuable` 仍然实现完好,seam 也接受它,但每份随附的 `cordis.yml` 都在 fork 委派工具上设置 `backgroundMode: one-shot`,因此该提供方的可继续路径没有生产调用方。重新开放它需要子 agent 的系统提示词与工具 schema 与父 agent 逐字节一致,而这一点目前被 [`report` 返回通道](../tool-subagent-report/README.zh.md)阻止。理由与重新开放条件见 [fork 保持 one-shot 的 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md)。
diff --git a/packages/subagent/subagent-in-process-driver/README.i18n.yaml b/packages/subagent/subagent-in-process-driver/README.i18n.yaml
index 7c6f691e73..505c480cfe 100644
--- a/packages/subagent/subagent-in-process-driver/README.i18n.yaml
+++ b/packages/subagent/subagent-in-process-driver/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-in-process-driver/README.md
README.md: 47a5c09fc1c80c5dc3062be82e7355b874a627d3
-README.zh.md: bcd6a2cf31c351722dfefe55e8b8ff75d4252c40
+README.zh.md: b96399795a0fbac05ef1795888aa93c620687f30
diff --git a/packages/subagent/subagent-in-process-driver/README.zh.md b/packages/subagent/subagent-in-process-driver/README.zh.md
index bcd6a2cf31..b96399795a 100644
--- a/packages/subagent/subagent-in-process-driver/README.zh.md
+++ b/packages/subagent/subagent-in-process-driver/README.zh.md
@@ -20,7 +20,7 @@
该结果边界成立,是因为提供方拥有从发布到完全停稳的隔离子 agent 生命周期。在该生命周期内提交的 steering(中途引导)属于子运行;提供方不会声称输出只归初始 follow-up 所有。
-驱动器通过共享的子 agent 辅助函数应用该 seam 的[委派策略](../subagent/README.md#delegated-policy):它会在创建子 agent 前捕获父级的显式沙箱覆盖项与 `'never'` 审批钉定,并在未发布的设置阶段追加带来源标记的事件,使其位于所有 fork 历史之后、会话发布之前。参见[委派策略决策](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md)。
+驱动器通过共享的子 agent 辅助函数应用该 seam 的[委派策略](../subagent/README.zh.md#delegated-policy):它会在创建子 agent 前捕获父级的显式沙箱覆盖项与 `'never'` 审批钉定,并在未发布的设置阶段追加带来源标记的事件,使其位于所有 fork 历史之后、会话发布之前。参见[委派策略决策](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md)。
## 取消与所有权
@@ -66,7 +66,7 @@
#### 模型看到的内容
-结构化运行会添加下方的结构化输出指令。它还会添加子 agent 作用域的 `structured_output` 定义,其精确描述为 `Report your final structured result. Call this exactly once, when your answer is complete; the arguments must match this tool's parameter schema exactly.`,参数使用请求的 schema。该仅运行时存在的定义不在已生成并随产品发布的[工具包索引](../../../docs/tool-catalog.md#tool-package-map)中。其规范确认值是 `{ recorded: true }`,渲染为 `Structured output recorded.`;后续调用会变为 ``Error: structured output already recorded: the run is complete, so `` is not executed``。
+结构化运行会添加下方的结构化输出指令。它还会添加子 agent 作用域的 `structured_output` 定义,其精确描述为 `Report your final structured result. Call this exactly once, when your answer is complete; the arguments must match this tool's parameter schema exactly.`,参数使用请求的 schema。该仅运行时存在的定义不在已生成并随产品发布的[工具包索引](../../../docs/tool-catalog.zh.md#tool-package-map)中。其规范确认值是 `{ recorded: true }`,渲染为 `Structured output recorded.`;后续调用会变为 ``Error: structured output already recorded: the run is complete, so `` is not executed``。
##### 结构化输出指令
diff --git a/packages/subagent/subagent-spawn-in-process/README.i18n.yaml b/packages/subagent/subagent-spawn-in-process/README.i18n.yaml
index 4286fabdd8..1246456cc0 100644
--- a/packages/subagent/subagent-spawn-in-process/README.i18n.yaml
+++ b/packages/subagent/subagent-spawn-in-process/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-spawn-in-process/README.md
README.md: f1fb96f2230359cb3ff55c630f29fd34345dbed7
-README.zh.md: 940701d6fac78ce75fda24519fa22b268085ea1a
+README.zh.md: 95a3b5cdb7084eb75666f8d62001221c57ac676c
diff --git a/packages/subagent/subagent-spawn-in-process/README.zh.md b/packages/subagent/subagent-spawn-in-process/README.zh.md
index 940701d6fa..95a3b5cdb7 100644
--- a/packages/subagent/subagent-spawn-in-process/README.zh.md
+++ b/packages/subagent/subagent-spawn-in-process/README.zh.md
@@ -6,7 +6,7 @@ spawn 提供方会在当前进程中创建一个全新的子 `Agent`。子 agent
## 行为
-`start(request)` 不传入 seed,直接委托给 [`startInProcessRun`](../subagent-in-process-driver/README.md),并在子 agent 发布后才返回。子 agent 获得父 agent 的工作目录/会话谱系,并默认继承父 agent 模型(除非覆盖),但以空对话开始运行。
+`start(request)` 不传入 seed,直接委托给 [`startInProcessRun`](../subagent-in-process-driver/README.zh.md),并在子 agent 发布后才返回。子 agent 获得父 agent 的工作目录/会话谱系,并默认继承父 agent 模型(除非覆盖),但以空对话开始运行。
共享驱动器负责深度检查、persona 与工具过滤器设置、结构化输出、通过必需的信号执行取消、单次执行、结果读取和完全停稳后的 dispose(资源释放)。启动遭拒不会留下已发布的子 agent;启动调用兑现后卸载提供方,也不会撤销由持有方拥有的运行。
diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml
index d58becf3bc..a645351bd8 100644
--- a/packages/subagent/subagent/README.i18n.yaml
+++ b/packages/subagent/subagent/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md
README.md: e84a6b486253e81ccf7e7df12c4149e6df4ed9f2
-README.zh.md: 4ca619a9f402f3c6b4f648adf350e7ed7a568799
+README.zh.md: e289863531c1686cedeccadfa76e2661dfa9bfc8
diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md
index 4ca619a9f4..e289863531 100644
--- a/packages/subagent/subagent/README.zh.md
+++ b/packages/subagent/subagent/README.zh.md
@@ -4,7 +4,7 @@
subagent seam 允许一个 agent(智能体)通过具名提供方把工作委派给子 agent。调用方统一使用 `ctx.subagents` 服务 API;提供方决定子 agent 在当前进程、其他进程,还是通过未来的传输方式运行。
-[subagent 家族概述](../README.md)列出了实现和面向模型的消费方。本包负责提供方注册表、共享请求和结果约定、持久描述符以及可继续子级编排。多个具名提供方可以在该约定背后共存。
+[subagent 家族概述](../README.zh.md)列出了实现和面向模型的消费方。本包负责提供方注册表、共享请求和结果约定、持久描述符以及可继续子级编排。多个具名提供方可以在该约定背后共存。
## 服务 API
@@ -41,7 +41,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
- `toolFilter`:应用请求的子 agent 工具限制;
- `persona`:应用每个子 agent 独立的 persona。
-每个进程内子 agent 都通过一次 `applyChildComposition(childCtx, parent, composition)` 调用完成组装:先加入父级的 agent-preset 组合,再应用子 agent 自己的 persona 和工具限制。加入父级组合正是子 agent 获得能力的途径:所有面向模型的行都位于 agent 平面,完全没有加入任何组合的子 agent 抵达模型时会看到空的工具注册表(见 [`dsh-agent-presets`](../../preset/agent-presets/README.md))。将父级作为参数是刻意设计:这让“组装子 agent 却不做该加入”在各调用点无法表达,而这正是这一次调用所要杜绝的缺陷。未组装 preset roster 的部署不加入任何组合、也不需要加入;其面向模型的行位于宿主组合中,子 agent 已能通过工具注册表的全局层解析到它们。
+每个进程内子 agent 都通过一次 `applyChildComposition(childCtx, parent, composition)` 调用完成组装:先加入父级的 agent-preset 组合,再应用子 agent 自己的 persona 和工具限制。加入父级组合正是子 agent 获得能力的途径:所有面向模型的行都位于 agent 平面,完全没有加入任何组合的子 agent 抵达模型时会看到空的工具注册表(见 [`dsh-agent-presets`](../../preset/agent-presets/README.zh.md))。将父级作为参数是刻意设计:这让“组装子 agent 却不做该加入”在各调用点无法表达,而这正是这一次调用所要杜绝的缺陷。未组装 preset roster 的部署不加入任何组合、也不需要加入;其面向模型的行位于宿主组合中,子 agent 已能通过工具注册表的全局层解析到它们。
`childSessionMeta()` 把所加入的 preset id 记在子 agent 的持久化 header 上,理由与顶层会话记录自己的那一个相同:preset 决定了模型所见的工具 schema 与提示段,因此冷读子 agent 的历史时必须重建那份组装,而不是部署默认值。该值从父方**活着的** scope 链读取,而不是从父方 header 读取,因为在空白期切换过 preset 的父方运行在更新的那份组装上,而它的 header 仍写着旧的那个。
@@ -57,15 +57,17 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
`inheritsParentContext` 只用于描述,不能强制执行。它仅说明子 agent 是否能看到父级已完成的对话历史(`fork` 可以;`spawn` 和各进程外一次性提供方不可以),不表示是否继承工具、服务或权限。
+
+
## 委派策略
-两条进程内委派路径都会通过共享的子 agent 辅助函数,在委派边界固定子 agent 的权限范围。`captureDelegatedPolicyOverrides(parent)` 会为父会话的显式沙箱覆盖项(`sandboxPolicy.overrideOf()`)创建快照,并在审批能力已组合时将子 agent 的审批策略固定为 `'never'`,无论父级自身采用何种策略。这样,被委派的子 agent 只能在继承的沙箱范围内行动,每次审批请求(例如 `sandbox_permissions` 升权)都会被确定性拒绝,而不会等待无人处理的提示(这两个服务都是可选的 `ctx.get` 消费方)。`appendDelegatedPolicyOverrides()` 则在未发布的设置阶段、在任何 fork 种子之后,把每个值作为一条 `source: 'delegation'` 的 `sandbox/mode` 或 `approval/policy` 事件写入子 agent 自己的日志。因此,新捕获的策略会覆盖种子中的陈旧状态,而子 agent 的生效策略始终可以仅凭其日志重建。沙箱的部署默认值绝不复制:未切换的父级不会记录 `sandbox/mode`,其子 agent 会动态跟随部署默认值。可继续启动会在第一次 await 前捕获策略,并且只为全新物化写入这些委派事件;冷恢复只会重放已持久化的委派事件,不会重新捕获父级策略,因此创建之后的父级切换绝不会追溯性地改变持久化子 agent。每个进程内子 agent 还会收到一条作用域内的运行时上下文声明(`subagent:delegation`),告知其权限范围已固定,需要更宽访问的任务应以上报限制收尾,而不是重试。参见[一次性](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md)与[可继续](../../../.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.md)两篇委派策略 Agent Note。
+两条进程内委派路径都会通过共享的子 agent 辅助函数,在委派边界固定子 agent 的权限范围。`captureDelegatedPolicyOverrides(parent)` 会为父会话的显式沙箱覆盖项(`sandboxPolicy.overrideOf()`)创建快照,并在审批能力已组合时将子 agent 的审批策略固定为 `'never'`,无论父级自身采用何种策略。这样,被委派的子 agent 只能在继承的沙箱范围内行动,每次审批请求(例如 `sandbox_permissions` 升权)都会被确定性拒绝,而不会等待无人处理的提示(这两个服务都是可选的 `ctx.get` 消费方)。`appendDelegatedPolicyOverrides()` 则在未发布的设置阶段、在任何 fork 种子之后,把每个值作为一条 `source: 'delegation'` 的 `sandbox/mode` 或 `approval/policy` 事件写入子 agent 自己的日志。因此,新捕获的策略会覆盖种子中的陈旧状态,而子 agent 的生效策略始终可以仅凭其日志重建。沙箱的部署默认值绝不复制:未切换的父级不会记录 `sandbox/mode`,其子 agent 会动态跟随部署默认值。可继续启动会在第一次 await 前捕获策略,并且只为全新物化写入这些委派事件;冷恢复只会重放已持久化的委派事件,不会重新捕获父级策略,因此创建之后的父级切换绝不会追溯性地改变持久化子 agent。每个进程内子 agent 还会收到一条作用域内的运行时上下文声明(`subagent:delegation`),告知其权限范围已固定,需要更宽访问的任务应以上报限制收尾,而不是重试。参见[一次性](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md)与[可继续](../../../.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.zh.md)两篇委派策略 Agent Note。
## 一次性所有权与生命周期
`provider.start(request): Promise` 是所有权转移边界;委派工具也会在其由 Task 支撑的一次性后台路径中使用它。兑现前,提供方拥有设置过程,并且在任何失败路径上都必须取消、回滚并使尚未发布的资源完全停稳。兑现后,run 的所有权转移给调用方;调用方必须在每条路径上调用 `dispose()`。剩余提示词和轮次工作属于 `SubagentRun.result`。
-`SubagentRun.result` 兑现为 `{ output, structured?, diagnostic?, stopReason }`。子 agent 级失败会以非 `completed` 原因兑现;只有 seam 无法表示的基础设施故障才可以拒绝。提供方可以为非完成结果附加安全的 `diagnostic`:它会先排除工具输入、文件内容、环境值、凭证与原始协议载荷,并把完整文本限制在 4096 个 UTF-8 字节以内。共享结果类型不定义提供方类别或生命周期阶段:进程外提供方可以从锁定版本产品提供的结构化事实与已观测的进程结果派生固定展示文本,而消费方只负责原样呈现,不解析该文本。该字段不是 assistant 输出;消费方会将它分开呈现,它也不会进入 `subagent/end.lastAssistantMessage`。`dispose()` 是幂等的,会取消剩余工作,并等待结果结算以及子 agent 资源完全停稳。result 的拒绝只通过 `result` 本身报告;只有独立的资源释放失败,才会使 `dispose()` 被拒绝。`output` 与 `subagent/end` 事件的 `lastAssistantMessage` 使用导出的 `AssistantOutputFold`/`finalAssistantOutput` 辅助函数选取子 agent 最后一条非空 assistant 消息;若没有这类消息,则选取其累积的 assistant 文本。子 agent 两种输出均未产生时,`output` 为 `[]`,该事件字段缺省(终态结果约定归 [`SubagentResult`](../../../docs/subsystems/subagent.md#the-terminal-result-subagentresult) 所有)。
+`SubagentRun.result` 兑现为 `{ output, structured?, diagnostic?, stopReason }`。子 agent 级失败会以非 `completed` 原因兑现;只有 seam 无法表示的基础设施故障才可以拒绝。提供方可以为非完成结果附加安全的 `diagnostic`:它会先排除工具输入、文件内容、环境值、凭证与原始协议载荷,并把完整文本限制在 4096 个 UTF-8 字节以内。共享结果类型不定义提供方类别或生命周期阶段:进程外提供方可以从锁定版本产品提供的结构化事实与已观测的进程结果派生固定展示文本,而消费方只负责原样呈现,不解析该文本。该字段不是 assistant 输出;消费方会将它分开呈现,它也不会进入 `subagent/end.lastAssistantMessage`。`dispose()` 是幂等的,会取消剩余工作,并等待结果结算以及子 agent 资源完全停稳。result 的拒绝只通过 `result` 本身报告;只有独立的资源释放失败,才会使 `dispose()` 被拒绝。`output` 与 `subagent/end` 事件的 `lastAssistantMessage` 使用导出的 `AssistantOutputFold`/`finalAssistantOutput` 辅助函数选取子 agent 最后一条非空 assistant 消息;若没有这类消息,则选取其累积的 assistant 文本。子 agent 两种输出均未产生时,`output` 为 `[]`,该事件字段缺省(终态结果约定归 [`SubagentResult`](../../../docs/subsystems/subagent.zh.md#the-terminal-result-subagentresult) 所有)。
本地运行会在 `start()` 兑现前发布普通的子 agent/会话,把该共享会话 id 作为 `SubagentRun.id` 返回,以 `SubagentRun.localAgent` 公开准确的子 agent,把 `request.parent.session.id` 记录到子 agent 的 `parentSession` header,并在其初始轮次内追加已解析的描述符。远程提供方则生成 parent 作用域的生命周期 id,并返回 `localAgent: undefined`;由于没有本地 child 会话,其一次性运行不会进入基于追踪的枚举结果。
@@ -103,12 +105,14 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
## 收集模型
-面向模型的工具默认同步收集:先等待子 agent 结果,再 dispose 运行,然后才返回。一次性后台委派会在工具中注册普通 Task,其通用状态、收集和取消工具负责后续交互,并将模型提供的 `description` 持久化为可选显示标签。可继续后台委派会调用 `ctx.subagents.startContinuable()`,只返回持久化子 agent id;子 agent 自 inbox 接受起就拥有自己的轮次,因此没有 Task、也没有结果 promise——调用方通过 `send_message` 后续操作工具发送后续工作,`interrupt()` 只停止当前轮次而不 dispose 子 agent,而持久化子 agent 会话仍是子 agent 详细输出的来源。只有 `ctx.agents` 可用时,继续执行管理器才会存在,而会话持久化按每项继续执行操作解析。与此独立,`listChildren()` 枚举在线会话存储与可选会话持久化的在线优先合并——持久化缺席时仅枚举在线 child,因为那时冷 child 本就无法恢复——并由已注册的 `subagent` 投影单元供给每个 child 的持久化模式与标签:在线 child 取注册表的水位快照;冷 child 先取可选投影缓存的持久化行,且仅当其 `seq` 门证明该值折叠自 child 自身后缀(fork 种子之后——自有描述符一经追加即不可变)才直接采用,否则经一次有界并发的持久化 inspect 再经注册表折叠,且 inspect 结果必须仍指向枚举时的生命周期(同 id 被重新发布的会话降级为 `corrupt` diagnostic)。缓存读取抛出异常时,不会据此作出分类判断,因为缓存只是派生数据;静默落到该权威重折。分类结果完全以投影折叠为准;列表操作本身不解析描述符。取得身份值即产出 child 行;已定局而折叠未产出身份的候选是 `corrupt` diagnostic,inspect 失败是瞬时的 `unavailable`(下次列表重试),运行中而暂无身份值的候选整行省略(描述符尚未追加的创建窗口)。它不查询继续执行管理器、Agent 注册信息、Activation 或提供方。每个 child 行都会根据合并结果中携带持久化 `origin: 'subagent'` 的 header 派生读取时的 `hasChildren` 提示;它不会读取后代事件日志,展开后仍以描述符支撑的 child 目录为权威依据。UI 等服务消费方可以保留两种模式,并为无标签的一次性 child 选择回退展示;面向模型的 `list_agents` 工具只投影 `continuable` 条目,通过在线 Agent 注册表细化状态,并把仅存于存储的状态映射为可恢复而非终态的 `ready`(`running`/`idle`/`ready`),并在 `descendants` scope 下遍历 `listDescendants()`。列表操作会把调用方的取消信号转发到每次持久化读取,在这些 await 前后检查取消,并将每次检测到的中止报告为 `SubagentError` 错误码 `CANCELLED`;投影注册表未挂载则以 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 响亮失败,会话存储缺失则以 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` 响亮失败。完整约定见[后台 subagent 任务 Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)、[持久化目录 Agent Note](../../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)、[服务合并 Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)、[能力 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)和 `src/types.ts`。
+面向模型的工具默认同步收集:先等待子 agent 结果,再 dispose 运行,然后才返回。一次性后台委派会在工具中注册普通 Task,其通用状态、收集和取消工具负责后续交互,并将模型提供的 `description` 持久化为可选显示标签。可继续后台委派会调用 `ctx.subagents.startContinuable()`,只返回持久化子 agent id;子 agent 自 inbox 接受起就拥有自己的轮次,因此没有 Task、也没有结果 promise——调用方通过 `send_message` 后续操作工具发送后续工作,`interrupt()` 只停止当前轮次而不 dispose 子 agent,而持久化子 agent 会话仍是子 agent 详细输出的来源。只有 `ctx.agents` 可用时,继续执行管理器才会存在,而会话持久化按每项继续执行操作解析。与此独立,`listChildren()` 枚举在线会话存储与可选会话持久化的在线优先合并——持久化缺席时仅枚举在线 child,因为那时冷 child 本就无法恢复——并由已注册的 `subagent` 投影单元供给每个 child 的持久化模式与标签:在线 child 取注册表的水位快照;冷 child 先取可选投影缓存的持久化行,且仅当其 `seq` 门证明该值折叠自 child 自身后缀(fork 种子之后——自有描述符一经追加即不可变)才直接采用,否则经一次有界并发的持久化 inspect 再经注册表折叠,且 inspect 结果必须仍指向枚举时的生命周期(同 id 被重新发布的会话降级为 `corrupt` diagnostic)。缓存读取抛出异常时,不会据此作出分类判断,因为缓存只是派生数据;静默落到该权威重折。分类结果完全以投影折叠为准;列表操作本身不解析描述符。取得身份值即产出 child 行;已定局而折叠未产出身份的候选是 `corrupt` diagnostic,inspect 失败是瞬时的 `unavailable`(下次列表重试),运行中而暂无身份值的候选整行省略(描述符尚未追加的创建窗口)。它不查询继续执行管理器、Agent 注册信息、Activation 或提供方。每个 child 行都会根据合并结果中携带持久化 `origin: 'subagent'` 的 header 派生读取时的 `hasChildren` 提示;它不会读取后代事件日志,展开后仍以描述符支撑的 child 目录为权威依据。UI 等服务消费方可以保留两种模式,并为无标签的一次性 child 选择回退展示;面向模型的 `list_agents` 工具只投影 `continuable` 条目,通过在线 Agent 注册表细化状态,并把仅存于存储的状态映射为可恢复而非终态的 `ready`(`running`/`idle`/`ready`),并在 `descendants` scope 下遍历 `listDescendants()`。列表操作会把调用方的取消信号转发到每次持久化读取,在这些 await 前后检查取消,并将每次检测到的中止报告为 `SubagentError` 错误码 `CANCELLED`;投影注册表未挂载则以 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 响亮失败,会话存储缺失则以 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` 响亮失败。完整约定见[后台 subagent 任务 Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.zh.md)、[可继续后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md)、[持久化目录 Agent Note](../../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md)、[服务合并 Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.zh.md)、[能力 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md)和 `src/types.ts`。
可继续 Activation 会等待 best-effort 的最终会话 flush,但不会把 listener 参与视为持久性确认。一次性运行保留尽力执行的会话检查点,因此已完成的一次性 child 只有在其会话确实进入持久化存储时,才可在 dispose 后继续被发现;如果该检查点缺失,服务不会根据 Task 历史虚构目录条目。
## 模型体验
+
+
### 结算通知
#### 模型看到的内容
diff --git a/packages/subagent/tool-subagent-control/README.i18n.yaml b/packages/subagent/tool-subagent-control/README.i18n.yaml
index 16b43e0b14..07ec431848 100644
--- a/packages/subagent/tool-subagent-control/README.i18n.yaml
+++ b/packages/subagent/tool-subagent-control/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/tool-subagent-control/README.md
README.md: 91f8d23ac092049e5315418070cdaae025054860
-README.zh.md: 66d4b5a446fc9e412c5fec7bccf27bdefd1f5f77
+README.zh.md: ca63383e541ccb157f5ee8a6a7a7de7be84b9f5a
diff --git a/packages/subagent/tool-subagent-control/README.zh.md b/packages/subagent/tool-subagent-control/README.zh.md
index 66d4b5a446..ca63383e54 100644
--- a/packages/subagent/tool-subagent-control/README.zh.md
+++ b/packages/subagent/tool-subagent-control/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-可选的全局具名 `send_message`、`interrupt_agent` 与 `list_agents` 工具是 `ctx.subagents` 之上的轻量适配器。绑定提供方的 `@deepseek-ai/dsh-tool-subagent` 实例会为每种传输注册不同的委派工具;这个单独加载的包只注册一次共享控制工具,因此多个委派工具绝不会重复注册全局控制工具。根插件注册 `send_message` 与 `interrupt_agent`,且只要求 `subagents`;可单独加载的 `./list-agents` 插件注册 `list_agents`,并将 `subagents` 与 `agents` 声明为加载时依赖。其目录读取在调用时还要求会话存储与投影注册表,但不要求任何查询服务。部署可保留根插件工具并省略列表工具。是否加载这些工具不会决定委派工具是否启动可继续工作。这些工具只负责父到子的方向;单独安装的 [`@deepseek-ai/dsh-tool-subagent-report`](../tool-subagent-report/README.md) 负责子到父的方向。
+可选的全局具名 `send_message`、`interrupt_agent` 与 `list_agents` 工具是 `ctx.subagents` 之上的轻量适配器。绑定提供方的 `@deepseek-ai/dsh-tool-subagent` 实例会为每种传输注册不同的委派工具;这个单独加载的包只注册一次共享控制工具,因此多个委派工具绝不会重复注册全局控制工具。根插件注册 `send_message` 与 `interrupt_agent`,且只要求 `subagents`;可单独加载的 `./list-agents` 插件注册 `list_agents`,并将 `subagents` 与 `agents` 声明为加载时依赖。其目录读取在调用时还要求会话存储与投影注册表,但不要求任何查询服务。部署可保留根插件工具并省略列表工具。是否加载这些工具不会决定委派工具是否启动可继续工作。这些工具只负责父到子的方向;单独安装的 [`@deepseek-ai/dsh-tool-subagent-report`](../tool-subagent-report/README.zh.md) 负责子到父的方向。
本工具不执行生命周期路由:驻留与冷恢复归 subagent 服务所有。它将 `exec.agent` 作为授权投递的确切在线父级传入,并把每条消息的来源记录为 `{ kind: 'coordinator', senderSessionId: parent.id }`;服务会保留该来源,但绝不将其视为权限。每条消息都会通过 `Agent.followup()` 成为 subagent 的下一个 FIFO 轮次:如果子 agent(智能体)仍在工作,该消息会等待其当前轮次结束,因此无法重定向已经在进行的工作。本工具会转发其执行信号,该信号只在 inbox 接受之前掌管准入;一旦子 agent 接受消息,已接受的轮次便无法再通过本工具取消。本次调用不会返回子 agent 的回复;通过该 id 查看其 transcript(文本记录),才是了解它完成了哪些工作的真源。拥有 `report` 的子 agent 会自行把内容作为一条单独的父级消息发回。投递失败会变为出错的工具结果,并明确说明消息未送达。
@@ -16,7 +16,7 @@
#### 模型看到的内容
-已生成的 [schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control):`send_message` 包含 `subagent_id` 和 `message`,说明消息会成为 subagent 的下一个轮次、本次调用不会返回 subagent 的回答,以及失败即表示消息未送达;`interrupt_agent` 包含 `agent_id`,说明只有当前轮次会停止、已排队消息保持暂停、后代继续运行,以及接受先于实际停止;`list_agents` 包含可选的 `scope` 枚举。
+已生成的 [schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent-control):`send_message` 包含 `subagent_id` 和 `message`,说明消息会成为 subagent 的下一个轮次、本次调用不会返回 subagent 的回答,以及失败即表示消息未送达;`interrupt_agent` 包含 `agent_id`,说明只有当前轮次会停止、已排队消息保持暂停、后代继续运行,以及接受先于实际停止;`list_agents` 包含可选的 `scope` 枚举。
#### Token 影响
diff --git a/packages/subagent/tool-subagent-report/README.i18n.yaml b/packages/subagent/tool-subagent-report/README.i18n.yaml
index 129ba8f92f..e3b06edfa9 100644
--- a/packages/subagent/tool-subagent-report/README.i18n.yaml
+++ b/packages/subagent/tool-subagent-report/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/tool-subagent-report/README.md
README.md: ab2bcc5e4b829b66810da4089edb7146ebc1c791
-README.zh.md: c9ae647a44cad8c04603877dddd81d3fbe5be0e3
+README.zh.md: 5bac66a637eeb36dcd2133d387fd304f0912be4a
diff --git a/packages/subagent/tool-subagent-report/README.zh.md b/packages/subagent/tool-subagent-report/README.zh.md
index c9ae647a44..5bac66a637 100644
--- a/packages/subagent/tool-subagent-report/README.zh.md
+++ b/packages/subagent/tool-subagent-report/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-可选的子级作用域 `report` 工具是 `ctx.subagents.reportFrom()` 之上的轻量适配器。它为每个可继续的进程内子级提供一条返回通道,指向启动该子级的 Agent(智能体),并安装指示子级使用该通道的提示词 section。本包注册的是可继续子级设置贡献,而不是全局工具,因此该工具及其指引只存在于这些子级内部。根 Agent、一次性 subagent、远程 subagent 提供方、同级作用域以及不关联 Agent 的工具执行都不会提供或执行它。安装本包只授予这项子级作用域能力;父到子方向仍由独立的 [`@deepseek-ai/dsh-tool-subagent-control`](../tool-subagent-control/README.md) 负责,可继续模式不依赖这两个包中的任一个。
+可选的子级作用域 `report` 工具是 `ctx.subagents.reportFrom()` 之上的轻量适配器。它为每个可继续的进程内子级提供一条返回通道,指向启动该子级的 Agent(智能体),并安装指示子级使用该通道的提示词 section。本包注册的是可继续子级设置贡献,而不是全局工具,因此该工具及其指引只存在于这些子级内部。根 Agent、一次性 subagent、远程 subagent 提供方、同级作用域以及不关联 Agent 的工具执行都不会提供或执行它。安装本包只授予这项子级作用域能力;父到子方向仍由独立的 [`@deepseek-ai/dsh-tool-subagent-control`](../tool-subagent-control/README.zh.md) 负责,可继续模式不依赖这两个包中的任一个。
子级作用域的 `tool:report` 提示词 section 要求子级在结束前调用一次 `report` 并给出自足的答案,并在部分发现会改变父级下一步动作时提前上报。该指令是引导而非强制:机制本身仍接受一个轮次中调用零次或多次,也没有任何运行时路径会拒绝从不上报的子级。调用成功既不会结束轮次或结算 Activation,也不会阻止父级后续消息;轮次结束也绝不会自动上报。该工具不接受接收方参数:`exec.agent` 是发送方确切在线的 Agent,也是权限凭据;服务根据该子级持久化的 `parentSession` 推导唯一接收方。成功时返回父级已接受消息的稳定 `MessageId`,不表示已读回执、inbox 中该次出现的 id、父级日志确认、轮次完成回执或持久化刷盘。父级解析由注册表中的存在性决定:父级不在注册表时,调用失败并返回 `direct parent is not live; report was not delivered`;已开始由宿主管理的 dispose(资源释放)但仍在注册表中的父级在其日志仍接受追加时仍会接受。服务不会执行注入、父级冷恢复或离线 mailbox 写入;持久化子级 transcript(文本记录)仍是恢复依据,且工具调用失败不能证明未送达(后续 `tools/post-execute` 否决可能让报告已被接受的调用以失败结束)。
@@ -18,7 +18,7 @@
#### 模型看到的内容
-已生成的 [`report` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-report):包含一个必填 `output` 字符串。其描述说明子级必须在结束前上报一次,上报只会到达启动该子级的 Agent,并且不会结束轮次。它不包含接收方或投递模式参数。独立的 `tool:report` 提示词 section 在 schema 之外重申该义务,使忽略工具描述的子级仍能读到。
+已生成的 [`report` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent-report):包含一个必填 `output` 字符串。其描述说明子级必须在结束前上报一次,上报只会到达启动该子级的 Agent,并且不会结束轮次。它不包含接收方或投递模式参数。独立的 `tool:report` 提示词 section 在 schema 之外重申该义务,使忽略工具描述的子级仍能读到。
#### Token 影响
diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml
index b5e6ebf724..29d65cf9da 100644
--- a/packages/subagent/tool-subagent/README.i18n.yaml
+++ b/packages/subagent/tool-subagent/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/tool-subagent/README.md
README.md: 28e6213b903ffffa7934e244b2a74ada519b32b2
-README.zh.md: deae0f0ff9e19b627a04704eccf4b8874f34068f
+README.zh.md: 74e8896a152c787abd0aebf055d6e13f6158bcd3
diff --git a/packages/subagent/tool-subagent/README.zh.md b/packages/subagent/tool-subagent/README.zh.md
index deae0f0ff9..74e8896a15 100644
--- a/packages/subagent/tool-subagent/README.zh.md
+++ b/packages/subagent/tool-subagent/README.zh.md
@@ -10,9 +10,9 @@
前台调用会让执行信号贯穿启动和执行,等待 `run.result`,并且在返回前总会等待 `run.dispose()`。只有 `completed` 会返回规范值 `{ kind: 'foreground', runId, output: JsonValue[] }`,并渲染为相同的最终文本。中止、拒绝、token 上限和其他失败都会变成出错的工具结果,其消息依次包含终止原因标题、可选的提供方 `SubagentResult.diagnostic`,以及子 agent 保留下来的部分 assistant 文本。诊断与 `SubagentResult.output` 保持分离,因此被截断的回答不会被报告为成功,也不会与基础设施说明混淆。如果结果收集与 dispose(资源释放)都 reject,出错结果会保留两项失败。
-`backgroundMode` 同时选择后台路由与省略 `run_in_background` 时的默认行为。`one-shot` 默认在前台等待;显式传入 `true` 时,它会注册一个归父级所有的普通 Task,并返回规范值 `{ kind: 'background', jobId }`,渲染为 `started background subagent job `,即使提供方支持可继续子 agent 也不例外。通用 Task 工具负责其后续状态、收集、取消和通知;失败 Task 的 detail 会保留终止原因与同一份可选提供方诊断。`continuable` 在参数省略或为 `true` 时于后台运行;显式传入 `false` 时则在前台等待结果。其后台路由要求提供方具备 `prepareContinuable` 能力,调用 `ctx.subagents.startContinuable()`,并返回 `{ kind: 'continuable', subagentId }`,渲染为 `started subagent `。该路由在 inbox 接受时结算:子 agent 自此拥有自己的轮次,因此该调用既不等待也不收集结果。通过该 id 查看其 transcript(文本记录)仍是其详细输出的来源,可选的全局 `send_message` 工具则向其发送更多工作。每当子 agent 的 Activation 结束,继续执行服务都会投递一条结算通知,其中包含结束结果及可能存在的最终 assistant 消息,且这项投递不依赖 `report`。启动可继续工作不要求加载 `send_message`。见[后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续的 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md)和[后台优先委派 Agent Note](../../../.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md)。
+`backgroundMode` 同时选择后台路由与省略 `run_in_background` 时的默认行为。`one-shot` 默认在前台等待;显式传入 `true` 时,它会注册一个归父级所有的普通 Task,并返回规范值 `{ kind: 'background', jobId }`,渲染为 `started background subagent job `,即使提供方支持可继续子 agent 也不例外。通用 Task 工具负责其后续状态、收集、取消和通知;失败 Task 的 detail 会保留终止原因与同一份可选提供方诊断。`continuable` 在参数省略或为 `true` 时于后台运行;显式传入 `false` 时则在前台等待结果。其后台路由要求提供方具备 `prepareContinuable` 能力,调用 `ctx.subagents.startContinuable()`,并返回 `{ kind: 'continuable', subagentId }`,渲染为 `started subagent `。该路由在 inbox 接受时结算:子 agent 自此拥有自己的轮次,因此该调用既不等待也不收集结果。通过该 id 查看其 transcript(文本记录)仍是其详细输出的来源,可选的全局 `send_message` 工具则向其发送更多工作。每当子 agent 的 Activation 结束,继续执行服务都会投递一条结算通知,其中包含结束结果及可能存在的最终 assistant 消息,且这项投递不依赖 `report`。启动可继续工作不要求加载 `send_message`。见[后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.zh.md)、[可继续的 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md)和[后台优先委派 Agent Note](../../../.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.zh.md)。
-`toolFilter` 会改变子 agent 的全局工具层,但不是从父级派生的权限上限。见 [agent 作用域的安全非目标](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals)。
+`toolFilter` 会改变子 agent 的全局工具层,但不是从父级派生的权限上限。见 [agent 作用域的安全非目标](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md#security-and-authority-are-non-goals)。
## 配置
@@ -29,7 +29,7 @@
## 并发
-前台调用和后台调用均并发安全:同一条 assistant 消息中的同级委派会在循环的滚动池(`maxParallelToolCalls`)下重叠执行,结果仍按模型顺序提交。子 agent 在各自的会话中工作,一次运行绝不变更父会话;一次性后台形态对父级拥有状态的唯一写入是注册一个 Task——这是一次同步、可交换、能容忍并发分发的插入,因此重叠的后台调用按分发竞态顺序获得各自的 job id。协调同级工作区效果由模型负责,正如模型已经对后台和可继续子 agent 所承担的那样。见 [并行 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.md) 和 [并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)。
+前台调用和后台调用均并发安全:同一条 assistant 消息中的同级委派会在循环的滚动池(`maxParallelToolCalls`)下重叠执行,结果仍按模型顺序提交。子 agent 在各自的会话中工作,一次运行绝不变更父会话;一次性后台形态对父级拥有状态的唯一写入是注册一个 Task——这是一次同步、可交换、能容忍并发分发的插入,因此重叠的后台调用按分发竞态顺序获得各自的 job id。协调同级工作区效果由模型负责,正如模型已经对后台和可继续子 agent 所承担的那样。见 [并行 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.zh.md) 和 [并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md)。
## 模型体验
@@ -37,7 +37,7 @@
#### 模型看到的内容
-当提供方存在时,以当前实例配置的名称公开已生成的默认 [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent)。提供方是否继承上下文会改变工具描述和提示词描述。启用后台模式会添加 `run_in_background`:可继续模式会记录其默认值为 `true`、运行时结算通知与显式前台覆盖;一次性模式会记录其默认值为 `false`,以及用 `job_output` 收集或用 `job_kill` 停止的 job id。当工具在本次组装的作用域中可见时,一个 `tool:` 系统提示词 section 会指示模型同时启动相互独立的可继续委派、在它们运行时继续工作,并且仅当下一步动作依赖结果时选择前台;工具限制会同时移除其 schema 和这段指引。
+当提供方存在时,以当前实例配置的名称公开已生成的默认 [`subagent` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent)。提供方是否继承上下文会改变工具描述和提示词描述。启用后台模式会添加 `run_in_background`:可继续模式会记录其默认值为 `true`、运行时结算通知与显式前台覆盖;一次性模式会记录其默认值为 `false`,以及用 `job_output` 收集或用 `job_kill` 停止的 job id。当工具在本次组装的作用域中可见时,一个 `tool:` 系统提示词 section 会指示模型同时启动相互独立的可继续委派、在它们运行时继续工作,并且仅当下一步动作依赖结果时选择前台;工具限制会同时移除其 schema 和这段指引。
#### Token 影响
@@ -65,7 +65,7 @@
#### 模型看到的内容
-在配置的可继续模式下,启动时返回内容恰为 `started subagent `;在配置的一次性模式下,则返回 `started background subagent job `。一次性模式下,通用 Task 接口提供后续状态、最终输出、取消响应和通知;若结果携带提供方诊断,失败状态的 detail 会包含它。可继续模式下,本工具不返回自己的结果;子 agent 的结算会以[服务负责的通知](../subagent/README.md#settlement-notice)到达父级,独立加载的 `send_message` 工具会投递后续消息,而通过其 id 查看子 agent 的 transcript 即是其详细输出来源。
+在配置的可继续模式下,启动时返回内容恰为 `started subagent `;在配置的一次性模式下,则返回 `started background subagent job `。一次性模式下,通用 Task 接口提供后续状态、最终输出、取消响应和通知;若结果携带提供方诊断,失败状态的 detail 会包含它。可继续模式下,本工具不返回自己的结果;子 agent 的结算会以[服务负责的通知](../subagent/README.zh.md#settlement-notice)到达父级,独立加载的 `send_message` 工具会投递后续消息,而通过其 id 查看子 agent 的 transcript 即是其详细输出来源。
#### Token 影响
diff --git a/packages/subprocess/README.i18n.yaml b/packages/subprocess/README.i18n.yaml
index b294c97415..a9aa1fcfe7 100644
--- a/packages/subprocess/README.i18n.yaml
+++ b/packages/subprocess/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subprocess/README.md
README.md: 72a30775f45a8140935a013c63e9427e9cbd94d6
-README.zh.md: 35c801a1f4a5e8ad161b8c4ff57ad6ffa07e699d
+README.zh.md: 790fa35412e5fd6cd14500d9deab6f8d3d603af0
diff --git a/packages/subprocess/README.zh.md b/packages/subprocess/README.zh.md
index 35c801a1f4..790fa35412 100644
--- a/packages/subprocess/README.zh.md
+++ b/packages/subprocess/README.zh.md
@@ -2,13 +2,13 @@
[English](README.md) | 中文
-这里集中提供一个执行世界的共享进程基底:可执行文件查找、具有原始或收集式 stdio 的完全明确指定的受管子进程树,以及一项底层终端进程原语,负责 PTY 分配、前台进程组和提供方仍可观察到的会话成员清理。命令默认值补全、shell 语义、时限、协议分帧、就绪状态与呈现留在消费方:[bash 执行器](../shell/README.md)、[LSP 主机](../lsp/README.md)、[PTY shell 后端](../terminal/README.md)与 [ACP(Agent Client Protocol)subagent 后端](../subagent/README.md)。参见 [subprocess seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md)。
+这里集中提供一个执行世界的共享进程基底:可执行文件查找、具有原始或收集式 stdio 的完全明确指定的受管子进程树,以及一项底层终端进程原语,负责 PTY 分配、前台进程组和提供方仍可观察到的会话成员清理。命令默认值补全、shell 语义、时限、协议分帧、就绪状态与呈现留在消费方:[bash 执行器](../shell/README.zh.md)、[LSP 主机](../lsp/README.zh.md)、[PTY shell 后端](../terminal/README.zh.md)与 [ACP(Agent Client Protocol)subagent 后端](../subagent/README.zh.md)。参见 [subprocess seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.zh.md)。
| 包 | ctx 键 | 角色 |
|---|---|---|
-| [`subprocess`](subprocess/README.md)(`@deepseek-ai/dsh-subprocess`) | `ctx.subprocess` | Service Definition:可执行文件查找、普通受管 spawn、终端进程原语、句柄生命周期,以及共享的环境/输出词汇 |
-| [`subprocess-local`](subprocess-local/README.md)(`@deepseek-ai/dsh-subprocess-local`) | 无 | 本地 Service Provider:detached 进程树、有界收集/spill、`node-pty`、前台/会话检查、进程树信号发送,以及先终止再等待退出的 dispose(资源释放) |
+| [`subprocess`](subprocess/README.zh.md)(`@deepseek-ai/dsh-subprocess`) | `ctx.subprocess` | Service Definition:可执行文件查找、普通受管 spawn、终端进程原语、句柄生命周期,以及共享的环境/输出词汇 |
+| [`subprocess-local`](subprocess-local/README.zh.md)(`@deepseek-ai/dsh-subprocess-local`) | 无 | 本地 Service Provider:detached 进程树、有界收集/spill、`node-pty`、前台/会话检查、进程树信号发送,以及先终止再等待退出的 dispose(资源释放) |
即使消费方重载,进程生命周期仍由服务负责管理;消费方负责定义进程的含义(一条 bash 命令、未来的非 shell 运行器),以及决定塑造该进程的每一项默认值。
-子系统参考——spawn spec、输出读取器、结果、`DSH_*` 环境——见 [docs/subsystems/subprocess.md](../../docs/subsystems/subprocess.md);seam 决定见 [subprocess seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md)。
+子系统参考——spawn spec、输出读取器、结果、`DSH_*` 环境——见 [docs/subsystems/subprocess.md](../../docs/subsystems/subprocess.zh.md);seam 决定见 [subprocess seam Agent Note](../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.zh.md)。
diff --git a/packages/subprocess/subprocess-local/README.i18n.yaml b/packages/subprocess/subprocess-local/README.i18n.yaml
index 6b02932a92..646dafcd0a 100644
--- a/packages/subprocess/subprocess-local/README.i18n.yaml
+++ b/packages/subprocess/subprocess-local/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subprocess/subprocess-local/README.md
README.md: 0935bb309bd10dec7503a74708a28442223bf296
-README.zh.md: e2e6c67e4dbe1890bcb5532594a62b650bfed85d
+README.zh.md: 2f37cbce8df0b5f454d2301b78b44961cf66ea31
diff --git a/packages/subprocess/subprocess-local/README.zh.md b/packages/subprocess/subprocess-local/README.zh.md
index e2e6c67e4d..2f37cbce8d 100644
--- a/packages/subprocess/subprocess-local/README.zh.md
+++ b/packages/subprocess/subprocess-local/README.zh.md
@@ -2,18 +2,18 @@
[English](README.md) | 中文
-[`@deepseek-ai/dsh-subprocess`](../subprocess/README.md) seam 的本地 Service Provider。`LocalSubprocessRuntime` 解析本地可执行文件,以显式 stdio spawn 普通 detached 进程树,并通过 `node-pty` 加平台进程检查实现终端进程。该实现没有任何配置:每项处置方式、限制、终端尺寸、宽限期与目录都来自调用方能力 seam([`dsh-bash-local`](../../shell/bash-local/README.md)、[`dsh-lsp-stdio`](../../lsp/lsp-stdio/README.md) 和 [`dsh-terminal-bash`](../../terminal/terminal-bash/README.md))。
+[`@deepseek-ai/dsh-subprocess`](../subprocess/README.zh.md) seam 的本地 Service Provider。`LocalSubprocessRuntime` 解析本地可执行文件,以显式 stdio spawn 普通 detached 进程树,并通过 `node-pty` 加平台进程检查实现终端进程。该实现没有任何配置:每项处置方式、限制、终端尺寸、宽限期与目录都来自调用方能力 seam([`dsh-bash-local`](../../shell/bash-local/README.zh.md)、[`dsh-lsp-stdio`](../../lsp/lsp-stdio/README.zh.md) 和 [`dsh-terminal-bash`](../../terminal/terminal-bash/README.zh.md))。
## 行为
- **以适合平台的方式发送信号的 detached 进程树**:POSIX 子进程使用 `detached` spawn(拥有独立进程组),信号以负 pgid 发送并以直接子进程作为回退;Windows 通过 `taskkill /PID /T /F` 终止进程树。`terminate()`(句柄唯一的终止操作)先发送 SIGTERM,经过 spec 的宽限期后再发送 SIGKILL(沿用 OpenCode 的升级策略;流水线与子 shell 会随父进程一起结束),进程树消亡后为空操作;`waitForExit()` 轮询整棵进程树的存活状态,使消费方的拆卸能确认真正的完全停稳。组长进程退出后,仍然打开的管道也只获得同样有界的排空宽限期,因此存活的后代进程无法无限期地拖住结果不结算。系统会容忍 ESRCH;重新指定父进程并脱离该组的 daemon 仍可能存活。
- **按流划分的处置方式**:`'pipe'` 把原始流原样交给调用方(协议分帧仍归消费方所有);`'inherit'` 直通父进程的描述符;收集模式(collect)在输出超过上限后于内存中保留尾部(错误与结果通常聚集在末尾,沿用 pi/OpenCode 的理由),并在配置了 spill 上限时把完整流追加到一个私有临时文件;省略 `spill` 则只保留用于诊断的尾部。某条流大于 spill 上限时,会丢弃已不完整的 spill,仅返回带截断标记的尾部;spill 文件描述符在结算时封存,最终关闭失败时则不公布路径,以免声称存在不完整的文件。spill 文件权限为 `0600`、名称随机,位于按需创建、权限为 `0700` 的每进程目录之下。
-- **凭据清除 + 显式合并**:以 `process.env` 为基础,移除形似凭据的变量(`*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`)和所有环境中已有的 `DSH_*` 名称;spec 的显式 `env` 在该清除之后合并且不做命名空间校验,因此有意提供的凭据或当前 `DSH_*` 事实会胜出,而陈旧的嵌套 harness 身份无法从环境中隐式漏入。提供的 stdin 会被写入后关闭;否则 fd 0 指向 `/dev/null`。参见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md)与[受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
+- **凭据清除 + 显式合并**:以 `process.env` 为基础,移除形似凭据的变量(`*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`)和所有环境中已有的 `DSH_*` 名称;spec 的显式 `env` 在该清除之后合并且不做命名空间校验,因此有意提供的凭据或当前 `DSH_*` 事实会胜出,而陈旧的嵌套 harness 身份无法从环境中隐式漏入。提供的 stdin 会被写入后关闭;否则 fd 0 指向 `/dev/null`。参见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.zh.md)与[受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.zh.md)。
- **基于偏移量的读取**:收集模式的读取器按完整流的字节坐标返回增量;服务自身从不持有游标,因此消费方自有的游标(bash 的后台读取路径)与完整流重读可以共存,结算前后皆然。
- **可执行文件查找**:`resolveExecutable` 检查绝对文件,或根据平台可执行文件扩展名在清理后的有效 PATH 中搜索;含分隔符的相对路径在该 seam 处被拒绝,相对 PATH 条目从宿主进程 cwd 解析。
- **终端进程所有权**:`spawnTerminal` 分配 `node-pty`,桥接 UTF-8 终端文本,检查当前前台进程组并向其发送信号,还会公开一项须等待的终止操作,在终止顶层 shell 前后清理后代进程。每次前台检查都会保留根进程树中的精确身份;Linux 还会在 POSIX 会话 leader 退出后枚举该会话。因此,之前观察到的 macOS 后代以及同会话 Linux 成员在重新设定父进程后仍受围栏保护,pid/start 身份则防止清理跟随 PID 复用。在 Windows 上,基于 koffi 的检查器通过 Toolhelp32 枚举进程表,把 GetProcessTimes 启动身份与进程句柄零时等待结合起来判断存活状态,并把 shell pid 作为伪前台进程组(Windows 没有 POSIX 进程组)。拆卸会验证 shell 已终止,因为被外部 taskkill 的 shell 可能永远不会触发 node-pty 的退出通知。上层 PTY 后端负责提示符就绪、缓冲区与面向模型的操作。
- **先终止再等待退出的 dispose(资源释放)**:服务保留存活句柄,使自身的 dispose 能对每个仍在运行的进程树执行升级并等待其退出;完全停稳与 spawn 失败的句柄会在整棵进程树或 terminal session 清理完成后离开存活集合。
-- **同步宿主退出最终清理**:服务 effect 仍有效时,Node `exit` listener 会强制终止同一组存活集合中仍存在的每棵普通进程树和可观察 terminal session。这些仅供本地实现使用的操作会向受管 POSIX 进程组发送 SIGKILL、在 Windows 运行 `taskkill /T /F`,并在终止 PTY root 前后同步向已捕获及当前可观察的 terminal 身份发送信号;它们不会创建 Promise 或 timer,不改变宿主退出码与诊断,会分别包含每个目标的失败,也不会声称已经完全停稳。正常 dispose 仍使用上面的须等待温和路径。参见[宿主退出清理决策](../../../.agents/notes/implemented/bug-fix/2026-08-11-synchronous-subprocess-exit-cleanup.md)。
+- **同步宿主退出最终清理**:服务 effect 仍有效时,Node `exit` listener 会强制终止同一组存活集合中仍存在的每棵普通进程树和可观察 terminal session。这些仅供本地实现使用的操作会向受管 POSIX 进程组发送 SIGKILL、在 Windows 运行 `taskkill /T /F`,并在终止 PTY root 前后同步向已捕获及当前可观察的 terminal 身份发送信号;它们不会创建 Promise 或 timer,不改变宿主退出码与诊断,会分别包含每个目标的失败,也不会声称已经完全停稳。正常 dispose 仍使用上面的须等待温和路径。参见[宿主退出清理决策](../../../.agents/notes/implemented/bug-fix/2026-08-11-synchronous-subprocess-exit-cleanup.zh.md)。
## 模型体验
diff --git a/packages/subprocess/subprocess/README.i18n.yaml b/packages/subprocess/subprocess/README.i18n.yaml
index c53673c2cd..74768ea0f9 100644
--- a/packages/subprocess/subprocess/README.i18n.yaml
+++ b/packages/subprocess/subprocess/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subprocess/subprocess/README.md
README.md: 5e80bc7205c2f06b3528bbcfcb775941d2fa4545
-README.zh.md: 62a8b0704d73bc433888f841273505fc92748fa9
+README.zh.md: 4744b4aeea07e18a8d149865050f7487f8816061
diff --git a/packages/subprocess/subprocess/README.zh.md b/packages/subprocess/subprocess/README.zh.md
index 62a8b0704d..4744b4aeea 100644
--- a/packages/subprocess/subprocess/README.zh.md
+++ b/packages/subprocess/subprocess/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-子进程 seam(`ctx.subprocess`)是一个执行世界的进程部分。抽象的 `SubprocessRuntime` 公开可执行文件查找、普通受管 `spawn` 和一项终端进程原语;其词汇涵盖原始/收集式 stdio、进程与终端句柄、退出事实、进程树/会话清理,以及受管的 `DSH_*` 环境命名空间。本地实现位于 [`dsh-subprocess-local`](../subprocess-local/README.md)。
+子进程 seam(`ctx.subprocess`)是一个执行世界的进程部分。抽象的 `SubprocessRuntime` 公开可执行文件查找、普通受管 `spawn` 和一项终端进程原语;其词汇涵盖原始/收集式 stdio、进程与终端句柄、退出事实、进程树/会话清理,以及受管的 `DSH_*` 环境命名空间。本地实现位于 [`dsh-subprocess-local`](../subprocess-local/README.zh.md)。
## 约定
@@ -15,7 +15,7 @@
- `scrubbedParentEnv()` / `SENSITIVE_ENV_PATTERN` 是唯一一份共享的环境清理定义:环境中形似凭据的名称与 `DSH_*` 名称都会被丢弃,显式 `env` 在清除之后合并。本地的普通 spawn 与终端 spawn 都应用该定义;拥有自身 spawn 的 SDK 管理传输可直接导入它。
- 服务自身的 dispose(资源释放)会终止所有仍在运行的受管进程并等待其退出。
-参见[子进程子系统页面](../../../docs/subsystems/subprocess.md)与[seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md)。
+参见[子进程子系统页面](../../../docs/subsystems/subprocess.zh.md)与[seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-26-subprocess-seam.zh.md)。
## 模型体验
diff --git a/packages/terminal/README.i18n.yaml b/packages/terminal/README.i18n.yaml
index 428fa2ac60..db1c3cdca2 100644
--- a/packages/terminal/README.i18n.yaml
+++ b/packages/terminal/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/terminal/README.md
README.md: 6204eb4de27bc2361f2a49849865f65729dce12c
-README.zh.md: 8b7d704440cb8f9082228fc1265500ebe41e1b58
+README.zh.md: 802e8ded884f9f35f9c6872abeca1dd84597add5
diff --git a/packages/terminal/README.zh.md b/packages/terminal/README.zh.md
index 8b7d704440..802e8ded88 100644
--- a/packages/terminal/README.zh.md
+++ b/packages/terminal/README.zh.md
@@ -6,10 +6,10 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`pty`](terminal/README.md)(`@deepseek-ai/dsh-terminal`) | 后端注册表、品牌化 id、精确的 Agent 所有权、会话操作与等待完成的清理 | `ctx.terminals` |
+| [`pty`](terminal/README.zh.md)(`@deepseek-ai/dsh-terminal`) | 后端注册表、品牌化 id、精确的 Agent 所有权、会话操作与等待完成的清理 | `ctx.terminals` |
| `terminal-bash`(`@deepseek-ai/dsh-terminal-bash`) | `ctx.subprocess.spawnTerminal` 之上的 shell 后端:就绪检测、有界终端状态、沙箱策略与会话操作 | 注册到 `ctx.terminals` |
| `tool-terminal`(`@deepseek-ai/dsh-tool-terminal`) | 6 个面向模型的工具,并为后台发送集成通用任务 | 注册到 `ctx.tools` |
-设计与暂缓边界记录在[持久 PTY Agent Note](../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md) 中。
+设计与暂缓边界记录在[持久 PTY Agent Note](../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md) 中。
-子系统参考——id、后端/会话约定、发送就绪、有界读取——见 [docs/subsystems/terminal.md](../../docs/subsystems/terminal.md);设计与暂缓边界见[持久 PTY Agent Note](../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md)。
+子系统参考——id、后端/会话约定、发送就绪、有界读取——见 [docs/subsystems/terminal.md](../../docs/subsystems/terminal.zh.md);设计与暂缓边界见[持久 PTY Agent Note](../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md)。
diff --git a/packages/terminal/tool-terminal/README.i18n.yaml b/packages/terminal/tool-terminal/README.i18n.yaml
index c99082f62f..2b36b16d7c 100644
--- a/packages/terminal/tool-terminal/README.i18n.yaml
+++ b/packages/terminal/tool-terminal/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/terminal/tool-terminal/README.md
README.md: f6fd4020d423425d7b20ad6bc21b4e009cf25c85
-README.zh.md: 827d179515779f87665a80920e72a11be051023f
+README.zh.md: dca0ce9280fc7a7d748e90a5c553c0e712e2453d
diff --git a/packages/terminal/tool-terminal/README.zh.md b/packages/terminal/tool-terminal/README.zh.md
index 827d179515..dca0ce9280 100644
--- a/packages/terminal/tool-terminal/README.zh.md
+++ b/packages/terminal/tool-terminal/README.zh.md
@@ -41,7 +41,7 @@ Use a terminal session only when work needs persistent terminal state or interac
#### 模型看到的内容
-6 个生成的 schema 列在 [`dsh-tool-terminal` 目录章节](../../../docs/tool-catalog.md#deepseek-aidsh-tool-terminal)中。此插件活跃时,请求中会包含它们的固定 schema token;按 agent 范围过滤工具时可能隐藏这些 schema。
+6 个生成的 schema 列在 [`dsh-tool-terminal` 目录章节](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-terminal)中。此插件活跃时,请求中会包含它们的固定 schema token;按 agent 范围过滤工具时可能隐藏这些 schema。
#### Token 影响
diff --git a/packages/test-support/README.i18n.yaml b/packages/test-support/README.i18n.yaml
index 484db0b32b..d229930962 100644
--- a/packages/test-support/README.i18n.yaml
+++ b/packages/test-support/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/test-support/README.md
README.md: 8f07d08828c2f4d69676487842373b600037e199
-README.zh.md: b56b082e0d58a32d6af39588fbe6f4ab422af9b7
+README.zh.md: 8793ebd9a4e6b456d34c6fe84983524dfc1d5fa2
diff --git a/packages/test-support/README.zh.md b/packages/test-support/README.zh.md
index b56b082e0d..8793ebd9a4 100644
--- a/packages/test-support/README.zh.md
+++ b/packages/test-support/README.zh.md
@@ -6,13 +6,13 @@
| 包 | 职责 |
|---|---|
-| [`acp-snapshot/`](acp-snapshot/README.md) | 提供 ACP(Agent Client Protocol)快照测试工具包 |
-| [`agent-loop-testkit/`](agent-loop-testkit/README.md) | 为 AgentLoop 测试挂载共享先决条件 |
-| [`invariants/`](../runtime-diagnostics/invariants/README.md) | 运行开发期运行时约定断言 |
-| [`loader-smoke/`](loader-smoke/README.md) | 启动由 Loader 组合的应用以执行冒烟测试 |
-| [`llm-mock-server/`](llm-mock-server/README.md) | 提供确定性的 OpenAI 兼容故障服务器 |
-| [`llm-replay/`](llm-replay/README.md) | 为无密钥测试和演示回放已记录的模型响应 |
+| [`acp-snapshot/`](acp-snapshot/README.zh.md) | 提供 ACP(Agent Client Protocol)快照测试工具包 |
+| [`agent-loop-testkit/`](agent-loop-testkit/README.zh.md) | 为 AgentLoop 测试挂载共享先决条件 |
+| [`invariants/`](../runtime-diagnostics/invariants/README.zh.md) | 运行开发期运行时约定断言 |
+| [`loader-smoke/`](loader-smoke/README.zh.md) | 启动由 Loader 组合的应用以执行冒烟测试 |
+| [`llm-mock-server/`](llm-mock-server/README.zh.md) | 提供确定性的 OpenAI 兼容故障服务器 |
+| [`llm-replay/`](llm-replay/README.zh.md) | 为无密钥测试和演示回放已记录的模型响应 |
当一个包获得产品约定和产品消费方时,它会移出 `test-support/`。
-不变式约定记录在 [docs/subsystems/invariants.md](../../docs/subsystems/invariants.md)。
+不变式约定记录在 [docs/subsystems/invariants.md](../../docs/subsystems/invariants.zh.md)。
diff --git a/packages/test-support/acp-snapshot/README.i18n.yaml b/packages/test-support/acp-snapshot/README.i18n.yaml
index 3f84ad8132..6a414b9c77 100644
--- a/packages/test-support/acp-snapshot/README.i18n.yaml
+++ b/packages/test-support/acp-snapshot/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/test-support/acp-snapshot/README.md
README.md: 31f4ec0caeb995a10202d4a452ee7e433749762f
-README.zh.md: e92d3f17c62b4fe7dea4f5b6c5a0738eef635327
+README.zh.md: 4e9a8236827002a55faf19e4af646ef72b0c725e
diff --git a/packages/test-support/acp-snapshot/README.zh.md b/packages/test-support/acp-snapshot/README.zh.md
index e92d3f17c6..4e9a823682 100644
--- a/packages/test-support/acp-snapshot/README.zh.md
+++ b/packages/test-support/acp-snapshot/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-ACP(Agent Client Protocol)快照套件工具包:无密钥快照层(`pnpm run test:snapshot`,见[测试策略](../../../docs/testing.md))背后的共享机制。示例只需场景表和 fixture(测试前置数据)目录就能获得完整快照套件;每项比较/保护机制都位于此处,受每文件覆盖率门禁约束,而不是在每个示例中复制。
+ACP(Agent Client Protocol)快照套件工具包:无密钥快照层(`pnpm run test:snapshot`,见[测试策略](../../../docs/testing.zh.md))背后的共享机制。示例只需场景表和 fixture(测试前置数据)目录就能获得完整快照套件;每项比较/保护机制都位于此处,受每文件覆盖率门禁约束,而不是在每个示例中复制。
四层可单独导入:
@@ -11,7 +11,7 @@ ACP(Agent Client Protocol)快照套件工具包:无密钥快照层(`pnpm
- **规范化器**:将已捕获内容转换为稳定文本或可移植 fixture 的纯函数:`normalizeStdout`(JSON-RPC id → 首次出现序列;UUID 以及生成 cwd 的每种原生/JavaScript 文件系统写法 → token,按最长优先;根据 cwd 的分隔符选择规范 `/` 或宿主原生形式;同时作为 stdout 纯度检查)、`normalizeSessionLog`(时间归零、保留 `seq`、使用同一 cwd 路径策略)、`tokenizeSessionFixtureCwd`(生成的 workspace 及其文件系统别名,包括已进行 token 化的 macOS `/private` 别名 → 单一规范 `{{cwd}}`;手工编写的临时路径保持不变)、`scrubSystemPrompts`(提示词文本 → `{{system}}`)、`scrubToolSchemas`(schema bulk → `{{tools}}`)、`scrubRequestHeaders`(每个 pin 之外的所有 header bulk → `{{system}}`/`{{tools}}`/`{{messagePrefix}}`,保留结构;见[header 固定 Agent Note](../../../.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md))和 `stabilizeFixtureMessageIds`(针对任意录制器已准备写入 fixture 的父级/子级日志,通过结构化方式仅改写 surface 和持久 inbox 中完整消息的 ID 字段,将已提交 UUID 带入未变化且双向唯一匹配的消息)。
- **`defineAcpSnapshotSuite`(工厂)**:为场景表注册完整 describe/it 树:每场景预期输出与重新持久化日志比较、录制/刷新 fixture 回写、拒绝结构化 `UNKNOWN_TOOL` 结果、每个 header 类别一个 token 化 pin(由可独立共享的 `system-prompt.expected.md` 和 `tool-schemas.expected.json` 伴随文件组合而成),以及实时一致性保护。其 fixture 保护会拒绝遗留场景目录、缺失文件、一个类别包含多个 pin、重复的伴随文件内容、带非规范 macOS 前缀的 cwd token、未擦除的 JSONL header,以及格式错误的 pin header。在录制或刷新模式写入 fixture 前,仅当一条未变化完整消息的 ID 及其去除身份后的指纹在场景可写入 fixture 的父级/子级日志中均唯一时,该消息才会保留已提交的 UUID;会话包的权威 surface 类型谓词负责选择 surface 载体,与其关联的 `agent/inbox/spliced` 副本也纳入同一映射,且仅改写这些载体中通过验证的 `id` 字段。新增、发生变化、格式错误以及图关系存在歧义的消息保留本次生成的 UUID。刷新会使用收集所得本次运行的 id、cwd 及全部 cwd 别名评估本次生成的叶值;只有完整逻辑记录布局对齐且易变字符串替换形成双射时,才会复用归一化后等价的叶值;surface 或 inbox 载体中的完整消息 ID 不参与此路径,因为后续结构化处理负责这些 ID;有歧义的日志保留本次生成的字符串,而本次生成的语义值仍为权威数据。它还会在对齐事件时间前展开打包时序 envelope,因此切换打包/非打包布局无法移动后续记录。新插入的 `session/title` 使用前一个事件的时间,因此功能驱动的插入不会扰动 fixture 余下部分。每个场景目录的 `session.jsonl` 和连续 `session..jsonl` 同级文件构成有序的主会话/子会话清单;场景表不重复其数量。必须在 vitest 收集时调用。
-签入仓库的会话 fixture 使用规范打包行;[临时仓库迁移器](../../../scripts/migrate-packed-session-fixtures.ts)(`pnpm run migrate:packed-session-fixtures`)会改写较旧的 fixture 布局,由其[移除提案](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md)负责删除该迁移器。
+签入仓库的会话 fixture 使用规范打包行;[临时仓库迁移器](../../../scripts/migrate-packed-session-fixtures.ts)(`pnpm run migrate:packed-session-fixtures`)会改写较旧的 fixture 布局,由其[移除提案](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.zh.md)负责删除该迁移器。
消费方 `*.snapshot.ts` 就是场景表加一次工厂调用:
@@ -59,7 +59,7 @@ defineAcpSnapshotSuite({
每个场景都比较 `stdout.expected.jsonl`,其中以 cwd 为根的分隔符规范化为 `/`。在 Windows 上,`pinsNativeWindowsStdout` 还会在共享预期输出之后比较完整 `stdout.expected.windows.jsonl`,并且仅在启用时要求存在该伴随文件。需要非 Windows 主机的场景声明 `posixOnly`,在 Windows 上跳过运行测试,但 fixture 保护仍在所有平台覆盖其已提交文件;示例包括 POSIX 进程语义(例如取消正在运行的 bash 调用会终止一个已脱离的进程组)和 Windows 无法表示的生成路径。组合需要可用 `pwsh` 的场景声明 `pwshOnly`;调用方提供的 `hasPwsh` 探测(随附的 acp-agent 套件遵循执行器自身的解析,因此 Program Files 安装也计入)在解析不到可用 `pwsh` 时跳过运行测试,而 fixture 保护仍处处覆盖其已提交文件。
-示例还发布 `cordis.snapshot.yml` 回放 overlay,位于 `cordis.yml` 旁边(bin 在 `DSH_SNAPSHOT=replay` 下交换它们,见[单源回放配置 Agent Note](../../../.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.md));回放 fixture 由 [`dsh-llm-replay`](../llm-replay/README.md) 提供,本包通过为子进程设置的 `DSH_SNAPSHOT_*` env var 指向它。`pnpm run test:snapshot:record` 调用在线 LLM(大语言模型),并重写已记录场景的模型 fixture;`pnpm run test:snapshot:refresh` 保持无密钥,运行回放 overlay,并从已提交模型脚本重写 stdout、可比较会话日志预期输出,以及各 pin 自有的提示词与工具 schema 伴随文件。Fixture 角色、录制/回放/刷新语义和场景表字段记录在 `Scenario` 以及[快照 Agent Note](../../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md) 中。
+示例还发布 `cordis.snapshot.yml` 回放 overlay,位于 `cordis.yml` 旁边(bin 在 `DSH_SNAPSHOT=replay` 下交换它们,见[单源回放配置 Agent Note](../../../.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.md));回放 fixture 由 [`dsh-llm-replay`](../llm-replay/README.zh.md) 提供,本包通过为子进程设置的 `DSH_SNAPSHOT_*` env var 指向它。`pnpm run test:snapshot:record` 调用在线 LLM(大语言模型),并重写已记录场景的模型 fixture;`pnpm run test:snapshot:refresh` 保持无密钥,运行回放 overlay,并从已提交模型脚本重写 stdout、可比较会话日志预期输出,以及各 pin 自有的提示词与工具 schema 伴随文件。Fixture 角色、录制/回放/刷新语义和场景表字段记录在 `Scenario` 以及[快照 Agent Note](../../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.zh.md) 中。
约束:`suite.ts` 与 `harness.ts` 导入 vitest(harness 通过 `vi.waitFor` 轮询其持久边界等待),因此包入口只能在 vitest 运行中导入(启动器和规范化器没有此依赖,但从同一入口发布)。启动器和套件工厂按设计专用于 ACP,启动器使用 SDK 的 `ClientSideConnection`;规范化器是与传输无关的会话日志/文本辅助工具,还由 JSON-RPC 和 Web 快照录制器消费。输入脚本覆盖初始化、新建会话、文本提示简写、精确结构化 ACP 提示词块、取消、预期 RPC 失败和持久轮次边界等待。权限往返是选项类别选择(`allow_once`、`reject_once` 等)的 FIFO 队列,映射到 agent 发出的 `optionId`;缺少或耗尽的队列回答 `cancelled`,未提供类别会拒绝运行。
@@ -75,4 +75,4 @@ defineAcpSnapshotSuite({
- **会话收集需要原始 JSONL mode**:`runScenario` 收集持久化 `.jsonl` 日志,因此快照配置使用 `persistenceCompression: 'none'`;压缩 JSONL 和 SQLite 组合没有快照收集路径。
- **构建 mode 需要当前产物**:先运行 `pnpm run build`,再选择 `DSH_EXAMPLE_MODE=lib`;源 mode 仍是零构建路径。
-- **后端覆盖仍使用 ACP 驱动器**:保留场景为何使用该传输,见[仅自动化 ACP 决策](../../../.agents/notes/implemented/simplification/2026-07-23-acp-automation-only-protocol.md#snapshot-boundary)。
+- **后端覆盖仍使用 ACP 驱动器**:保留场景为何使用该传输,见[仅自动化 ACP 决策](../../../.agents/notes/implemented/simplification/2026-07-23-acp-automation-only-protocol.zh.md#snapshot-boundary)。
diff --git a/packages/test-support/client-runtime/tests/remote.client.spec.ts b/packages/test-support/client-runtime/tests/remote.client.spec.ts
index 3f11c82323..90b1bde56f 100644
--- a/packages/test-support/client-runtime/tests/remote.client.spec.ts
+++ b/packages/test-support/client-runtime/tests/remote.client.spec.ts
@@ -30,7 +30,7 @@ describe('TestRemote', () => {
new TestRemote(ctx)
// No subscriber for this name: the emit must be inert rather than throwing,
// because the wire carries whatever the Host allowlist selected.
- expect(() => { ctx.remote.$dispatch('credentials/updated', ['DEEPSEEK_API_KEY']) }).not.toThrow()
+ expect(() => { ctx.remote.$dispatch('credentials/reference-updated', ['DEEPSEEK_API_KEY']) }).not.toThrow()
await ctx.fiber.dispose()
})
diff --git a/packages/test-support/llm-replay/README.i18n.yaml b/packages/test-support/llm-replay/README.i18n.yaml
index 8e84398842..4ca5510bb4 100644
--- a/packages/test-support/llm-replay/README.i18n.yaml
+++ b/packages/test-support/llm-replay/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/test-support/llm-replay/README.md
README.md: 24ee2684ad5ff393b95db0075950fe40bce2e0dd
-README.zh.md: 5adb16e08604081ebc8db4e07a8a22c031d02ae2
+README.zh.md: db45ce04fb4a63ba5dfe44ff7ddc1917aee89d4c
diff --git a/packages/test-support/llm-replay/README.zh.md b/packages/test-support/llm-replay/README.zh.md
index 5adb16e086..db45ce04fb 100644
--- a/packages/test-support/llm-replay/README.zh.md
+++ b/packages/test-support/llm-replay/README.zh.md
@@ -64,7 +64,7 @@ fixture 就是持久化的会话日志(`/session.jsonl`)。其 `as
## 插件导出形态
-命名导出 `name` / `inject` / `Config` / `apply`,且**没有默认导出**:Cordis Loader 的 `unwrapExports` 执行 `exports.default ?? exports`,因此意外的默认导出会将模块折叠为函数本身,并丢弃 `inject` 命名空间(见 [docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md))。
+命名导出 `name` / `inject` / `Config` / `apply`,且**没有默认导出**:Cordis Loader 的 `unwrapExports` 执行 `exports.default ?? exports`,因此意外的默认导出会将模块折叠为函数本身,并丢弃 `inject` 命名空间(见 [docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.zh.md))。
## 模型体验
diff --git a/packages/todo/README.i18n.yaml b/packages/todo/README.i18n.yaml
index 4df1b35bcb..81736fc9bd 100644
--- a/packages/todo/README.i18n.yaml
+++ b/packages/todo/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/todo/README.md
README.md: 6bbc9fe4f04cc4a82573c643c5c00bbecde069ed
-README.zh.md: ae7de52eeded877e92c94dbc6c6e1addf76aa8b4
+README.zh.md: fa2bd8a6cde5c322151b37b066f0d718c52dfcd0
diff --git a/packages/todo/README.zh.md b/packages/todo/README.zh.md
index ae7de52eed..fa2bd8a6cd 100644
--- a/packages/todo/README.zh.md
+++ b/packages/todo/README.zh.md
@@ -6,8 +6,8 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`tool-todo/`](tool-todo/README.md) | 存储并公开会话的 todo 列表。 | (注册到 `ctx.tools`) |
+| [`tool-todo/`](tool-todo/README.zh.md) | 存储并公开会话的 todo 列表。 | (注册到 `ctx.tools`) |
子级 README 负责工具、持久化和渲染约定。
-事件载荷记录在 [docs/subsystems/session.md](../../docs/subsystems/session.md)。
+事件载荷记录在 [docs/subsystems/session.md](../../docs/subsystems/session.zh.md)。
diff --git a/packages/todo/tool-todo/README.i18n.yaml b/packages/todo/tool-todo/README.i18n.yaml
index 469cb71023..116983a0bb 100644
--- a/packages/todo/tool-todo/README.i18n.yaml
+++ b/packages/todo/tool-todo/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/todo/tool-todo/README.md
README.md: 4848758dd8f2049221901744e5d09cef2dd31591
-README.zh.md: f3e9e50e1c548dc8c0d01b0bae5c91dd813b5899
+README.zh.md: 3c79ca6bba9bb0ffd7e1960dae7000bdb525bdfb
diff --git a/packages/todo/tool-todo/README.zh.md b/packages/todo/tool-todo/README.zh.md
index f3e9e50e1c..3c79ca6bba 100644
--- a/packages/todo/tool-todo/README.zh.md
+++ b/packages/todo/tool-todo/README.zh.md
@@ -26,15 +26,15 @@
## 渲染
-规范结果为 `{ todos, counts: { pending, inProgress, completed } }`;其 Native 渲染器返回精简的更新确认。工具还会写入完整 `todo/write` 会话事件。UI 订阅事件流,并自行渲染该持久化列表:[web 客户端](../../client/ui-conversation)基于当前有效计划(其后没有更晚 `turn/start` 的最近一次 `todo/write`)显示计划条和专属工具行([展示](../../../.agents/notes/implemented/feature/2026-07-23-web-todo-display.md)、[生命周期](../../../.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md))。
+规范结果为 `{ todos, counts: { pending, inProgress, completed } }`;其 Native 渲染器返回精简的更新确认。工具还会写入完整 `todo/write` 会话事件。UI 订阅事件流,并自行渲染该持久化列表:[web 客户端](../../client/ui-conversation)基于当前有效计划(其后没有更晚 `turn/start` 的最近一次 `todo/write`)显示计划条和专属工具行([展示](../../../.agents/notes/implemented/feature/2026-07-23-web-todo-display.zh.md)、[生命周期](../../../.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.zh.md))。
## 会话投影
-当组合挂载了 `ctx.sessionProjections`([`@deepseek-ai/dsh-session-projection`](../../session/session-projection/README.md))时,本包在一个注入的子插件中注册 `todos` 投影单元:`init` = `null`(尚无写入)、`apply` = 从每个 `todo/write` 取整表,并在每个 `turn/start` 清为 `null`(当前有效计划;`turn/end` 保留刚完成的清单;其余事件都返回同一个状态引用)、`view` = 恒等、`stateVersion` = 2。该键在本包中合并进 `SessionProjectionMap`(经 Service Definition 包的 `/types` 出口);框架驱动该单元,载体通过历史尾页与 `session/projection` 推送帧提供该值。未挂载注册表的组合不受影响。生命周期理由见 [在下一轮次清空 todo 计划](../../../.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md)。
+当组合挂载了 `ctx.sessionProjections`([`@deepseek-ai/dsh-session-projection`](../../session/session-projection/README.zh.md))时,本包在一个注入的子插件中注册 `todos` 投影单元:`init` = `null`(尚无写入)、`apply` = 从每个 `todo/write` 取整表,并在每个 `turn/start` 清为 `null`(当前有效计划;`turn/end` 保留刚完成的清单;其余事件都返回同一个状态引用)、`view` = 恒等、`stateVersion` = 2。该键在本包中合并进 `SessionProjectionMap`(经 Service Definition 包的 `/types` 出口);框架驱动该单元,载体通过历史尾页与 `session/projection` 推送帧提供该值。未挂载注册表的组合不受影响。生命周期理由见 [在下一轮次清空 todo 计划](../../../.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.zh.md)。
## 导出形状
-函数/命名空间插件:导出 `name`/`inject`/`apply`,不提供默认导出。意外的 `export default` 会被 Loader 的 `unwrapExports` 折叠为默认导出,并导致 `inject` 丢失(参见 [docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md))。
+函数/命名空间插件:导出 `name`/`inject`/`apply`,不提供默认导出。意外的 `export default` 会被 Loader 的 `unwrapExports` 折叠为默认导出,并导致 `inject` 丢失(参见 [docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.zh.md))。
## 模型体验
@@ -42,7 +42,7 @@
#### 模型看到的内容
-模型会看到生成的 [`todo_write` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-todo)。
+模型会看到生成的 [`todo_write` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-todo)。
#### Token 影响
diff --git a/packages/typert/README.i18n.yaml b/packages/typert/README.i18n.yaml
index 3a061e43fd..35034e4c11 100644
--- a/packages/typert/README.i18n.yaml
+++ b/packages/typert/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/typert/README.md
README.md: ad9f843e48be0e3be85921ed8fd3ca4e2c327160
-README.zh.md: aae74bf89f657f50f4519b2f8a0628fd391f8de3
+README.zh.md: 9ebd00f71a509e24a0b6c674c56d87c411dd7794
diff --git a/packages/typert/README.zh.md b/packages/typert/README.zh.md
index aae74bf89f..9ebd00f71a 100644
--- a/packages/typert/README.zh.md
+++ b/packages/typert/README.zh.md
@@ -6,6 +6,6 @@ Typert 将源代码分析、运行时存储和 Loader 发现机制分离。
| 包 | 职责 | Cordis 键 |
|---|---|---|
-| [`registry/`](registry/README.md) | 存储运行时包反射和 schema | `ctx.typert` |
-| [`loader/`](loader/README.md) | 发现 Loader 条目并注册生成的宿主产物 | 使用 `ctx.loader`、`ctx.typert` |
-| [`generator/`](generator/README.md) | 从源代码类型生成运行时产物 | 构建时库 |
+| [`registry/`](registry/README.zh.md) | 存储运行时包反射和 schema | `ctx.typert` |
+| [`loader/`](loader/README.zh.md) | 发现 Loader 条目并注册生成的宿主产物 | 使用 `ctx.loader`、`ctx.typert` |
+| [`generator/`](generator/README.zh.md) | 从源代码类型生成运行时产物 | 构建时库 |
diff --git a/packages/typert/generator/src/cordis-catalog.ts b/packages/typert/generator/src/cordis-catalog.ts
index c38b7fd9e1..8ce666b96c 100644
--- a/packages/typert/generator/src/cordis-catalog.ts
+++ b/packages/typert/generator/src/cordis-catalog.ts
@@ -951,6 +951,12 @@ function anchorFor(headingText: string): string[] {
return [``, '']
}
+/** Render a subsystem `file:line` source pointer as a file-only link. */
+function sourceLink(source: string): string {
+ const file = source.split(':')[0]
+ return `[\`${file}\`](../../${file})`
+}
+
/** Render one harness event entry onto its owning page, nested under its scope heading. */
function renderEvent(e: EventEntry, onPage: string, linkedTypePages: Readonly>): string[] {
const out = [...anchorFor(`${e.name} — ${e.mode}`), `#### \`${e.name}\` — ${e.mode}`, '']
@@ -958,7 +964,7 @@ function renderEvent(e: EventEntry, onPage: string, linkedTypePages: Readonly method.signature).join('\n'), onPage, linkedTypePages)
if (links) out.push(links, '')
}
- out.push(`Source: [\`${s.source}\`](../../${s.source.split(':')[0]})`, '')
+ out.push(`Source: ${sourceLink(s.source)}`, '')
return out
}
@@ -1010,7 +1016,7 @@ export function renderPageRegion(page: string, services: ServiceEntry[], events:
'',
'## 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).',
+ 'Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).',
'',
]
for (const s of services) lines.push(...renderService(s, page, policy.linkedTypePages))
diff --git a/packages/typert/generator/tests/cordis-catalog.spec.ts b/packages/typert/generator/tests/cordis-catalog.spec.ts
index 5cb9351999..ab62291aed 100644
--- a/packages/typert/generator/tests/cordis-catalog.spec.ts
+++ b/packages/typert/generator/tests/cordis-catalog.spec.ts
@@ -5,8 +5,16 @@ import {
projectCordisCatalog,
renderInheritedPage,
renderPageRegion,
+ type CordisCatalogPolicy,
} from '../src/cordis-catalog.ts'
-import { CORDIS_CATALOG_POLICY, EVENT_SCOPE_PAGE, REGION_BEGIN, REGION_END, SERVICE_PAGE } from '../../../../scripts/gen-cordis-catalog.ts'
+import {
+ CORDIS_CATALOG_POLICY,
+ EVENT_SCOPE_PAGE,
+ localizePageRegion,
+ REGION_BEGIN,
+ REGION_END,
+ SERVICE_PAGE,
+} from '../../../../scripts/gen-cordis-catalog.ts'
const workspaceRoot = resolve(import.meta.dirname, '../../../..')
@@ -15,7 +23,40 @@ let cached: ReturnType | undefined
const projection = (): ReturnType =>
(cached ??= projectCordisCatalog(workspaceRoot, CORDIS_CATALOG_POLICY))
+const SOURCE_LINK_POLICY: CordisCatalogPolicy = {
+ linkedTypePages: {},
+ foundationTypeNames: new Set(),
+ typeLinkExemptions: {},
+ inheritedEvents: [{ name: 'ready', summary: 'Ready.', source: 'vendor/cordis/src/events.ts:9' }],
+ inheritedServices: [{ name: 'ctx.root', summary: 'Root.', source: 'vendor/cordis/src/context.ts:12' }],
+}
+
describe('Typert-backed Cordis catalog', () => {
+ it('omits subsystem source lines while preserving inherited Cordis source lines', () => {
+ const page = renderPageRegion('fixture.md', [{
+ key: 'fixture',
+ type: 'Fixture',
+ abstract: false,
+ doc: 'Fixture.',
+ methods: [],
+ source: 'packages/fixture/service.ts:24',
+ }], [{
+ name: 'fixture/ready',
+ scope: 'fixture',
+ signature: "'fixture/ready'(): void",
+ jsDoc: '/** Ready. */',
+ mode: 'emit',
+ doc: 'Ready.',
+ source: 'packages/fixture/events.ts:42',
+ }], SOURCE_LINK_POLICY)
+ const inherited = renderInheritedPage(SOURCE_LINK_POLICY)
+
+ expect(page).toContain('Source: [`packages/fixture/events.ts`](../../packages/fixture/events.ts)')
+ expect(page).toContain('Source: [`packages/fixture/service.ts`](../../packages/fixture/service.ts)')
+ expect(inherited).toContain('([`vendor/cordis/src/events.ts:9`](../../vendor/cordis/src/events.ts))')
+ expect(inherited).toContain('([`vendor/cordis/src/context.ts:12`](../../vendor/cordis/src/context.ts))')
+ })
+
it('reproduces every committed catalog artifact byte for byte', { timeout: 480_000 }, () => {
const { projector, model } = projection()
const expected = (path: string): string => readFileSync(join(workspaceRoot, path), 'utf8')
@@ -29,11 +70,14 @@ describe('Typert-backed Cordis catalog', () => {
CORDIS_CATALOG_POLICY,
)
for (const side of [page, page.replace(/\.md$/, '.zh.md')]) {
- const committed = expected(`docs/subsystems/${side}`)
+ const rel = `docs/subsystems/${side}`
+ const committed = expected(rel)
const begin = committed.indexOf(REGION_BEGIN)
const end = committed.indexOf(REGION_END)
- expect(begin, `docs/subsystems/${side} carries the region`).toBeGreaterThanOrEqual(0)
- expect(committed.slice(begin, end + REGION_END.length)).toBe(region)
+ expect(begin, `${rel} carries the region`).toBeGreaterThanOrEqual(0)
+ expect(committed.slice(begin, end + REGION_END.length)).toBe(
+ localizePageRegion(region, rel, workspaceRoot),
+ )
}
}
expect(projector.renderRuntimeApi(model)).toBe(
diff --git a/packages/typert/loader/README.i18n.yaml b/packages/typert/loader/README.i18n.yaml
index 03abaafe53..b85f70d24a 100644
--- a/packages/typert/loader/README.i18n.yaml
+++ b/packages/typert/loader/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/typert/loader/README.md
README.md: ab9293de1630fdbe8c560bb9e6d00c272cc34161
-README.zh.md: 4e962c05b73982f03accab938cf3282dfe333dbc
+README.zh.md: d5f9d8e5de5d47ed424f4ed03a12c03cb1b53997
diff --git a/packages/typert/loader/README.zh.md b/packages/typert/loader/README.zh.md
index 4e962c05b7..d5f9d8e5de 100644
--- a/packages/typert/loader/README.zh.md
+++ b/packages/typert/loader/README.zh.md
@@ -12,7 +12,7 @@
## 模型体验
-无。loader 只向 [`ctx.typert`](../registry/README.md) 提供注册项;任何模型可见投影均由消费方负责。
+无。loader 只向 [`ctx.typert`](../registry/README.zh.md) 提供注册项;任何模型可见投影均由消费方负责。
#### KV Cache 影响
diff --git a/packages/typert/registry/README.i18n.yaml b/packages/typert/registry/README.i18n.yaml
index 5730472079..b3c081b128 100644
--- a/packages/typert/registry/README.i18n.yaml
+++ b/packages/typert/registry/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/typert/registry/README.md
README.md: fa227b1c8faf1abd5a6492d4b8fe7d0c51ceeef1
-README.zh.md: b708e536acb7a5c4dc47e8c83180728e2ce150cd
+README.zh.md: 62c5a2141de9ccdd564654658a464d721b77f97d
diff --git a/packages/typert/registry/README.zh.md b/packages/typert/registry/README.zh.md
index b708e536ac..62c5a2141d 100644
--- a/packages/typert/registry/README.zh.md
+++ b/packages/typert/registry/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-生成的 Typert 产物所用的运行时注册表。每个注册项包含某个包在一个 face 上的业务反射信息,以及可选的运行时 Zod schema;`ctx.typert` 会以原子方式同时注册两者,并在发起调用的 Cordis fiber 释放时一并移除它们。TypeScript 分析和代码生成由 [`dsh-typert-generator`](../generator/README.md) 负责。
+生成的 Typert 产物所用的运行时注册表。每个注册项包含某个包在一个 face 上的业务反射信息,以及可选的运行时 Zod schema;`ctx.typert` 会以原子方式同时注册两者,并在发起调用的 Cordis fiber 释放时一并移除它们。TypeScript 分析和代码生成由 [`dsh-typert-generator`](../generator/README.zh.md) 负责。
包反射信息以 `#` 为键。schema 以 `#` 为键,并保留生成方的 Zod 实例。系统按需在消费方边界计算 JSON Schema。
@@ -17,7 +17,7 @@
- `toJSONSchema(key, params?)` 使用 `z.toJSONSchema()` 投影当前有效的 schema,且不缓存结果。
- `typertKey()` 和 `typertPackageKey()` 构造两种稳定的标识形式。
-`@deepseek-ai/dsh-typert-registry/types` 子路径包含注册项和记录的纯类型约定。[`dsh-typert-loader`](../loader/README.md) 会在 Loader 组合中发现并注册生成的宿主侧产物;其他组合所有者可以直接调用 `ctx.typert.register()`。
+`@deepseek-ai/dsh-typert-registry/types` 子路径包含注册项和记录的纯类型约定。[`dsh-typert-loader`](../loader/README.zh.md) 会在 Loader 组合中发现并注册生成的宿主侧产物;其他组合所有者可以直接调用 `ctx.typert.register()`。
## 模型体验
diff --git a/packages/util/README.i18n.yaml b/packages/util/README.i18n.yaml
index c041aed49e..04314b33ef 100644
--- a/packages/util/README.i18n.yaml
+++ b/packages/util/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/util/README.md
README.md: 7867f6c235dfd3cb063214381ac2f40ed0f26ce3
-README.zh.md: 65c48de0de038b4910583f79d9fa0c31ea2d67d0
+README.zh.md: 33ca9912f3670dfd3419b7a2a33f462cb4be95a3
diff --git a/packages/util/README.zh.md b/packages/util/README.zh.md
index 65c48de0de..33ca9912f3 100644
--- a/packages/util/README.zh.md
+++ b/packages/util/README.zh.md
@@ -6,9 +6,9 @@
| 包 | 职责 |
|---|---|
-| [`brand/`](brand/README.md) | 提供带名义品牌的类型 |
-| [`paths/`](home-paths/README.md) | 解析 Harness 数据根目录和共享路径 |
-| [`timeout/`](timeout/README.md) | 提供截止时间和超时分类原语 |
-| [`retention/`](output-retention/README.md) | 限制保留文本和项集合的大小 |
-| [`atomic-write/`](atomic-write/README.md) | 以原子方式替换文件 |
-| [`native-command/`](native-command/README.md) | 不经 shell 运行宿主原生命令 |
+| [`brand/`](brand/README.zh.md) | 提供带名义品牌的类型 |
+| [`paths/`](home-paths/README.zh.md) | 解析 Harness 数据根目录和共享路径 |
+| [`timeout/`](timeout/README.zh.md) | 提供截止时间和超时分类原语 |
+| [`retention/`](output-retention/README.zh.md) | 限制保留文本和项集合的大小 |
+| [`atomic-write/`](atomic-write/README.zh.md) | 以原子方式替换文件 |
+| [`native-command/`](native-command/README.zh.md) | 不经 shell 运行宿主原生命令 |
diff --git a/packages/util/atomic-write/README.i18n.yaml b/packages/util/atomic-write/README.i18n.yaml
index c54e974be8..98e35ef83a 100644
--- a/packages/util/atomic-write/README.i18n.yaml
+++ b/packages/util/atomic-write/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/util/atomic-write/README.md
-README.md: 4d0b55291955c9d37f4788c7d37ad8e6ce728f70
-README.zh.md: c2d7f0b49fa123befbb663ac43862a40b4ef19b4
+README.md: 0e6501c0ae35bf26d7df67f28ce0ae24c85ecca6
+README.zh.md: 377f85f5c4aab74e8c04068b616de32a8ba063d6
diff --git a/packages/util/atomic-write/README.md b/packages/util/atomic-write/README.md
index 4d0b552919..0e6501c0ae 100644
--- a/packages/util/atomic-write/README.md
+++ b/packages/util/atomic-write/README.md
@@ -30,6 +30,8 @@ await withFileLock('/home/u/.dsh/settings.yaml', async () => {
`withFileLock` serializes the writers of one file across processes, for the read-render-commit cycles a bare atomic commit cannot make safe on its own. The lock is a `wx`-created `.lock` sibling, so readers never contend; waiters back off exponentially and fail with a timeout rather than block forever. `EEXIST` identifies contention directly; `EPERM` does so only when a fresh `lstat` confirms that the lock path exists, covering Windows exclusive-create behavior without hiding an unrelated permission failure. A contender never removes the existing lock: age cannot distinguish a crashed owner from a paused live writer.
+How long a contender waits is a property of the operation the holder runs, so it is stated per call through `waitMs`. The default is sized for file work alone; a holder whose cycle includes a network round trip — a credential mutation that refreshes an expired token — states a longer one, because leaving the default would fail every other writer of that file for the duration. The retry cadence stays fixed: it governs how often a contender asks, which no caller has a reason to vary.
+
## Model Experience
None, as this is a pure filesystem primitive; nothing here reaches a model request.
diff --git a/packages/util/atomic-write/README.zh.md b/packages/util/atomic-write/README.zh.md
index c2d7f0b49f..377f85f5c4 100644
--- a/packages/util/atomic-write/README.zh.md
+++ b/packages/util/atomic-write/README.zh.md
@@ -30,6 +30,8 @@ await withFileLock('/home/u/.dsh/settings.yaml', async () => {
`withFileLock` 跨进程串行化同一文件的写入方,服务于单靠原子提交无法保证安全的读-渲染-提交循环。锁是以 `wx` 创建的同目录 `.lock`,因此读取方从不参与竞争;等待方按指数退避,超时即失败而非无限阻塞。`EEXIST` 直接表示竞争;只有一次新的 `lstat` 确认锁路径存在时,`EPERM` 才表示竞争,从而兼容 Windows 的独占创建行为,又不掩盖无关的权限故障。竞争者绝不移除现有锁:锁龄无法区分已经崩溃的所有者与被暂停但仍存活的写入方。
+等待多久是持锁方所跑操作的属性,因此由每次调用经 `waitMs` 声明。默认值只按纯文件工作量级选定;若持锁方的循环包含一次网络往返——例如刷新过期 token 的凭据变更——就应声明更长的值,否则该文件的其他写入方会在这段时间内全部失败。退避节奏保持固定:它决定竞争者多久问一次,调用方没有理由改变它。
+
## 模型体验
无:本包是纯文件系统原语,此处没有任何内容会到达模型请求。
diff --git a/packages/util/atomic-write/src/index.ts b/packages/util/atomic-write/src/index.ts
index 21c9de5f35..3e5764a329 100644
--- a/packages/util/atomic-write/src/index.ts
+++ b/packages/util/atomic-write/src/index.ts
@@ -78,14 +78,36 @@ async function isLockContention(error: unknown, lockPath: string): Promise(
filename: string,
operation: () => Promise,
+ options?: FileLockOptions,
): Promise {
const lockPath = `${filename}.lock`
- const deadline = Date.now() + LOCK_TIMEOUT_MS
+ const deadline = Date.now() + (options?.waitMs ?? DEFAULT_LOCK_WAIT_MS)
let delay = LOCK_RETRY_INITIAL_MS
for (;;) {
try {
diff --git a/packages/util/atomic-write/tests/atomic-write.spec.ts b/packages/util/atomic-write/tests/atomic-write.spec.ts
index 42cbd287c0..683abe51bc 100644
--- a/packages/util/atomic-write/tests/atomic-write.spec.ts
+++ b/packages/util/atomic-write/tests/atomic-write.spec.ts
@@ -28,6 +28,18 @@ async function scratch(): Promise {
return mkdtemp(join(tmpdir(), 'dsh-atomic-write-'))
}
+/** Resolve once the lockfile exists, so contention is measured against a held lock. */
+async function waitForLock(lockPath: string): Promise {
+ for (;;) {
+ try {
+ await stat(lockPath)
+ return
+ } catch {
+ await new Promise(resolve => setTimeout(resolve, 5))
+ }
+ }
+}
+
describe('writeFileAtomic', () => {
it('creates the file and its parents with exactly the stated mode', async () => {
const dir = await scratch()
@@ -105,4 +117,33 @@ describe('withFileLock', () => {
})).rejects.toThrow(/ENOENT|ENOTDIR|not a directory/i)
expect(called).toBe(false)
})
+
+ it('waits for the caller-stated limit rather than the protocol default', async () => {
+ // An operation whose work includes a network round trip legitimately holds
+ // the lock far longer than the render-and-rename the default was sized
+ // for. The limit is per call so one such operation cannot fail every other
+ // writer of the same file, and a caller that states a short one still
+ // fails fast.
+ const dir = await scratch()
+ const target = join(dir, 'document')
+ let release = (): void => {}
+ const held = new Promise((resolve) => { release = resolve })
+ const holder = withFileLock(target, () => held)
+ // The holder owns the lock once its lockfile exists; contending before
+ // that would measure nothing.
+ await waitForLock(`${target}.lock`)
+
+ // Elapsed time is the assertion that distinguishes a honoured limit from
+ // the ignored argument: without it the contender simply waits out the
+ // protocol default and fails with the same message.
+ const startedAt = Date.now()
+ await expect(withFileLock(target, async () => 'impatient', { waitMs: 50 }))
+ .rejects.toThrow(/timed out waiting for the writer lock/)
+ expect(Date.now() - startedAt).toBeLessThan(1_000)
+
+ const patient = withFileLock(target, async () => 'patient', { waitMs: 10_000 })
+ release()
+ await holder
+ expect(await patient).toBe('patient')
+ })
})
diff --git a/packages/util/launch-environment/README.i18n.yaml b/packages/util/launch-environment/README.i18n.yaml
index b28e720234..53a8b13705 100644
--- a/packages/util/launch-environment/README.i18n.yaml
+++ b/packages/util/launch-environment/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/util/launch-environment/README.md
README.md: 80ec4d59c38ee296e836f8cff52e3a89b0700ff5
-README.zh.md: 5dc9bfd1fc70e5432ca9f62e81a7d7a23f4a48c9
+README.zh.md: 8fb8df559c1469b25543c15fe03ee7cf67492304
diff --git a/packages/util/launch-environment/README.zh.md b/packages/util/launch-environment/README.zh.md
index 5dc9bfd1fc..8fb8df559c 100644
--- a/packages/util/launch-environment/README.zh.md
+++ b/packages/util/launch-environment/README.zh.md
@@ -32,5 +32,5 @@ const endpoint = launchEnvironmentOf(ctx).get('DEEPSEEK_BASE_URL')?.value
## 已知限制与暂缓事项
-- **快照不是子进程边界**:每一层同样会被物化进 `process.env`,因此项目里的普通变量会按 [`dsh-subprocess`](../../subprocess/subprocess/README.md) 的清洗规则抵达子进程。产品启动器的 [`.env` 约定](../../boot/app-boot/README.md#profiles) 会在物化之前拒绝 bootstrap 变量。
+- **快照不是子进程边界**:每一层同样会被物化进 `process.env`,因此项目里的普通变量会按 [`dsh-subprocess`](../../subprocess/subprocess/README.zh.md) 的清洗规则抵达子进程。产品启动器的 [`.env` 约定](../../boot/app-boot/README.zh.md#profiles) 会在物化之前拒绝 bootstrap 变量。
- **没有按工作区划分的层**:项目层是*调用*目录,在启动时固定。之后在 Web UI 中选择的工作区不贡献任何内容,这是刻意的:跟随它等于让模型自己的工作区在会话中途改变 harness 的环境。
diff --git a/packages/util/native-command/README.i18n.yaml b/packages/util/native-command/README.i18n.yaml
index e890eca65d..abc1f7ed2a 100644
--- a/packages/util/native-command/README.i18n.yaml
+++ b/packages/util/native-command/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/util/native-command/README.md
README.md: 8a552ac36fac150ae18b2a8adaab1dd489ecb476
-README.zh.md: 618e50afe7cd157b2dfc4763adfbf1d5d88d213e
+README.zh.md: 7b5ea7fa1985d394b88fd5bcdbce9d9831eef8ee
diff --git a/packages/util/native-command/README.zh.md b/packages/util/native-command/README.zh.md
index 618e50afe7..7b5ea7fa19 100644
--- a/packages/util/native-command/README.zh.md
+++ b/packages/util/native-command/README.zh.md
@@ -4,7 +4,7 @@
宿主原生 OS 集成共享的**零依赖免 shell `execFile` 运行器**:一次 `runNativeCommand(command, args, signal)` 调用直接 spawn 可执行文件(绝不拼 shell 字符串),以 utf8 捕获 stdout/stderr,把调用方的 abort 传播为子进程终止,并在 Windows 上隐藏瞬时控制台窗口。失败时,调用会以错误拒绝;该错误附带退出 `code` 与两路已捕获输出,调用方无需重跑即可分类(工具缺失、已取消、真实失败)。
-它的两个消费方都是宿主侧原生集成:[`directory-picker-native`](../../host/directory-picker-native/README.md) 后端的 OS 选择器命令,以及网关将路径交由默认应用打开的操作([`dsh-host-apiproxy`](../../host/apiproxy/README.md) 的 `host.openPath`)。`NativeCommandRunner` 类型是这些调用方的可注入命令边界。
+它的两个消费方都是宿主侧原生集成:[`directory-picker-native`](../../host/directory-picker-native/README.zh.md) 后端的 OS 选择器命令,以及网关将路径交由默认应用打开的操作([`dsh-host-apiproxy`](../../host/apiproxy/README.zh.md) 的 `host.openPath`)。`NativeCommandRunner` 类型是这些调用方的可注入命令边界。
它是**库,不是服务或插件**:没有 `ctx`、不注册任何东西、不持有状态、不发事件。
diff --git a/packages/util/output-retention/README.i18n.yaml b/packages/util/output-retention/README.i18n.yaml
index ede6c1260f..aa30e2695f 100644
--- a/packages/util/output-retention/README.i18n.yaml
+++ b/packages/util/output-retention/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/util/output-retention/README.md
README.md: e1d2c828fd4c6526cc391e7f93d7fe20df26e268
-README.zh.md: 36730ab83dc7f6a93376a8f0272160d29935e6e0
+README.zh.md: 51055dd33e2cce3b82a1d15d6994f9d9f37a0579
diff --git a/packages/util/output-retention/README.zh.md b/packages/util/output-retention/README.zh.md
index 36730ab83d..51055dd33e 100644
--- a/packages/util/output-retention/README.zh.md
+++ b/packages/util/output-retention/README.zh.md
@@ -4,7 +4,7 @@
一个轻依赖的**保留**库:为必须限制返回上下文量的工具提供有界的面向模型输出。调用方将项或文本分片送入有界对象,然后取回保留的内容和精确的省略元数据。
-该库**只**负责这个机制问题:*「我们保留了什么,又省略了什么?」*。工具专用代码保留其业务语义:文件分组、行号、退出码、提供方错误状态、每行预览截断、spill 文件以及面向模型的文案。这就是 [Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md) 划定的边界。
+该库**只**负责这个机制问题:*「我们保留了什么,又省略了什么?」*。工具专用代码保留其业务语义:文件分组、行号、退出码、提供方错误状态、每行预览截断、spill 文件以及面向模型的文案。这就是 [Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.zh.md) 划定的边界。
它是**库,而非服务或插件**:没有 `ctx`,不注册任何内容,不发出任何事件。状态只存在于每个 retainer(一次累积)中,绝不跨调用。工具包直接导入它。
diff --git a/packages/util/timeout/README.i18n.yaml b/packages/util/timeout/README.i18n.yaml
index c000fae463..43c4165a7c 100644
--- a/packages/util/timeout/README.i18n.yaml
+++ b/packages/util/timeout/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/util/timeout/README.md
README.md: c29f6a7661abf40eadafc7637c1bbca509a62a24
-README.zh.md: ca05fcaf0ac7f3379d746e11a123294e3febf436
+README.zh.md: 38bbebe1462e4351a207b28a831d1274d9578e58
diff --git a/packages/util/timeout/README.zh.md b/packages/util/timeout/README.zh.md
index ca05fcaf0a..38bbebe146 100644
--- a/packages/util/timeout/README.zh.md
+++ b/packages/util/timeout/README.zh.md
@@ -4,7 +4,7 @@
超时的**时序与分类**部分:一个零依赖纯函数库(无运行时 harness 依赖),由每个需要限制调用方超时提示、启动 deadline,并在之后区分「已超时」与「已取消」的能力共享。
-它**不负责终止**。它发出的信号只会*通知*;真正停止工作仍由各能力负责,因为机制各不相同:bash 对操作系统进程组发送 SIGKILL,web 关闭 `fetch` 套接字,没有任何共享层能够承担全部终止机制。[Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md) 将边界划定为:共享时序/分类,将强制终止保留在本地。
+它**不负责终止**。它发出的信号只会*通知*;真正停止工作仍由各能力负责,因为机制各不相同:bash 对操作系统进程组发送 SIGKILL,web 关闭 `fetch` 套接字,没有任何共享层能够承担全部终止机制。[Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md) 将边界划定为:共享时序/分类,将强制终止保留在本地。
它是**库,而非服务或插件**:没有 `ctx`,不注册任何内容,不持有状态,也不发出事件。「超时服务」必须了解如何停止每项能力的工作,这正是微内核要排除在共享层之外的知识。
@@ -52,7 +52,7 @@ export async function runWithDeadline(upstream: AbortSignal | undefined, timeout
## 哪些操作不设置超时
-本地文件 `read`/`write`/`edit` 不接受 `timeoutMs`:文件 IO 不设时限地运行,因为截止时间会中止操作系统仍会完成的工作。详见[文件系统子系统页面](../../../docs/subsystems/filesystem.md)。
+本地文件 `read`/`write`/`edit` 不接受 `timeoutMs`:文件 IO 不设时限地运行,因为截止时间会中止操作系统仍会完成的工作。详见[文件系统子系统页面](../../../docs/subsystems/filesystem.zh.md)。
## 模型体验
diff --git a/packages/web/README.i18n.yaml b/packages/web/README.i18n.yaml
index 19da6ba386..06a41eba22 100644
--- a/packages/web/README.i18n.yaml
+++ b/packages/web/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/web/README.md
README.md: fc37d7cdead59138db149b5a86f0a0c031d40037
-README.zh.md: 53fe673ddaed235bbb938db4757e9cd240928d61
+README.zh.md: 40a64e09b85b0655739f73abe6388d6cc2b40a0d
diff --git a/packages/web/README.zh.md b/packages/web/README.zh.md
index 53fe673dda..40a64e09b8 100644
--- a/packages/web/README.zh.md
+++ b/packages/web/README.zh.md
@@ -6,13 +6,13 @@
| 包 | 职责 | ctx key |
|---|---|---|
-| [`web/`](web/README.md) | 定义 web 提供方注册、选择和共享错误 | `ctx.web` |
-| [`web-search-exa/`](web-search-exa/README.md) | 通过 Exa 提供 web 搜索 | 注册到 `ctx.web` |
-| [`web-search-perplexity/`](web-search-perplexity/README.md) | 通过 Perplexity 提供 web 搜索 | 注册到 `ctx.web` |
-| [`web-search-deepseek/`](web-search-deepseek/README.md) | 提供 DeepSeek 原生 web 搜索 | 注册到 `ctx.web` |
-| [`web-fetch-http/`](web-fetch-http/README.md) | 抓取公共 HTTP 和 HTTPS 资源 | 注册到 `ctx.web` |
-| [`tool-web/`](tool-web/README.md) | 向模型公开 web 搜索和抓取 | 注册到 `ctx.tools` |
+| [`web/`](web/README.zh.md) | 定义 web 提供方注册、选择和共享错误 | `ctx.web` |
+| [`web-search-exa/`](web-search-exa/README.zh.md) | 通过 Exa 提供 web 搜索 | 注册到 `ctx.web` |
+| [`web-search-perplexity/`](web-search-perplexity/README.zh.md) | 通过 Perplexity 提供 web 搜索 | 注册到 `ctx.web` |
+| [`web-search-deepseek/`](web-search-deepseek/README.zh.md) | 提供 DeepSeek 原生 web 搜索 | 注册到 `ctx.web` |
+| [`web-fetch-http/`](web-fetch-http/README.zh.md) | 抓取公共 HTTP 和 HTTPS 资源 | 注册到 `ctx.web` |
+| [`tool-web/`](tool-web/README.zh.md) | 向模型公开 web 搜索和抓取 | 注册到 `ctx.tools` |
-[web 能力决策](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)记录了搜索和抓取共用一项提供方选择服务的原因。
+[web 能力决策](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md)记录了搜索和抓取共用一项提供方选择服务的原因。
-子系统参考——搜索/抓取请求与结果、可用性、`WebError`——见 [docs/subsystems/web.md](../../docs/subsystems/web.md);依据(含延后的 SSRF 防护)见 [web 能力 seam Agent Note](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)。
+子系统参考——搜索/抓取请求与结果、可用性、`WebError`——见 [docs/subsystems/web.md](../../docs/subsystems/web.zh.md);依据(含延后的 SSRF 防护)见 [web 能力 seam Agent Note](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md)。
diff --git a/packages/web/tool-web/README.i18n.yaml b/packages/web/tool-web/README.i18n.yaml
index bfba84d22d..5af88ca380 100644
--- a/packages/web/tool-web/README.i18n.yaml
+++ b/packages/web/tool-web/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/web/tool-web/README.md
README.md: 787b70a5070f48a3bac6435d5d7e8b64c01e0341
-README.zh.md: bce95dbc0eea1bebe59b0b80cafae2a4eb948aa6
+README.zh.md: f0185deffa8643317f5f01f7e1c3af7af1ce1194
diff --git a/packages/web/tool-web/README.zh.md b/packages/web/tool-web/README.zh.md
index bce95dbc0e..f0185deffa 100644
--- a/packages/web/tool-web/README.zh.md
+++ b/packages/web/tool-web/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-面向模型的 web 工具套件 `web_search` 与 `web_fetch`,构建于 [web 能力 seam](../web/README.md)(`ctx.web`)之上。它只负责面向模型的事项:工具名称、JSON Schema、snake_case 参数名称、提示词区段、结果数量上限、结果格式、HTML→markdown 呈现,以及 UI 呈现投影——`presentCall`、`presentResult`(以 `kind: 'search' | 'fetch'` 区分的 `card: 'web'` 结果卡片),以及承载有损渲染文本无法携带的结构化搜索来源或抓取摘要的 `output.presentationMeta`(见 [web-result-card Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card.md))。所有 web 访问都通过 `ctx.web`;该包绝不导入具体提供方。两个工具都不公开面向模型的超时:每个工具的协作式工具调用超时预算通过配置在此声明(`fetchTimeoutMs`/`searchTimeoutMs`,附加为 `ToolDefinition.timeoutMs`),由 [`@deepseek-ai/dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.md)(`tools/execute` 包装层)强制执行。单项操作会转发 `exec.signal`;多查询搜索会把它与批次取消信号融合,使失败查询能够中止其余查询。
+面向模型的 web 工具套件 `web_search` 与 `web_fetch`,构建于 [web 能力 seam](../web/README.zh.md)(`ctx.web`)之上。它只负责面向模型的事项:工具名称、JSON Schema、snake_case 参数名称、提示词区段、结果数量上限、结果格式、HTML→markdown 呈现,以及 UI 呈现投影——`presentCall`、`presentResult`(以 `kind: 'search' | 'fetch'` 区分的 `card: 'web'` 结果卡片),以及承载有损渲染文本无法携带的结构化搜索来源或抓取摘要的 `output.presentationMeta`(见 [web-result-card Agent Note](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card.zh.md))。所有 web 访问都通过 `ctx.web`;该包绝不导入具体提供方。两个工具都不公开面向模型的超时:每个工具的协作式工具调用超时预算通过配置在此声明(`fetchTimeoutMs`/`searchTimeoutMs`,附加为 `ToolDefinition.timeoutMs`),由 [`@deepseek-ai/dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.zh.md)(`tools/execute` 包装层)强制执行。单项操作会转发 `exec.signal`;多查询搜索会把它与批次取消信号融合,使失败查询能够中止其余查询。
每个工具独立注册;只需要其中一个工具的产品可以通过配置禁用另一个(`{ search: false }`/`{ fetch: false }`)。仅当抓取也通过配置启用时,搜索指引才会提及 `web_fetch`;仅启用搜索的组合则会要求模型使用返回的 snippet 并引用其 URL。
@@ -29,7 +29,7 @@
| `searchTimeoutMs` | `30000` | `web_search` 的协作式工具调用超时预算(ms)。 |
| `fetchMaxOutputChars` | `200000` | 同步转换的源字符数与单次完整 `web_fetch` 输出的上限(状态头、渲染后的主体与页脚合并计算);主体被截断时,在能容纳的情况下附带截断提示。 |
-`searchMaxQueries` 在完全相同的字符串去重前限制可接受数组、提供方请求扇出与组合后的提供方答案增长;校验会在任何搜索开始前拒绝超限数组,随后分发只保留每个查询第一次出现的位置。该设置与各提供方自己的 `maxUses` 等控制项共同构成产品的搜索预算;通用 seam 不公开提供方内部的原生搜索计数。`fetchTimeoutMs`/`searchTimeoutMs` 声明每个工具的协作式超时预算(附加为 `ToolDefinition.timeoutMs`),由 [`@deepseek-ai/dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.md) 强制执行;面向模型的 schema 不公开超时参数。`fetchMaxOutputChars` 同时限制同步转换工作量和完整渲染结果:只转换至多该数量的源字符,随后对状态头、转换后的前缀和截断提示合并设限。默认值为本地提供方的 100,000 字符主体上限留出余量,但渲染膨胀仍可能使最终上限截断结果。
+`searchMaxQueries` 在完全相同的字符串去重前限制可接受数组、提供方请求扇出与组合后的提供方答案增长;校验会在任何搜索开始前拒绝超限数组,随后分发只保留每个查询第一次出现的位置。该设置与各提供方自己的 `maxUses` 等控制项共同构成产品的搜索预算;通用 seam 不公开提供方内部的原生搜索计数。`fetchTimeoutMs`/`searchTimeoutMs` 声明每个工具的协作式超时预算(附加为 `ToolDefinition.timeoutMs`),由 [`@deepseek-ai/dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.zh.md) 强制执行;面向模型的 schema 不公开超时参数。`fetchMaxOutputChars` 同时限制同步转换工作量和完整渲染结果:只转换至多该数量的源字符,随后对状态头、转换后的前缀和截断提示合并设限。默认值为本地提供方的 100,000 字符主体上限留出余量,但渲染膨胀仍可能使最终上限截断结果。
```yaml
- id: tool-web
@@ -80,7 +80,7 @@ Use the web_fetch tool to retrieve the content of a specific HTTP(S) URL (for ex
#### 模型看到的内容
-模型会看到生成的 [`web_search` 与 `web_fetch` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-web)。结果数量与超时预算属于部署设置,不是模型参数。
+模型会看到生成的 [`web_search` 与 `web_fetch` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-web)。结果数量与超时预算属于部署设置,不是模型参数。
#### Token 影响
@@ -150,5 +150,5 @@ schema 校验会在执行前拒绝缺失或非数组的 `queries` 字段以及
- **没有覆盖整个批次的原生搜索计数器**:`searchMaxQueries` 限制 `ctx.web.search` 调用数,但提供方可以在每次调用内执行多次原生搜索。例如,配置了 `maxUses` 的模型型提供方最多可以执行 `searchMaxQueries × maxUses` 次原生搜索;`searchMaxResults` 只限制返回给调用方的组合来源。部署通过这些独立的消费方与提供方设置控制成本,因为通用 seam 不知道提供方内部的搜索计量单位。
- **HTML→markdown 转换会在 GFM 无法安全表示的输入上降级**:[turndown](https://github.com/mixmark-io/turndown)(带 GFM 表格/删除线)通过真实 DOM 转换至多 `fetchMaxOutputChars` 个源字符。保守的 512 层词法守卫会将深层或嵌套有歧义的主体作为原始 HTML 直接透传,转换异常也会如此处理;表格的 `colspan` 会被忽略,因为 GFM 无法表示跨列单元格。这些限制可避免阻塞事件循环,也避免不受信任的数值属性使输出膨胀([已归档的依赖决策](../../../.agents/notes/archived/simplification/2026-07-26-turndown-for-tool-web-html-markdown.md))。
-- **面向模型的接口有意保持精简,后续扩展暂缓**:`max_results` 保持为配置上限(不是模型参数),`web_fetch` 只接受 `url`(没有 `format`/`prompt`/LLM(大语言模型)摘要模式);两项都列为 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md) 中的后续步骤。
+- **面向模型的接口有意保持精简,后续扩展暂缓**:`max_results` 保持为配置上限(不是模型参数),`web_fetch` 只接受 `url`(没有 `format`/`prompt`/LLM(大语言模型)摘要模式);两项都列为 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md) 中的后续步骤。
- **没有 web 专用权限策略**:两个工具都不会请求 `ctx.approval` 就直接执行;需要确认的部署必须添加 `tools/pre-execute` 策略,该包不定义持久化的 URL/域名授权。
diff --git a/packages/web/web-fetch-http/README.i18n.yaml b/packages/web/web-fetch-http/README.i18n.yaml
index 8ba306ea66..078606e11b 100644
--- a/packages/web/web-fetch-http/README.i18n.yaml
+++ b/packages/web/web-fetch-http/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/web/web-fetch-http/README.md
README.md: 5589a8e8605a64ae9ef5f6d9978a9b63331d5b0d
-README.zh.md: e9cf98feb9065947af1321c87562a2ae9ac21315
+README.zh.md: b0dff1d992f9f84cc8b9b9747544ef5e6c0fc3eb
diff --git a/packages/web/web-fetch-http/README.zh.md b/packages/web/web-fetch-http/README.zh.md
index e9cf98feb9..b0dff1d992 100644
--- a/packages/web/web-fetch-http/README.zh.md
+++ b/packages/web/web-fetch-http/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-一个匿名公共 HTTP(S) `WebFetchProvider`,用于 harness [web 能力 seam](../web/README.md)(`ctx.web`)。它获取具体 URL,返回状态码和长度受限的解码内容。
+一个匿名公共 HTTP(S) `WebFetchProvider`,用于 harness [web 能力 seam](../web/README.zh.md)(`ctx.web`)。它获取具体 URL,返回状态码和长度受限的解码内容。
这是一个**实现**包:它向 `ctx.web` 注册提供方,不拥有该键,也不注册面向模型的工具。它是函数/命名空间插件(`inject: ['web']`)。
@@ -10,7 +10,7 @@
提供方拥有**安全资源获取**:URL 验证、HTTP 传输、重定向策略、资源兜底超时、中止传播、字节上限、charset 解码、内容类型分类与二进制拒绝。`@deepseek-ai/dsh-tool-web` 拥有**呈现**(HTML→markdown、截断格式)。非 2xx HTTP 响应是*结果*(状态码 + 解码主体),不是错误;`WebError` 只用于无法安全获取或表示资源的失败。
-提供方的 `timeoutMs` 是直接 `ctx.web.fetch()` 调用方和配置有误的部署所用的资源兜底,不是面向模型的工具调用预算。[`dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.md) 拥有 `web_fetch` 工具调用预算,并让 `exec.signal` 在超时时触发,以强制执行该预算。
+提供方的 `timeoutMs` 是直接 `ctx.web.fetch()` 调用方和配置有误的部署所用的资源兜底,不是面向模型的工具调用预算。[`dsh-tool-call-timeout-policy`](../../guard/timeout-policy/README.zh.md) 拥有 `web_fetch` 工具调用预算,并让 `exec.signal` 在超时时触发,以强制执行该预算。
已交付的 web 工具部署会把提供方兜底设为高于工具预算,因此模型调用通常返回 `TOOL_TIMEOUT`。如果外层截止期限先于提供方的兜底超时触发,提供方会报告 `WEB_ABORTED`,外层策略再将其替换为 `TOOL_TIMEOUT`。因此,`WEB_FETCH_TIMEOUT` 表明直接服务调用方的提供方预算已经耗尽。
@@ -38,7 +38,7 @@
## 模型体验
-通过 [`dsh-tool-web`](../tool-web/README.md) 间接影响;该工具把此提供方经 `maxBodyChars` 限制的解码文本或由 HTML 转换得到的 markdown 置于抓取结果包装层中,并保留提供方失败;重定向、标头与传输机制保持隐藏。
+通过 [`dsh-tool-web`](../tool-web/README.zh.md) 间接影响;该工具把此提供方经 `maxBodyChars` 限制的解码文本或由 HTML 转换得到的 markdown 置于抓取结果包装层中,并保留提供方失败;重定向、标头与传输机制保持隐藏。
#### KV Cache 影响
@@ -46,6 +46,6 @@
## 已知限制与暂缓事项
-- **SSRF/私有网络防护暂缓**:不会阻止私有、loopback、link-local、multicast 或其他非公开目标,也不进行 DNS 解析后验证或逐跳重新验证(见 [web 能力 seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md))。在此功能落地前,该提供方是 SSRF 原语;能够访问敏感内部网络目标的部署**禁止启用它**。
+- **SSRF/私有网络防护暂缓**:不会阻止私有、loopback、link-local、multicast 或其他非公开目标,也不进行 DNS 解析后验证或逐跳重新验证(见 [web 能力 seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md))。在此功能落地前,该提供方是 SSRF 原语;能够访问敏感内部网络目标的部署**禁止启用它**。
- **只解码文本内容**:包括 html/xhtml 与 `text/*` 加 JSON/XML 家族;缺少 `Content-Type` 或任何二进制类型都会抛出 `WEB_UNSUPPORTED_CONTENT_TYPE`,可提取文本的 PDF 解码属于明确的暂缓工作。
- **charset 只来自 `Content-Type` 标头**(默认为 UTF-8):HTML `` 声明会被忽略;声明但无法识别的 charset 标签会抛出异常,而非回退。
diff --git a/packages/web/web-search-deepseek/README.i18n.yaml b/packages/web/web-search-deepseek/README.i18n.yaml
index 653d2b654f..f4eea9bf3d 100644
--- a/packages/web/web-search-deepseek/README.i18n.yaml
+++ b/packages/web/web-search-deepseek/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/web/web-search-deepseek/README.md
README.md: 25162a3e82a05bb17f31a69bc65f6552e42906ad
-README.zh.md: a7a2566ecb36558c581dd801531bd394eb63dea3
+README.zh.md: 65b3c9175357b44ab07f84bdd10a4a250f55f43f
diff --git a/packages/web/web-search-deepseek/README.zh.md b/packages/web/web-search-deepseek/README.zh.md
index a7a2566ecb..65b3c91753 100644
--- a/packages/web/web-search-deepseek/README.zh.md
+++ b/packages/web/web-search-deepseek/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-由 [DeepSeek](https://deepseek.com) 支持的 `WebSearchProvider`,用于 harness [web 能力 seam](../web/README.md)(`ctx.web`)。它调用 DeepSeek 的 **Anthropic 兼容 Messages API**(`POST {baseURL}/messages`),启用原生 `web_search_20250305` 服务器工具,并把 DeepSeek 返回的结构化 `web_search_tool_result` 块映射为 seam 规范化的 `WebSearchResult`。
+由 [DeepSeek](https://deepseek.com) 支持的 `WebSearchProvider`,用于 harness [web 能力 seam](../web/README.zh.md)(`ctx.web`)。它调用 DeepSeek 的 **Anthropic 兼容 Messages API**(`POST {baseURL}/messages`),启用原生 `web_search_20250305` 服务器工具,并把 DeepSeek 返回的结构化 `web_search_tool_result` 块映射为 seam 规范化的 `WebSearchResult`。
这是一个**实现**包:它向 `ctx.web` 注册提供方,通过可选的 `ctx.credentials` seam 为每次搜索解析凭据,若存在发起请求的 agent(智能体)会话,还会在其中记录该辅助请求,且不注册面向模型的工具。与 `@deepseek-ai/dsh-llm-deepseek` 一样,它是函数/命名空间插件(`inject: ['web']`)。Anthropic 协议格式(wire format)是提供方私有细节,并**不**使该提供方依赖 `ctx.llm`。
@@ -68,7 +68,7 @@ DeepSeek 返回的提供方生成答案均不被该提供方信任为 `content`
#### 模型看到的内容
-通过 [`dsh-tool-web`](../tool-web/README.md),会话模型会看到结构化搜索块中去重后的 URL、标题、日期与引用 snippet;提供方文本不会作为答案受到信任。该提供方的具体错误消息包括带有处理指引的凭据缺失消息、`DeepSeek search credential resolution failed: `、`DeepSeek search aborted`、`DeepSeek search request failed: `、`DeepSeek returned no web_search_tool_result blocks; the request may not have triggered native web search` 和 `DeepSeek returned an unprocessable response body: `;HTTP 失败保留提供方消息。错误包装属于消费方。
+通过 [`dsh-tool-web`](../tool-web/README.zh.md),会话模型会看到结构化搜索块中去重后的 URL、标题、日期与引用 snippet;提供方文本不会作为答案受到信任。该提供方的具体错误消息包括带有处理指引的凭据缺失消息、`DeepSeek search credential resolution failed: `、`DeepSeek search aborted`、`DeepSeek search request failed: `、`DeepSeek returned no web_search_tool_result blocks; the request may not have triggered native web search` 和 `DeepSeek returned an unprocessable response body: `;HTTP 失败保留提供方消息。错误包装属于消费方。
#### Token 影响
diff --git a/packages/web/web-search-exa/README.i18n.yaml b/packages/web/web-search-exa/README.i18n.yaml
index 4afbbcbc5d..d94873e683 100644
--- a/packages/web/web-search-exa/README.i18n.yaml
+++ b/packages/web/web-search-exa/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/web/web-search-exa/README.md
README.md: 5b1212c1fb210a3aca7836875cc295fad87e7437
-README.zh.md: fc9afbe44dcb5dd55c7007a11b35048217143351
+README.zh.md: 0d236cb1cb42ab3742d86872a2329060f8ceb09f
diff --git a/packages/web/web-search-exa/README.zh.md b/packages/web/web-search-exa/README.zh.md
index fc9afbe44d..0d236cb1cb 100644
--- a/packages/web/web-search-exa/README.zh.md
+++ b/packages/web/web-search-exa/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-由 [Exa](https://exa.ai) 支持的 `WebSearchProvider`,用于 harness [web 能力 seam](../web/README.md)(`ctx.web`)。它调用 Exa 的 `POST /search` 端点并请求高亮摘要内容,把扁平 `results[]` 映射为 seam 规范化的 `WebSearchResult`。
+由 [Exa](https://exa.ai) 支持的 `WebSearchProvider`,用于 harness [web 能力 seam](../web/README.zh.md)(`ctx.web`)。它调用 Exa 的 `POST /search` 端点并请求高亮摘要内容,把扁平 `results[]` 映射为 seam 规范化的 `WebSearchResult`。
这是一个**实现**包:它向 `ctx.web` 注册提供方,不拥有 `ctx.web` 键,也不注册面向模型的工具(后者属于 `@deepseek-ai/dsh-tool-web`)。与 `@deepseek-ai/dsh-llm-deepseek` 一样,它是函数/命名空间插件(`inject: ['web']`),负责注册后端,而非默认导出服务。
@@ -29,7 +29,7 @@ Exa 返回扁平 `results[]`,不返回生成答案,因此省略 `content`。
## 模型体验
-通过 [`dsh-tool-web`](../tool-web/README.md) 间接影响;该工具保留此提供方经 `maxResults` 限制的 URL、标题、首条 highlight 与发布日期,或将确切的错误消息 `Exa search aborted`、`Exa search request failed: ` 和 `Exa returned an unprocessable response body: ` 置于消费方的错误包装层内;生成答案与提供方私有字段不进入上下文。
+通过 [`dsh-tool-web`](../tool-web/README.zh.md) 间接影响;该工具保留此提供方经 `maxResults` 限制的 URL、标题、首条 highlight 与发布日期,或将确切的错误消息 `Exa search aborted`、`Exa search request failed: ` 和 `Exa returned an unprocessable response body: ` 置于消费方的错误包装层内;生成答案与提供方私有字段不进入上下文。
#### KV Cache 影响
@@ -38,5 +38,5 @@ Exa 返回扁平 `results[]`,不返回生成答案,因此省略 `content`。
## 已知限制与暂缓事项
- **没有非空白高亮摘要的结果会被整个丢弃**:没有可映射的可移植 snippet,因此返回源可能少于请求数量。
-- **只公开 `searchType`/`numResults`/`highlightsPerResult`**:Exa 的其他控制项(livecrawl、category、域名/日期过滤条件、全文内容)等待提供方无关的 Service Definition 字段(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md))。
+- **只公开 `searchType`/`numResults`/`highlightsPerResult`**:Exa 的其他控制项(livecrawl、category、域名/日期过滤条件、全文内容)等待提供方无关的 Service Definition 字段(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md))。
- **按错误形状分类中止**:只有 `DOMException` 且名为 `AbortError` 时才映射为 `WEB_ABORTED`;携带自定义原因的中止(例如 `dsh-timeout` 的 `TimeoutReason`)会呈现为 `WEB_PROVIDER_ERROR`。
diff --git a/packages/web/web-search-perplexity/README.i18n.yaml b/packages/web/web-search-perplexity/README.i18n.yaml
index e0af4f5cbb..0d597f7966 100644
--- a/packages/web/web-search-perplexity/README.i18n.yaml
+++ b/packages/web/web-search-perplexity/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/web/web-search-perplexity/README.md
README.md: 8800609a05e99d3fa8a61e12b2360e633f0fd330
-README.zh.md: 488c1463d28b7d36c34625a3ddbab1aa3417d70c
+README.zh.md: ddf2ef2ef767d7ac2e361e873cefade70ea78115
diff --git a/packages/web/web-search-perplexity/README.zh.md b/packages/web/web-search-perplexity/README.zh.md
index 488c1463d2..ddf2ef2ef7 100644
--- a/packages/web/web-search-perplexity/README.zh.md
+++ b/packages/web/web-search-perplexity/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-由 [Perplexity](https://perplexity.ai) 支持的 `WebSearchProvider`,用于 harness [web 能力 seam](../web/README.md)(`ctx.web`)。它调用 Perplexity 的 OpenAI 兼容 `POST /chat/completions` 端点,把生成答案与引用映射为 seam 规范化的 `WebSearchResult`。
+由 [Perplexity](https://perplexity.ai) 支持的 `WebSearchProvider`,用于 harness [web 能力 seam](../web/README.zh.md)(`ctx.web`)。它调用 Perplexity 的 OpenAI 兼容 `POST /chat/completions` 端点,把生成答案与引用映射为 seam 规范化的 `WebSearchResult`。
这是一个**实现**包:它向 `ctx.web` 注册提供方,不拥有该键,也不注册面向模型的工具。与 `@deepseek-ai/dsh-llm-deepseek` 一样,它是函数/命名空间插件(`inject: ['web']`)。OpenAI 兼容协议格式(wire format)是提供方私有细节,并**不**使该提供方依赖 `ctx.llm`。
@@ -47,7 +47,7 @@
#### 模型看到的内容
-通过 [`dsh-tool-web`](../tool-web/README.md),会话模型会看到生成答案及结构化结果元数据,或只含 URL 的引用。该提供方确切的错误消息为 `Perplexity search aborted`、`Perplexity search request failed: ` 和 `Perplexity returned an unprocessable response body: `;HTTP 失败保留提供方消息。错误包装层属于消费方。
+通过 [`dsh-tool-web`](../tool-web/README.zh.md),会话模型会看到生成答案及结构化结果元数据,或只含 URL 的引用。该提供方确切的错误消息为 `Perplexity search aborted`、`Perplexity search request failed: ` 和 `Perplexity returned an unprocessable response body: `;HTTP 失败保留提供方消息。错误包装层属于消费方。
#### Token 影响
@@ -61,5 +61,5 @@
- **引用回退源只含 URL**:Perplexity 省略结构化 `search_results[]` 时,源不含 `title`/`snippet`/`publishedAt`,因此工具只渲染纯主机名标签。
- **超量返回的来源仍会增加 token 消耗和延迟**:协议没有结果数量控制,`maxResults` 只能由 seam 在事后截断。
-- **只公开 `model`/`maxTokens`/`searchRecency`**:Perplexity 的其他搜索控制项(域名过滤条件、`web_search_options` 上下文大小、图片)有待提供方无关的 Service Definition 字段支持(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md))。
+- **只公开 `model`/`maxTokens`/`searchRecency`**:Perplexity 的其他搜索控制项(域名过滤条件、`web_search_options` 上下文大小、图片)有待提供方无关的 Service Definition 字段支持(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md))。
- **按错误形状分类中止**:只有 `DOMException` 且名为 `AbortError` 时才映射为 `WEB_ABORTED`;携带自定义原因的中止(例如 `dsh-timeout` 的 `TimeoutReason`)会呈现为 `WEB_PROVIDER_ERROR`。
diff --git a/packages/web/web/README.i18n.yaml b/packages/web/web/README.i18n.yaml
index 0b2f806c2a..0b8f351822 100644
--- a/packages/web/web/README.i18n.yaml
+++ b/packages/web/web/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/web/web/README.md
README.md: 8dfc7f032e25e40207bb3880777b814e67175a20
-README.zh.md: 3354037bfcd77ca2b1a07da710b565065baf543b
+README.zh.md: 98922ad83fedb7ded07b2e180f8a6e4261032112
diff --git a/packages/web/web/README.zh.md b/packages/web/web/README.zh.md
index 3354037bfc..98922ad83f 100644
--- a/packages/web/web/README.zh.md
+++ b/packages/web/web/README.zh.md
@@ -56,6 +56,6 @@
## 已知限制与暂缓事项
- **没有观测接口**:没有提供方变更事件或能力状态查询;可用性只能通过执行 `search()`/`fetch()` 并按抛出的 `WebError` code 路由来观测,无提供方失败是通用的 `WEB_PROVIDER_UNAVAILABLE`,不会枚举逐提供方原因(见 [Agent Note](../../../.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md))。
-- **`WebSearchRequest` 只携带 `query` + `maxResults`**:提供方无关的控制项(新近程度、域名过滤条件、区域提示、搜索深度)暂缓至 Exa 与 Perplexity 都能诚实支持时(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md))。
+- **`WebSearchRequest` 只携带 `query` + `maxResults`**:提供方无关的控制项(新近程度、域名过滤条件、区域提示、搜索深度)暂缓至 Exa 与 Perplexity 都能诚实支持时(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md))。
- **`WebFetchBody` 没有 `pdf` 分支**:可提取文本的 PDF 支持属于明确的暂缓工作;封闭联合会使新增该分支成为三个 web 包中由编译强制执行的变更。
- **提供方支持的页面提取不属于 `fetch()` 范围**:Firecrawl/Tavily 风格的 `web_extract` 能力暂缓,而不会扩展抓取操作。
diff --git a/packages/workflow/README.i18n.yaml b/packages/workflow/README.i18n.yaml
index 1b39e5fc7a..ff979da66e 100644
--- a/packages/workflow/README.i18n.yaml
+++ b/packages/workflow/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/workflow/README.md
README.md: 156e9f0d84ced4eab8c25da91d29d239d18b0629
-README.zh.md: 24687f81d612c23786150c5b5a70bce352954663
+README.zh.md: ec65b32bfe2983ae163cb208842969f6f79b5f79
diff --git a/packages/workflow/README.zh.md b/packages/workflow/README.zh.md
index 24687f81d6..ec65b32bfe 100644
--- a/packages/workflow/README.zh.md
+++ b/packages/workflow/README.zh.md
@@ -6,11 +6,11 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`workflow/`](workflow/README.md) | 定义工作流执行和生命周期事件 | `ctx.workflowEngine` |
-| [`workflow-worker-thread/`](workflow-worker-thread/README.md) | 在线程中运行工作流脚本 | 注册到 `ctx.workflowEngine` |
-| [`tool-workflow/`](tool-workflow/README.md) | 向模型公开通用工作流执行 | 注册到 `ctx.tools` |
-| [`tool-ralph/`](tool-ralph/README.md) | 公开使用全新 agent(智能体)的固定 Ralph 工作流 | 注册到 `ctx.tools` |
+| [`workflow/`](workflow/README.zh.md) | 定义工作流执行和生命周期事件 | `ctx.workflowEngine` |
+| [`workflow-worker-thread/`](workflow-worker-thread/README.zh.md) | 在线程中运行工作流脚本 | 注册到 `ctx.workflowEngine` |
+| [`tool-workflow/`](tool-workflow/README.zh.md) | 向模型公开通用工作流执行 | 注册到 `ctx.tools` |
+| [`tool-ralph/`](tool-ralph/README.zh.md) | 公开使用全新 agent(智能体)的固定 Ralph 工作流 | 注册到 `ctx.tools` |
-worker thread 将工作流执行与宿主事件循环隔离,但不构成安全边界。参见[动态工作流](../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md)和 [Ralph 工具](../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md)决策。
+worker thread 将工作流执行与宿主事件循环隔离,但不构成安全边界。参见[动态工作流](../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.zh.md)和 [Ralph 工具](../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.zh.md)决策。
-子系统参考——启动请求、`WorkflowMeta`、结果、实时运行、`workflow/*` 事件——见 [docs/subsystems/workflow.md](../../docs/subsystems/workflow.md);决策见[动态工作流](../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md)与 [Ralph 消费方](../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md) Agent Note。
+子系统参考——启动请求、`WorkflowMeta`、结果、实时运行、`workflow/*` 事件——见 [docs/subsystems/workflow.md](../../docs/subsystems/workflow.zh.md);决策见[动态工作流](../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.zh.md)与 [Ralph 消费方](../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.zh.md) Agent Note。
diff --git a/packages/workflow/tool-ralph/README.i18n.yaml b/packages/workflow/tool-ralph/README.i18n.yaml
index 07a851efb9..d6a1526129 100644
--- a/packages/workflow/tool-ralph/README.i18n.yaml
+++ b/packages/workflow/tool-ralph/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/workflow/tool-ralph/README.md
README.md: ae9d7c80763f751860bc2073950dde331fdd5a8e
-README.zh.md: 692ebb693b54ade2c2fbde18f818ced10e9d5863
+README.zh.md: 9e41b9e527f865a090eaa78ffef30bf6b890357b
diff --git a/packages/workflow/tool-ralph/README.zh.md b/packages/workflow/tool-ralph/README.zh.md
index 692ebb693b..9e41b9e527 100644
--- a/packages/workflow/tool-ralph/README.zh.md
+++ b/packages/workflow/tool-ralph/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-面向模型的 `ralph` 工具运行固定的前台工作流,把一个不可变目标依次交给多个全新子 agent(智能体)。它展示如何把专用编排策略实现为基于 [`ctx.workflowEngine`](../workflow/README.md) 和 [`ctx.subagents`](../../subagent/subagent/README.md) 的普通插件:不会向 `agent-loop` 添加 Ralph 模式或全新 agent loop(智能体循环),同会话的[目标领域](../../goal/goal/README.md)也保持独立。策略和暂缓事项由 [Ralph Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md)负责。
+面向模型的 `ralph` 工具运行固定的前台工作流,把一个不可变目标依次交给多个全新子 agent(智能体)。它展示如何把专用编排策略实现为基于 [`ctx.workflowEngine`](../workflow/README.zh.md) 和 [`ctx.subagents`](../../subagent/subagent/README.zh.md) 的普通插件:不会向 `agent-loop` 添加 Ralph 模式或全新 agent loop(智能体循环),同会话的[目标领域](../../goal/goal/README.zh.md)也保持独立。策略和暂缓事项由 [Ralph Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.zh.md)负责。
## 契约
@@ -59,7 +59,7 @@ Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop o
#### 模型看到的内容
-已生成的 [`ralph` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-ralph)公开一个必填 `objective` 字符串和一个可选 `maxRounds` 数字。提供方选择、交接大小、报告 schema、工作流脚本和编排行为均由部署侧控制,不在调用 schema 中。
+已生成的 [`ralph` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-ralph)公开一个必填 `objective` 字符串和一个可选 `maxRounds` 数字。提供方选择、交接大小、报告 schema、工作流脚本和编排行为均由部署侧控制,不在调用 schema 中。
#### Token 影响
diff --git a/packages/workflow/tool-workflow/README.i18n.yaml b/packages/workflow/tool-workflow/README.i18n.yaml
index 197b47832a..f87a12407f 100644
--- a/packages/workflow/tool-workflow/README.i18n.yaml
+++ b/packages/workflow/tool-workflow/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/workflow/tool-workflow/README.md
README.md: 918fe5b9aff74b3b959107ec1eebfd7966afe6d1
-README.zh.md: f7e62560a1b581cf64d78dc379b821d17f29b3aa
+README.zh.md: 2546a0fc92ef39d71949cc0061dcf363ecc2e22d
diff --git a/packages/workflow/tool-workflow/README.zh.md b/packages/workflow/tool-workflow/README.zh.md
index f7e62560a1..2546a0fc92 100644
--- a/packages/workflow/tool-workflow/README.zh.md
+++ b/packages/workflow/tool-workflow/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-面向模型的 **`workflow` 工具**:运行一段扇出 subagent 的 JavaScript 编排脚本,并返回脚本的最终值。本包负责基于 [`ctx.workflowEngine`](../workflow/README.md) 定义面向模型的 schema 和运行生命周期;脚本解析、执行、上限与取消位于 seam 之后,消费方仍负责面向父级的 schema 和结果包络。
+面向模型的 **`workflow` 工具**:运行一段扇出 subagent 的 JavaScript 编排脚本,并返回脚本的最终值。本包负责基于 [`ctx.workflowEngine`](../workflow/README.zh.md) 定义面向模型的 schema 和运行生命周期;脚本解析、执行、上限与取消位于 seam 之后,消费方仍负责面向父级的 schema 和结果包络。
## 模型看到的内容
@@ -10,7 +10,7 @@
## 生命周期
-收集是同步的(类似 [`dsh-tool-subagent`](../../subagent/tool-subagent/README.md)):`execute` 启动运行并等待 `run.result`;这些操作位于 `try/finally` 中,该结构总会 dispose(资源释放)运行,使脚本及其子 agent(智能体)在每条路径上完全停稳。`exec.signal` 会桥接到 `run.cancel()`,包括启动前已经中止的情况。非 `completed` 结束原因会映射为报告原因的 `isError` 结果,绝不会把局部输出当作成功;`start()` 同步抛出的解析/meta 失败会变成模型可据以修正的 `isError`。完成时返回规范值 `{ runId, agentsStarted, result }`;Native 渲染器保留 meta 名称、agent 数量和 JSON 值,只会在 `maxResultChars` 处截断该投影。
+收集是同步的(类似 [`dsh-tool-subagent`](../../subagent/tool-subagent/README.zh.md)):`execute` 启动运行并等待 `run.result`;这些操作位于 `try/finally` 中,该结构总会 dispose(资源释放)运行,使脚本及其子 agent(智能体)在每条路径上完全停稳。`exec.signal` 会桥接到 `run.cancel()`,包括启动前已经中止的情况。非 `completed` 结束原因会映射为报告原因的 `isError` 结果,绝不会把局部输出当作成功;`start()` 同步抛出的解析/meta 失败会变成模型可据以修正的 `isError`。完成时返回规范值 `{ runId, agentsStarted, result }`;Native 渲染器保留 meta 名称、agent 数量和 JSON 值,只会在 `maxResultChars` 处截断该投影。
对于根 transport 执行(`exec.parent` 缺省),工具还会把运行投影到调用 Agent 的 Session:`start()` 返回后写 run-start,只记录 `run.id` 匹配的成员开始与结束,并且只在 `run.result` 已取得且 `dispose()` 完全停稳后写 run-end。嵌套 transport 调用照常执行,但不写工作流记录。任一次 Session append 首次失败后,本运行会停止后续记录并只告警一次,留下空记录或合法连续前缀,同时不改变工具结果和清理。
@@ -18,7 +18,7 @@
## 渲染意图
-渲染意图预先确定(见[渲染意图 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md)):使用一个 `generic` 卡片,标题为 `workflow: `,直接从 `args.meta.name` 读取(呈现是参数的纯函数,不要求引擎解析);脚本文本作为 `rawInput` 携带。结果继续使用 generic 卡片。
+渲染意图预先确定(见[渲染意图 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.zh.md)):使用一个 `generic` 卡片,标题为 `workflow: `,直接从 `args.meta.name` 读取(呈现是参数的纯函数,不要求引擎解析);脚本文本作为 `rawInput` 携带。结果继续使用 generic 卡片。
## 配置
@@ -53,7 +53,7 @@ Use the tool ONLY when the user explicitly asks for a workflow or for
#### 模型看到的内容
-工具可见时,已生成的默认 [`workflow` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-workflow) 包含完整的 JavaScript 钩子与元数据约定;`toolName` 可以重命名该定义,模型会提交脚本、元数据和可选 args。
+工具可见时,已生成的默认 [`workflow` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-workflow) 包含完整的 JavaScript 钩子与元数据约定;`toolName` 可以重命名该定义,模型会提交脚本、元数据和可选 args。
#### Token 影响
diff --git a/packages/workflow/workflow-worker-thread/README.i18n.yaml b/packages/workflow/workflow-worker-thread/README.i18n.yaml
index 19bb92d976..c761f76437 100644
--- a/packages/workflow/workflow-worker-thread/README.i18n.yaml
+++ b/packages/workflow/workflow-worker-thread/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/workflow/workflow-worker-thread/README.md
README.md: ba32c9dab4e870dd52c8f8acba3cfa627fa78000
-README.zh.md: 99107aec40a45c8460f10082f2b75b908e3e8692
+README.zh.md: b4c30eac3221b7091b2fc5d73149c244f5abf173
diff --git a/packages/workflow/workflow-worker-thread/README.zh.md b/packages/workflow/workflow-worker-thread/README.zh.md
index 99107aec40..b4c30eac32 100644
--- a/packages/workflow/workflow-worker-thread/README.zh.md
+++ b/packages/workflow/workflow-worker-thread/README.zh.md
@@ -105,7 +105,7 @@ worker 错误、消息失败或提前退出会在清理前关闭消息接纳,
#### 模型看到的内容
-通过 [`dsh-tool-workflow`](../tool-workflow/README.md),成功结果只会在该消费方的包装层中公开实体化的最终 JSON 值和子 agent 数量。本引擎提供稳定错误,包括 `workflow script does not parse: `、`invalid meta: `、`agent() requires a non-empty prompt string`、`agent() could not start a child: `、`child agent run failed: `,以及其精确的 `parallel()`、`pipeline()`、`phase()`、选项、schema 和 JSON 边界校验消息。中间子 agent 输出可供脚本使用,但不提供给父模型。
+通过 [`dsh-tool-workflow`](../tool-workflow/README.zh.md),成功结果只会在该消费方的包装层中公开实体化的最终 JSON 值和子 agent 数量。本引擎提供稳定错误,包括 `workflow script does not parse: `、`invalid meta: `、`agent() requires a non-empty prompt string`、`agent() could not start a child: `、`child agent run failed: `,以及其精确的 `parallel()`、`pipeline()`、`phase()`、选项、schema 和 JSON 边界校验消息。中间子 agent 输出可供脚本使用,但不提供给父模型。
#### Token 影响
diff --git a/packages/workflow/workflow/README.i18n.yaml b/packages/workflow/workflow/README.i18n.yaml
index e8f1900e7c..4ed7c36d13 100644
--- a/packages/workflow/workflow/README.i18n.yaml
+++ b/packages/workflow/workflow/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/workflow/workflow/README.md
README.md: cc2c24f62512273ea1657542d0d904b0b870e236
-README.zh.md: 22bda1bd33bc6350c8ff51f27538258bee872fdb
+README.zh.md: 0b02cea896b3cb64559ce110c0da2478728f513c
diff --git a/packages/workflow/workflow/README.zh.md b/packages/workflow/workflow/README.zh.md
index 22bda1bd33..0b02cea896 100644
--- a/packages/workflow/workflow/README.zh.md
+++ b/packages/workflow/workflow/README.zh.md
@@ -58,4 +58,4 @@
- **没有 token 预算词汇**:引擎会限制并发、条目和子 agent,但请求与结果都不会统计跨子 agent 的模型 token。
- **运行由持有方负责,不由服务跟踪**:卸载引擎不会发现独立的活动句柄;每个消费方都必须 dispose 自己启动的运行。
-暂缓实现的工作流接口见[动态工作流 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md)。
+暂缓实现的工作流接口见[动态工作流 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.zh.md)。
diff --git a/packages/workspace/README.i18n.yaml b/packages/workspace/README.i18n.yaml
index 42e824497a..b8e94e910c 100644
--- a/packages/workspace/README.i18n.yaml
+++ b/packages/workspace/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/workspace/README.md
README.md: d12eb9434ec092834a7c34e424edc1d003430b35
-README.zh.md: 796c48cdbd1916c06e9bb914c6a5149769a58b4e
+README.zh.md: 220a18cfeb6ecb9569dfd2bce18bafe6c7aac879
diff --git a/packages/workspace/README.zh.md b/packages/workspace/README.zh.md
index 796c48cdbd..220a18cfeb 100644
--- a/packages/workspace/README.zh.md
+++ b/packages/workspace/README.zh.md
@@ -6,8 +6,8 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
-| [`workspace/`](workspace/README.md) | 注册 workspace 并记录其会话归属 | `ctx.workspaceRegistry` |
+| [`workspace/`](workspace/README.zh.md) | 注册 workspace 并记录其会话归属 | `ctx.workspaceRegistry` |
-[workspace 包参考](workspace/README.md)负责生命周期、持久化和删除语义。
+[workspace 包参考](workspace/README.zh.md)负责生命周期、持久化和删除语义。
-子系统参考——实体、realpath 规范、注册/解析——见 [docs/subsystems/workspace.md](../../docs/subsystems/workspace.md);存储设计见 [domain KV 存储 Agent Note](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md)。
+子系统参考——实体、realpath 规范、注册/解析——见 [docs/subsystems/workspace.md](../../docs/subsystems/workspace.zh.md);存储设计见 [domain KV 存储 Agent Note](../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md)。
diff --git a/packages/workspace/workspace/README.i18n.yaml b/packages/workspace/workspace/README.i18n.yaml
index 20f5d3b867..2c2bd8d4ea 100644
--- a/packages/workspace/workspace/README.i18n.yaml
+++ b/packages/workspace/workspace/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/workspace/workspace/README.md
README.md: c1c4309efdc85f4bc7a6b0f128df6d9f4f810ae1
-README.zh.md: f44ad04be0c63061200a256c5ce8cb24a4d3c54b
+README.zh.md: 4ba66cb5bb83a86cec00b33769f0ac85fef41190
diff --git a/packages/workspace/workspace/README.zh.md b/packages/workspace/workspace/README.zh.md
index f44ad04be0..4ba66cb5bb 100644
--- a/packages/workspace/workspace/README.zh.md
+++ b/packages/workspace/workspace/README.zh.md
@@ -4,7 +4,7 @@
DeepSeek Harness 的 Workspace 实体注册表(`ctx.workspaceRegistry`):通过领域数据形式存储持久 workspace 记录、稳定 workspace 顺序和按新到旧排列的候选会话索引。消费方看到 `Workspace` 接口;实体实现保持包私有。
-实体/存储理由见[领域 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md);仅使用头部的引导初始化和 GUI 排序见 [Workspace UI 产品流 Agent Note](../../../.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.md)。
+实体/存储理由见[领域 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md);仅使用头部的引导初始化和 GUI 排序见 [Workspace UI 产品流 Agent Note](../../../.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.zh.md)。
## 结构
@@ -40,5 +40,5 @@ DeepSeek Harness 的 Workspace 实体注册表(`ctx.workspaceRegistry`):
## 已知限制与暂缓事项
-- 会话删除与破坏性的文件夹移除是彼此独立且尚未提供的功能;删除 Workspace 注册记录绝不能替代二者(参见[决策记录](../../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.md))。
+- 会话删除与破坏性的文件夹移除是彼此独立且尚未提供的功能;删除 Workspace 注册记录绝不能替代二者(参见[决策记录](../../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.zh.md))。
- 头部索引会在启动时刷新,也会在 attach 必须解析未缓存持久 id 时刷新;另一进程执行的删除或造成的 cwd 损坏会在下次刷新或重启后被发现。
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b0078df065..4958f305ce 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2891,9 +2891,15 @@ importers:
'@types/react':
specifier: ~18.3.1
version: 18.3.31
+ '@types/react-dom':
+ specifier: ~18.3.0
+ version: 18.3.7(@types/react@18.3.31)
react:
specifier: ^18.2.0
version: 18.3.1
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.3.1(react@18.3.1)
packages/client/ui-theme:
dependencies:
@@ -3815,6 +3821,21 @@ importers:
specifier: workspace:^
version: link:../../interaction/user-approval
+ packages/credentials/authorization:
+ devDependencies:
+ '@deepseek-ai/cordis':
+ specifier: workspace:^
+ version: link:../../../vendor/cordis
+ '@deepseek-ai/dsh-credentials':
+ specifier: workspace:^
+ version: link:../credentials
+ '@deepseek-ai/dsh-invariants':
+ specifier: workspace:^
+ version: link:../../runtime-diagnostics/invariants
+ '@deepseek-ai/dsh-llm':
+ specifier: workspace:^
+ version: link:../../llm/llm
+
packages/credentials/credentials:
devDependencies:
'@deepseek-ai/cordis':
@@ -5536,6 +5557,9 @@ importers:
'@deepseek-ai/dsh-attachment':
specifier: workspace:^
version: link:../../attachment/attachment
+ '@deepseek-ai/dsh-authorization':
+ specifier: workspace:^
+ version: link:../../credentials/authorization
'@deepseek-ai/dsh-credentials':
specifier: workspace:^
version: link:../../credentials/credentials
@@ -8724,6 +8748,9 @@ importers:
'@deepseek-ai/dsh-attachment':
specifier: workspace:^
version: link:../../packages/attachment/attachment
+ '@deepseek-ai/dsh-authorization':
+ specifier: workspace:^
+ version: link:../../packages/credentials/authorization
'@deepseek-ai/dsh-bash-local':
specifier: workspace:^
version: link:../../packages/shell/bash-local
diff --git a/python/README.i18n.yaml b/python/README.i18n.yaml
index 342b3b0017..8c5a23e37b 100644
--- a/python/README.i18n.yaml
+++ b/python/README.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write python/README.md
README.md: 75276a915eb4b63f84e0876de46e6d8d63540b59
-README.zh.md: cb6d05cdc2b6395cb460671d863169f5184351b9
+README.zh.md: f41822d66520869e5235a6fedd895b83a52639c9
diff --git a/python/README.zh.md b/python/README.zh.md
index cb6d05cdc2..f41822d665 100644
--- a/python/README.zh.md
+++ b/python/README.zh.md
@@ -8,13 +8,13 @@
| 目录 | 分发名/模块 | 职责 |
|---|---|---|
-| [sdk](sdk/README.md) | `deepseek-harness-sdk` / `deepseek_harness` | 高层轮次 API 与低层 JSON-RPC 客户端 |
-| [sdk-runtime](sdk-runtime/README.md) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | 内置运行时二进制与默认 agent(智能体)配置 |
+| [sdk](sdk/README.zh.md) | `deepseek-harness-sdk` / `deepseek_harness` | 高层轮次 API 与低层 JSON-RPC 客户端 |
+| [sdk-runtime](sdk-runtime/README.zh.md) | `deepseek-harness-runtime-bin` / `deepseek_harness_runtime` | 内置运行时二进制与默认 agent(智能体)配置 |
## 行为
-除非调用方选择显式通道,否则 SDK 会启动匹配的内置运行时。客户端选择通道并提供默认配置;运行时本身始终要求显式配置。[SDK 参考](sdk/README.md)和[运行时载体参考](sdk-runtime/README.md)定义完整的运行时选择与配置约定。
+除非调用方选择显式通道,否则 SDK 会启动匹配的内置运行时。客户端选择通道并提供默认配置;运行时本身始终要求显式配置。[SDK 参考](sdk/README.zh.md)和[运行时载体参考](sdk-runtime/README.zh.md)定义完整的运行时选择与配置约定。
## 贡献者工作流
-[Python 贡献者工作流](development.md)介绍运行时产物构建、包验证、源码模式开发和分发。
+[Python 贡献者工作流](development.zh.md)介绍运行时产物构建、包验证、源码模式开发和分发。
diff --git a/python/development.i18n.yaml b/python/development.i18n.yaml
index a9cdaa8daa..64a2cff09b 100644
--- a/python/development.i18n.yaml
+++ b/python/development.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write python/development.md
development.md: 617d030294dafa51aea513adb811bb5f377431c9
-development.zh.md: 2049ad12856c4107788aec168e3724965cebd5af
+development.zh.md: be2a6196aed13a4f748e1b178f34603bbd08e5ac
diff --git a/python/development.zh.md b/python/development.zh.md
index 2049ad1285..be2a6196ae 100644
--- a/python/development.zh.md
+++ b/python/development.zh.md
@@ -2,7 +2,7 @@
[English](development.md) | 中文
-根据所需的贡献者成果选择工作流:构建运行时产物、验证 SDK、从源码运行或构建分发包。包行为分别见 [SDK 参考](sdk/README.md) 和[运行时载体参考](sdk-runtime/README.md)。
+根据所需的贡献者成果选择工作流:构建运行时产物、验证 SDK、从源码运行或构建分发包。包行为分别见 [SDK 参考](sdk/README.zh.md) 和[运行时载体参考](sdk-runtime/README.zh.md)。
## 构建运行时产物
@@ -25,7 +25,7 @@ uv sync --project python/sdk --group test
uv run --project python/sdk pytest
```
-`python/sdk/tests/test_bundled_runtime.py` 会运行可用的内置载体;某个载体的产物尚未构建时,会跳过该载体。仓库级测试政策见 [测试](../docs/testing.md)。
+`python/sdk/tests/test_bundled_runtime.py` 会运行可用的内置载体;某个载体的产物尚未构建时,会跳过该载体。仓库级测试政策见 [测试](../docs/testing.zh.md)。
该套件面向的是伪造的运行时对端。`scripts/smoke-python-runtime.py` 面向真实的打包运行时;必需的 `python-runtime` CI 任务会用新构建的可执行文件运行全部场景:
diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json
index 86689a6bf6..befad374c1 100644
--- a/python/sdk-runtime/package.json
+++ b/python/sdk-runtime/package.json
@@ -30,6 +30,7 @@
"@deepseek-ai/dsh-compaction-basic": "workspace:^",
"@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:^",
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
+ "@deepseek-ai/dsh-authorization": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-fs": "workspace:^",
diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts
index 721c9987b6..ac4535bc1b 100644
--- a/scripts/ci-workflow.spec.ts
+++ b/scripts/ci-workflow.spec.ts
@@ -417,24 +417,63 @@ describe('Python release workflows', () => {
})
describe('Issue lifecycle workflow', () => {
- it('uses explicit review handoff events without rerunning when a draft becomes ready', () => {
+ it('runs the lifecycle job on every PR/review event but gates token and board steps', () => {
const lifecycle = loadWorkflow('.github/workflows/issue-lifecycle.yml')
+ const policy = loadWorkflow('.github/workflows/issue-policy.yml')
+ const lifecycleJob = workflowJob(lifecycle, 'lifecycle')
+ if (!Array.isArray(lifecycleJob.steps)) throw new TypeError('Issue lifecycle job must define steps')
+
+ // The job has no job-level `if`, so it is listed on every pull_request /
+ // pull_request_review event and reports success instead of a gray skip. The
+ // write-capable steps are gated at step level so approved/commented reviews
+ // never mint a Project/Issue App token nor touch the board.
+ expect(lifecycle.on).toHaveProperty('pull_request')
+ expect(lifecycle.on).toHaveProperty('pull_request_review')
+ expect(lifecycleJob.if).toBeUndefined()
+ // Keep the subscription-type gates: issue-lifecycle does not re-subscribe
+ // ready_for_review (issue-policy owns that) and only reacts to submitted
+ // review events.
const lifecyclePullRequest = workflowEvent(lifecycle, 'pull_request')
const lifecycleReview = workflowEvent(lifecycle, 'pull_request_review')
- const lifecycleJob = workflowJob(lifecycle, 'lifecycle')
- const policy = loadWorkflow('.github/workflows/issue-policy.yml')
- const policyPullRequest = workflowEvent(policy, 'pull_request')
-
expect(lifecyclePullRequest.types).not.toContain('ready_for_review')
expect(lifecyclePullRequest.types).toContain('review_requested')
expect(lifecycleReview.types).toEqual(['submitted'])
- expect(lifecycleJob.if).toBe(
- "${{ github.event_name != 'pull_request_review' || (github.event.action == 'submitted' && github.event.review.state == 'changes_requested') }}",
- )
+ const gated = "${{ github.event_name != 'pull_request_review' || github.event.review.state == 'changes_requested' }}"
+ const steps = lifecycleJob.steps.filter(isRecord)
+ const tokenStep = steps.find(s => s.name === 'Create project token')
+ const handleStep = steps.find(s => s.name === 'Handle repository event')
+ expect(tokenStep).toMatchObject({ if: gated })
+ expect(handleStep).toMatchObject({ if: gated })
+
+ // issue-policy owns PR validation; it is read-only and a real gate.
+ const policyPullRequest = workflowEvent(policy, 'pull_request')
expect(policyPullRequest.types).toContain('ready_for_review')
})
})
+describe('npm release workflows', () => {
+ it('keeps publication dispatch-only and pack in the PR workflow', () => {
+ // pack stays in the PR/master release workflows so a PR proves the set packs.
+ for (const file of ['release.yml', 'release-vendor.yml']) {
+ const workflow = loadWorkflow(`.github/workflows/${file}`)
+ if (!isRecord(workflow.jobs)) throw new TypeError(`${file} must define jobs`)
+ expect(Object.keys(workflow.jobs).sort()).toEqual(['pack'])
+ }
+
+ // publication is workflow_dispatch-only (never a PR check) and keeps the
+ // npm-publish environment plus the shared dist-tag group.
+ for (const file of ['release-publish.yml', 'release-vendor-publish.yml']) {
+ const workflow = loadWorkflow(`.github/workflows/${file}`)
+ if (!isRecord(workflow.on) || !isRecord(workflow.jobs)) throw new TypeError(`${file} must define on and jobs`)
+ expect(Object.keys(workflow.on)).toEqual(['workflow_dispatch'])
+ const publish = workflow.jobs.publish
+ if (!isRecord(publish)) throw new TypeError(`${file} must define a publish job`)
+ expect(publish.environment).toBe('npm-publish')
+ expect(publish.concurrency).toMatchObject({ group: 'Release-publish' })
+ }
+ })
+})
+
describe('Git hooks', () => {
it('leaves frozen Agent Note sidecars to the archive verifier', () => {
const lefthook = loadWorkflow('lefthook.yml')
diff --git a/scripts/client-build-environment.client.spec.ts b/scripts/client-build-environment.client.spec.ts
index cce5cff2a0..e2eb0ebe72 100644
--- a/scripts/client-build-environment.client.spec.ts
+++ b/scripts/client-build-environment.client.spec.ts
@@ -26,6 +26,7 @@ const dshBuildWorkflows = [
'e2b-e2e.yml',
'e2e.yml',
'release.yml',
+ 'release-publish.yml',
'sandbox.yml',
]
diff --git a/scripts/gen-cordis-catalog-record.spec.ts b/scripts/gen-cordis-catalog-record.spec.ts
index 73720e8be8..3b51562768 100644
--- a/scripts/gen-cordis-catalog-record.spec.ts
+++ b/scripts/gen-cordis-catalog-record.spec.ts
@@ -10,7 +10,13 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'nod
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
-import { maybeRecordPair, REGION_BEGIN, REGION_END, spliceRegion } from './gen-cordis-catalog.ts'
+import {
+ localizePageRegion,
+ maybeRecordPair,
+ REGION_BEGIN,
+ REGION_END,
+ spliceRegion,
+} from './gen-cordis-catalog.ts'
import { blobHash, renderPairMeta } from './translation-pairing.ts'
const PAGE = 'docs/subsystems/fix.md'
@@ -142,3 +148,28 @@ describe('spliceRegion', () => {
.toThrow('found 2 BEGIN/2 END')
})
})
+
+describe('localizePageRegion', () => {
+ it('changes only paired Markdown paths for the Chinese generated region', () => {
+ const root = mkdtempSync(join(tmpdir(), 'cordis-region-locale-'))
+ roots.push(root)
+ mkdirSync(join(root, 'docs/subsystems'), { recursive: true })
+ mkdirSync(join(root, 'packages'), { recursive: true })
+ mkdirSync(join(root, 'scripts'), { recursive: true })
+ writeFileSync(join(root, 'docs/subsystems/target.md'), '# Target\n')
+ writeFileSync(join(root, 'docs/subsystems/target.zh.md'), '# 目标\n')
+ writeFileSync(join(root, 'docs/subsystems/excluded.md'), '# Excluded\n')
+ writeFileSync(join(root, 'docs/subsystems/excluded.zh.md'), '# 排除\n')
+ writeFileSync(join(root, 'packages/outside.md'), '# Outside\n')
+ writeFileSync(join(root, 'packages/outside.zh.md'), '# 范围外\n')
+ writeFileSync(join(root, 'scripts/translation-pairing.manifest.json'), JSON.stringify({
+ excluded: ['docs/subsystems/excluded.md'],
+ }))
+ const region = `${REGION_BEGIN}\n[Target](target.md#api) [Excluded](excluded.md) [Outside](../../packages/outside.md)\n${REGION_END}`
+
+ expect(localizePageRegion(region, 'docs/subsystems/page.md', root)).toBe(region)
+ expect(localizePageRegion(region, 'docs/subsystems/page.zh.md', root)).toBe(
+ `${REGION_BEGIN}\n[Target](target.zh.md#api) [Excluded](excluded.md) [Outside](../../packages/outside.md)\n${REGION_END}`,
+ )
+ })
+})
diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts
index e932ff9fa6..615a03b5e3 100644
--- a/scripts/gen-cordis-catalog.ts
+++ b/scripts/gen-cordis-catalog.ts
@@ -3,8 +3,9 @@
* Typert catalog projection. Every harness `ctx.` service and event scope
* maps to exactly one `docs/subsystems/` page through the curated tables below;
* the generator injects each page's Cordis API reference between its GENERATED markers —
- * byte-identically into both language sides of the pair — and re-records a
- * pair's `.i18n.yaml` only when nothing outside the region changed. The
+ * into both language sides of the pair, localizing paired document paths for
+ * the Chinese side while retaining every other byte — and re-records a pair's
+ * `.i18n.yaml` only when nothing outside the region changed. The
* projection enforces event modes, JSDoc parameter/return completeness, and
* signature type-link coverage; the inherited (vendor) tier renders to
* `docs/cordis-api/inherited.md`. `--check` verifies every generated artifact.
@@ -32,9 +33,12 @@ import { contextKeyMap, contextMergeFiles, eventNameList } from './cordis-walk.t
import {
blobHash,
parsePairMeta,
+ parseTranslationPairingManifest,
partitionGeneratedRegions,
renderPairMeta,
+ translationPairSourcePredicate,
} from './translation-pairing.ts'
+import { rewriteTranslationLinkLocales } from './translation-links.ts'
const root = resolve(import.meta.dirname, '..')
const SUBSYSTEMS_DIR = 'docs/subsystems'
@@ -64,6 +68,7 @@ export const SERVICE_PAGE: Record = {
commands: 'commands.md',
compaction: 'compaction.md',
cordisInspect: 'extensions.md',
+ authorization: 'credentials.md',
credentials: 'credentials.md',
directoryPicker: 'workspace.md',
dynamicCordisRunner: 'extensions.md',
@@ -172,6 +177,7 @@ export const EVENT_SCOPE_PAGE: Record = {
'approval': 'approval.md',
'commands': 'commands.md',
'cordis': 'extensions.md',
+ 'authorization': 'credentials.md',
'credentials': 'credentials.md',
'domain': 'storage.md',
'fs': 'filesystem.md',
@@ -184,6 +190,7 @@ export const EVENT_SCOPE_PAGE: Record = {
'system-prompt': 'system-prompt.md',
'session-telemetry': 'session-telemetry.md',
'tools': 'tools.md',
+ 'webserver': 'web-server.md',
'workflow': 'workflow.md',
}
@@ -464,8 +471,23 @@ export const LINK_MAP: Readonly> = {
SettingsPathOp: 'settings.md',
SettingsDescribeOptions: 'settings.md',
SettingsUpdateSource: 'settings.md',
+ AuthorizationEntry: 'credentials.md',
+ AuthorizationFlow: 'credentials.md',
+ AuthorizationInteraction: 'credentials.md',
+ AuthorizationMethod: 'credentials.md',
+ AuthorizationNotice: 'credentials.md',
+ AuthorizationOutcome: 'credentials.md',
+ AuthorizationPrompt: 'credentials.md',
+ AuthorizationRequest: 'credentials.md',
+ AuthorizationSession: 'credentials.md',
+ AuthorizationSettlement: 'credentials.md',
+ AuthorizationStatus: 'credentials.md',
CredentialRef: 'credentials.md',
+ CredentialKey: 'credentials.md',
CredentialInfo: 'credentials.md',
+ CredentialRecord: 'credentials.md',
+ CredentialRecordEntry: 'credentials.md',
+ CredentialRecordInfo: 'credentials.md',
ResolvedCredential: 'credentials.md',
AskUserQuestionAnswer: 'user-questions.md',
AskUserQuestionRequest: 'user-questions.md',
@@ -481,6 +503,7 @@ export const LINK_MAP: Readonly> = {
PresetSpec: 'permission-presets.md',
InvariantInstaller: 'invariants.md',
WebRoute: 'web-server.md',
+ IndexInjection: 'web-server.md',
StorageBackend: 'storage.md',
StorageForms: 'storage.md',
Domain: 'storage.md',
@@ -721,6 +744,19 @@ export interface WalkPartitionMaps {
readonly eventWalkExemptions: Readonly>
}
+/** Project paired Markdown destinations in one generated region to the page's locale. */
+export function localizePageRegion(region: string, pageRel: string, scanRoot: string = root): string {
+ if (!pageRel.endsWith('.zh.md')) return region
+ const manifest = parseTranslationPairingManifest(
+ readFileSync(resolve(scanRoot, 'scripts/translation-pairing.manifest.json'), 'utf8'),
+ )
+ return rewriteTranslationLinkLocales(region, {
+ repoRoot: scanRoot,
+ sourcePath: pageRel,
+ isTranslationPairSource: translationPairSourcePredicate(manifest),
+ }).content
+}
+
/**
* Judge the rendered API and the independent AST scan against the curated
* partition maps, fail-closed in both directions for services AND events: a
@@ -844,6 +880,7 @@ export function computeOutputs(): [string, string][] {
)
for (const side of [page, page.replace(/\.md$/, '.zh.md')]) {
const rel = `${SUBSYSTEMS_DIR}/${side}`
+ const localizedRegion = localizePageRegion(region, rel)
let current: string
try {
current = readFileSync(resolve(root, rel), 'utf8')
@@ -854,7 +891,7 @@ export function computeOutputs(): [string, string][] {
continue
}
try {
- outputs.push([rel, spliceRegion(current, region)])
+ outputs.push([rel, spliceRegion(current, localizedRegion)])
} catch (error) {
problems.push(`${rel}: ${error instanceof Error ? error.message : String(error)}`)
}
diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts
index 420bad476c..5e19c20113 100644
--- a/scripts/gen-doc-graphs.ts
+++ b/scripts/gen-doc-graphs.ts
@@ -189,6 +189,15 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['llm-deepseek', 'llm-pi-ai', 'apiproxy'],
note: '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.',
},
+ {
+ key: 'authorization',
+ pkg: 'authorization',
+ title: 'Authorization flow registry',
+ mode: 'seam',
+ implementations: [],
+ consumers: ['llm-pi-ai'],
+ note: 'Flows are registered by the plugin that knows how to obtain one credential and keyed by the record they write; the seam owns the conversation and the one-attempt-per-key lifecycle, never the protocol.',
+ },
{
key: 'sessionTelemetry',
pkg: 'session-telemetry',
diff --git a/scripts/gen-translation-brief.ts b/scripts/gen-translation-brief.ts
index 0a0d03cc61..fac9e9bcb4 100644
--- a/scripts/gen-translation-brief.ts
+++ b/scripts/gen-translation-brief.ts
@@ -16,11 +16,13 @@ import { existsSync, globSync, mkdtempSync, readFileSync, rmSync, writeFileSync
import { tmpdir } from 'node:os'
import { basename, join, resolve, sep } from 'node:path'
import {
+ isTranslationPairingManifestExcluded,
isTranslationScopeFile,
pairAnchorOfArgument,
parseTranslationMarkdown,
parseTranslationPairingManifest,
TRANSLATION_SCOPE_GLOB_EXCLUDES,
+ translationPairSourcePredicate,
translationStructureDiff,
translationStructureSignature,
} from './translation-pairing.ts'
@@ -41,10 +43,11 @@ import {
const root = resolve(import.meta.dirname, '..')
const manifest = parseTranslationPairingManifest(readFileSync(join(root, 'scripts/translation-pairing.manifest.json'), 'utf8'))
+const isTranslationPairSource = translationPairSourcePredicate(manifest)
const terminology = readFileSync(join(root, 'docs/i18n/terminology.md'), 'utf8')
function isExcluded(file: string): boolean {
- return manifest.excluded.some(entry => (entry.endsWith('/') ? file.startsWith(entry) : file === entry))
+ return isTranslationPairingManifestExcluded(file, manifest)
}
/** Recorded hashes of one consistency record: basename → blob hash. */
@@ -215,12 +218,23 @@ function planScope(
/** Validate a computed mechanical counterpart and write it. */
function applyMechanical(counterpartPath: string, sourceCurrent: string, result: string): void {
const counterpartBase = basename(counterpartPath)
+ const sourcePath = counterpartPath.endsWith('.zh.md')
+ ? counterpartPath.replace(/\.zh\.md$/, '.md')
+ : counterpartPath.replace(/\.md$/, '.zh.md')
const sourceBase = counterpartBase.endsWith('.zh.md')
? counterpartBase.replace(/\.zh\.md$/, '.md')
: counterpartBase.replace(/\.md$/, '.zh.md')
const errors = translationStructureDiff(
- translationStructureSignature(parseTranslationMarkdown(sourceCurrent), counterpartBase),
- translationStructureSignature(parseTranslationMarkdown(result), sourceBase),
+ translationStructureSignature(
+ parseTranslationMarkdown(sourceCurrent),
+ counterpartBase,
+ { repoRoot: root, sourcePath, isTranslationPairSource, markdown: sourceCurrent },
+ ),
+ translationStructureSignature(
+ parseTranslationMarkdown(result),
+ sourceBase,
+ { repoRoot: root, sourcePath: counterpartPath, isTranslationPairSource, markdown: result },
+ ),
)
if (errors.length > 0) {
throw new Error(`gen-translation-brief: computed mechanical update for ${counterpartPath} violates the pair structure: ${errors.join('; ')}`)
diff --git a/scripts/markdown.ts b/scripts/markdown.ts
index 1d40e1d8bb..f221d1c342 100644
--- a/scripts/markdown.ts
+++ b/scripts/markdown.ts
@@ -56,6 +56,101 @@ export function visitMarkdown(node: Nodes, visitor: (node: Nodes) => boolean | v
}
}
+/** Markdown nodes whose authored destination occupies a replaceable source range. */
+export type MarkdownDestinationNode = Extract
+
+/** One authored Markdown destination and its absolute source offsets. */
+export interface MarkdownDestination {
+ start: number
+ end: number
+ url: string
+}
+
+/** Whether a Markdown URL is external, repository-root absolute, or purely in-page. */
+export function isExternalOrAbsoluteMarkdownUrl(url: string): boolean {
+ return url.startsWith('#')
+ || url.startsWith('//')
+ || url.startsWith('/')
+ || /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)
+}
+
+/** Split one Markdown URL without normalizing its query or fragment suffix. */
+export function splitMarkdownUrlTarget(url: string): { path: string; suffix: string } {
+ const boundary = url.search(/[?#]/)
+ if (boundary === -1) return { path: url, suffix: '' }
+ return { path: url.slice(0, boundary), suffix: url.slice(boundary) }
+}
+
+function skipWhitespace(source: string, start: number): number {
+ let index = start
+ while (/\s/.test(source[index] ?? '')) index += 1
+ return index
+}
+
+function labelEnd(source: string): number {
+ const first = source.indexOf('[')
+ if (first === -1) return -1
+ let depth = 0
+ for (let index = first; index < source.length; index += 1) {
+ const char = source[index]
+ if (char === '\\') index += 1
+ else if (char === '[') depth += 1
+ else if (char === ']') {
+ depth -= 1
+ if (depth === 0) return index
+ }
+ }
+ return -1
+}
+
+function destinationRange(rawNode: string, type: MarkdownDestinationNode['type']): { start: number; end: number } {
+ const endOfLabel = labelEnd(rawNode)
+ if (endOfLabel === -1) throw new Error(`markdown: cannot locate label end in ${JSON.stringify(rawNode)}`)
+ let start: number
+ if (type === 'definition') {
+ const colon = rawNode.indexOf(':', endOfLabel + 1)
+ if (colon === -1) throw new Error(`markdown: cannot locate definition separator in ${JSON.stringify(rawNode)}`)
+ start = skipWhitespace(rawNode, colon + 1)
+ } else {
+ if (rawNode[endOfLabel + 1] !== '(') {
+ throw new Error(`markdown: cannot locate inline destination in ${JSON.stringify(rawNode)}`)
+ }
+ start = skipWhitespace(rawNode, endOfLabel + 2)
+ }
+ if (rawNode[start] === '<') {
+ for (let index = start + 1; index < rawNode.length; index += 1) {
+ if (rawNode[index] === '\\') index += 1
+ else if (rawNode[index] === '>') return { start: start + 1, end: index }
+ }
+ throw new Error(`markdown: cannot locate angle-bracket destination end in ${JSON.stringify(rawNode)}`)
+ }
+ let depth = 0
+ for (let index = start; index < rawNode.length; index += 1) {
+ const char = rawNode[index]
+ if (char === '\\') index += 1
+ else if (char === '(') depth += 1
+ else if (char === ')') {
+ if (depth === 0) return { start, end: index }
+ depth -= 1
+ } else if (/\s/.test(char ?? '') && depth === 0) {
+ return { start, end: index }
+ }
+ }
+ return { start, end: rawNode.length }
+}
+
+/** Locate one parsed destination in the original Markdown without reserializing it. */
+export function markdownDestination(source: string, node: MarkdownDestinationNode): MarkdownDestination {
+ const start = node.position?.start.offset
+ const end = node.position?.end.offset
+ if (start === undefined || end === undefined) {
+ throw new Error(`markdown: destination ${JSON.stringify(node.url)} has no source offsets`)
+ }
+ const range = destinationRange(source.slice(start, end), node.type)
+ const absolute = { start: start + range.start, end: start + range.end }
+ return { ...absolute, url: source.slice(absolute.start, absolute.end) }
+}
+
/**
* Extract every parsed code block with its info string, in document order.
* @param source - Markdown source to scan.
diff --git a/scripts/merge-translation-pairing.ts b/scripts/merge-translation-pairing.ts
index dda2621a36..b14305bddb 100644
--- a/scripts/merge-translation-pairing.ts
+++ b/scripts/merge-translation-pairing.ts
@@ -4,6 +4,7 @@ import { execFileSync } from 'node:child_process'
import { readFileSync, writeFileSync } from 'node:fs'
import {
mergeTranslationPairingRecords,
+ repositoryTranslationPairSource,
resolveTranslationPairingConflicts,
} from './translation-pairing-merge.ts'
@@ -16,7 +17,7 @@ try {
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim()
if (args[0] === '--resolve') {
if (args.length !== 1) throw new Error('--resolve takes no paths; it inspects the unmerged index')
- const resolved = resolveTranslationPairingConflicts(root)
+ const resolved = resolveTranslationPairingConflicts(root, repositoryTranslationPairSource(root))
if (resolved.length === 0) {
console.log('merge-translation-pairing: no unresolved pairing records')
} else {
@@ -36,6 +37,7 @@ try {
readFileSync(ancestorPath, 'utf8'),
readFileSync(currentPath, 'utf8'),
readFileSync(otherPath, 'utf8'),
+ repositoryTranslationPairSource(root),
)
writeFileSync(currentPath, result.record)
}
diff --git a/scripts/project-doc-site.spec.ts b/scripts/project-doc-site.spec.ts
index bacb6996ec..200263bb5a 100644
--- a/scripts/project-doc-site.spec.ts
+++ b/scripts/project-doc-site.spec.ts
@@ -223,10 +223,15 @@ describe('rewriteMarkdown', () => {
)
})
- it('routes a pair switcher across locales while ordinary links stay in locale', () => {
+ it('routes switchers across locales and explicit locale siblings within their locale', () => {
const { root, pages } = fixture()
writeFileSync(join(root, 'docs/a.zh.md'), '# A\n')
- const paired = pages.filter(page => page.source !== 'docs/a.md')
+ writeFileSync(join(root, 'docs/b.zh.md'), '# B\n')
+ const paired = pages.filter(page => page.source !== 'docs/a.md').map(page => (
+ page.locale === 'root' && page.source === 'docs/b.md'
+ ? { ...page, source: 'docs/b.zh.md', sourceAliases: ['docs/b.md'] }
+ : page
+ ))
paired.push(
{
locale: 'root', contentLocale: 'zh-CN', source: 'docs/a.zh.md', sourceAliases: ['docs/a.md'],
@@ -237,7 +242,7 @@ describe('rewriteMarkdown', () => {
route: 'en/guide/a.md', label: 'A', sidebar: 'en-guide', section: 'Test', order: 1,
},
)
- expect(rewriteMarkdown('[English](a.md) [B](b.md)\n', {
+ expect(rewriteMarkdown('[English](a.md) [B](b.zh.md)\n', {
locale: 'root',
sourcePath: 'docs/a.zh.md',
route: 'guide/a.md',
@@ -245,6 +250,14 @@ describe('rewriteMarkdown', () => {
repoRoot: root,
repositoryRef: 'abc123',
})).toBe('[English](../en/guide/a.md) [B](../reference-root/b.md)\n')
+ expect(rewriteMarkdown('[中文](a.zh.md) [B](b.md)\n', {
+ locale: 'en',
+ sourcePath: 'docs/a.md',
+ route: 'en/guide/a.md',
+ pages: paired,
+ repoRoot: root,
+ repositoryRef: 'abc123',
+ })).toBe('[中文](../../guide/a.md) [B](../reference/b.md)\n')
})
it('fails loud when a relative target is missing', () => {
@@ -297,6 +310,37 @@ describe('docsPages locale routes', () => {
}
})
+ it('projects the audited tutorial entry links from explicit locale index pages', () => {
+ const entries = [
+ ['docs/user/develop/basic/config.md', '../framework/index.md'],
+ ['docs/user/develop/basic/publish.md', '../framework/index.md'],
+ ['docs/user/develop/basic/tool.md', './index.md'],
+ ['docs/user/develop/basic/tool.md', '../practice/index.md'],
+ ['docs/user/develop/framework/events.md', '../practice/index.md'],
+ ['docs/user/develop/framework/service.md', '../practice/index.md'],
+ ['docs/user/develop/practice/index.md', '../basic/index.md'],
+ ['docs/user/guide/index.md', '../develop/basic/index.md'],
+ ] as const
+
+ for (const [englishSource, englishTarget] of entries) {
+ for (const locale of ['en', 'root'] as const) {
+ const source = locale === 'root' ? englishSource.replace(/\.md$/, '.zh.md') : englishSource
+ const target = locale === 'root' ? englishTarget.replace(/\.md$/, '.zh.md') : englishTarget
+ const page = docsPages.find(candidate => candidate.locale === locale && candidate.source === source)
+ expect(page, `${locale}:${source}`).toBeDefined()
+ expect(readFileSync(resolve(repositoryRoot, source), 'utf8')).toContain(`](${target})`)
+ expect(rewriteMarkdown(`[Entry](${target})\n`, {
+ locale,
+ sourcePath: source,
+ route: page!.route,
+ pages: docsPages,
+ repoRoot: repositoryRoot,
+ repositoryRef: 'abc123',
+ })).toBe(`[Entry](${englishTarget})\n`)
+ }
+ }
+ })
+
it('indexes every subsystem page in both sides of the folder README', () => {
const pages = globSync(join(repositoryRoot, 'docs/subsystems/*.md'))
.map(page => basename(page))
@@ -305,11 +349,22 @@ describe('docsPages locale routes', () => {
expect(pages.length).toBeGreaterThan(0)
for (const readme of ['README.md', 'README.zh.md']) {
const rows = readFileSync(join(repositoryRoot, 'docs/subsystems', readme), 'utf8')
- const missing = pages.filter(page => !rows.includes(`| [${page}](${page}) |`))
+ const missing = pages.filter((page) => {
+ const target = readme.endsWith('.zh.md') ? page.replace(/\.md$/, '.zh.md') : page
+ return !rows.includes(`| [${page}](${target}) |`)
+ })
expect(missing, `${readme} must carry one table row per subsystem page`).toEqual([])
}
})
+ it('places the shared todo fragment alias on the translated todo section', () => {
+ const catalog = readFileSync(resolve(repositoryRoot, 'docs/tool-catalog.zh.md'), 'utf8')
+ expect(catalog.match(/<\/a>/g)).toHaveLength(1)
+ expect(catalog).toContain(
+ '\n\n## `@deepseek-ai/dsh-tool-todo`',
+ )
+ })
+
it('projects every published subsystem page in Chinese', () => {
const rootPages = docsPages.filter(page => (
page.locale === 'root' && page.route.startsWith('reference/subsystems/')
diff --git a/scripts/project-doc-site.ts b/scripts/project-doc-site.ts
index 9150780c14..582a306df3 100644
--- a/scripts/project-doc-site.ts
+++ b/scripts/project-doc-site.ts
@@ -14,6 +14,11 @@ import { gfmFromMarkdown } from 'mdast-util-gfm'
import { gfm } from 'micromark-extension-gfm'
import type { Nodes } from 'mdast'
import { docsPages, type DocsLocale, type DocsPage } from '../website/docs.ts'
+import {
+ isExternalOrAbsoluteMarkdownUrl,
+ markdownDestination,
+ splitMarkdownUrlTarget,
+} from './markdown.ts'
const REPOSITORY_URL = 'https://github.com/deepseek-ai/deepseek-harness'
const root = resolve(import.meta.dirname, '..')
@@ -35,11 +40,6 @@ interface Replacement {
value: string
}
-interface DestinationRange {
- start: number
- end: number
-}
-
type RewritableNode = Extract
/** Inputs for rewriting one canonical Markdown page. */
@@ -65,94 +65,12 @@ function repoPath(absPath: string, repoRoot: string): string {
return relative(repoRoot, absPath).split(sep).join('/')
}
-function isExternalOrSiteAbsolute(url: string): boolean {
- return url.startsWith('#')
- || url.startsWith('//')
- || url.startsWith('/')
- || /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)
-}
-
-function skipWhitespace(source: string, start: number): number {
- let index = start
- while (/\s/.test(source[index] ?? '')) index += 1
- return index
-}
-
-function labelEnd(source: string): number {
- const first = source.indexOf('[')
- if (first === -1) return -1
- let depth = 0
- for (let index = first; index < source.length; index += 1) {
- const char = source[index]
- if (char === '\\') {
- index += 1
- } else if (char === '[') {
- depth += 1
- } else if (char === ']') {
- depth -= 1
- if (depth === 0) return index
- }
- }
- return -1
-}
-
-function destinationRange(rawNode: string, type: 'link' | 'image' | 'definition'): DestinationRange {
- const endOfLabel = labelEnd(rawNode)
- if (endOfLabel === -1) {
- throw new Error(`project-doc-site: cannot locate label end in ${JSON.stringify(rawNode)}.`)
- }
-
- let start: number
- if (type === 'definition') {
- const colon = rawNode.indexOf(':', endOfLabel + 1)
- if (colon === -1) {
- throw new Error(`project-doc-site: cannot locate definition separator in ${JSON.stringify(rawNode)}.`)
- }
- start = skipWhitespace(rawNode, colon + 1)
- } else {
- if (rawNode[endOfLabel + 1] !== '(') {
- throw new Error(`project-doc-site: cannot locate inline destination in ${JSON.stringify(rawNode)}.`)
- }
- start = skipWhitespace(rawNode, endOfLabel + 2)
- }
-
- if (rawNode[start] === '<') {
- for (let index = start + 1; index < rawNode.length; index += 1) {
- if (rawNode[index] === '\\') index += 1
- else if (rawNode[index] === '>') return { start: start + 1, end: index }
- }
- throw new Error(`project-doc-site: cannot locate angle-bracket destination end in ${JSON.stringify(rawNode)}.`)
- }
-
- let depth = 0
- for (let index = start; index < rawNode.length; index += 1) {
- const char = rawNode[index]
- if (char === '\\') {
- index += 1
- } else if (char === '(') {
- depth += 1
- } else if (char === ')') {
- if (depth === 0) return { start, end: index }
- depth -= 1
- } else if (/\s/.test(char ?? '') && depth === 0) {
- return { start, end: index }
- }
- }
- return { start, end: rawNode.length }
-}
-
// `#fragment` suffixes pass through verbatim. Generated cordis-surface
// headings carry explicit `` anchors with the GitHub slug, so those
// fragments resolve on the published site too; hand-written headings rely on
// VitePress's own slugger, which differs from GitHub's for punctuation-heavy
// text — hand-authored cross-page fragments should prefer plain-text headings
// or explicit anchors.
-function splitTarget(url: string): { path: string; suffix: string } {
- const boundary = url.search(/[?#]/)
- if (boundary === -1) return { path: url, suffix: '' }
- return { path: url.slice(0, boundary), suffix: url.slice(boundary) }
-}
-
function decodePath(path: string): string {
try {
return decodeURIComponent(path)
@@ -239,8 +157,8 @@ export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions)
const replacements: Replacement[] = []
const rewrite = (node: RewritableNode): void => {
- if (isExternalOrSiteAbsolute(node.url)) return
- const { path, suffix } = splitTarget(node.url)
+ if (isExternalOrAbsoluteMarkdownUrl(node.url)) return
+ const { path, suffix } = splitMarkdownUrlTarget(node.url)
if (path === '') return
const { absPath, line } = resolveRepositoryTarget(sourceAbs, path, options.repoRoot)
const targetPath = repoPath(absPath, options.repoRoot)
@@ -257,16 +175,10 @@ export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions)
? `${options.placeImage(absPath)}${suffix}`
: githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
- const start = node.position?.start.offset
- const end = node.position?.end.offset
- if (start === undefined || end === undefined) {
- throw new Error(`project-doc-site: link ${JSON.stringify(node.url)} has no source offsets.`)
- }
- const rawNode = source.slice(start, end)
- const rawDestination = destinationRange(rawNode, node.type)
+ const destination = markdownDestination(source, node)
replacements.push({
- start: start + rawDestination.start,
- end: start + rawDestination.end,
+ start: destination.start,
+ end: destination.end,
value: nextUrl,
})
}
diff --git a/scripts/snapshots/translation-prompt-v4/request-response.expected.json b/scripts/snapshots/translation-prompt-v4/request-response.expected.json
index 9f3c3a59dc..a4b4a2e7c5 100644
--- a/scripts/snapshots/translation-prompt-v4/request-response.expected.json
+++ b/scripts/snapshots/translation-prompt-v4/request-response.expected.json
@@ -4,7 +4,7 @@
"messages": [
{
"role": "system",
- "content": "# Translation Prompt\n\nYou are a senior technical translator specializing in LLM and agent development documentation. Your task is to translate the complete source document from English to Chinese, producing natural, professional technical prose.\n\nRead each complete semantic unit, understand it, and restate it as a native technical author would write it in the target language. Do not mechanically preserve source-language syntax. Then verify the translation against the source clause by clause: preserve every proposition and add none. Fluency never justifies losing or altering meaning, and completeness never justifies unnatural word-for-word prose.\n\n## Priority\n\nApply these authorities in order:\n\n1. Preserve the source meaning and the required document structure, protected content, and formatting.\n2. Follow the injected terminology table exactly.\n3. Use the injected whole-document gold pairs to calibrate target-language voice and phrasing.\n4. Apply the general writing guidance and illustrative examples in this prompt.\n\nA lower-priority rule may refine but never override a higher-priority requirement. Gold pairs calibrate voice; they are not a translation memory. No style preference, gold-pair phrasing, or embedded example may override source meaning, required structure, protected content, or the terminology table.\n\n## Quality Requirements\n\n### Structure and Format Preservation\n- Output a complete translated document that maintains the same document frame as the source: heading hierarchy and order, list kinds and item counts, ordered-list starts, table rows and columns, link targets, and code blocks.\n- Paragraph boundaries may change within the same structural unit when the target language needs different semantic grouping. Do not merge or move content across headings, list items, table cells, or other independent structural units.\n- Keep each prose paragraph on one physical line. Use paragraph breaks, not hard-wrapped lines inside a paragraph.\n- Fenced code blocks must be byte-identical to the source, including info strings, whitespace, and ALL comments inside them. Do NOT translate or reformat any content inside code blocks. This is a hard rule with no exceptions.\n- Inline code spans must be kept verbatim. This includes commands, flags, paths, identifiers, API and event names, config keys, protocol values, version numbers, and other machine-readable tokens. Never translate or reformat them.\n- Every relative link must point to the same target as in the source. Translate link text; do not change link targets.\n- Language switcher line: when an English source contains `English | [中文](source-filename.zh.md)`, write `[English](source-filename.md) | 中文`. When a Chinese source contains `[English](source-filename.md) | 中文`, write `English | [中文](source-filename.zh.md)`. Do NOT copy the source switcher unchanged. If the source has no switcher, do not invent a filename or switcher; the pipeline inserts the canonical target switcher after parsing ``.\n- Preserve emphasis marker types and the semantic spans they cover. Do not add, remove, move, or change bold and italic markers.\n\n### Faithfulness\n- Preserve every proposition in the source and add none. Every sentence, list item, note, FIXME, warning, example, caveat, prerequisite, and guarantee must have an equivalent in the translation. Count list items on both sides.\n- Preserve actors, objects, conditions, exceptions, negation, modality, causal relationships, and distinctions between concepts.\n- Preserve the exact strength and orientation of contracts. Completion and lifecycle conditions, failure behavior, directions and data flow, normal and exceptional result channels, ownership changes, and quantitative bounds must not be weakened, strengthened, reversed, or merged.\n- Translate ideas rather than source-language idioms, but never use fluency as a reason to omit or alter meaning.\n\n### Tone and Style\n- 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.\n- Write in a professional, formal tone appropriate for developer documentation. Never use colloquial or casual expressions.\n- 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.\n- Prefer established target-language engineering terms over literal renderings. Replace metaphors with direct descriptions that preserve the source meaning.\n- Use polite imperative forms where the text instructs the reader to do something. In Chinese, address the reader as `你`, not `您`.\n- Keep the author's register: concise stays concise, detailed stays detailed.\n\n### Sentence Structure\n- Break long sentences where the target language needs a pause. Avoid run-on sentences.\n- Use active voice when it improves clarity without changing or inventing the actor. Retain passive voice when the actor is unknown, irrelevant, or intentionally omitted.\n- Restructure source-language syntax into clear target-language syntax. Preserve the logical scope of conditions, concessions, negation, coordination, and modifiers.\n- Split or combine clauses when needed for readability, provided every source relationship remains explicit.\n- Translate meaning, not words. Do not invent words or expressions that a native technical author would not use.\n\n### Word Choice\n- Prefer precise, formal vocabulary over casual or colloquial alternatives.\n- When multiple synonyms exist, choose the one most commonly used in professional technical documentation of the target language.\n- Translate ordinary prose when an established target-language expression is clear. Preserve proper nouns, canonical product names, code identifiers, APIs, paths, package names, and terms that the terminology table requires to remain in the source language.\n- Use context to resolve polysemous words. A familiar word does not have one fixed rendering in every technical domain.\n- Avoid slang, internal jargon, or overly literal translations that would not be recognized by the general developer audience.\n- Do not use the same word to translate distinct source-language concepts when their distinction matters.\n- Avoid repeating the same ordinary verb in close proximity when a natural equivalent preserves the exact meaning. Never vary a terminology-table form, defined concept, or contract verb merely for stylistic variety.\n\n#### When translating into Chinese\n- When a number modifies a noun, include a natural Chinese classifier or measure word when Chinese grammar requires one. For example: \"three-role capability seam\" → \"包含三种角色的能力 seam\", not \"三角色 seam\". Do not add classifiers to code, identifiers, versions, units, or fixed names.\n\n### Punctuation\n\n#### When translating into Chinese\n- Use full-width Chinese punctuation in Chinese prose: `,。:;?!()「」`. Keep half-width punctuation inside code spans, numbers, and complete verbatim English text.\n- Prefer colons, periods, commas, or parentheses over em dashes when they make the sentence clearer or more natural. Keep an em dash when it is the clearest natural punctuation.\n- Use enumeration commas (、) between parallel Chinese items, not regular commas.\n- Keep list-item endings consistent with their grammar. Complete sentences may end with periods or other grammatically required punctuation; do not end list items with commas.\n- Put one half-width space between Chinese text and Latin words or numerals. Do not add a space next to full-width punctuation, and do not leave a meaningless half-width space between two Chinese characters.\n- Markdown emphasis markers do not create a word boundary. Determine spacing from the rendered adjacent characters: Chinese next to Chinese takes no space, while Chinese next to a Latin word or numeral takes one half-width space.\n- Use half-width digits and Latin letters, never full-width forms.\n- For RFC 2119 keywords (MUST, MUST NOT, SHOULD, MAY), translate to the corresponding Chinese term (必须、禁止、应当、可以), preserve the SOURCE emphasis span exactly, and do not weaken its normative strength: plain source stays plain (必须), italic source stays italic (*必须*), and bold source stays bold (**必须**).\n\n#### When translating into English\n- Use half-width English punctuation and standard English spacing. Preserve full-width punctuation only in verbatim Chinese text.\n- Convert enumeration commas (、) to English commas and Chinese prose quotation marks to English double quotes.\n- Convert Chinese topic-comment sentences and omitted-subject constructions into clear English subjects when the actor is stated or unambiguously implied. Do not invent an actor.\n- Use concise professional developer prose and established English technical terms. Do not transliterate Chinese engineering idioms literally.\n- Use the terminology table's English column exactly and do not carry Chinese first-occurrence glosses into English prose.\n\n## Terminology\n\nA terminology table is provided below. Follow it strictly:\n- Render every listed term exactly as specified.\n- When the target language is Chinese, use the \"中文\" column. On the document's first prose occurrence, write the \"首次出现\" value when one is specified; on later occurrences, write only the part before the parenthetical gloss.\n- When the target language is English, use the \"English\" column without a Chinese gloss; do not copy the \"中文\" or \"首次出现\" value into English prose.\n- If a term has already been glossed as part of a compound term, do not gloss it again when it appears alone later.\n- NEVER use translations listed in the \"不要译作\" column.\n- Code spans and other protected tokens remain verbatim even when their text resembles a listed term.\n- For an unlisted technical term, use an established target-language technical term when its meaning is unambiguous in context. For a Chinese target, use an established Chinese rendering from a major Chinese-language OSS or vendor source; if you cannot reliably determine such a rendering, preserve the source term and record `[Terminology: pending]` in `` with a tentative rendering for human review. For an English target, use the established English technical term; if the source term has no unambiguous established equivalent, preserve it with the shortest English gloss needed to make it intelligible and record `[Terminology: pending]` in ``. A tentative rendering may appear in `` but must not be silently adopted in `` or ``, and you must not invent or claim a specific external precedent. This rule applies to terminology only; for general prose, freely restructure and paraphrase for natural expression.\n\n# Terminology\n\n本表约定本仓库的中英术语统一译法。\n\n**通用规则:**\n- \"中文\"列为中文译文的正文默认用词。若该列为英文,则中文译文的正文中保留英文不翻译。\n- 首次出现按\"首次出现\"列书写(带括号注释);后续出现只写括号前的部分(可能为中文,也可能为英文),不出现括号内的注释。\n- \"不要译作\"列为严格禁止的译法。\n- 如果某术语已经作为另一个术语的组成部分被括注过(如 `agent loop(智能体循环)` 中已包含 `agent` 的括注),则该术语后续单独出现时无需再次括注。\n\n## 缩写类(中英文文本中均使用缩写)\n\n| English | 中文 | 首次出现 | 不要译作 | 备注 |\n|---|---|---|---|---|\n| ACP | ACP | ACP(Agent Client Protocol) | | |\n| AI | AI | AI(人工智能) | | |\n| API | API | | | |\n| CI | CI | | | |\n| CLI | CLI | CLI(命令行界面) | | |\n| e2e | e2e | | | |\n| HMR | HMR | HMR(热模块替换) | | |\n| JSON Schema | JSON Schema | | | |\n| JSONL | JSONL | | | |\n| LLM | LLM | LLM(大语言模型) | | |\n| MCP | MCP | | | |\n| PR | PR | PR(Pull Request) | | |\n| RAG | RAG | RAG(检索增强生成) | | |\n| SDK | SDK | | | 只指受支持的 Python 与 TypeScript SDK 所使用的 JSON-RPC 客户端/服务器协议;DeepSeek Harness 项目本身不是 SDK |\n| SSE | SSE | SSE(Server-Sent Events) | | |\n\n## 英文类(中英文文本中均使用英文)\n\n| English | 中文 | 首次出现 | 不要译作 | 备注 |\n|---|---|---|---|---|\n| agent | agent | agent(智能体) | | |\n| Agent Note | Agent Note | | 智能体注记、智能体笔记 | 仓库定义的文档类型,涵盖提案、已实现决策和被否决提案;中文对侧 H1 保持固定前缀 `# Agent Note: `,标题中不加术语括注 |\n| agent harness | agent harness | agent harness(智能体框架) | | agent 组合词(agent harness/workflow/loop/skill 等)整体保留英文;未括注过 agent 时首现按对应组合词或 agent 行处理 |\n| agent loop | agent loop | agent loop(智能体循环) | | |\n| blob hash | blob hash | | | `git hash-object` 的结果 |\n| coding agent | coding agent | coding agent(编程智能体) | | agent 组合词,正文保留英文 |\n| Cordis | Cordis | | | |\n| dispose | dispose | dispose(资源释放) | | |\n| doc-sync | doc-sync | doc-sync(文档同步门禁) | | |\n| fiber | fiber | | | |\n| fixture | fixture | fixture(测试前置数据) | | |\n| fork | fork | | | |\n| Function Calling | Function Calling | Function Calling(函数调用) | | |\n| harness | harness | | | |\n| harness engineering | harness engineering | | | |\n| KV Cache | KV Cache | | | 专有技术名称,保持大小写与空格 |\n| lint | lint | | | |\n| mock | mock | | | 保留英文;指测试替身 |\n| loader | loader | | | |\n| manifest | manifest | manifest(元数据清单) | | |\n| monorepo | monorepo | | | |\n| Round | Round | | 回合、目标回合、Ralph 回合 | 外层策略使用 Round 时,领域层级为 Session > Round > Turn(轮次) > Step(步骤);Round 是可选的外层策略迭代,并非每个会话轮次都具有的通用层级。Goal Round 与 Ralph Round 均保留英文。一个 Round 承载一个轮次,步骤隶属于该轮次;明确的零步骤轮次仍保持原义。 |\n| schema | schema | | | |\n| schema DSL | schema DSL | | | |\n| seam | seam | | 接缝 | 一个可替换能力的整体,包含 Service Definition / Service Provider / Consumer 三种角色;角色需要独立演化时才拆包,也可由同一包承担多个角色。以 `packages/shell` 为范例;Service Definition 是 Cordis `Service`(抽象类或具体 registry 服务),不是 TypeScript interface。任何单一角色、普通边界或扩展点都不能称为 seam。本仓库正文保留英文;与 `extension point` 是不同概念 |\n| Service Provider | Service Provider | | Service provider | 能力 seam 的命名角色;单数固定写作 Service Provider,复数写作 Service Providers。泛指提供服务的 provider 不适用本词条 |\n| skill | skill | skill(技能) | | |\n| slot | slot | | 坑位、孔位 | 客户端架构中的具名可注册位置,保留英文 |\n| spill | spill | | | 工具输出超限落盘机制;组合词写 `spill 文件`、`spill 路径` |\n| spawn | spawn | | | |\n| steering | steering | steering(中途引导) | | |\n| job id | job id | | 任务 id | 保留英文 |\n| subagent | subagent | | | |\n| transcript | transcript | transcript(文本记录) | | 指会话渲染给用户或编辑器的完整文本,区别于事件日志 |\n| Typert | Typert | | TypeRT、typeRT、Type RT | DeepSeek Harness 类型图、生成器、loader 与运行时 registry 的产品拼写 |\n| waterfall | waterfall | waterfall(瀑布式事件) | | |\n| wheel | wheel 包 | | | Python 打包格式 |\n| worktree | worktree | | | git 工作区概念 |\n| Zstandard | Zstandard | | | RFC 8878 compression format; `zstd` remains a code value. |\n\n## 双语类(中英文文本各自使用中英文)\n\n| English | 中文 | 首次出现 | 不要译作 | 备注 |\n|---|---|---|---|---|\n| adapter | 适配器 | | | |\n| adapter contract | 适配器约定 | 适配器约定(adapter contract) | | |\n| append-only | 仅追加 | | | |\n| artifact | 产物 | | 制品 | |\n| backend | 后端 | | | |\n| binder | 绑定器 | | | 命名角色:把已声明接口绑定到调用方 context 或生命周期 |\n| config | 配置 | | | 命名角色:一个已解析配置值或边界严格的配置记录 |\n| controller | 控制器 | | | 命名角色:接受意图并改变一项既有领域或展示状态 |\n| directory | 目录 | | | 命名角色:暴露供发现或选择的条目及元数据 |\n| engine | 引擎 | | | 命名角色:实现领域算法或有状态执行模型 |\n| gateway | 网关 | | | 命名角色:适配进程、网络、RPC 或 API 边界 |\n| handle | 句柄 | | | 命名角色:引用并控制或观察一个实时资源 |\n| policy | 策略 | | | 命名角色:决定允许、选择、限制或观察什么 |\n| presenter | 展示转换器 | | | 命名角色:把领域值纯转换为渲染意图 |\n| resolver | 解析器 | | | 命名角色:根据输入计算或定位一个答案 |\n| store | 存储 | | | 命名角色:拥有一组数据并主要提供数据操作 |\n| background job | 后台任务 | | | |\n| block | 块 | | | |\n| build target | 构建目标 | | | |\n| cancel | 取消 | | | |\n| canary test | canary 测试 | | 金丝雀测试 | 本仓库保留 `canary` |\n| capability | 能力 | | | 必须与 `feature` → `功能` 区分 |\n| capability seam | 能力 seam | | 功能 seam、能力接缝 | 本仓库 Service Definition、Service Provider 与 Consumer 三种角色组成完整可替换能力的命名架构概念;普通 `seam` 仍按其词条处理 |\n| feature | 功能 | | 能力 | SDK 产品与工程模型中的可管理产品单元 |\n| feature option | 功能选项 | | variant | 一项 SDK 功能内有限、可选择的实现或配置 |\n| checkpoint | 检查点 | | | |\n| chunk | 分片 | | | |\n| compaction | 压缩 | 压缩(compaction) | | |\n| companion tool | 配套工具 | | | |\n| composition bundle | 组合包 | | | 只约束应用或插件的组合语境,不约束所有 `bundle` |\n| Cordis plugin config | Cordis 插件配置 | | | Cordis 插件公开的 `Config` 对象或配置结构 |\n| config key | 配置键 | | | Cordis 插件配置中的单个字段 |\n| consumer | 消费方 | | 消费者 | |\n| content block | 内容块 | | | |\n| Cookbook | 实操手册 | | | 文档标题用语 |\n| context | 上下文 | | | |\n| counterpart | 对侧文件 | | 对应物、配对物 | 双语配对语境;泛指\"另一侧\"时可写「另一侧」 |\n| configurable-provider directory | 可配置提供方目录 | | | llm seam 中 `registerConfigurableProviders()` 维护的目录;沿用 Service Catalog →「服务目录」先例 |\n| context compaction | 上下文压缩 | 上下文压缩(context compaction) | | |\n| contract | 约定 | | | 如:`pairing contract` →`配对约定` |\n| Cordis config entry | Cordis 配置项 | | | 指 `cordis.yml` 插件列表中的一项;插件实现本身写`Cordis 插件` |\n| Cordis plugin | Cordis 插件 | | | Cordis 加载的插件实现,不指 `cordis.yml` 中的一项配置 |\n| crash recovery | 崩溃恢复 | | | |\n| deploy root | 部署根目录 | | | |\n| dormant | 休眠 | | 睡眠、蛰伏 | 指已声明可配置但当前未注册路由的提供方 |\n| durability | 持久性 | | | |\n| feature requirement | 功能依赖 | | | 功能或功能选项通过 `requires` 声明的关系 |\n| event | 事件 | | | |\n| event log | 事件日志 | | | |\n| event stream | 事件流 | | | |\n| event-sourced | 事件溯源 | | | 沿用 DDD 社区通行译法 |\n| Executive summary | 摘要 | | | 事故复盘标题用语 |\n| executor | 执行器 | | | |\n| expected output | 预期输出 | | 金标 | 指 snapshot 比较产物;翻译语料的人工校准样例不在此列 |\n| extension | 扩展 | | | |\n| extension point | 扩展点 | | | 注意与 `seam` 区分 |\n| fail-fast | 快速失败 | | | |\n| fenced code block | 围栏代码块 | | | 沿用 MDN 中文翻译 |\n| fingerprint | 指纹 | | | 通用内容指纹;双语配对机制使用 sidecar record 记录两侧 blob hash |\n| finish reason | 结束原因 | | | |\n| fold | 折叠区 | | | 配置界面语境:默认收起的字段分区(collapsed →「收起」)|\n| foreground run | 前台运行 | | | |\n| freshness | 新鲜度 | | | 沿用 MDN 中文翻译;在本项目中指译文相对源文的同步状态 |\n| hook | 钩子 | | | |\n| implementation | 实现 | | | |\n| inference | 推理 | 推理(inference) | | 需要和 `reasoning` 区分时保留英文括注 |\n| info string | 信息字符串 | | | 沿用 CommonMark 中文翻译;指代码围栏 ``` 之后的语言标注 |\n| injection | 注入 | | | |\n| integration | 集成 | | | |\n| interface | 接口 | | | |\n| language switcher | 语言切换行 | | | i18n 配对机制用语:双语配对文件顶部的互链行 |\n| merge | 合并 | | | |\n| message | 消息 | | | |\n| mod | 模组 | | | |\n| model provider | 模型提供方 | | | |\n| model selection | 模型选择 | | 模型目标 | 面向 Agent 的提供方、模型和可选推理强度选择。 |\n| module | 模块 | | | |\n| non-escalation | 非升权 | | 非升级、不可升级 | 仅用于安全与权限语境,指主体不得获得超出既有授权的权限;普通升级不适用此行 |\n| npm dependency | NPM 依赖 | | | `package.json` 中的包关系;`dependencies`、`devDependencies` 等字段保持原样 |\n| opt-out ratio | opt-out 比例 | | 退出检查比例 | |\n| orphan | 遗留 | | 孤儿、孤立 | 指英文源已不存在的 `.zh.md`(如「遗留译文」);进程语境按 OS 惯用语译「孤儿进程」 |\n| orphan branch | 孤立分支 | | 孤儿分支 | 沿用 git 官方中文翻译 |\n| package | 包 | | | 指 npm 包(`@deepseek-ai/dsh-*`);`package.json` 等代码标识保持原样 |\n| pairing | 配对 | | | |\n| parent-subset grants | 父级子集授权 | | 父集合授权 | 指授权范围仅限于父级所持授权的子集 |\n| peer dependency | 对等依赖 | 对等依赖(peer dependency) | | |\n| permission | 权限 | | | |\n| persistence | 持久化 | | | |\n| pipeline | 流水线 | | | |\n| plugin | 插件 | | | |\n| postmortem | 事故复盘 | 事故复盘(postmortem) | 事后分析、事故记录 | 事故记录与分析文档;目录或路径中的 `postmortem` 保持代码形式 |\n| prompt | 提示词 | | | |\n| provider | 提供方 | | | |\n| provider-neutral | 提供方无关 | | 提供方中立 | |\n| quality gate | 质量门禁 | | | |\n| quiescence | 完全停稳 | | 静默、静止状态 | 指生命周期工作全部结算后的状态 |\n| reasoning | 推理 | 推理(reasoning) | | 需要和 `inference` 区分时保留英文括注 |\n| reasoning_content | 思考内容 | | | |\n| registry | 注册表 | | | |\n| replay | 回放 | | | |\n| resume | 恢复 | | | |\n| runtime | 运行时 | | | |\n| same-world subprocess | 与宿主共享文件系统和内核的子进程 | | 同世界子进程 | |\n| sandbox | 沙箱 | | | |\n| service | 服务 | | | |\n| serving interface | 对外服务接口 | | | |\n| session | 会话 | | | |\n| session event | 会话事件 | | | |\n| setup card | 设置卡片 | | | 首次运行时代替行卡直接展开的配置卡 |\n| sidecar file | 伴随文件 | | | 指与文档同目录的普通伴随文件 |\n| sidecar record | 伴随记录 | | 旁挂记录 | 指与文档同目录的伴随记录文件 |\n| smoke test | 冒烟测试 | | | |\n| snapshot | 快照 | | | |\n| source of truth | 真源 | | 事实来源、唯一来源 | |\n| spine | 主干 | | | |\n| stale | 陈旧 | | 过期 | 与 `fresh`(`新鲜`)成对;门禁输出中保留英文 `stale` 不翻译;`expired` 才译为`过期` |\n| step | 步骤 | | | |\n| stream | 流 | | | |\n| structural signature | 结构签名 | | | i18n 配对机制用语:门禁比对两侧文件时提取的有序结构序列(标题层级、代码块、列表等) |\n| Summary | 概述 | | | 事故复盘标题用语 |\n| system prompt | 系统提示词 | | | |\n| taxonomy | 分类体系 | | | |\n| token usage | token 用量 | | | |\n| tool | 工具 | | | |\n| tool call | 工具调用 | | | |\n| tool result | 工具结果 | | | |\n| tool schema | 工具 schema | | | |\n| toolkit | 工具包 | | | |\n| turn | 轮次 | | | |\n| VFS | VFS | 虚拟文件系统(VFS) | | |\n| typecheck | 类型检查 | | | |\n| vocabulary | 词汇 | | | |\n| wire format | 协议格式 | 协议格式(wire format) | | |\n| workflow | 工作流 | | | |\n| wrapper | 包装层 | | | 软件层或 SDK 包装层 |\n| wrapper script | 包装脚本 | | | 可执行脚本包装层 |\n\n\n## Output Format\n\nReturn 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.\n\nThe 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.\n\n```xml\n\n(First pass: the complete translation, written as natural target-language technical prose)\n\n\n\n(Second pass: actual corrections only, one correction per line with a category tag, e.g.)\n- [Tone] \"旁挂记录\" → \"伴随记录\"(生造词)\n- [Sentence] 第 3 段补充逗号断句\n- [Punctuation] 两处破折号替换为冒号\n- [Terminology: pending] source term → tentative rendering\n- 无修正\n\n\n\n(Complete final translation after corrections)\n\n```\n\n## Self-Review Instructions\n\nAfter 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.\n\n**Structure**\n- Are the heading hierarchy and order, list kind and item count, ordered-list start, table dimensions, and code block content identical to the source?\n- Are ALL comments and info strings inside code blocks left untranslated and byte-identical to the source?\n- Are inline code spans and machine-readable tokens verbatim?\n- Is an existing language switcher correctly flipped, and is no switcher or filename invented when the source lacks one?\n- Are link targets and emphasis spans preserved?\n- Does spacing across emphasis boundaries follow the same Chinese/Latin/numeral rule as ordinary prose?\n- Are wrapper-tag lines inside section bodies escaped with one additional backslash?\n\n**Faithfulness**\n- Clause by clause, is anything added, dropped, weakened, strengthened, reversed, merged, or re-bounded? Are list item counts identical on both sides?\n- Do actors, objects, conditions, exceptions, negation, modality, causal relationships, guarantees, contract directions, result channels, ownership changes, and quantities survive exactly?\n\n**Tone & Style**\n- Does every sentence read as if originally written by a native technical author?\n- Is there any colloquial, casual, overly informal, promotional, or metaphorical phrasing?\n- Are actors explicit where the target language needs them, without inventing responsibility?\n\n**Sentence Structure**\n- Are there run-on sentences that need breaking?\n- Are there stiff passive constructions that can safely become active, or active constructions that invent an actor?\n- Are conditions, concessions, negation, coordination, and modifiers scoped clearly?\n\n**Word Choice**\n- Are there overly literal translations that sound unnatural?\n- Are ordinary prose words left untranslated despite an established target-language expression?\n- Does each polysemous word fit its local context?\n- Is the same target-language word used for distinct source concepts, or is a defined term varied merely to avoid repetition?\n- Is any slang or internal jargon present?\n\n**Terminology**\n- For a Chinese target, are first-occurrence glosses correctly applied to the true first prose occurrence, neither missing nor repeated? For an English target, are Chinese glosses absent?\n- Are any \"不要译作\" forbidden translations present?\n- Do protected tokens remain untouched even when they resemble terminology entries?\n- For an unlisted term, does a Chinese target use an established Chinese rendering or preserve the source term as pending when no reliable rendering is known, and does an English target use the established English technical term or preserve only an ambiguous source term with the shortest necessary gloss and a pending notice?\n\n**Punctuation** (when target is Chinese)\n- Are punctuation, mixed-script spacing, quotation marks, Latin letters, and digits in their required forms?\n- Are there em dashes that make the sentence less clear and should be replaced, while natural em dashes remain intact?\n- Are list-item endings grammatically consistent, with none ending in commas?\n- Do RFC 2119 keywords preserve the source emphasis span and normative strength exactly?\n\nRecord actual corrections in ``, then output the corrected complete document in ``. If no correction or pending terminology notice is needed, write exactly `- 无修正` in `` and copy `` unchanged into ``. If `` contains only pending terminology notices, copy `` unchanged into ``.\n\n## Examples\n\nBelow are representative examples of common problems and their corrections. Follow the \"Good\" versions within the rule each example illustrates; examples do not override source context or higher-priority requirements.\n\n### Colloquial verb → Professional verb\n- Source: `The repo pins pnpm@11.7.0 in package.json`\n- Bad: `仓库在 package.json 中钉住 pnpm@11.7.0`\n- Good: `该仓库在 package.json 中固定使用 pnpm@11.7.0`\n\n### Run-on sentence → Natural phrasing with pause\n- Source: `Read docs/architecture.md before changing anything under packages/.`\n- Bad: `改动 packages/ 下的任何东西之前先读 docs/architecture.md。`\n- Good: `在修改 packages/ 目录下的任何内容之前,请先阅读 docs/architecture.md。`\n\n### Stiff passive voice → Active and natural\n- Source: `a green gate means the pair was confirmed consistent at these exact contents, not that the confirmation was sound.`\n- Bad: `门禁绿意味着这对文档曾在当前内容上被确认一致,不意味着这次确认本身是对的。`\n- Good: `门禁通过意味着这组文档在当前内容上的一致性得到了确认,不代表确认本身正确可靠。`\n\n### Invented word → Natural expression\n- Source: `A sidecar record of both blob hashes makes consistency checkable`\n- Bad: `旁挂记录两侧 blob hash,使一致性可检查`\n- Good: `伴随记录保存两侧 blob hash,使一致性可检查`\n\n### Em-dash → Colon/period\n- Source: `FIXME — an issue that should block a new release. A release should not ship with an open FIXME unless reviewers explicitly agree the change can be merged anyway.`\n- Bad: `FIXME——应当阻塞新版本发布的问题。除非评审者明确同意可以照常合入,发布不应带着未解决的 FIXME 出门。`\n- Good: `FIXME:应当阻塞新版本发布的问题。除非评审者明确同意该更改可以合并,否则发布版本不应包含未解决的 FIXME。`\n\n### Overly literal → Meaningful rendering\n- Source: `awkward phrasing is easier to notice when you read the translation without comparing it with the source`\n- Bad: `不把译文和原文比较时,尴尬的措辞更容易被注意`\n- Good: `不对照原文阅读译文时,更容易察觉别扭的表达`\n\n### Terminology — do not translate what should be kept in English\n- Source: `typed service seams, and explicit extension points`\n- Bad: `类型化的服务 seam(扩展点)与显式扩展点`\n- Good: `类型化的服务 seam 与显式扩展点`\n\n### Slang/jargon → Professional phrasing\n- Source: `The committed agent workflow lives in .agents/skills/dsh-translate-docs`\n- Bad: `进仓的 agent 工作流见 .agents/skills/dsh-translate-docs`\n- Good: `仓库内置的 agent 工作流见 .agents/skills/dsh-translate-docs`\n\n### \"For humans\" — translate the intent, not the word\n- Source: `For humans, start with the development guide`\n- Bad: `对于人工读者,请先从开发指南开始`(\"人工读者\"生硬)\n- Good: `面向开发者:请先阅读开发指南`(\"开发者\"自然,且中文里冒号在此处更自然)\n\n### Code block comments — NEVER translate\n- Source code block contains: `# full-screen TUI coding agent (needs DEEPSEEK_API_KEY)`\n- Bad: `# 全屏 TUI coding agent(需要 DEEPSEEK_API_KEY)`\n- Good: `# full-screen TUI coding agent (needs DEEPSEEK_API_KEY)` (keep exactly as-is, byte-for-byte)\n\n### Language switcher — flip direction\n- Source file (English) has: `English | [中文](README.zh.md)`\n- Bad (copying source unchanged): `English | [中文](README.zh.md)`\n- Good (flipped for Chinese file): `[English](README.md) | 中文`\n\n---\n\nNow translate the following document:"
+ "content": "# Translation Prompt\n\nYou are a senior technical translator specializing in LLM and agent development documentation. Your task is to translate the complete source document from English to Chinese, producing natural, professional technical prose.\n\nRead each complete semantic unit, understand it, and restate it as a native technical author would write it in the target language. Do not mechanically preserve source-language syntax. Then verify the translation against the source clause by clause: preserve every proposition and add none. Fluency never justifies losing or altering meaning, and completeness never justifies unnatural word-for-word prose.\n\n## Priority\n\nApply these authorities in order:\n\n1. Preserve the source meaning and the required document structure, protected content, and formatting.\n2. Follow the injected terminology table exactly.\n3. Use the injected whole-document gold pairs to calibrate target-language voice and phrasing.\n4. Apply the general writing guidance and illustrative examples in this prompt.\n\nA lower-priority rule may refine but never override a higher-priority requirement. Gold pairs calibrate voice; they are not a translation memory. No style preference, gold-pair phrasing, or embedded example may override source meaning, required structure, protected content, or the terminology table.\n\n## Quality Requirements\n\n### Structure and Format Preservation\n- Output a complete translated document that maintains the same document frame as the source: heading hierarchy and order, list kinds and item counts, ordered-list starts, table rows and columns, link order and semantic targets, and code blocks.\n- Paragraph boundaries may change within the same structural unit when the target language needs different semantic grouping. Do not merge or move content across headings, list items, table cells, or other independent structural units.\n- Keep each prose paragraph on one physical line. Use paragraph breaks, not hard-wrapped lines inside a paragraph.\n- Fenced code blocks must be byte-identical to the source, including info strings, whitespace, and ALL comments inside them. Do NOT translate or reformat any content inside code blocks. This is a hard rule with no exceptions.\n- Inline code spans must be kept verbatim. This includes commands, flags, paths, identifiers, API and event names, config keys, protocol values, version numbers, and other machine-readable tokens. Never translate or reformat them.\n- Every repository-relative document link must keep the source link's semantic target and exact query/fragment suffix. When the target belongs to the active bilingual corpus, English output uses its `.md` path and Chinese output uses its `.zh.md` path; a missing counterpart in that corpus is an error, while targets outside it keep the original path. External URLs, images, and pure in-page fragments stay unchanged. Translate link text.\n- Language switcher line: when an English source contains `English | [中文](source-filename.zh.md)`, write `[English](source-filename.md) | 中文`. When a Chinese source contains `[English](source-filename.md) | 中文`, write `English | [中文](source-filename.zh.md)`. Do NOT copy the source switcher unchanged. If the source has no switcher, do not invent a filename or switcher; the pipeline inserts the canonical target switcher after parsing ``.\n- Preserve emphasis marker types and the semantic spans they cover. Do not add, remove, move, or change bold and italic markers.\n\n### Faithfulness\n- Preserve every proposition in the source and add none. Every sentence, list item, note, FIXME, warning, example, caveat, prerequisite, and guarantee must have an equivalent in the translation. Count list items on both sides.\n- Preserve actors, objects, conditions, exceptions, negation, modality, causal relationships, and distinctions between concepts.\n- Preserve the exact strength and orientation of contracts. Completion and lifecycle conditions, failure behavior, directions and data flow, normal and exceptional result channels, ownership changes, and quantitative bounds must not be weakened, strengthened, reversed, or merged.\n- Translate ideas rather than source-language idioms, but never use fluency as a reason to omit or alter meaning.\n\n### Tone and Style\n- 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.\n- Write in a professional, formal tone appropriate for developer documentation. Never use colloquial or casual expressions.\n- 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.\n- Prefer established target-language engineering terms over literal renderings. Replace metaphors with direct descriptions that preserve the source meaning.\n- Use polite imperative forms where the text instructs the reader to do something. In Chinese, address the reader as `你`, not `您`.\n- Keep the author's register: concise stays concise, detailed stays detailed.\n\n### Sentence Structure\n- Break long sentences where the target language needs a pause. Avoid run-on sentences.\n- Use active voice when it improves clarity without changing or inventing the actor. Retain passive voice when the actor is unknown, irrelevant, or intentionally omitted.\n- Restructure source-language syntax into clear target-language syntax. Preserve the logical scope of conditions, concessions, negation, coordination, and modifiers.\n- Split or combine clauses when needed for readability, provided every source relationship remains explicit.\n- Translate meaning, not words. Do not invent words or expressions that a native technical author would not use.\n\n### Word Choice\n- Prefer precise, formal vocabulary over casual or colloquial alternatives.\n- When multiple synonyms exist, choose the one most commonly used in professional technical documentation of the target language.\n- Translate ordinary prose when an established target-language expression is clear. Preserve proper nouns, canonical product names, code identifiers, APIs, paths, package names, and terms that the terminology table requires to remain in the source language.\n- Use context to resolve polysemous words. A familiar word does not have one fixed rendering in every technical domain.\n- Avoid slang, internal jargon, or overly literal translations that would not be recognized by the general developer audience.\n- Do not use the same word to translate distinct source-language concepts when their distinction matters.\n- Avoid repeating the same ordinary verb in close proximity when a natural equivalent preserves the exact meaning. Never vary a terminology-table form, defined concept, or contract verb merely for stylistic variety.\n\n#### When translating into Chinese\n- When a number modifies a noun, include a natural Chinese classifier or measure word when Chinese grammar requires one. For example: \"three-role capability seam\" → \"包含三种角色的能力 seam\", not \"三角色 seam\". Do not add classifiers to code, identifiers, versions, units, or fixed names.\n\n### Punctuation\n\n#### When translating into Chinese\n- Use full-width Chinese punctuation in Chinese prose: `,。:;?!()「」`. Keep half-width punctuation inside code spans, numbers, and complete verbatim English text.\n- Prefer colons, periods, commas, or parentheses over em dashes when they make the sentence clearer or more natural. Keep an em dash when it is the clearest natural punctuation.\n- Use enumeration commas (、) between parallel Chinese items, not regular commas.\n- Keep list-item endings consistent with their grammar. Complete sentences may end with periods or other grammatically required punctuation; do not end list items with commas.\n- Put one half-width space between Chinese text and Latin words or numerals. Do not add a space next to full-width punctuation, and do not leave a meaningless half-width space between two Chinese characters.\n- Markdown emphasis markers do not create a word boundary. Determine spacing from the rendered adjacent characters: Chinese next to Chinese takes no space, while Chinese next to a Latin word or numeral takes one half-width space.\n- Use half-width digits and Latin letters, never full-width forms.\n- For RFC 2119 keywords (MUST, MUST NOT, SHOULD, MAY), translate to the corresponding Chinese term (必须、禁止、应当、可以), preserve the SOURCE emphasis span exactly, and do not weaken its normative strength: plain source stays plain (必须), italic source stays italic (*必须*), and bold source stays bold (**必须**).\n\n#### When translating into English\n- Use half-width English punctuation and standard English spacing. Preserve full-width punctuation only in verbatim Chinese text.\n- Convert enumeration commas (、) to English commas and Chinese prose quotation marks to English double quotes.\n- Convert Chinese topic-comment sentences and omitted-subject constructions into clear English subjects when the actor is stated or unambiguously implied. Do not invent an actor.\n- Use concise professional developer prose and established English technical terms. Do not transliterate Chinese engineering idioms literally.\n- Use the terminology table's English column exactly and do not carry Chinese first-occurrence glosses into English prose.\n\n## Terminology\n\nA terminology table is provided below. Follow it strictly:\n- Render every listed term exactly as specified.\n- When the target language is Chinese, use the \"中文\" column. On the document's first prose occurrence, write the \"首次出现\" value when one is specified; on later occurrences, write only the part before the parenthetical gloss.\n- When the target language is English, use the \"English\" column without a Chinese gloss; do not copy the \"中文\" or \"首次出现\" value into English prose.\n- If a term has already been glossed as part of a compound term, do not gloss it again when it appears alone later.\n- NEVER use translations listed in the \"不要译作\" column.\n- Code spans and other protected tokens remain verbatim even when their text resembles a listed term.\n- For an unlisted technical term, use an established target-language technical term when its meaning is unambiguous in context. For a Chinese target, use an established Chinese rendering from a major Chinese-language OSS or vendor source; if you cannot reliably determine such a rendering, preserve the source term and record `[Terminology: pending]` in `` with a tentative rendering for human review. For an English target, use the established English technical term; if the source term has no unambiguous established equivalent, preserve it with the shortest English gloss needed to make it intelligible and record `[Terminology: pending]` in ``. A tentative rendering may appear in `` but must not be silently adopted in `` or ``, and you must not invent or claim a specific external precedent. This rule applies to terminology only; for general prose, freely restructure and paraphrase for natural expression.\n\n# Terminology\n\n本表约定本仓库的中英术语统一译法。\n\n**通用规则:**\n- \"中文\"列为中文译文的正文默认用词。若该列为英文,则中文译文的正文中保留英文不翻译。\n- 首次出现按\"首次出现\"列书写(带括号注释);后续出现只写括号前的部分(可能为中文,也可能为英文),不出现括号内的注释。\n- \"不要译作\"列为严格禁止的译法。\n- 如果某术语已经作为另一个术语的组成部分被括注过(如 `agent loop(智能体循环)` 中已包含 `agent` 的括注),则该术语后续单独出现时无需再次括注。\n\n## 缩写类(中英文文本中均使用缩写)\n\n| English | 中文 | 首次出现 | 不要译作 | 备注 |\n|---|---|---|---|---|\n| ACP | ACP | ACP(Agent Client Protocol) | | |\n| AI | AI | AI(人工智能) | | |\n| API | API | | | |\n| CI | CI | | | |\n| CLI | CLI | CLI(命令行界面) | | |\n| e2e | e2e | | | |\n| HMR | HMR | HMR(热模块替换) | | |\n| JSON Schema | JSON Schema | | | |\n| JSONL | JSONL | | | |\n| LLM | LLM | LLM(大语言模型) | | |\n| MCP | MCP | | | |\n| PR | PR | PR(Pull Request) | | |\n| RAG | RAG | RAG(检索增强生成) | | |\n| SDK | SDK | | | 只指受支持的 Python 与 TypeScript SDK 所使用的 JSON-RPC 客户端/服务器协议;DeepSeek Harness 项目本身不是 SDK |\n| SSE | SSE | SSE(Server-Sent Events) | | |\n\n## 英文类(中英文文本中均使用英文)\n\n| English | 中文 | 首次出现 | 不要译作 | 备注 |\n|---|---|---|---|---|\n| agent | agent | agent(智能体) | | |\n| Agent Note | Agent Note | | 智能体注记、智能体笔记 | 仓库定义的文档类型,涵盖提案、已实现决策和被否决提案;中文对侧 H1 保持固定前缀 `# Agent Note: `,标题中不加术语括注 |\n| agent harness | agent harness | agent harness(智能体框架) | | agent 组合词(agent harness/workflow/loop/skill 等)整体保留英文;未括注过 agent 时首现按对应组合词或 agent 行处理 |\n| agent loop | agent loop | agent loop(智能体循环) | | |\n| blob hash | blob hash | | | `git hash-object` 的结果 |\n| coding agent | coding agent | coding agent(编程智能体) | | agent 组合词,正文保留英文 |\n| Cordis | Cordis | | | |\n| dispose | dispose | dispose(资源释放) | | |\n| doc-sync | doc-sync | doc-sync(文档同步门禁) | | |\n| fiber | fiber | | | |\n| fixture | fixture | fixture(测试前置数据) | | |\n| fork | fork | | | |\n| Function Calling | Function Calling | Function Calling(函数调用) | | |\n| harness | harness | | | |\n| harness engineering | harness engineering | | | |\n| KV Cache | KV Cache | | | 专有技术名称,保持大小写与空格 |\n| lint | lint | | | |\n| mock | mock | | | 保留英文;指测试替身 |\n| loader | loader | | | |\n| manifest | manifest | manifest(元数据清单) | | |\n| monorepo | monorepo | | | |\n| Round | Round | | 回合、目标回合、Ralph 回合 | 外层策略使用 Round 时,领域层级为 Session > Round > Turn(轮次) > Step(步骤);Round 是可选的外层策略迭代,并非每个会话轮次都具有的通用层级。Goal Round 与 Ralph Round 均保留英文。一个 Round 承载一个轮次,步骤隶属于该轮次;明确的零步骤轮次仍保持原义。 |\n| schema | schema | | | |\n| schema DSL | schema DSL | | | |\n| seam | seam | | 接缝 | 一个可替换能力的整体,包含 Service Definition / Service Provider / Consumer 三种角色;角色需要独立演化时才拆包,也可由同一包承担多个角色。以 `packages/shell` 为范例;Service Definition 是 Cordis `Service`(抽象类或具体 registry 服务),不是 TypeScript interface。任何单一角色、普通边界或扩展点都不能称为 seam。本仓库正文保留英文;与 `extension point` 是不同概念 |\n| Service Provider | Service Provider | | Service provider | 能力 seam 的命名角色;单数固定写作 Service Provider,复数写作 Service Providers。泛指提供服务的 provider 不适用本词条 |\n| skill | skill | skill(技能) | | |\n| slot | slot | | 坑位、孔位 | 客户端架构中的具名可注册位置,保留英文 |\n| spill | spill | | | 工具输出超限落盘机制;组合词写 `spill 文件`、`spill 路径` |\n| spawn | spawn | | | |\n| steering | steering | steering(中途引导) | | |\n| job id | job id | | 任务 id | 保留英文 |\n| subagent | subagent | | | |\n| transcript | transcript | transcript(文本记录) | | 指会话渲染给用户或编辑器的完整文本,区别于事件日志 |\n| Typert | Typert | | TypeRT、typeRT、Type RT | DeepSeek Harness 类型图、生成器、loader 与运行时 registry 的产品拼写 |\n| waterfall | waterfall | waterfall(瀑布式事件) | | |\n| wheel | wheel 包 | | | Python 打包格式 |\n| worktree | worktree | | | git 工作区概念 |\n| Zstandard | Zstandard | | | RFC 8878 compression format; `zstd` remains a code value. |\n\n## 双语类(中英文文本各自使用中英文)\n\n| English | 中文 | 首次出现 | 不要译作 | 备注 |\n|---|---|---|---|---|\n| adapter | 适配器 | | | |\n| adapter contract | 适配器约定 | 适配器约定(adapter contract) | | |\n| append-only | 仅追加 | | | |\n| artifact | 产物 | | 制品 | |\n| backend | 后端 | | | |\n| binder | 绑定器 | | | 命名角色:把已声明接口绑定到调用方 context 或生命周期 |\n| config | 配置 | | | 命名角色:一个已解析配置值或边界严格的配置记录 |\n| controller | 控制器 | | | 命名角色:接受意图并改变一项既有领域或展示状态 |\n| directory | 目录 | | | 命名角色:暴露供发现或选择的条目及元数据 |\n| engine | 引擎 | | | 命名角色:实现领域算法或有状态执行模型 |\n| gateway | 网关 | | | 命名角色:适配进程、网络、RPC 或 API 边界 |\n| handle | 句柄 | | | 命名角色:引用并控制或观察一个实时资源 |\n| policy | 策略 | | | 命名角色:决定允许、选择、限制或观察什么 |\n| presenter | 展示转换器 | | | 命名角色:把领域值纯转换为渲染意图 |\n| resolver | 解析器 | | | 命名角色:根据输入计算或定位一个答案 |\n| store | 存储 | | | 命名角色:拥有一组数据并主要提供数据操作 |\n| background job | 后台任务 | | | |\n| block | 块 | | | |\n| build target | 构建目标 | | | |\n| cancel | 取消 | | | |\n| canary test | canary 测试 | | 金丝雀测试 | 本仓库保留 `canary` |\n| capability | 能力 | | | 必须与 `feature` → `功能` 区分 |\n| capability seam | 能力 seam | | 功能 seam、能力接缝 | 本仓库 Service Definition、Service Provider 与 Consumer 三种角色组成完整可替换能力的命名架构概念;普通 `seam` 仍按其词条处理 |\n| feature | 功能 | | 能力 | SDK 产品与工程模型中的可管理产品单元 |\n| feature option | 功能选项 | | variant | 一项 SDK 功能内有限、可选择的实现或配置 |\n| checkpoint | 检查点 | | | |\n| chunk | 分片 | | | |\n| compaction | 压缩 | 压缩(compaction) | | |\n| companion tool | 配套工具 | | | |\n| composition bundle | 组合包 | | | 只约束应用或插件的组合语境,不约束所有 `bundle` |\n| Cordis plugin config | Cordis 插件配置 | | | Cordis 插件公开的 `Config` 对象或配置结构 |\n| config key | 配置键 | | | Cordis 插件配置中的单个字段 |\n| consumer | 消费方 | | 消费者 | |\n| content block | 内容块 | | | |\n| Cookbook | 实操手册 | | | 文档标题用语 |\n| context | 上下文 | | | |\n| counterpart | 对侧文件 | | 对应物、配对物 | 双语配对语境;泛指\"另一侧\"时可写「另一侧」 |\n| configurable-provider directory | 可配置提供方目录 | | | llm seam 中 `registerConfigurableProviders()` 维护的目录;沿用 Service Catalog →「服务目录」先例 |\n| context compaction | 上下文压缩 | 上下文压缩(context compaction) | | |\n| contract | 约定 | | | 如:`pairing contract` →`配对约定` |\n| Cordis config entry | Cordis 配置项 | | | 指 `cordis.yml` 插件列表中的一项;插件实现本身写`Cordis 插件` |\n| Cordis plugin | Cordis 插件 | | | Cordis 加载的插件实现,不指 `cordis.yml` 中的一项配置 |\n| crash recovery | 崩溃恢复 | | | |\n| deploy root | 部署根目录 | | | |\n| dormant | 休眠 | | 睡眠、蛰伏 | 指已声明可配置但当前未注册路由的提供方 |\n| durability | 持久性 | | | |\n| feature requirement | 功能依赖 | | | 功能或功能选项通过 `requires` 声明的关系 |\n| event | 事件 | | | |\n| event log | 事件日志 | | | |\n| event stream | 事件流 | | | |\n| event-sourced | 事件溯源 | | | 沿用 DDD 社区通行译法 |\n| Executive summary | 摘要 | | | 事故复盘标题用语 |\n| executor | 执行器 | | | |\n| expected output | 预期输出 | | 金标 | 指 snapshot 比较产物;翻译语料的人工校准样例不在此列 |\n| extension | 扩展 | | | |\n| extension point | 扩展点 | | | 注意与 `seam` 区分 |\n| fail-fast | 快速失败 | | | |\n| fenced code block | 围栏代码块 | | | 沿用 MDN 中文翻译 |\n| fingerprint | 指纹 | | | 通用内容指纹;双语配对机制使用 sidecar record 记录两侧 blob hash |\n| finish reason | 结束原因 | | | |\n| fold | 折叠区 | | | 配置界面语境:默认收起的字段分区(collapsed →「收起」)|\n| foreground run | 前台运行 | | | |\n| freshness | 新鲜度 | | | 沿用 MDN 中文翻译;在本项目中指译文相对源文的同步状态 |\n| hook | 钩子 | | | |\n| implementation | 实现 | | | |\n| inference | 推理 | 推理(inference) | | 需要和 `reasoning` 区分时保留英文括注 |\n| info string | 信息字符串 | | | 沿用 CommonMark 中文翻译;指代码围栏 ``` 之后的语言标注 |\n| injection | 注入 | | | |\n| integration | 集成 | | | |\n| interface | 接口 | | | |\n| language switcher | 语言切换行 | | | i18n 配对机制用语:双语配对文件顶部的互链行 |\n| merge | 合并 | | | |\n| message | 消息 | | | |\n| mod | 模组 | | | |\n| model provider | 模型提供方 | | | |\n| model selection | 模型选择 | | 模型目标 | 面向 Agent 的提供方、模型和可选推理强度选择。 |\n| module | 模块 | | | |\n| non-escalation | 非升权 | | 非升级、不可升级 | 仅用于安全与权限语境,指主体不得获得超出既有授权的权限;普通升级不适用此行 |\n| npm dependency | NPM 依赖 | | | `package.json` 中的包关系;`dependencies`、`devDependencies` 等字段保持原样 |\n| opt-out ratio | opt-out 比例 | | 退出检查比例 | |\n| orphan | 遗留 | | 孤儿、孤立 | 指英文源已不存在的 `.zh.md`(如「遗留译文」);进程语境按 OS 惯用语译「孤儿进程」 |\n| orphan branch | 孤立分支 | | 孤儿分支 | 沿用 git 官方中文翻译 |\n| package | 包 | | | 指 npm 包(`@deepseek-ai/dsh-*`);`package.json` 等代码标识保持原样 |\n| pairing | 配对 | | | |\n| parent-subset grants | 父级子集授权 | | 父集合授权 | 指授权范围仅限于父级所持授权的子集 |\n| peer dependency | 对等依赖 | 对等依赖(peer dependency) | | |\n| permission | 权限 | | | |\n| persistence | 持久化 | | | |\n| pipeline | 流水线 | | | |\n| plugin | 插件 | | | |\n| postmortem | 事故复盘 | 事故复盘(postmortem) | 事后分析、事故记录 | 事故记录与分析文档;目录或路径中的 `postmortem` 保持代码形式 |\n| prompt | 提示词 | | | |\n| provider | 提供方 | | | |\n| provider-neutral | 提供方无关 | | 提供方中立 | |\n| quality gate | 质量门禁 | | | |\n| quiescence | 完全停稳 | | 静默、静止状态 | 指生命周期工作全部结算后的状态 |\n| reasoning | 推理 | 推理(reasoning) | | 需要和 `inference` 区分时保留英文括注 |\n| reasoning_content | 思考内容 | | | |\n| registry | 注册表 | | | |\n| replay | 回放 | | | |\n| resume | 恢复 | | | |\n| runtime | 运行时 | | | |\n| same-world subprocess | 与宿主共享文件系统和内核的子进程 | | 同世界子进程 | |\n| sandbox | 沙箱 | | | |\n| service | 服务 | | | |\n| serving interface | 对外服务接口 | | | |\n| session | 会话 | | | |\n| session event | 会话事件 | | | |\n| setup card | 设置卡片 | | | 首次运行时代替行卡直接展开的配置卡 |\n| sidecar file | 伴随文件 | | | 指与文档同目录的普通伴随文件 |\n| sidecar record | 伴随记录 | | 旁挂记录 | 指与文档同目录的伴随记录文件 |\n| smoke test | 冒烟测试 | | | |\n| snapshot | 快照 | | | |\n| source of truth | 真源 | | 事实来源、唯一来源 | |\n| spine | 主干 | | | |\n| stale | 陈旧 | | 过期 | 与 `fresh`(`新鲜`)成对;门禁输出中保留英文 `stale` 不翻译;`expired` 才译为`过期` |\n| step | 步骤 | | | |\n| stream | 流 | | | |\n| structural signature | 结构签名 | | | i18n 配对机制用语:门禁比对两侧文件时提取的有序结构序列(标题层级、代码块、列表等) |\n| Summary | 概述 | | | 事故复盘标题用语 |\n| system prompt | 系统提示词 | | | |\n| taxonomy | 分类体系 | | | |\n| token usage | token 用量 | | | |\n| tool | 工具 | | | |\n| tool call | 工具调用 | | | |\n| tool result | 工具结果 | | | |\n| tool schema | 工具 schema | | | |\n| toolkit | 工具包 | | | |\n| turn | 轮次 | | | |\n| VFS | VFS | 虚拟文件系统(VFS) | | |\n| typecheck | 类型检查 | | | |\n| vocabulary | 词汇 | | | |\n| wire format | 协议格式 | 协议格式(wire format) | | |\n| workflow | 工作流 | | | |\n| wrapper | 包装层 | | | 软件层或 SDK 包装层 |\n| wrapper script | 包装脚本 | | | 可执行脚本包装层 |\n\n\n## Output Format\n\nReturn 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.\n\nThe 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.\n\n```xml\n\n(First pass: the complete translation, written as natural target-language technical prose)\n\n\n\n(Second pass: actual corrections only, one correction per line with a category tag, e.g.)\n- [Tone] \"旁挂记录\" → \"伴随记录\"(生造词)\n- [Sentence] 第 3 段补充逗号断句\n- [Punctuation] 两处破折号替换为冒号\n- [Terminology: pending] source term → tentative rendering\n- 无修正\n\n\n\n(Complete final translation after corrections)\n\n```\n\n## Self-Review Instructions\n\nAfter 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.\n\n**Structure**\n- Are the heading hierarchy and order, list kind and item count, ordered-list start, table dimensions, and code block content identical to the source?\n- Are ALL comments and info strings inside code blocks left untranslated and byte-identical to the source?\n- Are inline code spans and machine-readable tokens verbatim?\n- Is an existing language switcher correctly flipped, and is no switcher or filename invented when the source lacks one?\n- Do links preserve their semantic targets and exact query/fragment suffixes while using target-locale paths, and are emphasis spans preserved?\n- Does spacing across emphasis boundaries follow the same Chinese/Latin/numeral rule as ordinary prose?\n- Are wrapper-tag lines inside section bodies escaped with one additional backslash?\n\n**Faithfulness**\n- Clause by clause, is anything added, dropped, weakened, strengthened, reversed, merged, or re-bounded? Are list item counts identical on both sides?\n- Do actors, objects, conditions, exceptions, negation, modality, causal relationships, guarantees, contract directions, result channels, ownership changes, and quantities survive exactly?\n\n**Tone & Style**\n- Does every sentence read as if originally written by a native technical author?\n- Is there any colloquial, casual, overly informal, promotional, or metaphorical phrasing?\n- Are actors explicit where the target language needs them, without inventing responsibility?\n\n**Sentence Structure**\n- Are there run-on sentences that need breaking?\n- Are there stiff passive constructions that can safely become active, or active constructions that invent an actor?\n- Are conditions, concessions, negation, coordination, and modifiers scoped clearly?\n\n**Word Choice**\n- Are there overly literal translations that sound unnatural?\n- Are ordinary prose words left untranslated despite an established target-language expression?\n- Does each polysemous word fit its local context?\n- Is the same target-language word used for distinct source concepts, or is a defined term varied merely to avoid repetition?\n- Is any slang or internal jargon present?\n\n**Terminology**\n- For a Chinese target, are first-occurrence glosses correctly applied to the true first prose occurrence, neither missing nor repeated? For an English target, are Chinese glosses absent?\n- Are any \"不要译作\" forbidden translations present?\n- Do protected tokens remain untouched even when they resemble terminology entries?\n- For an unlisted term, does a Chinese target use an established Chinese rendering or preserve the source term as pending when no reliable rendering is known, and does an English target use the established English technical term or preserve only an ambiguous source term with the shortest necessary gloss and a pending notice?\n\n**Punctuation** (when target is Chinese)\n- Are punctuation, mixed-script spacing, quotation marks, Latin letters, and digits in their required forms?\n- Are there em dashes that make the sentence less clear and should be replaced, while natural em dashes remain intact?\n- Are list-item endings grammatically consistent, with none ending in commas?\n- Do RFC 2119 keywords preserve the source emphasis span and normative strength exactly?\n\nRecord actual corrections in ``, then output the corrected complete document in ``. If no correction or pending terminology notice is needed, write exactly `- 无修正` in `` and copy `` unchanged into ``. If `` contains only pending terminology notices, copy `` unchanged into ``.\n\n## Examples\n\nBelow are representative examples of common problems and their corrections. Follow the \"Good\" versions within the rule each example illustrates; examples do not override source context or higher-priority requirements.\n\n### Colloquial verb → Professional verb\n- Source: `The repo pins pnpm@11.7.0 in package.json`\n- Bad: `仓库在 package.json 中钉住 pnpm@11.7.0`\n- Good: `该仓库在 package.json 中固定使用 pnpm@11.7.0`\n\n### Run-on sentence → Natural phrasing with pause\n- Source: `Read docs/architecture.md before changing anything under packages/.`\n- Bad: `改动 packages/ 下的任何东西之前先读 docs/architecture.md。`\n- Good: `在修改 packages/ 目录下的任何内容之前,请先阅读 docs/architecture.md。`\n\n### Stiff passive voice → Active and natural\n- Source: `a green gate means the pair was confirmed consistent at these exact contents, not that the confirmation was sound.`\n- Bad: `门禁绿意味着这对文档曾在当前内容上被确认一致,不意味着这次确认本身是对的。`\n- Good: `门禁通过意味着这组文档在当前内容上的一致性得到了确认,不代表确认本身正确可靠。`\n\n### Invented word → Natural expression\n- Source: `A sidecar record of both blob hashes makes consistency checkable`\n- Bad: `旁挂记录两侧 blob hash,使一致性可检查`\n- Good: `伴随记录保存两侧 blob hash,使一致性可检查`\n\n### Em-dash → Colon/period\n- Source: `FIXME — an issue that should block a new release. A release should not ship with an open FIXME unless reviewers explicitly agree the change can be merged anyway.`\n- Bad: `FIXME——应当阻塞新版本发布的问题。除非评审者明确同意可以照常合入,发布不应带着未解决的 FIXME 出门。`\n- Good: `FIXME:应当阻塞新版本发布的问题。除非评审者明确同意该更改可以合并,否则发布版本不应包含未解决的 FIXME。`\n\n### Overly literal → Meaningful rendering\n- Source: `awkward phrasing is easier to notice when you read the translation without comparing it with the source`\n- Bad: `不把译文和原文比较时,尴尬的措辞更容易被注意`\n- Good: `不对照原文阅读译文时,更容易察觉别扭的表达`\n\n### Terminology — do not translate what should be kept in English\n- Source: `typed service seams, and explicit extension points`\n- Bad: `类型化的服务 seam(扩展点)与显式扩展点`\n- Good: `类型化的服务 seam 与显式扩展点`\n\n### Slang/jargon → Professional phrasing\n- Source: `The committed agent workflow lives in .agents/skills/dsh-translate-docs`\n- Bad: `进仓的 agent 工作流见 .agents/skills/dsh-translate-docs`\n- Good: `仓库内置的 agent 工作流见 .agents/skills/dsh-translate-docs`\n\n### \"For humans\" — translate the intent, not the word\n- Source: `For humans, start with the development guide`\n- Bad: `对于人工读者,请先从开发指南开始`(\"人工读者\"生硬)\n- Good: `面向开发者:请先阅读开发指南`(\"开发者\"自然,且中文里冒号在此处更自然)\n\n### Code block comments — NEVER translate\n- Source code block contains: `# full-screen TUI coding agent (needs DEEPSEEK_API_KEY)`\n- Bad: `# 全屏 TUI coding agent(需要 DEEPSEEK_API_KEY)`\n- Good: `# full-screen TUI coding agent (needs DEEPSEEK_API_KEY)` (keep exactly as-is, byte-for-byte)\n\n### Language switcher — flip direction\n- Source file (English) has: `English | [中文](README.zh.md)`\n- Bad (copying source unchanged): `English | [中文](README.zh.md)`\n- Good (flipped for Chinese file): `[English](README.md) | 中文`\n\n---\n\nNow translate the following document:"
},
{
"role": "user",
@@ -12,7 +12,7 @@
},
{
"role": "assistant",
- "content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness(`dsh`)是由 [DeepSeek AI](https://deepseek.com) 开发的开源 agent harness(智能体框架)。\n\n它采用**一切皆插件**的架构,并由 [Cordis](https://github.com/cordiverse/cordis) 驱动,其设计参见论文 [_A Programming Paradigm for Spatiotemporal Composability_](https://github.com/cordiverse/paper)。\n\n## 开发者预览\n\nDeepSeek Harness 目前处于 _开发者预览_ 阶段,正在快速迭代。**未来将出现破坏兼容性的变更。**\n\n## 运行\n\n### 通过 `npm` 运行\n\n安装 `Node.js`,然后运行:\n\n```sh\nnpx @deepseek-ai/dsh web\n```\n\n该命令默认会在 `http://127.0.0.1:3080` 启动 Web UI,本机启动时还会用默认浏览器打开页面。通过 SSH 启动时只打印宿主机 URL,因为本地转发地址由 SSH 客户端或编辑器持有。传入 `--no-open` 可仅运行服务器而不打开浏览器。详见 [Web UI 指南](docs/user/guide/index.md)。\n\n### 从源码运行\n\n如需从仓库源码运行:\n\n```sh\ngit clone https://github.com/deepseek-ai/deepseek-harness.git\ncd deepseek-harness\npnpm install\npnpm run build\npnpm dsh web\n```\n\n`pnpm run build` 会准备仓库产物。`pnpm dsh web` 会直接使用这些已构建产物,不会重新构建。\n\n## 社区与支持\n\n- 欢迎通过 [GitHub Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions) 提交反馈或 bug 报告。\n- 为你的插件仓库添加 [`dsh-plugin`](https://github.com/topics/dsh-plugin) 话题,便于被发现。\n- 欢迎加入 DeepSeek Harness 企微群:扫码添加企微小助手并填写入群问卷,完成后小助手会邀请你入群。\n\n\n \n \n | 企微小助手 | \n 入群问卷 | \n 微信公众号 | \n
\n \n \n \n  | \n  | \n  | \n
\n \n
\n\n## 参与贡献\n\n参见 [CONTRIBUTING.md](CONTRIBUTING.md)。\n\n## 开发\n\n请先阅读[开发指南](docs/development.md)与[架构文档](docs/architecture.md)。\n\n面向 agent:请遵循 [AGENTS.md](AGENTS.md)。\n\n## 许可证\n\n[MIT](LICENSE)\n\n第三方依赖及其许可证见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。\n"
+ "content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness(`dsh`)是由 [DeepSeek AI](https://deepseek.com) 开发的开源 agent harness(智能体框架)。\n\n它采用**一切皆插件**的架构,并由 [Cordis](https://github.com/cordiverse/cordis) 驱动,其设计参见论文 [_A Programming Paradigm for Spatiotemporal Composability_](https://github.com/cordiverse/paper)。\n\n## 开发者预览\n\nDeepSeek Harness 目前处于 _开发者预览_ 阶段,正在快速迭代。**未来将出现破坏兼容性的变更。**\n\n\n\n## 运行\n\n### 通过 `npm` 运行\n\n安装 `Node.js`,然后运行:\n\n```sh\nnpx @deepseek-ai/dsh web\n```\n\n该命令默认会在 `http://127.0.0.1:3080` 启动 Web UI,本机启动时还会用默认浏览器打开页面。通过 SSH 启动时只打印宿主机 URL,因为本地转发地址由 SSH 客户端或编辑器持有。传入 `--no-open` 可仅运行服务器而不打开浏览器。详见 [Web UI 指南](docs/user/guide/index.zh.md)。\n\n\n\n### 从源码运行\n\n如需从仓库源码运行:\n\n```sh\ngit clone https://github.com/deepseek-ai/deepseek-harness.git\ncd deepseek-harness\npnpm install\npnpm run build\npnpm dsh web\n```\n\n`pnpm run build` 会准备仓库产物。`pnpm dsh web` 会直接使用这些已构建产物,不会重新构建。\n\n## 社区与支持\n\n- 欢迎通过 [GitHub Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions) 提交反馈或 bug 报告。\n- 为你的插件仓库添加 [`dsh-plugin`](https://github.com/topics/dsh-plugin) 话题,便于被发现。\n- 欢迎加入 DeepSeek Harness 企微群:扫码添加企微小助手并填写入群问卷,完成后小助手会邀请你入群。\n\n\n \n \n | 企微小助手 | \n 入群问卷 | \n 微信公众号 | \n
\n \n \n \n  | \n  | \n  | \n
\n \n
\n\n## 参与贡献\n\n参见 [CONTRIBUTING.md](CONTRIBUTING.zh.md)。\n\n## 开发\n\n请先阅读[开发指南](docs/development.zh.md)与[架构文档](docs/architecture.zh.md)。\n\n面向 agent:请遵循 [AGENTS.md](AGENTS.md)。\n\n## 许可证\n\n[MIT](LICENSE)\n\n第三方依赖及其许可证见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。\n"
},
{
"role": "user",
@@ -20,31 +20,31 @@
},
{
"role": "assistant",
- "content": "# 开发指南\n\n[English](development.md) | 中文\n\n搭建教程引导新贡献者从准备前置条件开始,直到检出目录通过检查。后面的贡献者参考介绍仓库布局、日常工作流和 CI 组织方式。设计依据与实现细节属于链接的 Agent Note 和脚本。\n\n## 搭建教程\n\n### 前置条件\n\n- Node.js 支持 22.19+ 与 24+。CI 覆盖 22.19、24 和 26;见 [Node 引擎下限 Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md)。\n- 启用了 Corepack 的 pnpm。仓库在 `package.json` 中固定使用 `pnpm@11.7.0`;如果 `pnpm --version` 无法通过 Corepack 解析,请先运行 `corepack enable`。\n- Git 2.26 或更高版本;钩子设置会启用 Git 的 worktree 专属配置扩展。\n- 可选:一个 DeepSeek API key,用于 Web、headless 和 ACP(Agent Client Protocol)自动化 agent(智能体)演示以及真实 API 的 e2e 测试。\n\n### 首次搭建\n\n在仓库根目录安装依赖:\n\n```sh\npnpm install\n```\n\n安装过程还会通过 `scripts/install-lefthook.mjs` 配置 worktree 本地的 Lefthook 钩子和 `dsh-translation-pairing` Git 合并驱动。[worktree 本地钩子 Agent Note](../.agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.md) 负责钩子路径的安全约定;[自动配对合并 Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) 负责合并驱动。\n\n如果依赖是从缓存恢复或 `postinstall` 被跳过而导致任一集成缺失,请手动安装:\n\n```sh\nnode scripts/install-lefthook.mjs\n```\n\n如果包装脚本拒绝现有 Git 配置或报告陈旧锁,请遵循其诊断和所链接的 Agent Note,不要凭猜测编辑 worktree 元数据。移动检出目录后,请重新运行包装脚本以重新生成自有路径。\n\n新克隆后请先运行一次类型检查:\n\n```sh\npnpm run typecheck\n```\n\n`pnpm run typecheck` 成功退出即表示搭建完成。\n\n## 贡献者参考\n\n### TypeScript 项目布局\n\n仓库使用相互隔离的 Host 与 Client aggregate。普通包只登记进其中一个 aggregate;Host 包进入 `tsconfig.host.json`,Client 包进入 `tsconfig.client.json`。\n\n| 文件 | 角色 | 是否构成 program? |\n|---|---|---|\n| `tsconfig.json` | solution 根:`extends` base、`files: []`、引用两个 aggregate。它是 tsserver 发现入口,也是显式执行整张 Project Reference 图时的入口;经继承的 `paths` 充当 tsx 运行 `examples/` 与 `scripts/` 时的解析配置。 | 否 |\n| `tsconfig.host.json` | Host aggregate:Host 包、示例、测试、脚本和 website,以及 `api/remotes` 的 Host 特例 project。 | 是 |\n| `tsconfig.client.json` | Client aggregate:`packages/client/*` 包及其测试、`apps/web`,以及 `api/remotes` 的 Client 特例 project。 | 是 |\n| `tsconfig.base.json` | 共享 compilerOptions 与源码 `paths` 映射。同时是各 vitest 配置让 vite-tsconfig-paths 指向的解析门面:它没有 `include`,因此其 `paths` 适用于任何 importer。 | 否 |\n| `tsconfig.base.client.json` | 浏览器编译设置(`jsx`、DOM lib、`types: []`),由 Client aggregate 和每个 `packages/client/*` 包 extends。 | 否 |\n\nHost 与 Client 保持两个 aggregate program,是因为两侧在相同键下以不同服务对 cordis `Context` 接口做声明合并;单一 program 同时看到两份合并会报冲突。这种冲突只存在于 `ts.Program` 内部——模块解析永远不会触发它——所以 solution 可以同时引用两个 aggregate,一个 paths 门面也可以横跨两侧。由此推出三条纪律:\n\n- `tsconfig.base.json` 永不添加 `include` 或 `files`:它们会泄漏进每个 extends 它的包项目,并收窄门面的全匹配范围。\n- 构造全仓 `ts.Program` 的脚本显式以 `tsconfig.host.json` 或 `tsconfig.client.json` 为种子——根 solution 永不作为种子,因为把两个 aggregate 展平进一个 program 会撞上 `Context` 合并冲突。\n- 新包只登记进一个 aggregate。包同时具有 Node loader 入口和 browser 入口并不构成拆分理由;普通 Client 插件的两份运行时产物都在 Client 构建阶段生成。\n\n`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;该门禁按「两个 leaf 配置同时存在」自动发现拆分包,所以新拆分的包会自动纳入管辖。不要把该结构推广到其他包;[`api-remotes` README](../packages/api/remotes/README.md) 说明 Host/Client 拆分与构建顺序。\n\n根构建按生成依赖排序:\n\n```sh\ntsc -b tsconfig.host.json\ntsdown --env.DSH_BUILD_FACE host\ntsc -b tsconfig.client.json\ntsdown --env.DSH_BUILD_FACE client\npnpm run build:web\n```\n\n两次 tsdown 都使用同一组完整 workspace 匹配,不扫描构建产物来发现 Client 包,也不维护 Host/Client 包过滤表。包内 tsdown 配置根据 `DSH_BUILD_FACE` 决定当前阶段的入口:普通 Client 插件在 Client 阶段同时生成 Node loader 与 browser bundle;`api-remotes` 通过 `hostPhase: true` 提前生成 Host 入口,再在 Client 阶段只生成 browser bundle。tsdown 只消费 `lib/types` 中由前置 tsc 发射的 JavaScript。\n\nTypert 只在 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)。\n\n`pnpm run build` 会内联调用方精确的 `DSH_CLIENT_*` 环境;未设置时不使用任何公开 client 值。`pnpm run build:official` 是与 CI 和 release 产物构建等价的跨平台本地命令。每次完整构建成功后都会写入一份被 gitignore 的记录,把这些值与 Vite 输出及动态 client bundle 绑定;release 打包和 built Web 测试会拒绝缺少记录或被后续局部构建改动的产物。\n\n静态分析和测试通过 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)。\n\n业务服务在 Host 使用 `@Remote` 或 `@RemoteScope` 声明可调用方法;Host 构建生成 Host-for-Client 类型与运行时贡献,Client 的 `api-remotes` 组合加载这些贡献并挂到 `ctx.remote` 与作用域 `agentCtx.remote` namespace。两侧的生成产物、装配关系、SRC 开发回退和 Web 构建顺序见 [API Gateway](api-gateway.md)。\n\n如果相关的本地检查需要使用构建后的包产物,请先构建一次:\n\n```sh\npnpm run build\n```\n\n`pnpm run hygiene` 包含 `publint`(用构建出的 `lib/*.js` 文件校验包入口点)和 `verify-node-next-types`(用一个临时的 NodeNext 消费方校验构建出的声明文件)。新 worktree 在 `pnpm run build` 运行之前没有打包的 JS 和声明文件;普通提交和推送无需构建,除非所选检查会使用这些产物。\n\n### 环境变量\n\n真实的 DeepSeek 适配器和需要密钥的 agent 演示从环境变量或仓库根目录一个被 gitignore 的 `.env` 文件读取凭证:\n\n```sh\nDEEPSEEK_API_KEY=sk-...\nDEEPSEEK_BASE_URL=https://... # optional\n```\n\n`DEEPSEEK_BASE_URL` 可选,默认为公开 API。请勿提交真实凭证。未设置 `DEEPSEEK_API_KEY` 时,真实 API 的 e2e 套件会自动跳过。\n\n### Git 集成\n\n当两种语言的文件都使用 Git 默认文本策略且能干净合并时,配对合并驱动会根据已确认的祖先、当前和另一侧的配对文档 blob,推导出发生冲突的 `.i18n.yaml` 记录。配对文档发生冲突、存在非文本合并配置或记录无效时,它会拒绝处理并保留冲突;如果合并已经因冲突而停止,请运行 `pnpm run resolve-translation-pairing-conflicts`,该命令会暂存每份可安全生成的配对记录;如果其他配对冲突仍需手工处理,则以非零状态退出。[双语文档约定](i18n/README.md#the-pairing-contract)列出该驱动接受的确切文件和状态。\n\n安装脚本在发布 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)负责记录。\n\nlefthook 在 `lefthook.yml` 中配置,作为快速的本地检查点:\n\n- `pre-commit` 对照暂存的配对文档 blob 校验暂存的配对记录,使用不加载项目的 `.oxlintrc.staged.json` 配置验证暂存文件,并通过一次有界重试应用 Oxlint 修复,在暂存文件属于 `THIRD_PARTY_NOTICES.md` 的输入时重新生成该文件,然后检查暂存 diff 中的空白错误,并运行 vendor manifest(元数据清单)守卫;\n- `pre-merge-commit` 在 Git 创建自动合并提交前执行同样以索引为准的配对检查;\n- `pre-push` 运行 `pnpm run typecheck`;该命令会先完成包含 Typert 约定生成的完整 Host lib 阶段,再运行 Client TypeScript 检查。\n\nvendor manifest 守卫检查 `vendor/*/src` 下的改动是否连同对应的 `vendor/README.md` manifest 更新一起暂存。请在编辑 vendor 代码前先阅读 `vendor/README.md`。\n\n除限定范围的暂存记录校验外,这些钩子有意不运行测试、快照、文档检查、构建或 `hygiene`。贡献者只运行一次[与改动行为相关的检查](../AGENTS.md#run-relevant-checks-locally);CI 负责全量覆盖率门禁、构建产物冒烟测试,以及 Node 22.19、24 和 26 兼容性矩阵。\n\n贡献者可以选择运行 `pnpm run check:all`,执行全面的本地门禁集。该命令独立于 Git 钩子,也不是对 agent 的指令。\n\n### CI 门禁\n\nkeyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若干宽粒度 lane,并在受支持的 Node 版本上运行一组较小的兼容性检查。产物消费方在各自 lane 内等待一次 build。单独的真实 API 工作流按其配置的 worker 上限运行 `pnpm run test:e2e`。当前门禁和 job 清单以 [scripts/run-gates.ts](../scripts/run-gates.ts) 和工作流文件为准。\n\n### 日常命令\n\n根目录的[贡献者说明](../AGENTS.md#commands)概述常用命令,[`package.json`](../package.json) 与 [scripts/run-gates.ts](../scripts/run-gates.ts) 则负责当前脚本和门禁清单。请选择覆盖变更表面的最小检查集。文档变更使用 `pnpm run doc-sync`;包公开行为变更还需更新所属 README 或 JSDoc,而基于构建产物的检查需要先运行 `pnpm run build`。\n\n### 演示\n\n从源码 checkout 运行这些演示前,请单独执行仓库构建:\n\n```sh\npnpm run build\n```\n\n单次运行的 Headless coding agent 需要环境变量或仓库根目录 `.env` 中的 `DEEPSEEK_API_KEY`:\n\n```sh\npnpm dsh --profile headless \"summarize this workspace\"\n```\n\n自指的 cordis 演示可以检查并修改其实时插件运行时,并需要相同的凭证(默认 `web`,也可用 `acp`):\n\n```sh\npnpm run demo:cordis\n```\n\nACP 自动化服务器通过 JSON-RPC stdio 提供全新 agent 会话,同样需要 `DEEPSEEK_API_KEY`:\n\n```sh\npnpm run demo:acp\n```\n\n### TODO 标记\n\n请使用以下三种注释标签之一标记代码中的已知问题,按紧急程度排序:\n\n- `FIXME`:应当阻塞新版本发布的问题。除非评审者明确同意该更改可以合并,否则发布版本不应包含未解决的 `FIXME`;\n- `TODO`:应当尽快修复的问题,等资源到位即可处理;\n- `XXX`:也许某天会修复的问题,优先级最低,不作承诺。\n\n请选择与紧急程度匹配的标签,让浏览代码的人一眼分清「发布阻塞」和「有空再说」。\n\n### 逐字记录类型定义(`ts type-equiv`)\n\n[子系统](subsystems/README.md)页面会把与源码等价的声明及其原始 JSDoc 一并粘贴,让读者看到确切类型定义和源码约定。为防止粘贴内容在源码变化时漂移,请将其围栏为 ` ```ts type-equiv `(而不是 ` ```ts `),并在 `scripts/type-equiv.manifest.json` 中登记它镜像的源文件和符号:\n\n```json\n{ \"doc\": \"docs/subsystems/session.md\", \"symbol\": \"SessionEvent\", \"source\": \"packages/core/session/src/types.ts\" }\n```\n\n`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。\n"
+ "content": "# 开发指南\n\n[English](development.md) | 中文\n\n搭建教程引导新贡献者从准备前置条件开始,直到检出目录通过检查。后面的贡献者参考介绍仓库布局、日常工作流和 CI 组织方式。设计依据与实现细节属于链接的 Agent Note 和脚本。\n\n\n\n## 搭建教程\n\n### 前置条件\n\n- Node.js 支持 22.19+ 与 24+。CI 覆盖 22.19、24 和 26;见 [Node 引擎下限 Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.zh.md)。\n- 启用了 Corepack 的 pnpm。仓库在 `package.json` 中固定使用 `pnpm@11.7.0`;如果 `pnpm --version` 无法通过 Corepack 解析,请先运行 `corepack enable`。\n- Git 2.26 或更高版本;钩子设置会启用 Git 的 worktree 专属配置扩展。\n- 可选:一个 DeepSeek API key,用于 Web、headless 和 ACP(Agent Client Protocol)自动化 agent(智能体)演示以及真实 API 的 e2e 测试。\n\n### 首次搭建\n\n在仓库根目录安装依赖:\n\n```sh\npnpm install\n```\n\n安装过程还会通过 `scripts/install-lefthook.mjs` 配置 worktree 本地的 Lefthook 钩子和 `dsh-translation-pairing` Git 合并驱动。[worktree 本地钩子 Agent Note](../.agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.zh.md) 负责钩子路径的安全约定;[自动配对合并 Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.zh.md) 负责合并驱动。\n\n如果依赖是从缓存恢复或 `postinstall` 被跳过而导致任一集成缺失,请手动安装:\n\n```sh\nnode scripts/install-lefthook.mjs\n```\n\n如果包装脚本拒绝现有 Git 配置或报告陈旧锁,请遵循其诊断和所链接的 Agent Note,不要凭猜测编辑 worktree 元数据。移动检出目录后,请重新运行包装脚本以重新生成自有路径。\n\n新克隆后请先运行一次类型检查:\n\n```sh\npnpm run typecheck\n```\n\n`pnpm run typecheck` 成功退出即表示搭建完成。\n\n## 贡献者参考\n\n\n\n### TypeScript 项目布局\n\n仓库使用相互隔离的 Host 与 Client aggregate。普通包只登记进其中一个 aggregate;Host 包进入 `tsconfig.host.json`,Client 包进入 `tsconfig.client.json`。\n\n| 文件 | 角色 | 是否构成 program? |\n|---|---|---|\n| `tsconfig.json` | solution 根:`extends` base、`files: []`、引用两个 aggregate。它是 tsserver 发现入口,也是显式执行整张 Project Reference 图时的入口;经继承的 `paths` 充当 tsx 运行 `examples/` 与 `scripts/` 时的解析配置。 | 否 |\n| `tsconfig.host.json` | Host aggregate:Host 包、示例、测试、脚本和 website,以及 `api/remotes` 的 Host 特例 project。 | 是 |\n| `tsconfig.client.json` | Client aggregate:`packages/client/*` 包及其测试、`apps/web`,以及 `api/remotes` 的 Client 特例 project。 | 是 |\n| `tsconfig.base.json` | 共享 compilerOptions 与源码 `paths` 映射。同时是各 vitest 配置让 vite-tsconfig-paths 指向的解析门面:它没有 `include`,因此其 `paths` 适用于任何 importer。 | 否 |\n| `tsconfig.base.client.json` | 浏览器编译设置(`jsx`、DOM lib、`types: []`),由 Client aggregate 和每个 `packages/client/*` 包 extends。 | 否 |\n\nHost 与 Client 保持两个 aggregate program,是因为两侧在相同键下以不同服务对 cordis `Context` 接口做声明合并;单一 program 同时看到两份合并会报冲突。这种冲突只存在于 `ts.Program` 内部——模块解析永远不会触发它——所以 solution 可以同时引用两个 aggregate,一个 paths 门面也可以横跨两侧。由此推出三条纪律:\n\n- `tsconfig.base.json` 永不添加 `include` 或 `files`:它们会泄漏进每个 extends 它的包项目,并收窄门面的全匹配范围。\n- 构造全仓 `ts.Program` 的脚本显式以 `tsconfig.host.json` 或 `tsconfig.client.json` 为种子——根 solution 永不作为种子,因为把两个 aggregate 展平进一个 program 会撞上 `Context` 合并冲突。\n- 新包只登记进一个 aggregate。包同时具有 Node loader 入口和 browser 入口并不构成拆分理由;普通 Client 插件的两份运行时产物都在 Client 构建阶段生成。\n\n`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;该门禁按「两个 leaf 配置同时存在」自动发现拆分包,所以新拆分的包会自动纳入管辖。不要把该结构推广到其他包;[`api-remotes` README](../packages/api/remotes/README.zh.md) 说明 Host/Client 拆分与构建顺序。\n\n根构建按生成依赖排序:\n\n```sh\ntsc -b tsconfig.host.json\ntsdown --env.DSH_BUILD_FACE host\ntsc -b tsconfig.client.json\ntsdown --env.DSH_BUILD_FACE client\npnpm run build:web\n```\n\n两次 tsdown 都使用同一组完整 workspace 匹配,不扫描构建产物来发现 Client 包,也不维护 Host/Client 包过滤表。包内 tsdown 配置根据 `DSH_BUILD_FACE` 决定当前阶段的入口:普通 Client 插件在 Client 阶段同时生成 Node loader 与 browser bundle;`api-remotes` 通过 `hostPhase: true` 提前生成 Host 入口,再在 Client 阶段只生成 browser bundle。tsdown 只消费 `lib/types` 中由前置 tsc 发射的 JavaScript。\n\nTypert 只在 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.zh.md)。\n\n`pnpm run build` 会内联调用方精确的 `DSH_CLIENT_*` 环境;未设置时不使用任何公开 client 值。`pnpm run build:official` 是与 CI 和 release 产物构建等价的跨平台本地命令。每次完整构建成功后都会写入一份被 gitignore 的记录,把这些值与 Vite 输出及动态 client bundle 绑定;release 打包和 built Web 测试会拒绝缺少记录或被后续局部构建改动的产物。\n\n静态分析和测试通过 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.zh.md),tsc-first 发射职责见 [ts-build-config Note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.zh.md),门禁准备约定见 [Typert Remote Agent Note](../.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.zh.md)。\n\n业务服务在 Host 使用 `@Remote` 或 `@RemoteScope` 声明可调用方法;Host 构建生成 Host-for-Client 类型与运行时贡献,Client 的 `api-remotes` 组合加载这些贡献并挂到 `ctx.remote` 与作用域 `agentCtx.remote` namespace。两侧的生成产物、装配关系、SRC 开发回退和 Web 构建顺序见 [API Gateway](api-gateway.zh.md)。\n\n如果相关的本地检查需要使用构建后的包产物,请先构建一次:\n\n```sh\npnpm run build\n```\n\n`pnpm run hygiene` 包含 `publint`(用构建出的 `lib/*.js` 文件校验包入口点)和 `verify-node-next-types`(用一个临时的 NodeNext 消费方校验构建出的声明文件)。新 worktree 在 `pnpm run build` 运行之前没有打包的 JS 和声明文件;普通提交和推送无需构建,除非所选检查会使用这些产物。\n\n### 环境变量\n\n真实的 DeepSeek 适配器和需要密钥的 agent 演示从环境变量或仓库根目录一个被 gitignore 的 `.env` 文件读取凭证:\n\n```sh\nDEEPSEEK_API_KEY=sk-...\nDEEPSEEK_BASE_URL=https://... # optional\n```\n\n`DEEPSEEK_BASE_URL` 可选,默认为公开 API。请勿提交真实凭证。未设置 `DEEPSEEK_API_KEY` 时,真实 API 的 e2e 套件会自动跳过。\n\n### Git 集成\n\n当两种语言的文件都使用 Git 默认文本策略且能干净合并时,配对合并驱动会根据已确认的祖先、当前和另一侧的配对文档 blob,推导出发生冲突的 `.i18n.yaml` 记录。配对文档发生冲突、存在非文本合并配置或记录无效时,它会拒绝处理并保留冲突;如果合并已经因冲突而停止,请运行 `pnpm run resolve-translation-pairing-conflicts`,该命令会暂存每份可安全生成的配对记录;如果其他配对冲突仍需手工处理,则以非零状态退出。[双语文档约定](i18n/README.zh.md#the-pairing-contract)列出该驱动接受的确切文件和状态。\n\n安装脚本在发布 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.zh.md#failure-contract)负责记录。\n\nlefthook 在 `lefthook.yml` 中配置,作为快速的本地检查点:\n\n- `pre-commit` 对照暂存的配对文档 blob 校验暂存的配对记录,使用不加载项目的 `.oxlintrc.staged.json` 配置验证暂存文件,并通过一次有界重试应用 Oxlint 修复,在暂存文件属于 `THIRD_PARTY_NOTICES.md` 的输入时重新生成该文件,然后检查暂存 diff 中的空白错误,并运行 vendor manifest(元数据清单)守卫;\n- `pre-merge-commit` 在 Git 创建自动合并提交前执行同样以索引为准的配对检查;\n- `pre-push` 运行 `pnpm run typecheck`;该命令会先完成包含 Typert 约定生成的完整 Host lib 阶段,再运行 Client TypeScript 检查。\n\nvendor manifest 守卫检查 `vendor/*/src` 下的改动是否连同对应的 `vendor/README.md` manifest 更新一起暂存。请在编辑 vendor 代码前先阅读 `vendor/README.md`。\n\n除限定范围的暂存记录校验外,这些钩子有意不运行测试、快照、文档检查、构建或 `hygiene`。贡献者只运行一次[与改动行为相关的检查](../AGENTS.md#run-relevant-checks-locally);CI 负责全量覆盖率门禁、构建产物冒烟测试,以及 Node 22.19、24 和 26 兼容性矩阵。\n\n贡献者可以选择运行 `pnpm run check:all`,执行全面的本地门禁集。该命令独立于 Git 钩子,也不是对 agent 的指令。\n\n### CI 门禁\n\nkeyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若干宽粒度 lane,并在受支持的 Node 版本上运行一组较小的兼容性检查。产物消费方在各自 lane 内等待一次 build。单独的真实 API 工作流按其配置的 worker 上限运行 `pnpm run test:e2e`。当前门禁和 job 清单以 [scripts/run-gates.ts](../scripts/run-gates.ts) 和工作流文件为准。\n\n### 日常命令\n\n根目录的[贡献者说明](../AGENTS.md#commands)概述常用命令,[`package.json`](../package.json) 与 [scripts/run-gates.ts](../scripts/run-gates.ts) 则负责当前脚本和门禁清单。请选择覆盖变更表面的最小检查集。文档变更使用 `pnpm run doc-sync`;包公开行为变更还需更新所属 README 或 JSDoc,而基于构建产物的检查需要先运行 `pnpm run build`。\n\n### 演示\n\n从源码 checkout 运行这些演示前,请单独执行仓库构建:\n\n```sh\npnpm run build\n```\n\n单次运行的 Headless coding agent 需要环境变量或仓库根目录 `.env` 中的 `DEEPSEEK_API_KEY`:\n\n```sh\npnpm dsh --profile headless \"summarize this workspace\"\n```\n\n自指的 cordis 演示可以检查并修改其实时插件运行时,并需要相同的凭证(默认 `web`,也可用 `acp`):\n\n```sh\npnpm run demo:cordis\n```\n\nACP 自动化服务器通过 JSON-RPC stdio 提供全新 agent 会话,同样需要 `DEEPSEEK_API_KEY`:\n\n```sh\npnpm run demo:acp\n```\n\n### TODO 标记\n\n请使用以下三种注释标签之一标记代码中的已知问题,按紧急程度排序:\n\n- `FIXME`:应当阻塞新版本发布的问题。除非评审者明确同意该更改可以合并,否则发布版本不应包含未解决的 `FIXME`;\n- `TODO`:应当尽快修复的问题,等资源到位即可处理;\n- `XXX`:也许某天会修复的问题,优先级最低,不作承诺。\n\n请选择与紧急程度匹配的标签,让浏览代码的人一眼分清「发布阻塞」和「有空再说」。\n\n\n\n### 逐字记录类型定义(`ts type-equiv`)\n\n[子系统](subsystems/README.zh.md)页面会把与源码等价的声明及其原始 JSDoc 一并粘贴,让读者看到确切类型定义和源码约定。为防止粘贴内容在源码变化时漂移,请将其围栏为 ` ```ts type-equiv `(而不是 ` ```ts `),并在 `scripts/type-equiv.manifest.json` 中登记它镜像的源文件和符号:\n\n```json\n{ \"doc\": \"docs/subsystems/session.md\", \"symbol\": \"SessionEvent\", \"source\": \"packages/core/session/src/types.ts\" }\n```\n\n`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。\n"
},
{
"role": "user",
- "content": "# Bilingual documentation\n\nEnglish | [中文](README.zh.md)\n\nThis 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.\n\n## The pairing contract\n\n- **Both languages carry equal authority.** A document may be authored and reviewed in either language first — a Chinese-first Agent Note is as legitimate as an English-first one — and the counterpart is translated from it. Neither file outranks the other; what binds them is that they must say the same thing.\n- **A pair is three sibling files.** The English `foo.md`, the Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`, all in the same directory. No locale directories, no separate translation repo, no interleaved bilingual files. Pairs merge whole: a PR never lands one language without the other two files.\n- **The consistency record.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last time the two were confirmed to say the same thing:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n 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).\n\n 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.\n- **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. A README published outside GitHub, such as PyPI project metadata, may use the canonical `https://github.com/deepseek-ai/deepseek-harness/blob/master/` URL to the same counterpart so the switcher still resolves there.\n- **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`).\n\n## The gate: verify-translation-pairing\n\n`pnpm run verify-translation-pairing` (part of `doc-sync`, which contributors run locally for documentation changes and CI runs exhaustively) enforces the contract mechanically:\n\n1. Every document in scope has a complete pair. README discovery is case-insensitive on the basename, so `missions/readme.md` is in scope alongside the other documentation roots.\n2. Every pair artifact that exists at all is complete and consistent: all three files present, each side's current blob hash equals the recorded one (editing either side without re-confirming the pair goes red), the Chinese side and every authored English source carry their language switchers (listed generated English sources are exempt), and the structural signatures match in order — heading depths, verbatim code blocks (info string and content), table row and column counts, list kinds, ordered-list starts, item counts, and every link target apart from the switcher.\n3. Files listed as `excluded` have no `.zh.md` and no `.i18n.yaml` at all. Frozen Agent Notes under `.agents/notes/archived/` are outside this evolving gate; their dedicated verifier requires and seals the complete existing triplet instead.\n\nSource-oriented code gates consume an exact `.zh.md` fence sequence as a derivative of its unsuffixed sibling instead of compiling or manifesting the same code twice. The sequence must match in length, order, fence kind, and byte-exact body; otherwise both copies remain independently checked and the pairing gate reports the structural mismatch.\n\n`pnpm run verify-translation-pairing --list` prints the current pairing state of every document in scope — missing, out-of-sync, or ok. It never fails; `missing` and `out-of-sync` rows identify violations that the normal check rejects.\n\n`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.\n\nThe 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.\n\nThe 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.\n\n## Scope and exclusions\n\n**Scope**: the root CONTRIBUTING and BRAND_GUIDELINES documents, 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.\n\nGenerated 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.\n\n**Excluded** (never paired, and the gate rejects a `.zh.md` or `.i18n.yaml` for them):\n\n- [cordis-api/inherited.md](../cordis-api/inherited.md) — generated without a reviewed Chinese counterpart, so both website locales project the English source.\n- `docs/AGENTS.md`, `.agents/notes/**/AGENTS.md`, and their `CLAUDE.md` instruction symlinks — agent instructions, maintained in English only like the root `AGENTS.md`.\n- `docs/i18n/terminology.md` and [style-samples.md](style-samples.md) — both are bilingual by construction.\n- [translation-prompt.md](translation-prompt.md) — the automated pipeline's prompt template; its body is machine-consumed verbatim, so a paired translation would change pipeline behavior.\n- `.agents/notes/archived/` — frozen historical triplets. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) validates their completeness and content seals; translation maintenance must never rewrite them.\n\n**Universal requirement**: every current or future document in scope must merge as a complete bilingual pair. [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) contains only explicit exclusions; there is no per-file rollout list, date cutoff, or README-specific policy class.\n\n## Division of labor\n\nRoutine 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`.\n"
+ "content": "# Bilingual documentation\n\nEnglish | [中文](README.zh.md)\n\nThis 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.\n\n## The pairing contract\n\n- **Both languages carry equal authority.** A document may be authored and reviewed in either language first — a Chinese-first Agent Note is as legitimate as an English-first one — and the counterpart is translated from it. Neither file outranks the other; what binds them is that they must say the same thing.\n- **A pair is three sibling files.** The English `foo.md`, the Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`, all in the same directory. No locale directories, no separate translation repo, no interleaved bilingual files. Pairs merge whole: a PR never lands one language without the other two files.\n- **The consistency record.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last time the two were confirmed to say the same thing:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n 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).\n\n 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.\n- **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. A README published outside GitHub, such as PyPI project metadata, may use the canonical `https://github.com/deepseek-ai/deepseek-harness/blob/master/` URL to the same counterpart so the switcher still resolves there.\n- **Structure mirrors the counterpart.** Heading depths and order, list kinds, ordered-list starts, list item counts, table row and column counts, semantic link targets with exact query/fragment suffixes, and verbatim code blocks match one to one across the pair. When a relative document link targets the active bilingual corpus, the English side uses its `.md` path and the Chinese side uses its `.zh.md` path. A missing counterpart in that corpus is a pair-completeness error rather than a fallback; targets outside the active corpus keep the authored path. 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`).\n\n## The gate: verify-translation-pairing\n\n`pnpm run verify-translation-pairing` (part of `doc-sync`, which contributors run locally for documentation changes and CI runs exhaustively) enforces the contract mechanically:\n\n1. Every document in scope has a complete pair. README discovery is case-insensitive on the basename, so `missions/readme.md` is in scope alongside the other documentation roots.\n2. Every pair artifact that exists at all is complete and consistent: all three files present, each side's current blob hash equals the recorded one (editing either side without re-confirming the pair goes red), the Chinese side and every authored English source carry their language switchers (listed generated English sources are exempt), every ordinary relative document link uses its source side's target locale, and the structural signatures match in order — heading depths, verbatim code blocks (info string and content), table row and column counts, list kinds, ordered-list starts, item counts, and semantic link targets with exact query/fragment suffixes apart from the switcher.\n3. Files listed as `excluded` have no `.zh.md` and no `.i18n.yaml` at all. Frozen Agent Notes under `.agents/notes/archived/` are outside this evolving gate; their dedicated verifier requires and seals the complete existing triplet instead.\n\nSource-oriented code gates consume an exact `.zh.md` fence sequence as a derivative of its unsuffixed sibling instead of compiling or manifesting the same code twice. The sequence must match in length, order, fence kind, and byte-exact body; otherwise both copies remain independently checked and the pairing gate reports the structural mismatch.\n\n`pnpm run verify-translation-pairing --list` prints the current pairing state of every document in scope — missing, out-of-sync, or ok. It never fails; `missing` and `out-of-sync` rows identify violations that the normal check rejects.\n\n`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.\n\nThe 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.\n\nThe 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.\n\n## Scope and exclusions\n\n**Scope**: the root CONTRIBUTING and BRAND_GUIDELINES documents, 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.\n\nGenerated 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. A generator that owns both sides, such as the Cordis subsystem-region generator, projects paired document paths to each output locale while keeping every other generated byte equal. 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.\n\n**Excluded** (never paired, and the gate rejects a `.zh.md` or `.i18n.yaml` for them):\n\n- [cordis-api/inherited.md](../cordis-api/inherited.md) — generated without a reviewed Chinese counterpart, so both website locales project the English source.\n- `docs/AGENTS.md`, `.agents/notes/**/AGENTS.md`, and their `CLAUDE.md` instruction symlinks — agent instructions, maintained in English only like the root `AGENTS.md`.\n- `docs/i18n/terminology.md` and [style-samples.md](style-samples.md) — both are bilingual by construction.\n- [translation-prompt.md](translation-prompt.md) — the automated pipeline's prompt template; its body is machine-consumed verbatim, so a paired translation would change pipeline behavior.\n- `.agents/notes/archived/` — frozen historical triplets. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) validates their completeness and content seals; translation maintenance must never rewrite them.\n\n**Universal requirement**: every current or future document in scope must merge as a complete bilingual pair. [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) contains only explicit exclusions; there is no per-file rollout list, date cutoff, or README-specific policy class.\n\n## Division of labor\n\nRoutine 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`.\n"
},
{
"role": "assistant",
- "content": "# 双语文档\n\n[English](README.md) | 中文\n\n本仓库的文档会被公司内外的人和 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) 工作流仅在用户显式调用时可用。\n\n## 配对约定\n\n- **两种语言同权。** 一篇文档可以先用任一语言撰写和评审(先写中文的 Agent Note 与先写英文的一样正当),另一侧由它翻译而来。两个文件谁也不高于谁;约束它们的是二者必须说同样的话。\n- **一对文档是三个同目录文件。** 英文 `foo.md`、中文 `foo.zh.md`,加一份一致性记录 `foo.i18n.yaml`,都在同一目录。不用语言目录,不用独立翻译仓库,不用中英混排的单文件。配对必须整体合并:PR(Pull Request)永远不会只带一种语言而缺其余两个文件。\n- **一致性记录。**`foo.i18n.yaml` 保存两侧文件在上一次被确认「说同样的话」时各自的完整 Git blob hash:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n 用 blob hash 而不是 commit hash,这样同一个 PR 里改动的文件也能算出记录(`git hash-object foo.md`),一致性是纯内容比较。`--write` 会先把这些快照存入本地 Git 对象库再写下记录,未提交的 worktree 内容也不例外;它还会在内容寻址的 `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` 是显式的全语料形式)。\n\n 当两个分支都包含同一配对的有效确认时,已安装的 `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) 负责记录该机制与备选方案。\n- **语言切换行。** 中文文件一律在 H1 标题后立即以 `[English](foo.md) | 中文` 链回英文。普通撰写的英文文件在同一位置以 `English | [中文](foo.zh.md)` 互链;清单内的生成英文源省略此行,以便与生成器输出逐字节一致。发布到 GitHub 以外位置的 README(例如 PyPI 项目元数据)可以改用指向同一对侧文件的规范 `https://github.com/deepseek-ai/deepseek-harness/blob/master/` URL,使切换行在该位置仍可访问。\n- **结构与另一侧一一对应。** 标题深度与顺序、列表类型、有序列表起始编号、列表项数量、表格行列数、链接目标与逐字节一致的代码块在配对两侧一一对应;完整保持规则见 [translation-rules.md](translation-rules.md)。既有 Markdown 门禁对 `.zh.md` 文件原样生效(`verify-md-wrap`、`verify-md-links`)。\n\n## 门禁:verify-translation-pairing\n\n`pnpm run verify-translation-pairing`(`doc-sync`(文档同步门禁)的一环,贡献者会针对文档变更在本地运行,CI 则会完整运行)机械地强制执行这份约定:\n\n1. 范围内的每篇文档都有完整配对。发现 README 时,basename 不区分大小写,因此 `missions/readme.md` 与其他文档根一样属于范围。\n2. 任何已存在的配对产物都完整且一致:三个文件齐全、每一侧的当前 blob hash 等于记录值(改了任一侧而没重新确认配对就变红)、中文侧和所有普通撰写的英文源都带语言切换行(清单内的生成英文源除外)、结构签名按序一致:标题深度、逐字节一致的代码块(信息字符串与内容)、表格行列数、列表类型、有序列表起始编号、列表项数量,以及除切换行之外的每个链接目标。\n3. 列为 `excluded` 的文件完全没有 `.zh.md`,也没有 `.i18n.yaml`。`.agents/notes/archived/` 下冻结的 Agent Note 不受这个持续演进的门禁约束;专用校验器会要求其现有的三个配对文件完整,并将其封存。\n\n面向源码的代码门禁会把精确的 `.zh.md` 围栏序列视为其无后缀兄弟文件的派生内容,而不会再次编译相同代码或在 manifest(元数据清单)中重复登记。该序列必须在长度、顺序、围栏类型和按字节精确的正文上一致;否则两份副本仍会独立受检,配对门禁也会报告结构不匹配。\n\n`pnpm run verify-translation-pairing --list` 打印范围内每篇文档的当前配对状态(missing、out-of-sync 或 ok)。它从不失败;其中 missing 与 out-of-sync 行指出普通检查会拒绝的违规。\n\n`pnpm run verify-translation-pairing ` 只检查被点名的配对——配对的三个文件中的任意一个(或其裸词干)都能点名它——因此更新循环几秒内就能验证自己的配对,而不必重新扫描全语料。`doc-sync` 与 CI 运行的是无参数的全语料形式;限定范围的绿灯在 PR 层面永远不能替代它。\n\n这个门禁带来的实际规则是:**当一个 PR 修改了已配对文档的任一侧时,同一个 PR 在术语指导下直接一次完成对侧文件的更新,并用 `--write ` 重新记录配对**,与本仓库既有的代码与 README 的 doc-sync 规则完全一致。留下失去同步的配对的 PR 会在 CI 变红。\n\n门禁的限制很明确:**门禁通过意味着这组文档在当前内容上的一致性得到了确认,不代表确认本身正确可靠。** 它检查记录的 hash 与 Markdown 结构;它无法判断两侧是否真的在说同样的话,也无法判断措辞是否准确、术语是否得当、行文是否自然;这部分约定由评审者把关,见 [translation-rules.md](translation-rules.md)。重新记录了 hash 但另一侧翻得潦草的配对能通过门禁;它不得通过评审。\n\n## 范围与排除\n\n**范围**:根目录 CONTRIBUTING 与 BRAND_GUIDELINES 文档、除 vendor 源码外的全部 README,以及 `.agents/notes/**`、`docs/**` 与 `python/**` 下的全部活跃文档。匹配 README 时只看文件名且不区分大小写,因此今后新增的目录无需再修改 manifest。依赖目录、被忽略的构建产物目录以及冻结的 `.agents/notes/archived/` 目录树只在发现阶段排除,不属于持续演进的翻译源文档。\n\n有经评审的中文对侧的生成英文参考文档和图文档遵循配对规则。生成器仍是英文真源,新鲜度门禁与配对门禁各自独立强制其约束;重新生成导致英文变化后,配对会保持失去同步状态,直至经评审的中文对侧完成更新并重新记录。生成的英文源文件不含普通撰写文档所带的语言切换行,因为添加该行会使生成器新鲜度检查失败;中文对侧仍链接回英文源。生成页的中文对侧只能改写若直译便不再符合经评审译文事实的自指生成与维护说明;所有技术内容仍受普通忠实性规则约束。\n\n**排除**(永不配对,门禁拒绝为它们建 `.zh.md` 或 `.i18n.yaml`):\n\n- [cordis-api/inherited.md](../cordis-api/inherited.md):该生成文档没有经评审的中文对侧,因此网站的两个 locale 都投影英文源文件。\n- `docs/AGENTS.md`、`.agents/notes/**/AGENTS.md` 以及指向它们的 `CLAUDE.md` 指令符号链接:agent 指令,与根 `AGENTS.md` 一样只以英文维护。\n- `docs/i18n/terminology.md` 与 [style-samples.md](style-samples.md):二者本身即为中英对照文档。\n- [translation-prompt.md](translation-prompt.md):自动翻译流水线的提示词模板;正文逐字进入模型请求,配对翻译会改变流水线行为。\n- `.agents/notes/archived/`:冻结的历史三文件配对。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 校验其完整性和内容封存记录;翻译维护绝不能重写这些文件。\n\n**统一要求**:当前及今后纳入范围的每篇文档,合并时都必须构成完整的双语配对。[scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) 只包含显式排除项;不存在逐文件推进清单、日期分界或 README 专用政策类别。\n\n## 分工\n\n日常更新对侧文件时,负责处理的 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` 会检查两个渲染方向与仓库内示例。\n"
+ "content": "# 双语文档\n\n[English](README.md) | 中文\n\n本仓库的文档会被公司内外的人和 agent(智能体)阅读,因此范围内的每篇文档都以英文和简体中文维护。本页定义配对约定、检查、范围与排除规则;[translation-rules.md](translation-rules.zh.md) 定义如何翻译;[terminology.md](terminology.md) 是术语真源。agent 的日常工作遵循 [docs/AGENTS.md](../AGENTS.md) 中的轻量路径;扩展版 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) 工作流仅在用户显式调用时可用。\n\n\n\n## 配对约定\n\n- **两种语言同权。** 一篇文档可以先用任一语言撰写和评审(先写中文的 Agent Note 与先写英文的一样正当),另一侧由它翻译而来。两个文件谁也不高于谁;约束它们的是二者必须说同样的话。\n- **一对文档是三个同目录文件。** 英文 `foo.md`、中文 `foo.zh.md`,加一份一致性记录 `foo.i18n.yaml`,都在同一目录。不用语言目录,不用独立翻译仓库,不用中英混排的单文件。配对必须整体合并:PR(Pull Request)永远不会只带一种语言而缺其余两个文件。\n- **一致性记录。**`foo.i18n.yaml` 保存两侧文件在上一次被确认「说同样的话」时各自的完整 Git blob hash:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n 用 blob hash 而不是 commit hash,这样同一个 PR 里改动的文件也能算出记录(`git hash-object foo.md`),一致性是纯内容比较。`--write` 会先把这些快照存入本地 Git 对象库再写下记录,未提交的 worktree 内容也不例外;它还会在内容寻址的 `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.zh.md))。两侧对齐后,`pnpm run verify-translation-pairing --write ` 重新记录两个 hash;那份 YAML diff 就是「确认一致」这个动作本身,可以被评审,也正因如此,`--write` 要求点名你确认过的配对(`--write --all` 是显式的全语料形式)。\n\n 当两个分支都包含同一配对的有效确认时,已安装的 `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.zh.md) 负责记录该机制与备选方案。\n- **语言切换行。** 中文文件一律在 H1 标题后立即以 `[English](foo.md) | 中文` 链回英文。普通撰写的英文文件在同一位置以 `English | [中文](foo.zh.md)` 互链;清单内的生成英文源省略此行,以便与生成器输出逐字节一致。发布到 GitHub 以外位置的 README(例如 PyPI 项目元数据)可以改用指向同一对侧文件的规范 `https://github.com/deepseek-ai/deepseek-harness/blob/master/` URL,使切换行在该位置仍可访问。\n- **结构与另一侧一一对应。** 标题深度与顺序、列表类型、有序列表起始编号、列表项数量、表格行列数、保留原样 query/fragment 后缀的语义链接目标,以及逐字节一致的代码块在配对两侧一一对应。相对文档链接的目标属于活跃双语语料时,英文侧使用其 `.md` 路径,中文侧使用其 `.zh.md` 路径。该范围内缺少对侧属于配对完整性错误,不得回退;范围外的目标保留原路径。完整保持规则见 [translation-rules.md](translation-rules.zh.md)。既有 Markdown 门禁对 `.zh.md` 文件原样生效(`verify-md-wrap`、`verify-md-links`)。\n\n## 门禁:verify-translation-pairing\n\n`pnpm run verify-translation-pairing`(`doc-sync`(文档同步门禁)的一环,贡献者会针对文档变更在本地运行,CI 则会完整运行)机械地强制执行这份约定:\n\n1. 范围内的每篇文档都有完整配对。发现 README 时,basename 不区分大小写,因此 `missions/readme.md` 与其他文档根一样属于范围。\n2. 任何已存在的配对产物都完整且一致:三个文件齐全、每一侧的当前 blob hash 等于记录值(改了任一侧而没重新确认配对就变红)、中文侧和所有普通撰写的英文源都带语言切换行(清单内的生成英文源除外)、每条普通相对文档链接都使用源文件一侧对应的目标 locale,且结构签名按序一致:标题深度、逐字节一致的代码块(信息字符串与内容)、表格行列数、列表类型、有序列表起始编号、列表项数量,以及除切换行之外保留原样 query/fragment 后缀的语义链接目标。\n3. 列为 `excluded` 的文件完全没有 `.zh.md`,也没有 `.i18n.yaml`。`.agents/notes/archived/` 下冻结的 Agent Note 不受这个持续演进的门禁约束;专用校验器会要求其现有的三个配对文件完整,并将其封存。\n\n面向源码的代码门禁会把精确的 `.zh.md` 围栏序列视为其无后缀兄弟文件的派生内容,而不会再次编译相同代码或在 manifest(元数据清单)中重复登记。该序列必须在长度、顺序、围栏类型和按字节精确的正文上一致;否则两份副本仍会独立受检,配对门禁也会报告结构不匹配。\n\n`pnpm run verify-translation-pairing --list` 打印范围内每篇文档的当前配对状态(missing、out-of-sync 或 ok)。它从不失败;其中 missing 与 out-of-sync 行指出普通检查会拒绝的违规。\n\n`pnpm run verify-translation-pairing ` 只检查被点名的配对——配对的三个文件中的任意一个(或其裸词干)都能点名它——因此更新循环几秒内就能验证自己的配对,而不必重新扫描全语料。`doc-sync` 与 CI 运行的是无参数的全语料形式;限定范围的绿灯在 PR 层面永远不能替代它。\n\n这个门禁带来的实际规则是:**当一个 PR 修改了已配对文档的任一侧时,同一个 PR 在术语指导下直接一次完成对侧文件的更新,并用 `--write ` 重新记录配对**,与本仓库既有的代码与 README 的 doc-sync 规则完全一致。留下失去同步的配对的 PR 会在 CI 变红。\n\n门禁的限制很明确:**门禁通过意味着这组文档在当前内容上的一致性得到了确认,不代表确认本身正确可靠。** 它检查记录的 hash 与 Markdown 结构;它无法判断两侧是否真的在说同样的话,也无法判断措辞是否准确、术语是否得当、行文是否自然;这部分约定由评审者把关,见 [translation-rules.md](translation-rules.zh.md)。重新记录了 hash 但另一侧翻得潦草的配对能通过门禁;它不得通过评审。\n\n## 范围与排除\n\n**范围**:根目录 CONTRIBUTING 与 BRAND_GUIDELINES 文档、除 vendor 源码外的全部 README,以及 `.agents/notes/**`、`docs/**` 与 `python/**` 下的全部活跃文档。匹配 README 时只看文件名且不区分大小写,因此今后新增的目录无需再修改 manifest。依赖目录、被忽略的构建产物目录以及冻结的 `.agents/notes/archived/` 目录树只在发现阶段排除,不属于持续演进的翻译源文档。\n\n有经评审的中文对侧的生成英文参考文档和图文档遵循配对规则。生成器仍是英文真源,新鲜度门禁与配对门禁各自独立强制其约束;重新生成导致英文变化后,配对会保持失去同步状态,直至经评审的中文对侧完成更新并重新记录。Cordis subsystem 区块生成器等同时拥有两侧输出的生成器,会把配对文档路径投影到各自 locale,同时保持其余生成字节一致。生成的英文源文件不含普通撰写文档所带的语言切换行,因为添加该行会使生成器新鲜度检查失败;中文对侧仍链接回英文源。生成页的中文对侧只能改写若直译便不再符合经评审译文事实的自指生成与维护说明;所有技术内容仍受普通忠实性规则约束。\n\n**排除**(永不配对,门禁拒绝为它们建 `.zh.md` 或 `.i18n.yaml`):\n\n- [cordis-api/inherited.md](../cordis-api/inherited.md):该生成文档没有经评审的中文对侧,因此网站的两个 locale 都投影英文源文件。\n- `docs/AGENTS.md`、`.agents/notes/**/AGENTS.md` 以及指向它们的 `CLAUDE.md` 指令符号链接:agent 指令,与根 `AGENTS.md` 一样只以英文维护。\n- `docs/i18n/terminology.md` 与 [style-samples.md](style-samples.md):二者本身即为中英对照文档。\n- [translation-prompt.md](translation-prompt.md):自动翻译流水线的提示词模板;正文逐字进入模型请求,配对翻译会改变流水线行为。\n- `.agents/notes/archived/`:冻结的历史三文件配对。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 校验其完整性和内容封存记录;翻译维护绝不能重写这些文件。\n\n**统一要求**:当前及今后纳入范围的每篇文档,合并时都必须构成完整的双语配对。[scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) 只包含显式排除项;不存在逐文件推进清单、日期分界或 README 专用政策类别。\n\n## 分工\n\n日常更新对侧文件时,负责处理的 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` 会检查两个渲染方向与仓库内示例。\n"
},
{
"role": "user",
- "content": "# Translation rules\n\nEnglish | [中文](translation-rules.zh.md)\n\nHow 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.\n\n## Faithfulness\n\n- The counterpart *MUST* say what the authored side says — no added behavior, prerequisites, warnings, version claims, or examples, and no dropped ones. If the pair disagrees on substance, neither language wins by default: fix the side that is wrong, then bring the other along in the same change.\n- The counterpart *SHOULD* read as natural technical writing in its own language, not word-by-word gloss. Translate meaning, restructure sentences where the target grammar wants it, and keep the author's register — terse stays terse.\n- Do not translate the untranslatable: if a sentence resists natural rendering because it leans on an idiom of the source language, translate the idea, not the idiom.\n\n## Voice\n\n- 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.\n- 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.\n- 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 (系统、门禁、评审人).\n- 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.\n- Split long paragraphs by semantic unit — one idea per paragraph. Paragraph boundaries MAY differ from the source; the structural signature does not count paragraphs.\n- When translating into Chinese, category nouns use Chinese with a first-mention English annotation (实操手册(cookbook)); when translating into English, use the conventional English category name. Literal directory or file references stay code-formatted English.\n\n## Structure preservation\n\nThe pairing gate checks heading depths, fenced code blocks, table row and column counts, list kinds, ordered-list starts, list item counts, and link targets. Preserve the rest of the frame manually; the paired files MUST match one to one in:\n\n- heading hierarchy (same levels, same order — heading TEXT is translated),\n- list shape and numbering,\n- tables (same columns, same row order; header cells translated per terminology),\n- fenced code blocks — **byte-identical, including comments**; the pairing signature compares their info strings and contents, and ` ```ts ` blocks compile under `doc-typecheck`,\n- inline code spans (commands, flags, config keys, file paths, event names, API names, version numbers) — verbatim, never translated or reformatted,\n- links and anchors: every relative link MUST point at the same target in both files — by convention the `.md` path, not the `.zh.md` sibling — so links never dangle when one pair lands before its neighbors. The ONLY zh-specific link is the language switcher. A README rendered outside GitHub MAY use the canonical public repository URL to its exact counterpart as documented in [README.md](README.md). Link TEXT is translated; the target is not.\n\nThe repo's Markdown conventions apply to `.zh.md` files unchanged: one physical line per paragraph (`verify-md-wrap`), resolving relative links (`verify-md-links`), exactly one trailing newline.\n\n## Terminology\n\n- [terminology.md](terminology.md) is the source of truth in both directions. Before translating, load it; every listed term MUST follow its row and its \"不要译作\" prohibitions. A Chinese target uses the \"中文\" column and its \"首次出现\" annotation; an English target uses the \"English\" column without adding a Chinese gloss.\n- For a Chinese target, an unlisted technical term MAY use an established rendering from a major Chinese-language OSS or vendor source (K8s/Vue/MDN Chinese docs, 微软简中风格指南, big-tech project docs), cited in the PR. Without such precedent it MUST stay in English and be listed under 「待定术语」(pending terms) with a suggested rendering.\n- For an English target, use the established English technical term. If the source term has no unambiguous established equivalent, preserve it with a short explanatory gloss and list it under pending terms. Neither direction may invent a rendering inline; a decided term enters [terminology.md](terminology.md) in the same PR or a follow-up.\n\n## Typography\n\nThese rules govern the Chinese side; the English side follows the repo's normal Markdown conventions (root `AGENTS.md`). The mixed-script rules below follow the cross-project consensus of the [MDN Simplified Chinese translation guide](https://github.com/mdn/translated-content/blob/main/docs/zh-cn/translation-guide.md), the [Kubernetes zh-cn localization guide](https://kubernetes.io/zh-cn/docs/contribute/localization_zh/), the [Vue.js Chinese translation conventions](https://github.com/vuejs-translations/docs-zh-cn/wiki/%E7%BF%BB%E8%AF%91%E9%A1%BB%E7%9F%A5), and [中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines), which in turn ground in [W3C clreq](https://www.w3.org/TR/clreq/) and GB/T 15834—2011:\n\n- MUST put one half-width space between Chinese text and Latin words, and between Chinese text and numerals: `每个 plugin 注册 3 个 tool`。No space between a full-width punctuation mark and anything.\n- MUST use full-width (Chinese) punctuation in Chinese prose: `,。:;?!()「」`. Half-width punctuation stays inside code spans, inside complete English sentences quoted as-is, and in numbers (`3.5`, `1,024`).\n- Chinese prose *SHOULD* prefer colons, periods, commas, or parentheses over em dashes. Keep an em dash only when no other punctuation preserves the sentence naturally.\n- Enumeration commas: a Chinese list of parallel items uses 顿号(、), not commas.\n- MUST NOT use full-width digits or full-width Latin letters — `123` never, `123` always.\n- Proper nouns keep their canonical casing: GitHub, TypeScript, DeepSeek — never `github`/`Github` unless quoting code.\n- Second person is 你, not 您 (matches the Vue and Kubernetes Chinese conventions and this repo's direct voice).\n- Emphasis markers (`**bold**`, `*italic*`) stay on the same spans as the source; Chinese has no italics, so the rendered emphasis may look identical — do not substitute quotation marks or other decoration.\n\n## Quality bar\n\n- 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.\n- 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.\n\n## References\n\nAuthorities cited by these rules, for humans and agents who want the underlying reasoning:\n\n- [中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines) — the de-facto community standard for mixed CJK/Latin spacing and punctuation.\n- [MDN zh-CN translation guide](https://github.com/mdn/translated-content/blob/main/docs/zh-cn/translation-guide.md) — an in-repo translation-rules file of the same shape as this one; spacing, punctuation, and glossary practice.\n- [Kubernetes zh-cn localization guide](https://kubernetes.io/zh-cn/docs/contribute/localization_zh/) — terminology-first-occurrence and punctuation practice from the largest zh localization team.\n- [Vue.js docs-zh-cn 翻译须知](https://github.com/vuejs-translations/docs-zh-cn/wiki/%E7%BF%BB%E8%AF%91%E9%A1%BB%E7%9F%A5) — per-term translate/keep decisions and tone.\n- [zh-style-guide](https://zh-style-guide.readthedocs.io) — a community Chinese technical-writing style guide whose rule-level taxonomy (and RFC 2119 keyword levels) this file borrows; aggregates GB/T 15834/15835, clreq, and vendor guides.\n- [W3C clreq](https://www.w3.org/TR/clreq/) and the [Microsoft Simplified Chinese style guide](https://learn.microsoft.com/en-us/globalization/reference/microsoft-style-guides) — the formal typographic and vendor-localization baselines.\n- GB/T 19682-2005《翻译服务译文质量要求》 — the national standard whose three base requirements (忠实原文、术语统一、行文通顺) this file's Faithfulness and Terminology sections operationalize.\n"
+ "content": "# Translation rules\n\nEnglish | [中文](translation-rules.zh.md)\n\nHow 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.\n\n## Faithfulness\n\n- The counterpart *MUST* say what the authored side says — no added behavior, prerequisites, warnings, version claims, or examples, and no dropped ones. If the pair disagrees on substance, neither language wins by default: fix the side that is wrong, then bring the other along in the same change.\n- The counterpart *SHOULD* read as natural technical writing in its own language, not word-by-word gloss. Translate meaning, restructure sentences where the target grammar wants it, and keep the author's register — terse stays terse.\n- Do not translate the untranslatable: if a sentence resists natural rendering because it leans on an idiom of the source language, translate the idea, not the idiom.\n\n## Voice\n\n- 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.\n- 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.\n- 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 (系统、门禁、评审人).\n- 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.\n- Split long paragraphs by semantic unit — one idea per paragraph. Paragraph boundaries MAY differ from the source; the structural signature does not count paragraphs.\n- When translating into Chinese, category nouns use Chinese with a first-mention English annotation (实操手册(cookbook)); when translating into English, use the conventional English category name. Literal directory or file references stay code-formatted English.\n\n## Structure preservation\n\nThe pairing gate checks heading depths, fenced code blocks, table row and column counts, list kinds, ordered-list starts, list item counts, link locale, and semantic targets. Preserve the rest of the frame manually; the paired files MUST match one to one in:\n\n- heading hierarchy (same levels, same order — heading TEXT is translated),\n- list shape and numbering,\n- tables (same columns, same row order; header cells translated per terminology),\n- fenced code blocks — **byte-identical, including comments**; the pairing signature compares their info strings and contents, and ` ```ts ` blocks compile under `doc-typecheck`,\n- inline code spans (commands, flags, config keys, file paths, event names, API names, version numbers) — verbatim, never translated or reformatted,\n- links and anchors: every relative document link MUST keep the same semantic target and exact query/fragment suffix. When the target belongs to the active bilingual corpus, the English side uses its `.md` path and the Chinese side uses its `.zh.md` path; a missing counterpart in that corpus is an error, while targets outside it keep the original path. External URLs, images, and pure in-page fragments stay unchanged. The language switcher remains the explicit cross-locale exception, and a README rendered outside GitHub MAY use the canonical public repository URL to its exact counterpart as documented in [README.md](README.md). Link TEXT is translated.\n\nThe repo's Markdown conventions apply to `.zh.md` files unchanged: one physical line per paragraph (`verify-md-wrap`), resolving relative links (`verify-md-links`), exactly one trailing newline.\n\n## Terminology\n\n- [terminology.md](terminology.md) is the source of truth in both directions. Before translating, load it; every listed term MUST follow its row and its \"不要译作\" prohibitions. A Chinese target uses the \"中文\" column and its \"首次出现\" annotation; an English target uses the \"English\" column without adding a Chinese gloss.\n- For a Chinese target, an unlisted technical term MAY use an established rendering from a major Chinese-language OSS or vendor source (K8s/Vue/MDN Chinese docs, 微软简中风格指南, big-tech project docs), cited in the PR. Without such precedent it MUST stay in English and be listed under 「待定术语」(pending terms) with a suggested rendering.\n- For an English target, use the established English technical term. If the source term has no unambiguous established equivalent, preserve it with a short explanatory gloss and list it under pending terms. Neither direction may invent a rendering inline; a decided term enters [terminology.md](terminology.md) in the same PR or a follow-up.\n\n## Typography\n\nThese rules govern the Chinese side; the English side follows the repo's normal Markdown conventions (root `AGENTS.md`). The mixed-script rules below follow the cross-project consensus of the [MDN Simplified Chinese translation guide](https://github.com/mdn/translated-content/blob/main/docs/zh-cn/translation-guide.md), the [Kubernetes zh-cn localization guide](https://kubernetes.io/zh-cn/docs/contribute/localization_zh/), the [Vue.js Chinese translation conventions](https://github.com/vuejs-translations/docs-zh-cn/wiki/%E7%BF%BB%E8%AF%91%E9%A1%BB%E7%9F%A5), and [中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines), which in turn ground in [W3C clreq](https://www.w3.org/TR/clreq/) and GB/T 15834—2011:\n\n- MUST put one half-width space between Chinese text and Latin words, and between Chinese text and numerals: `每个 plugin 注册 3 个 tool`。No space between a full-width punctuation mark and anything.\n- MUST use full-width (Chinese) punctuation in Chinese prose: `,。:;?!()「」`. Half-width punctuation stays inside code spans, inside complete English sentences quoted as-is, and in numbers (`3.5`, `1,024`).\n- Chinese prose *SHOULD* prefer colons, periods, commas, or parentheses over em dashes. Keep an em dash only when no other punctuation preserves the sentence naturally.\n- Enumeration commas: a Chinese list of parallel items uses 顿号(、), not commas.\n- MUST NOT use full-width digits or full-width Latin letters — `123` never, `123` always.\n- Proper nouns keep their canonical casing: GitHub, TypeScript, DeepSeek — never `github`/`Github` unless quoting code.\n- Second person is 你, not 您 (matches the Vue and Kubernetes Chinese conventions and this repo's direct voice).\n- Emphasis markers (`**bold**`, `*italic*`) stay on the same spans as the source; Chinese has no italics, so the rendered emphasis may look identical — do not substitute quotation marks or other decoration.\n\n## Quality bar\n\n- 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.\n- 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.\n\n## References\n\nAuthorities cited by these rules, for humans and agents who want the underlying reasoning:\n\n- [中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines) — the de-facto community standard for mixed CJK/Latin spacing and punctuation.\n- [MDN zh-CN translation guide](https://github.com/mdn/translated-content/blob/main/docs/zh-cn/translation-guide.md) — an in-repo translation-rules file of the same shape as this one; spacing, punctuation, and glossary practice.\n- [Kubernetes zh-cn localization guide](https://kubernetes.io/zh-cn/docs/contribute/localization_zh/) — terminology-first-occurrence and punctuation practice from the largest zh localization team.\n- [Vue.js docs-zh-cn 翻译须知](https://github.com/vuejs-translations/docs-zh-cn/wiki/%E7%BF%BB%E8%AF%91%E9%A1%BB%E7%9F%A5) — per-term translate/keep decisions and tone.\n- [zh-style-guide](https://zh-style-guide.readthedocs.io) — a community Chinese technical-writing style guide whose rule-level taxonomy (and RFC 2119 keyword levels) this file borrows; aggregates GB/T 15834/15835, clreq, and vendor guides.\n- [W3C clreq](https://www.w3.org/TR/clreq/) and the [Microsoft Simplified Chinese style guide](https://learn.microsoft.com/en-us/globalization/reference/microsoft-style-guides) — the formal typographic and vendor-localization baselines.\n- GB/T 19682-2005《翻译服务译文质量要求》 — the national standard whose three base requirements (忠实原文、术语统一、行文通顺) this file's Faithfulness and Terminology sections operationalize.\n"
},
{
"role": "assistant",
- "content": "# 翻译规则\n\n[English](translation-rules.md) | 中文\n\n本文规定:如何在本仓库文档配对的中英文两种语言之间进行翻译。两种语言同权(见 [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)** 自行裁量。\n\n## 忠实性\n\n- 对侧文件*必须*传达与撰写侧相同的内容:不添加行为、前置条件、警告、版本声明或示例,也不漏掉任何一项。如果两侧在实质内容上不一致,没有哪种语言默认获胜;请修正错误的一侧,并在同一个变更里同步更新另一侧。\n- 对侧文件读起来*应当*是其语言自然的技术文字,而非逐词对照的译文。请根据语义翻译,在目标语言语法需要时重组句子,并保持原作者的语域(比如:简练的保持简练)。\n- 不要翻译不可译的内容:如果一句话依赖源语言的习语、无法自然转换,请翻译它的意思,而非习语本身。\n\n## 行文\n\n- 语体以 [style-samples.md](style-samples.md) 为校准锚点。人工定稿的金标样例按文体各一组,译文必须参照文体最接近的样例,采用其中目标语言一侧的语体;如果样例与本文的行文规则冲突,以样例为准。译成中文时,采用规范的技术制度文;译成英文时,采用简洁、专业的开发者文档语体。\n- 以母语技术作者的身份重述内容,而不是以译者身份逐句转写,同时保留原文的每个语义成分:不添加、不遗漏——流畅永远不是丢掉语义成分的理由。\n- 如果直译会让执行主体含糊,请明确写出实际执行者;译成中文时,应由「系统、门禁、评审人」等实际执行者作主语,避免含糊的被动句或抽象主语。\n- 优先采用目标语言中通行的工程表达,避免生硬直译(false positive/negative→误报/漏检、enforcement frontier→执行红线);隐喻应自然改写,名词链则按目标语言的习惯拆开。\n- 长段按语义单元拆分,一段一件事。段落边界可以与原文不同;结构签名不比对段落数。\n- 翻译为中文时,类别名词使用中文并在首现括注英文(实操手册(cookbook));翻译为英文时,使用通行的英文类别名。指目录或文件本身时保留代码体英文。\n\n## 结构保持\n\n配对门禁会检查标题深度、围栏代码块、表格行列数、列表类型、有序列表起始编号、列表项数量与链接目标;门禁未覆盖的结构仍需人工核对。两个配对文件必须在以下方面一一对应:\n\n- 标题层级(相同级别、相同顺序;标题的**文字**要翻译);\n- 列表形态与编号;\n- 表格(相同的列、相同的行序;表头单元格按术语表翻译);\n- 围栏代码块:**逐字节一致,包括注释**。配对签名比对信息字符串与内容,` ```ts ` 块还要通过 `doc-typecheck` 编译;\n- 行内代码(命令、flag、配置键、文件路径、事件名、API 名、版本号):原样保留,从不翻译或重排;\n- 链接与锚点:每个相对链接在两个文件中必须指向相同的目标(按约定是 `.md` 路径而非 `.zh.md` 兄弟文件),这样即使某对文档先于相邻文件落地,链接也不会悬空。唯一的 zh 特有链接是语言切换行。在 GitHub 以外位置渲染的 README 可以按 [README.md](README.md) 的规定,使用指向确切对侧文件的规范公开仓库 URL。链接**文字**翻译;链接目标不翻。\n\n本仓库的 Markdown 约定对 `.zh.md` 文件原样生效:一个段落一个物理行(`verify-md-wrap`)、相对链接必须可解析(`verify-md-links`)、文件末尾恰好一个换行。\n\n## 术语\n\n- [terminology.md](terminology.md) 是双向的术语真源。翻译前请先加载它;表内术语必须遵守对应行与「不要译作」禁项。译成中文时,采用「中文」列,并按「首次出现」列括注;译成英文时,采用「English」列,不加中文括注。\n- 译成中文时,术语表未收录的技术术语只有在主流中文 OSS 文档或厂商资料中已有通行译法时才可以翻译(K8s/Vue/MDN 中文文档、微软简中风格指南、大厂项目文档),并须在 PR 中注明出处;否则必须保留英文,并在 PR 描述的「待定术语」中给出建议译法。\n- 译成英文时,采用通行的英文技术术语。如果源术语没有明确的通行对应词,则保留原词、附上简短说明,并列入「待定术语」。两个方向都不得自行创造译法;确定后的术语须在同一个 PR 或后续 PR 中加入 [terminology.md](terminology.md)。\n\n## 排版\n\n本节规则约束中文一侧;英文一侧遵循仓库常规的 Markdown 约定(根 `AGENTS.md`)。以下中西文混排规则遵循 [MDN 简体中文翻译指南](https://github.com/mdn/translated-content/blob/main/docs/zh-cn/translation-guide.md)、[Kubernetes 中文本地化指南](https://kubernetes.io/zh-cn/docs/contribute/localization_zh/)、[Vue.js 中文翻译须知](https://github.com/vuejs-translations/docs-zh-cn/wiki/%E7%BF%BB%E8%AF%91%E9%A1%BB%E7%9F%A5) 与[中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines)的跨项目共识,其根据是 [W3C clreq](https://www.w3.org/TR/clreq/) 与 GB/T 15834—2011:\n\n- 必须在中文与拉丁词之间、中文与数字之间各留一个半角空格:`每个 plugin 注册 3 个 tool`。全角标点与任何字符之间不加空格。\n- 中文行文必须使用全角(中文)标点:`,。:;?!()「」`。半角标点保留在代码内、按原样引用的完整英文句子内、以及数字内(`3.5`、`1,024`)。\n- 中文行文*应当*优先使用冒号、句号、逗号或括号,尽量不用破折号;只有其他标点都无法自然表达时才保留破折号。\n- 顿号:中文的并列项之间使用顿号(、),而非逗号。\n- 禁止使用全角数字或全角拉丁字母:永远不写 `123`,永远写 `123`。\n- 专有名词保持规范大小写:GitHub、TypeScript、DeepSeek。除非引用代码,否则绝不写 `github`/`Github`。\n- 第二人称用「你」,不用「您」(与 Vue、Kubernetes 中文约定及本仓库的直接语气一致)。\n- 强调标记(`**加粗**`、`*斜体*`)落在与对侧相同的文字段上。中文没有斜体,渲染效果可能看不出差别,不要用引号或其他装饰替代。\n\n## 质量标准\n\n- 一对文档的完成标准:一位双语工程师只读其中任一文件,能获得与另一文件读者完全相同的信息(相同的事实、相同的告诫、相同的语气),并且没有任何多余的内容。\n- 请运行 `pnpm run verify-translation-pairing` 与 `doc-sync` 的其余门禁。这些门禁会检查一致性记录、切换行、标题深度、代码块、表格行列数、列表类型、有序列表起始编号、列表项数量、链接及仓库 Markdown 规则;列表与表格的顺序、非常规列表编号、行内代码、强调标记、语义、术语和语体则由人工评审负责。\n\n## 参考资料\n\n本文各规则引用的权威出处,供想了解底层依据的人和 agent 查阅:\n\n- [中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines):中西文混排空格与标点的社区事实标准。\n- [MDN 简体中文翻译指南](https://github.com/mdn/translated-content/blob/main/docs/zh-cn/translation-guide.md):与本文同形态的仓库内置翻译规则文件;空格、标点与术语表实践。\n- [Kubernetes 中文本地化指南](https://kubernetes.io/zh-cn/docs/contribute/localization_zh/):最大的中文本地化团队的术语首现与标点实践。\n- [Vue.js docs-zh-cn 翻译须知](https://github.com/vuejs-translations/docs-zh-cn/wiki/%E7%BF%BB%E8%AF%91%E9%A1%BB%E7%9F%A5):逐术语的译/留决策与语气。\n- [zh-style-guide](https://zh-style-guide.readthedocs.io):社区中文技术文档写作规范,本文借用了它的规则级别分类体系(与 RFC 2119 关键词分级);它聚合了 GB/T 15834/15835、clreq 与各厂商指南。\n- [W3C clreq](https://www.w3.org/TR/clreq/) 与[微软简体中文风格指南](https://learn.microsoft.com/en-us/globalization/reference/microsoft-style-guides):排版学与厂商本地化的正式基线。\n- GB/T 19682-2005《翻译服务译文质量要求》:国家标准;本文「忠实性」与「术语」两节将其三项基本要求(忠实原文、术语统一、行文通顺)落实为可操作的规则。\n"
+ "content": "# 翻译规则\n\n[English](translation-rules.md) | 中文\n\n本文规定:如何在本仓库文档配对的中英文两种语言之间进行翻译。两种语言同权(见 [README.md](README.zh.md)):每次变更可以用任一语言撰写,被编辑的一侧即为本次更新的源;本文的规则约束如何产出或更新对侧文件。这些规则对人类和 agent(智能体)同等生效。日常工作中,agent 会在术语指导下直接一次完成有改动内容的翻译;扩展版 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) 工作流仅在用户显式调用时运行。规则级别沿用 RFC 2119 的用法:**必须(MUST)** / **禁止(MUST NOT)** 会卡门禁或评审;**应当(SHOULD)** 偏离时要说明理由;**可以(MAY)** 自行裁量。\n\n## 忠实性\n\n- 对侧文件*必须*传达与撰写侧相同的内容:不添加行为、前置条件、警告、版本声明或示例,也不漏掉任何一项。如果两侧在实质内容上不一致,没有哪种语言默认获胜;请修正错误的一侧,并在同一个变更里同步更新另一侧。\n- 对侧文件读起来*应当*是其语言自然的技术文字,而非逐词对照的译文。请根据语义翻译,在目标语言语法需要时重组句子,并保持原作者的语域(比如:简练的保持简练)。\n- 不要翻译不可译的内容:如果一句话依赖源语言的习语、无法自然转换,请翻译它的意思,而非习语本身。\n\n## 行文\n\n- 语体以 [style-samples.md](style-samples.md) 为校准锚点。人工定稿的金标样例按文体各一组,译文必须参照文体最接近的样例,采用其中目标语言一侧的语体;如果样例与本文的行文规则冲突,以样例为准。译成中文时,采用规范的技术制度文;译成英文时,采用简洁、专业的开发者文档语体。\n- 以母语技术作者的身份重述内容,而不是以译者身份逐句转写,同时保留原文的每个语义成分:不添加、不遗漏——流畅永远不是丢掉语义成分的理由。\n- 如果直译会让执行主体含糊,请明确写出实际执行者;译成中文时,应由「系统、门禁、评审人」等实际执行者作主语,避免含糊的被动句或抽象主语。\n- 优先采用目标语言中通行的工程表达,避免生硬直译(false positive/negative→误报/漏检、enforcement frontier→执行红线);隐喻应自然改写,名词链则按目标语言的习惯拆开。\n- 长段按语义单元拆分,一段一件事。段落边界可以与原文不同;结构签名不比对段落数。\n- 翻译为中文时,类别名词使用中文并在首现括注英文(实操手册(cookbook));翻译为英文时,使用通行的英文类别名。指目录或文件本身时保留代码体英文。\n\n## 结构保持\n\n配对门禁会检查标题深度、围栏代码块、表格行列数、列表类型、有序列表起始编号、列表项数量、链接 locale 与语义目标;门禁未覆盖的结构仍需人工核对。两个配对文件必须在以下方面一一对应:\n\n- 标题层级(相同级别、相同顺序;标题的**文字**要翻译);\n- 列表形态与编号;\n- 表格(相同的列、相同的行序;表头单元格按术语表翻译);\n- 围栏代码块:**逐字节一致,包括注释**。配对签名比对信息字符串与内容,` ```ts ` 块还要通过 `doc-typecheck` 编译;\n- 行内代码(命令、flag、配置键、文件路径、事件名、API 名、版本号):原样保留,从不翻译或重排;\n- 链接与锚点:每个相对文档链接必须保持相同的语义目标和完全相同的 query/fragment 后缀。目标属于活跃双语语料时,英文侧使用其 `.md` 路径,中文侧使用其 `.zh.md` 路径;该范围内缺少对侧属于错误,范围外的目标保留原路径。外部 URL、图片与纯页内 fragment 保持不变。语言切换行仍是显式跨 locale 例外;在 GitHub 以外位置渲染的 README 可以按 [README.md](README.zh.md) 的规定,使用指向确切对侧文件的规范公开仓库 URL。链接**文字**翻译。\n\n本仓库的 Markdown 约定对 `.zh.md` 文件原样生效:一个段落一个物理行(`verify-md-wrap`)、相对链接必须可解析(`verify-md-links`)、文件末尾恰好一个换行。\n\n## 术语\n\n- [terminology.md](terminology.md) 是双向的术语真源。翻译前请先加载它;表内术语必须遵守对应行与「不要译作」禁项。译成中文时,采用「中文」列,并按「首次出现」列括注;译成英文时,采用「English」列,不加中文括注。\n- 译成中文时,术语表未收录的技术术语只有在主流中文 OSS 文档或厂商资料中已有通行译法时才可以翻译(K8s/Vue/MDN 中文文档、微软简中风格指南、大厂项目文档),并须在 PR 中注明出处;否则必须保留英文,并在 PR 描述的「待定术语」中给出建议译法。\n- 译成英文时,采用通行的英文技术术语。如果源术语没有明确的通行对应词,则保留原词、附上简短说明,并列入「待定术语」。两个方向都不得自行创造译法;确定后的术语须在同一个 PR 或后续 PR 中加入 [terminology.md](terminology.md)。\n\n## 排版\n\n本节规则约束中文一侧;英文一侧遵循仓库常规的 Markdown 约定(根 `AGENTS.md`)。以下中西文混排规则遵循 [MDN 简体中文翻译指南](https://github.com/mdn/translated-content/blob/main/docs/zh-cn/translation-guide.md)、[Kubernetes 中文本地化指南](https://kubernetes.io/zh-cn/docs/contribute/localization_zh/)、[Vue.js 中文翻译须知](https://github.com/vuejs-translations/docs-zh-cn/wiki/%E7%BF%BB%E8%AF%91%E9%A1%BB%E7%9F%A5) 与[中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines)的跨项目共识,其根据是 [W3C clreq](https://www.w3.org/TR/clreq/) 与 GB/T 15834—2011:\n\n- 必须在中文与拉丁词之间、中文与数字之间各留一个半角空格:`每个 plugin 注册 3 个 tool`。全角标点与任何字符之间不加空格。\n- 中文行文必须使用全角(中文)标点:`,。:;?!()「」`。半角标点保留在代码内、按原样引用的完整英文句子内、以及数字内(`3.5`、`1,024`)。\n- 中文行文*应当*优先使用冒号、句号、逗号或括号,尽量不用破折号;只有其他标点都无法自然表达时才保留破折号。\n- 顿号:中文的并列项之间使用顿号(、),而非逗号。\n- 禁止使用全角数字或全角拉丁字母:永远不写 `123`,永远写 `123`。\n- 专有名词保持规范大小写:GitHub、TypeScript、DeepSeek。除非引用代码,否则绝不写 `github`/`Github`。\n- 第二人称用「你」,不用「您」(与 Vue、Kubernetes 中文约定及本仓库的直接语气一致)。\n- 强调标记(`**加粗**`、`*斜体*`)落在与对侧相同的文字段上。中文没有斜体,渲染效果可能看不出差别,不要用引号或其他装饰替代。\n\n## 质量标准\n\n- 一对文档的完成标准:一位双语工程师只读其中任一文件,能获得与另一文件读者完全相同的信息(相同的事实、相同的告诫、相同的语气),并且没有任何多余的内容。\n- 请运行 `pnpm run verify-translation-pairing` 与 `doc-sync` 的其余门禁。这些门禁会检查一致性记录、切换行、标题深度、代码块、表格行列数、列表类型、有序列表起始编号、列表项数量、链接及仓库 Markdown 规则;列表与表格的顺序、非常规列表编号、行内代码、强调标记、语义、术语和语体则由人工评审负责。\n\n## 参考资料\n\n本文各规则引用的权威出处,供想了解底层依据的人和 agent 查阅:\n\n- [中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines):中西文混排空格与标点的社区事实标准。\n- [MDN 简体中文翻译指南](https://github.com/mdn/translated-content/blob/main/docs/zh-cn/translation-guide.md):与本文同形态的仓库内置翻译规则文件;空格、标点与术语表实践。\n- [Kubernetes 中文本地化指南](https://kubernetes.io/zh-cn/docs/contribute/localization_zh/):最大的中文本地化团队的术语首现与标点实践。\n- [Vue.js docs-zh-cn 翻译须知](https://github.com/vuejs-translations/docs-zh-cn/wiki/%E7%BF%BB%E8%AF%91%E9%A1%BB%E7%9F%A5):逐术语的译/留决策与语气。\n- [zh-style-guide](https://zh-style-guide.readthedocs.io):社区中文技术文档写作规范,本文借用了它的规则级别分类体系(与 RFC 2119 关键词分级);它聚合了 GB/T 15834/15835、clreq 与各厂商指南。\n- [W3C clreq](https://www.w3.org/TR/clreq/) 与[微软简体中文风格指南](https://learn.microsoft.com/en-us/globalization/reference/microsoft-style-guides):排版学与厂商本地化的正式基线。\n- GB/T 19682-2005《翻译服务译文质量要求》:国家标准;本文「忠实性」与「术语」两节将其三项基本要求(忠实原文、术语统一、行文通顺)落实为可操作的规则。\n"
},
{
"role": "user",
- "content": "# Agent Note: Bilingual documentation via paired sibling files and a pairing gate\n\nStatus: implemented\n\nEnglish | [中文](2026-07-02-bilingual-docs-and-pairing-gate.zh.md)\n\n## Problem\n\nThis repo's documentation corpus is read by people and agents inside and outside the company, in both English and Chinese. Maintaining a second language by hand, with no mechanism, is how translations rot: one side moves on, the other silently lies, and no gate notices. The repo's standing answer to invariants of this kind is to encode them as a mechanical check (see [quality gates](2026-06-11-quality-gates.md) and [doc-sync enforcement](../../archived/process/2026-06-11-doc-sync-enforcement.md)), so the bilingual policy ships with one.\n\n## Decision\n\n- **Paired sibling files with equal authority.** A documentation pair is three sibling files: English `foo.md`, Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`. Neither language is canonical — a document may be authored and reviewed Chinese-first and translated to English afterwards, or the reverse; what binds the pair is that both sides must say the same thing, and pairs merge whole (both languages plus the record, never one alone). Policy: [docs/i18n/README.md](../../../../docs/i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md).\n- **A sidecar record of both blob hashes makes consistency checkable.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last confirmed-consistent state. An edit to either side without re-confirming the pair is then mechanically detectable as a pure content comparison — no history lookup — and the hashes are computable for files edited in the same PR, which a commit-hash record is not. Re-recording (`verify-translation-pairing --write `, which requires naming the confirmed pairs — bulk re-record is an explicit `--write --all`) produces a reviewable yaml diff: confirming consistency is an explicit, visible act in the PR.\n- **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: every discovered, non-excluded source has a complete pair; every existing pair is complete (all three files) and consistent (both hashes match, the Chinese side and every authored English source carry their switchers while listed generated English sources are exempt, structural signatures identical); and excluded generated, instruction, or bilingual-by-construction files stay unpaired. [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) contains only explicit exclusions, so no requirement can bypass discovery and receive a weaker check. Source-oriented code gates consume a `.zh.md` fence sequence as a derivative only when its unsuffixed sibling has the same tracked fences in the same order with byte-identical bodies; an incomplete, reordered, reclassified, or changed sequence stays independent, so the owning code gate or pairing gate reports the mismatch.\n- **One corpus-wide requirement.** Every document in scope requires a complete pair from creation; the policy has no per-file rollout state, date cutoff, or README-specific class. README discovery covers every case-insensitive README basename outside vendored, dependency, and ignored build-output trees, including future top-level directories. A site-published pair uses `pairedPages()` so the root locale projects `.zh.md` and `/en/` projects `.md`; creating a counterpart alone does not publish it.\n- **Pairing records are metadata, not Cordis Loader configuration.** Cordis configuration discovery accepts actual `.cordis.yml` and `.cordis.yaml` files while excluding `*.i18n.yaml`, even when the document name contains `cordis`. This preserves validation of executable Loader entries without parsing translation hashes as configuration.\n- **Translation is agent work with human review.** Routine changes use the direct one-pass path owned by the [lightweight-translation decision](2026-08-08-lightweight-routine-documentation-translation.md). The [extended translation skill](../../../skills/dsh-translate-docs/SKILL.md) retains delegated translation and the other heavier mechanisms for explicit user invocation; both paths defer to the documentation contracts as their sources of truth.\n\n## Verification\n\nThe verification contract covers each boundary independently. `verify-translation-pairing` pins pair completeness, hashes, switchers, and structure; [`project-doc-site.spec.ts`](../../../../scripts/project-doc-site.spec.ts) pins locale-specific source selection for published pairs; [`cordis-config-files.spec.ts`](../../../../scripts/cordis-config-files.spec.ts) pins discovery of Loader YAML and exclusion of translation records; and the [translation-prompt runnable snapshot](../../../../scripts/translation-prompt.snapshot.ts) pins the rendered system message, five reviewed example pairs, source request, and consumed response. Together these checks make pair drift, publication drift, configuration misclassification, and model-visible prompt drift review-visible.\n\n## Alternatives considered\n\n- **English as the canonical source with a fingerprint inside the translation** — `.zh.md` files would carry an HTML comment recording the English source's blob hash, and translation would flow EN → ZH only. Rejected: the team wants Chinese-first authoring (write and review a Chinese Agent Note, then translate to English) with the two languages holding equal authority, which a one-directional canonical model cannot express. The sidecar record covering BOTH sides replaced the in-file one-directional fingerprint; the blob-hash mechanics survived unchanged.\n- **Locale directories (`docs/en/` + `docs/zh/`, the Kubernetes/ECharts model)** — rejected: this repo has no docs-site framework to map locales to routes, moving every English file would churn every existing cross-reference, and `verify-md-links`/`verify-doc-refs` would need path-mapping logic instead of working unchanged.\n- **A separate translation repo (the PingCAP `docs`/`docs-cn` model)** — rejected: right for a docs product with independent release trains, overkill for a monorepo's own documentation; it also puts the translation outside the reach of this repo's gates.\n- **Interleaved bilingual files (single file, both languages)** — rejected: doubles every diff, breaks the one-line-per-paragraph convention's diff ergonomics, and makes partial inconsistency invisible.\n- **Commit-hash records (the MDN `l10n.sourceCommit` model)** — rejected in favor of blob hashes: a same-PR edit has no commit hash yet, so the MDN model cannot express \"consistent as of the state this PR introduces\", and verifying it requires git history instead of file content.\n- **Comparing git timestamps of the pair (no record)** — rejected: formatting-only edits would false-positive, and a counterpart committed after an unrelated edit would false-negative; content identity is the only signal that means what the gate claims.\n\n## Industry precedent\n\nPaired sibling files with locale suffixes are the dominant Chinese big-tech convention (ant-design `index.zh-CN.md`/`index.en-US.md`; arco-design `README.zh-CN.md` with a top-of-file switcher; Apache ShardingSphere's 387 `.cn.md`/`.en.md` pairs) — but none of those repos *enforce* pairing or consistency in CI; the convention holds by review alone. Consistency automation exists outside China: MDN's `l10n.sourceCommit` front-matter fingerprint, Vue's Ryu-Cho action (upstream-commit watcher that opens issues/PRs for stale translations), Kubernetes' localization drift scripts, and Microsoft's Azure co-op-translator (source-hash-driven LLM re-translation in CI). This design combines the two: the Chinese-ecosystem file layout with a hash-pair gate, plus an agent-run workflow in place of a bot service.\n\n## Consequences\n\n- Editing either side of a paired document obligates the same PR to update the counterpart and re-record the pair — the gate makes the doc-sync rule bilingual, and CI (not reviewer memory) carries the invariant.\n- Every pair adds a third file to the tree. The record is machine-written (`--write`), so the cost is directory noise, not maintenance effort; in exchange, \"who confirmed these consistent, and when\" is answerable from git blame on the yaml.\n- When the two sides disagree, no mechanical rule picks a winner — the PR review does. That is the price of equal authority, accepted deliberately: the alternative (a canonical language) forbids Chinese-first authoring.\n- Generated English documents remain derived from source and freshness-gated by their owning generators. A generated page with a reviewed Chinese counterpart participates in the three-file pairing workflow, with one structural exception: the generated English source has no language switcher because adding one would make the generator stale, while the Chinese counterpart links back to it. Generated pages without a reviewed counterpart remain explicit exclusions and use an English website projection.\n- The exclusions-only manifest makes every current and future in-scope document mandatory through the same path. There is no explicit requirement, cutoff, or class entry that can fall outside discovery while appearing enforced.\n- The recorded hashes double as the update tool: [gen-translation-brief](2026-07-26-briefed-minimal-translation-updates.md) recovers either side's last-confirmed text from them and assembles the minimal-update briefing, so re-translation of whole files is never forced by the mechanism.\n"
+ "content": "# Agent Note: Bilingual documentation via paired sibling files and a pairing gate\n\nStatus: implemented\n\nEnglish | [中文](2026-07-02-bilingual-docs-and-pairing-gate.zh.md)\n\n## Problem\n\nThis repo's documentation corpus is read by people and agents inside and outside the company, in both English and Chinese. Maintaining a second language by hand, with no mechanism, is how translations rot: one side moves on, the other silently lies, and no gate notices. The repo's standing answer to invariants of this kind is to encode them as a mechanical check (see [quality gates](2026-06-11-quality-gates.md) and [doc-sync enforcement](../../archived/process/2026-06-11-doc-sync-enforcement.md)), so the bilingual policy ships with one.\n\n## Decision\n\n- **Paired sibling files with equal authority.** A documentation pair is three sibling files: English `foo.md`, Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`. Neither language is canonical — a document may be authored and reviewed Chinese-first and translated to English afterwards, or the reverse; what binds the pair is that both sides must say the same thing, and pairs merge whole (both languages plus the record, never one alone). Policy: [docs/i18n/README.md](../../../../docs/i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md).\n- **A sidecar record of both blob hashes makes consistency checkable.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last confirmed-consistent state. An edit to either side without re-confirming the pair is then mechanically detectable as a pure content comparison — no history lookup — and the hashes are computable for files edited in the same PR, which a commit-hash record is not. Re-recording (`verify-translation-pairing --write `, which requires naming the confirmed pairs — bulk re-record is an explicit `--write --all`) produces a reviewable yaml diff: confirming consistency is an explicit, visible act in the PR.\n- **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: every discovered, non-excluded source has a complete pair; every existing pair is complete (all three files) and consistent (both hashes match, the Chinese side and every authored English source carry their switchers while listed generated English sources are exempt, structural signatures identical); and excluded generated, instruction, or bilingual-by-construction files stay unpaired. Relative document links whose targets belong to that active corpus use the target sibling matching the source locale, while the structure signature normalizes `.md` and `.zh.md` siblings to one semantic target and retains the exact query/fragment suffix; the [localized bilingual links decision](2026-08-18-localized-bilingual-links.md) owns that refinement. [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) contains only explicit exclusions, so no requirement can bypass discovery and receive a weaker check. Source-oriented code gates consume a `.zh.md` fence sequence as a derivative only when its unsuffixed sibling has the same tracked fences in the same order with byte-identical bodies; an incomplete, reordered, reclassified, or changed sequence stays independent, so the owning code gate or pairing gate reports the mismatch.\n- **One corpus-wide requirement.** Every document in scope requires a complete pair from creation; the policy has no per-file rollout state, date cutoff, or README-specific class. README discovery covers every case-insensitive README basename outside vendored, dependency, and ignored build-output trees, including future top-level directories. A site-published pair uses `pairedPages()` so the root locale projects `.zh.md` and `/en/` projects `.md`; creating a counterpart alone does not publish it.\n- **Pairing records are metadata, not Cordis Loader configuration.** Cordis configuration discovery accepts actual `.cordis.yml` and `.cordis.yaml` files while excluding `*.i18n.yaml`, even when the document name contains `cordis`. This preserves validation of executable Loader entries without parsing translation hashes as configuration.\n- **Translation is agent work with human review.** Routine changes use the direct one-pass path owned by the [lightweight-translation decision](2026-08-08-lightweight-routine-documentation-translation.md). The [extended translation skill](../../../skills/dsh-translate-docs/SKILL.md) retains delegated translation and the other heavier mechanisms for explicit user invocation; both paths defer to the documentation contracts as their sources of truth.\n\n## Verification\n\nThe verification contract covers each boundary independently. `verify-translation-pairing` pins pair completeness, hashes, switchers, and structure; [`project-doc-site.spec.ts`](../../../../scripts/project-doc-site.spec.ts) pins locale-specific source selection for published pairs; [`cordis-config-files.spec.ts`](../../../../scripts/cordis-config-files.spec.ts) pins discovery of Loader YAML and exclusion of translation records; and the [translation-prompt runnable snapshot](../../../../scripts/translation-prompt.snapshot.ts) pins the rendered system message, five reviewed example pairs, source request, and consumed response. Together these checks make pair drift, publication drift, configuration misclassification, and model-visible prompt drift review-visible.\n\n## Alternatives considered\n\n- **English as the canonical source with a fingerprint inside the translation** — `.zh.md` files would carry an HTML comment recording the English source's blob hash, and translation would flow EN → ZH only. Rejected: the team wants Chinese-first authoring (write and review a Chinese Agent Note, then translate to English) with the two languages holding equal authority, which a one-directional canonical model cannot express. The sidecar record covering BOTH sides replaced the in-file one-directional fingerprint; the blob-hash mechanics survived unchanged.\n- **Locale directories (`docs/en/` + `docs/zh/`, the Kubernetes/ECharts model)** — rejected: this repo has no docs-site framework to map locales to routes, moving every English file would churn every existing cross-reference, and `verify-md-links`/`verify-doc-refs` would need path-mapping logic instead of working unchanged.\n- **A separate translation repo (the PingCAP `docs`/`docs-cn` model)** — rejected: right for a docs product with independent release trains, overkill for a monorepo's own documentation; it also puts the translation outside the reach of this repo's gates.\n- **Interleaved bilingual files (single file, both languages)** — rejected: doubles every diff, breaks the one-line-per-paragraph convention's diff ergonomics, and makes partial inconsistency invisible.\n- **Commit-hash records (the MDN `l10n.sourceCommit` model)** — rejected in favor of blob hashes: a same-PR edit has no commit hash yet, so the MDN model cannot express \"consistent as of the state this PR introduces\", and verifying it requires git history instead of file content.\n- **Comparing git timestamps of the pair (no record)** — rejected: formatting-only edits would false-positive, and a counterpart committed after an unrelated edit would false-negative; content identity is the only signal that means what the gate claims.\n\n## Industry precedent\n\nPaired sibling files with locale suffixes are the dominant Chinese big-tech convention (ant-design `index.zh-CN.md`/`index.en-US.md`; arco-design `README.zh-CN.md` with a top-of-file switcher; Apache ShardingSphere's 387 `.cn.md`/`.en.md` pairs) — but none of those repos *enforce* pairing or consistency in CI; the convention holds by review alone. Consistency automation exists outside China: MDN's `l10n.sourceCommit` front-matter fingerprint, Vue's Ryu-Cho action (upstream-commit watcher that opens issues/PRs for stale translations), Kubernetes' localization drift scripts, and Microsoft's Azure co-op-translator (source-hash-driven LLM re-translation in CI). This design combines the two: the Chinese-ecosystem file layout with a hash-pair gate, plus an agent-run workflow in place of a bot service.\n\n## Consequences\n\n- Editing either side of a paired document obligates the same PR to update the counterpart and re-record the pair — the gate makes the doc-sync rule bilingual, and CI (not reviewer memory) carries the invariant.\n- Every pair adds a third file to the tree. The record is machine-written (`--write`), so the cost is directory noise, not maintenance effort; in exchange, \"who confirmed these consistent, and when\" is answerable from git blame on the yaml.\n- When the two sides disagree, no mechanical rule picks a winner — the PR review does. That is the price of equal authority, accepted deliberately: the alternative (a canonical language) forbids Chinese-first authoring.\n- Generated English documents remain derived from source and freshness-gated by their owning generators. A generated page with a reviewed Chinese counterpart participates in the three-file pairing workflow, with one structural exception: the generated English source has no language switcher because adding one would make the generator stale, while the Chinese counterpart links back to it. Generated pages without a reviewed counterpart remain explicit exclusions and use an English website projection.\n- The exclusions-only manifest makes every current and future in-scope document mandatory through the same path. There is no explicit requirement, cutoff, or class entry that can fall outside discovery while appearing enforced.\n- The recorded hashes double as the update tool: [gen-translation-brief](2026-07-26-briefed-minimal-translation-updates.md) recovers either side's last-confirmed text from them and assembles the minimal-update briefing, so re-translation of whole files is never forced by the mechanism.\n"
},
{
"role": "assistant",
- "content": "# Agent Note: 通过配对兄弟文件与配对门禁实现双语文档\n\nStatus: implemented\n\n[English](2026-07-02-bilingual-docs-and-pairing-gate.md) | 中文\n\n## 问题\n\n本仓库的文档语料会被公司内外的人和 agent(智能体)以中英两种语言阅读。在没有机制的情况下纯靠手工维护第二语言,正是译文腐烂的根源:一侧持续演进,另一侧默默失实,而没有门禁能够发现。对于这类不变式,本仓库一贯的做法是将其编码为机械检查(见[质量门禁](2026-06-11-quality-gates.md)与 [doc-sync(文档同步门禁)强制](../../archived/process/2026-06-11-doc-sync-enforcement.md)),因此双语政策随附一道门禁一起交付。\n\n## 决策\n\n- **配对兄弟文件,两种语言同权。** 一对文档由三个兄弟文件组成:英文 `foo.md`、中文 `foo.zh.md`,以及一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典:一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是:两侧必须表达相同的内容,且配对整体合并(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../../docs/i18n/README.md);翻译规则见 [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md);术语真源见 [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md)。\n- **伴随记录保存两侧 blob hash,使一致性可检查。** `foo.i18n.yaml` 保存两侧文件在上一次确认一致时各自的完整 Git blob hash。此后修改了任一侧而未重新确认配对,都能被机械检测出来(纯内容比较,无需查询历史),而且同一个 PR(Pull Request)内改动的文件也能计算出 hash,commit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write `,要求点名所确认的配对;批量重新记录是显式的 `--write --all`)会产生一份可评审的 YAML diff:确认一致在 PR 中是一个显式、可见的动作。\n- **`verify-translation-pairing` 加入 `doc-sync`。** 门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行以下规则:每个已发现且未排除的源文档都有完整配对;每个现有配对都完整(三个文件齐全)且一致(两侧的 hash 均与记录匹配、中文侧和所有人工撰写的英文源都带语言切换行而清单内的生成英文源除外、结构签名一致);被排除的生成文档、指令文档或本身即双语的文档不得配对。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 只包含显式排除项,因此任何要求都无法绕过发现流程而接受较弱的检查。只有当 `.zh.md` 围栏序列与其无后缀兄弟文件拥有顺序相同、正文按字节一致的同一组受跟踪围栏时,面向源码的代码门禁才会将其作为派生内容消费;不完整、顺序变更、重分类或已改动的序列仍会独立受检,因此由其所属的代码门禁或配对门禁报告不匹配。\n- **全语料统一要求。** 范围内的每篇文档从创建起就必须有完整配对;政策没有逐文件推进状态、日期分界或 README 专用类别。README 发现会覆盖 vendor 源码、依赖目录与被忽略的构建产物目录之外所有文件名不区分大小写匹配 README 的文件,包括今后新增的顶层目录。发布到文档站的配对使用 `pairedPages()`,由根 locale 投影 `.zh.md`,由 `/en/` 投影 `.md`;仅创建对侧文件并不会发布它。\n- **配对记录是元数据,而不是 Cordis Loader 配置。** Cordis 配置发现会接受实际的 `.cordis.yml` 和 `.cordis.yaml` 文件,同时排除 `*.i18n.yaml`,即使文档名中包含 `cordis` 也不例外。这样既能继续校验可执行的 Loader 配置项,又不会把翻译 hash 当作配置来解析。\n- **翻译是 agent 的工作,由人评审。** 常规改动采用由[轻量翻译决策](2026-08-08-lightweight-routine-documentation-translation.md)确立的直接单遍路径。[扩展翻译 skill(技能)](../../../skills/dsh-translate-docs/SKILL.md)保留委派翻译和其他较重机制,供用户显式调用;两条路径均以文档契约为真源。\n\n## 验证\n\n验证约定分别覆盖每个边界。`verify-translation-pairing` 固定配对完整性、hash、语言切换行和结构;[`project-doc-site.spec.ts`](../../../../scripts/project-doc-site.spec.ts) 固定已发布配对按 locale 选择对应源文件;[`cordis-config-files.spec.ts`](../../../../scripts/cordis-config-files.spec.ts) 固定 Loader YAML 的发现以及翻译记录的排除;[翻译提示词可运行快照](../../../../scripts/translation-prompt.snapshot.ts)则固定渲染后的系统消息、五对经评审的示例、源请求和所消费的响应。这些检查共同使配对漂移、发布漂移、配置误分类和模型可见提示词漂移都可在评审中看见。\n\n## 曾考虑的替代方案\n\n- **英文为正典源、指纹放在译文内**:`.zh.md` 文件携带一条 HTML 注释记录英文源的 blob hash,翻译只沿 EN → ZH 单向流动。否决:团队需要中文先行的撰写方式(先写、先审中文 Agent Note,再译英文),两种语言同权,而单向正典模型无法表达这一点。覆盖**两侧**的伴随记录取代了文件内的单向指纹;blob hash 的机制本身保持不变。\n- **语言目录(`docs/en/` + `docs/zh/`,Kubernetes/ECharts 模式)**:否决。本仓库没有将 locale 映射到路由的文档站框架;如果移动所有英文文件,所有既有交叉引用都要随之修改;且 `verify-md-links`/`verify-doc-refs` 将需要路径映射逻辑,而非原样工作。\n- **独立翻译仓库(PingCAP `docs`/`docs-cn` 模式)**:否决。适合有独立发布节奏的文档产品,对 monorepo 自身的文档而言过重;还会把译文置于本仓库门禁触及不到的地方。\n- **中英混排单文件(一个文件、两种语言)**:否决。每个 diff 都翻倍,破坏一段一行约定的 diff 易读性,且局部不一致不可见。\n- **Commit hash 式记录(MDN `l10n.sourceCommit` 模式)**:否决,改用 blob hash。同一个 PR 内的改动还没有 commit hash,MDN 模式无法表达「与本 PR 引入的状态一致」,且校验它需要 git 历史而非文件内容。\n- **比较配对两侧的 git 时间戳(无记录)**:否决。纯格式化的改动会误报,一次无关改动之后提交的对侧文件会漏报;只有内容同一性这个信号才与门禁的承诺名实相符。\n\n## 业界先例\n\n带语言后缀的配对兄弟文件是中国大厂的主流约定(ant-design 的 `index.zh-CN.md`/`index.en-US.md`;arco-design 的 `README.zh-CN.md` 加顶部切换行;Apache ShardingSphere 的 387 对 `.cn.md`/`.en.md`),但这些仓库都没有在 CI 中**强制**配对或一致性检查;约定纯靠评审维系。一致性自动化存在于中国以外:MDN 的 `l10n.sourceCommit` front-matter 指纹、Vue 的 Ryu-Cho action(监视上游 commit,为陈旧译文自动开 issue/PR)、Kubernetes 的本地化漂移脚本、微软 Azure co-op-translator(CI 中由源 hash 驱动的 LLM 重译)。本设计将两者结合:中文生态的文件布局,加上 hash 配对门禁,再加一个由 agent 运行的工作流替代 bot 服务。\n\n## 后果\n\n- 修改已配对文档的任一侧,同一个 PR 就有义务更新对侧并重新记录配对。门禁将 doc-sync 规则双语化,不变式由 CI(而非评审者的记忆)承载。\n- 每个配对给目录树多添一个文件。记录由机器写入(`--write`),代价是目录噪音而非维护负担;换来的是「谁在何时确认过这对文档一致」可以从 yaml 的 git blame 直接回答。\n- 两侧说法冲突时,没有机械规则裁决谁赢,由 PR 评审裁决。这是同权的代价,且是有意接受的:另一个选项(正典语言)会禁止中文先行撰写。\n- 生成的英文文档仍由源码派生,并由各自的生成器实施新鲜度门禁。有经评审中文对侧的生成页面遵循三文件配对工作流,但有一项结构例外:生成的英文源文件不含语言切换行,因为添加该行会使生成器新鲜度检查失败;中文对侧仍链接回英文源。没有经评审对侧的生成页面保留为显式排除项,并在网站上投影英文。\n- 只含排除项的 manifest(元数据清单)通过同一路径,要求当前及今后纳入范围的每篇文档都必须配对。不存在显式要求、分界或类别条目可以落在发现范围之外,却看似已经强制执行。\n- 记录的 hash 兼作更新工具:[gen-translation-brief](2026-07-26-briefed-minimal-translation-updates.md) 会从中还原任一侧上次确认的文本并组装最小更新简报,因此这套机制从不强迫整篇重译。\n"
+ "content": "# Agent Note: 通过配对兄弟文件与配对门禁实现双语文档\n\nStatus: implemented\n\n[English](2026-07-02-bilingual-docs-and-pairing-gate.md) | 中文\n\n## 问题\n\n本仓库的文档语料会被公司内外的人和 agent(智能体)以中英两种语言阅读。在没有机制的情况下纯靠手工维护第二语言,正是译文腐烂的根源:一侧持续演进,另一侧默默失实,而没有门禁能够发现。对于这类不变式,本仓库一贯的做法是将其编码为机械检查(见[质量门禁](2026-06-11-quality-gates.zh.md)与 [doc-sync(文档同步门禁)强制](../../archived/process/2026-06-11-doc-sync-enforcement.md)),因此双语政策随附一道门禁一起交付。\n\n## 决策\n\n- **配对兄弟文件,两种语言同权。** 一对文档由三个兄弟文件组成:英文 `foo.md`、中文 `foo.zh.md`,以及一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典:一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是:两侧必须表达相同的内容,且配对整体合并(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../../docs/i18n/README.zh.md);翻译规则见 [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.zh.md);术语真源见 [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md)。\n- **伴随记录保存两侧 blob hash,使一致性可检查。** `foo.i18n.yaml` 保存两侧文件在上一次确认一致时各自的完整 Git blob hash。此后修改了任一侧而未重新确认配对,都能被机械检测出来(纯内容比较,无需查询历史),而且同一个 PR(Pull Request)内改动的文件也能计算出 hash,commit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write `,要求点名所确认的配对;批量重新记录是显式的 `--write --all`)会产生一份可评审的 YAML diff:确认一致在 PR 中是一个显式、可见的动作。\n- **`verify-translation-pairing` 加入 `doc-sync`。** 门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行以下规则:每个已发现且未排除的源文档都有完整配对;每个现有配对都完整(三个文件齐全)且一致(两侧的 hash 均与记录匹配、中文侧和所有人工撰写的英文源都带语言切换行而清单内的生成英文源除外、结构签名一致);被排除的生成文档、指令文档或本身即双语的文档不得配对。目标属于该活跃语料的相对文档链接使用与源文件 locale 相同的目标兄弟文件;结构签名则把 `.md` 与 `.zh.md` 兄弟文件规范化为同一个语义目标,并保留完全相同的 query/fragment 后缀;该细化规则由[双语文档链接本地化决策](2026-08-18-localized-bilingual-links.zh.md)负责。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 只包含显式排除项,因此任何要求都无法绕过发现流程而接受较弱的检查。只有当 `.zh.md` 围栏序列与其无后缀兄弟文件拥有顺序相同、正文按字节一致的同一组受跟踪围栏时,面向源码的代码门禁才会将其作为派生内容消费;不完整、顺序变更、重分类或已改动的序列仍会独立受检,因此由其所属的代码门禁或配对门禁报告不匹配。\n- **全语料统一要求。** 范围内的每篇文档从创建起就必须有完整配对;政策没有逐文件推进状态、日期分界或 README 专用类别。README 发现会覆盖 vendor 源码、依赖目录与被忽略的构建产物目录之外所有文件名不区分大小写匹配 README 的文件,包括今后新增的顶层目录。发布到文档站的配对使用 `pairedPages()`,由根 locale 投影 `.zh.md`,由 `/en/` 投影 `.md`;仅创建对侧文件并不会发布它。\n- **配对记录是元数据,而不是 Cordis Loader 配置。** Cordis 配置发现会接受实际的 `.cordis.yml` 和 `.cordis.yaml` 文件,同时排除 `*.i18n.yaml`,即使文档名中包含 `cordis` 也不例外。这样既能继续校验可执行的 Loader 配置项,又不会把翻译 hash 当作配置来解析。\n- **翻译是 agent 的工作,由人评审。** 常规改动采用由[轻量翻译决策](2026-08-08-lightweight-routine-documentation-translation.zh.md)确立的直接单遍路径。[扩展翻译 skill(技能)](../../../skills/dsh-translate-docs/SKILL.md)保留委派翻译和其他较重机制,供用户显式调用;两条路径均以文档契约为真源。\n\n## 验证\n\n验证约定分别覆盖每个边界。`verify-translation-pairing` 固定配对完整性、hash、语言切换行和结构;[`project-doc-site.spec.ts`](../../../../scripts/project-doc-site.spec.ts) 固定已发布配对按 locale 选择对应源文件;[`cordis-config-files.spec.ts`](../../../../scripts/cordis-config-files.spec.ts) 固定 Loader YAML 的发现以及翻译记录的排除;[翻译提示词可运行快照](../../../../scripts/translation-prompt.snapshot.ts)则固定渲染后的系统消息、五对经评审的示例、源请求和所消费的响应。这些检查共同使配对漂移、发布漂移、配置误分类和模型可见提示词漂移都可在评审中看见。\n\n## 曾考虑的替代方案\n\n- **英文为正典源、指纹放在译文内**:`.zh.md` 文件携带一条 HTML 注释记录英文源的 blob hash,翻译只沿 EN → ZH 单向流动。否决:团队需要中文先行的撰写方式(先写、先审中文 Agent Note,再译英文),两种语言同权,而单向正典模型无法表达这一点。覆盖**两侧**的伴随记录取代了文件内的单向指纹;blob hash 的机制本身保持不变。\n- **语言目录(`docs/en/` + `docs/zh/`,Kubernetes/ECharts 模式)**:否决。本仓库没有将 locale 映射到路由的文档站框架;如果移动所有英文文件,所有既有交叉引用都要随之修改;且 `verify-md-links`/`verify-doc-refs` 将需要路径映射逻辑,而非原样工作。\n- **独立翻译仓库(PingCAP `docs`/`docs-cn` 模式)**:否决。适合有独立发布节奏的文档产品,对 monorepo 自身的文档而言过重;还会把译文置于本仓库门禁触及不到的地方。\n- **中英混排单文件(一个文件、两种语言)**:否决。每个 diff 都翻倍,破坏一段一行约定的 diff 易读性,且局部不一致不可见。\n- **Commit hash 式记录(MDN `l10n.sourceCommit` 模式)**:否决,改用 blob hash。同一个 PR 内的改动还没有 commit hash,MDN 模式无法表达「与本 PR 引入的状态一致」,且校验它需要 git 历史而非文件内容。\n- **比较配对两侧的 git 时间戳(无记录)**:否决。纯格式化的改动会误报,一次无关改动之后提交的对侧文件会漏报;只有内容同一性这个信号才与门禁的承诺名实相符。\n\n## 业界先例\n\n带语言后缀的配对兄弟文件是中国大厂的主流约定(ant-design 的 `index.zh-CN.md`/`index.en-US.md`;arco-design 的 `README.zh-CN.md` 加顶部切换行;Apache ShardingSphere 的 387 对 `.cn.md`/`.en.md`),但这些仓库都没有在 CI 中**强制**配对或一致性检查;约定纯靠评审维系。一致性自动化存在于中国以外:MDN 的 `l10n.sourceCommit` front-matter 指纹、Vue 的 Ryu-Cho action(监视上游 commit,为陈旧译文自动开 issue/PR)、Kubernetes 的本地化漂移脚本、微软 Azure co-op-translator(CI 中由源 hash 驱动的 LLM 重译)。本设计将两者结合:中文生态的文件布局,加上 hash 配对门禁,再加一个由 agent 运行的工作流替代 bot 服务。\n\n## 后果\n\n- 修改已配对文档的任一侧,同一个 PR 就有义务更新对侧并重新记录配对。门禁将 doc-sync 规则双语化,不变式由 CI(而非评审者的记忆)承载。\n- 每个配对给目录树多添一个文件。记录由机器写入(`--write`),代价是目录噪音而非维护负担;换来的是「谁在何时确认过这对文档一致」可以从 yaml 的 git blame 直接回答。\n- 两侧说法冲突时,没有机械规则裁决谁赢,由 PR 评审裁决。这是同权的代价,且是有意接受的:另一个选项(正典语言)会禁止中文先行撰写。\n- 生成的英文文档仍由源码派生,并由各自的生成器实施新鲜度门禁。有经评审中文对侧的生成页面遵循三文件配对工作流,但有一项结构例外:生成的英文源文件不含语言切换行,因为添加该行会使生成器新鲜度检查失败;中文对侧仍链接回英文源。没有经评审对侧的生成页面保留为显式排除项,并在网站上投影英文。\n- 只含排除项的 manifest(元数据清单)通过同一路径,要求当前及今后纳入范围的每篇文档都必须配对。不存在显式要求、分界或类别条目可以落在发现范围之外,却看似已经强制执行。\n- 记录的 hash 兼作更新工具:[gen-translation-brief](2026-07-26-briefed-minimal-translation-updates.zh.md) 会从中还原任一侧上次确认的文本并组装最小更新简报,因此这套机制从不强迫整篇重译。\n"
},
{
"role": "user",
diff --git a/scripts/translation-brief.spec.ts b/scripts/translation-brief.spec.ts
index 0e0bc8b24c..85a49a0179 100644
--- a/scripts/translation-brief.spec.ts
+++ b/scripts/translation-brief.spec.ts
@@ -215,6 +215,8 @@ describe('brief rendering', () => {
expect(brief).toContain('## First-occurrence notes')
expect(brief).toContain('agent(智能体)')
expect(brief).toContain('首次出现 annotations attach to the document-wide first occurrence only')
+ expect(brief).toContain('targets in the active bilingual corpus use `.zh.md` for Chinese')
+ expect(brief).toContain('a missing in-scope counterpart is an error')
expect(brief).toContain('verify-translation-pairing --write docs/foo.md')
})
@@ -266,6 +268,8 @@ describe('brief rendering', () => {
scope: { kind: 'units', bundles: [bundle], firstOccurrenceNotes: [] },
})
expect(brief).toContain('exactly what the new Chinese states')
+ expect(brief).toContain('targets in the active bilingual corpus use `.md` for English')
+ expect(brief).toContain('targets outside the corpus keep the authored path')
expect(brief).toContain('verify-translation-pairing --write docs/foo.md')
})
diff --git a/scripts/translation-brief.ts b/scripts/translation-brief.ts
index 9380974fa0..1c76bccc31 100644
--- a/scripts/translation-brief.ts
+++ b/scripts/translation-brief.ts
@@ -386,7 +386,7 @@ const ZH_TARGET_DIGEST = [
'- Nothing added, nothing dropped: the Chinese must state exactly what the new English states.',
'- Write natural institutional technical Chinese, not word-by-word gloss; terse stays terse.',
'- Code fences byte-identical to the English side, comments included; inline code spans verbatim.',
- '- Relative links keep the `.md` target; only the switcher line links `.zh.md`.',
+ '- Repository-relative document links keep the same semantic target and exact query/fragment; targets in the active bilingual corpus use `.zh.md` for Chinese, a missing in-scope counterpart is an error, and targets outside the corpus keep the authored path. The switcher remains the cross-locale exception.',
'- Structure mirrors the counterpart: heading depths and order, list kinds and item counts, table rows and columns.',
'- 首次出现 annotations attach to the document-wide first occurrence only; later occurrences use the bare form, and an empty 首次出现 cell means never gloss.',
'- Typography: one half-width space between Chinese and Latin or digits; full-width punctuation in Chinese prose; 顿号 for enumerations; second person is 你.',
@@ -398,7 +398,7 @@ const EN_TARGET_DIGEST = [
'- Nothing added, nothing dropped: the English must state exactly what the new Chinese states.',
'- Write concise professional developer prose, not word-by-word gloss; terse stays terse.',
'- Code fences byte-identical to the Chinese side, comments included; inline code spans verbatim.',
- '- Relative links keep the `.md` target; only the switcher line links `.zh.md`.',
+ '- Repository-relative document links keep the same semantic target and exact query/fragment; targets in the active bilingual corpus use `.md` for English, a missing in-scope counterpart is an error, and targets outside the corpus keep the authored path. The switcher remains the cross-locale exception.',
'- Structure mirrors the counterpart: heading depths and order, list kinds and item counts, table rows and columns.',
'- One physical line per paragraph; exactly one trailing newline.',
]
diff --git a/scripts/translation-links.spec.ts b/scripts/translation-links.spec.ts
new file mode 100644
index 0000000000..35efde120e
--- /dev/null
+++ b/scripts/translation-links.spec.ts
@@ -0,0 +1,253 @@
+/** Regression coverage for locale-aware bilingual Markdown links. */
+
+import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+import {
+ normalizeTranslationMarkdownLinks,
+ rewriteTranslationLinkLocales,
+ translationLinkLocaleViolations,
+ type TranslationLinkContext,
+} from './translation-links.ts'
+import { removeFixtureSafely } from './test-fixture-cleanup.ts'
+
+const roots: string[] = []
+
+afterEach(() => {
+ for (const root of roots.splice(0)) removeFixtureSafely(root)
+})
+
+function fixture(): string {
+ const root = mkdtempSync(join(tmpdir(), 'dsh-translation-links-'))
+ roots.push(root)
+ mkdirSync(join(root, 'docs/section'), { recursive: true })
+ mkdirSync(join(root, 'packages'), { recursive: true })
+ writeFileSync(join(root, 'docs/guide.md'), '# Guide\n')
+ writeFileSync(join(root, 'docs/guide.zh.md'), '# 指南\n')
+ writeFileSync(join(root, 'docs/reference.md'), '# Overview\n')
+ writeFileSync(join(root, 'docs/reference.zh.md'), '# 概览\n')
+ writeFileSync(join(root, 'docs/unpaired.md'), '# Only\n')
+ writeFileSync(join(root, 'docs/section/index.md'), '# Section\n')
+ writeFileSync(join(root, 'docs/section/index.zh.md'), '# 章节\n')
+ writeFileSync(join(root, 'packages/outside.md'), '# Outside\n')
+ writeFileSync(join(root, 'packages/outside.zh.md'), '# 范围外\n')
+ return root
+}
+
+function linkContext(
+ root: string,
+ sourcePath: string,
+ repositoryFileExists?: (repoPath: string) => boolean,
+): TranslationLinkContext {
+ return {
+ repoRoot: root,
+ sourcePath,
+ isTranslationPairSource: path => path.startsWith('docs/'),
+ ...(repositoryFileExists === undefined ? {} : { repositoryFileExists }),
+ }
+}
+
+function expectUnchangedLinkInput(root: string, input: string): void {
+ const context = linkContext(root, 'docs/guide.md')
+ expect(translationLinkLocaleViolations(input, context)).toEqual([])
+ expect(rewriteTranslationLinkLocales(input, context)).toEqual({ content: input, rewritten: 0 })
+ expect(normalizeTranslationMarkdownLinks(input, context)).toBe(input)
+}
+
+describe('translation link locale validation', () => {
+ it('rejects a Chinese link to the English sibling with an exact diagnostic', () => {
+ const root = fixture()
+ expect(translationLinkLocaleViolations(
+ '# 指南\n\n正文。\n\n[概览](reference.md?view=full#overview)\n',
+ linkContext(root, 'docs/guide.zh.md'),
+ )).toEqual([{
+ sourcePath: 'docs/guide.zh.md',
+ line: 5,
+ url: 'reference.md?view=full#overview',
+ expectedUrl: 'reference.zh.md?view=full#overview',
+ }])
+ })
+
+ it('rewrites an encoded exact filename without changing its query or fragment suffix', () => {
+ const root = fixture()
+ const input = '[概览](reference%2Emd?view=full&mode=all#overview)\n'
+ expect(translationLinkLocaleViolations(
+ input,
+ linkContext(root, 'docs/guide.zh.md'),
+ )[0]).toMatchObject({
+ url: 'reference%2Emd?view=full&mode=all#overview',
+ expectedUrl: 'reference.zh.md?view=full&mode=all#overview',
+ })
+ expect(rewriteTranslationLinkLocales(input, linkContext(root, 'docs/guide.zh.md'))).toEqual({
+ content: '[概览](reference.zh.md?view=full&mode=all#overview)\n',
+ rewritten: 1,
+ })
+ })
+
+ it('encodes each exact path segment with only RFC 3986 unreserved characters', () => {
+ const root = fixture()
+ const input = '[保留](a%29%23%3Fb%2Emd?view=full#section)\n'
+ const repositoryFiles = new Set(['docs/a)#?b.md', 'docs/a)#?b.zh.md'])
+ expect(rewriteTranslationLinkLocales(
+ input,
+ linkContext(root, 'docs/guide.zh.md', path => repositoryFiles.has(path)),
+ )).toEqual({
+ content: '[保留](a%29%23%3Fb.zh.md?view=full#section)\n',
+ rewritten: 1,
+ })
+ })
+
+ it('accepts the target-locale sibling and an out-of-scope target with its own sibling', () => {
+ const root = fixture()
+ expect(translationLinkLocaleViolations(
+ '[paired](reference.zh.md) [outside](../packages/outside.md)\n',
+ linkContext(root, 'docs/guide.zh.md'),
+ )).toEqual([])
+ })
+
+ it('does not fall back when an active target is missing its locale sibling', () => {
+ const root = fixture()
+ expect(translationLinkLocaleViolations(
+ '[missing](unpaired.md)\n',
+ linkContext(root, 'docs/guide.zh.md'),
+ )[0]).toMatchObject({ expectedUrl: 'unpaired.zh.md' })
+ })
+
+ it('requires English sources to use the English sibling', () => {
+ const root = fixture()
+ expect(translationLinkLocaleViolations(
+ '[Reference](reference.zh.md)\n',
+ linkContext(root, 'docs/guide.md'),
+ )[0]).toMatchObject({
+ url: 'reference.zh.md',
+ expectedUrl: 'reference.md',
+ })
+ })
+
+ it('does not infer an index page from a directory target', () => {
+ const root = fixture()
+ const input = '[Section](section/)\n'
+ expect(translationLinkLocaleViolations(input, linkContext(root, 'docs/guide.zh.md'))).toEqual([])
+ expect(rewriteTranslationLinkLocales(input, linkContext(root, 'docs/guide.zh.md')))
+ .toEqual({ content: input, rewritten: 0 })
+ })
+
+ it('exempts the language switcher target explicitly', () => {
+ const root = fixture()
+ expect(translationLinkLocaleViolations(
+ '# 指南\n\n[English](guide.md) | 中文\n',
+ linkContext(root, 'docs/guide.zh.md'),
+ ['guide.md'],
+ )).toEqual([])
+ })
+
+ it('does not exempt an ordinary body link to the counterpart', () => {
+ const root = fixture()
+ const markdown = '# 指南\n\n[English](guide.md) | 中文\n\n[正文](guide.md)\n'
+ expect(translationLinkLocaleViolations(
+ markdown,
+ linkContext(root, 'docs/guide.zh.md'),
+ ['guide.md'],
+ )).toEqual([{
+ sourcePath: 'docs/guide.zh.md',
+ line: 5,
+ url: 'guide.md',
+ expectedUrl: 'guide.zh.md',
+ }])
+ expect(rewriteTranslationLinkLocales(
+ markdown,
+ linkContext(root, 'docs/guide.zh.md'),
+ ['guide.md'],
+ ).content).toBe('# 指南\n\n[English](guide.md) | 中文\n\n[正文](guide.zh.md)\n')
+ })
+
+ it('uses the selected content plane for target existence without deriving scope from siblings', () => {
+ const root = fixture()
+ const staged = new Set(['docs/reference.md', 'docs/reference.zh.md'])
+ expect(translationLinkLocaleViolations(
+ '[概览](reference.md)\n',
+ linkContext(root, 'docs/guide.zh.md', path => staged.has(path)),
+ )).toHaveLength(1)
+ staged.delete('docs/reference.zh.md')
+ expect(translationLinkLocaleViolations(
+ '[概览](reference.md)\n',
+ linkContext(root, 'docs/guide.zh.md', path => staged.has(path)),
+ )).toHaveLength(1)
+ staged.delete('docs/reference.md')
+ expect(translationLinkLocaleViolations(
+ '[概览](reference.md)\n',
+ linkContext(root, 'docs/guide.zh.md', path => staged.has(path)),
+ )).toEqual([])
+ })
+})
+
+describe('translation link rewriting and normalization', () => {
+ it('rewrites only the destination while preserving the suffix and title', () => {
+ const root = fixture()
+ const input = '[概览](reference.md?view=full&mode=all#overview "reference.md title")\n'
+ expect(rewriteTranslationLinkLocales(
+ input,
+ linkContext(root, 'docs/guide.zh.md'),
+ )).toEqual({
+ content: '[概览](reference.zh.md?view=full&mode=all#overview "reference.md title")\n',
+ rewritten: 1,
+ })
+ })
+
+ it('rewrites link definitions without changing their labels', () => {
+ const root = fixture()
+ expect(rewriteTranslationLinkLocales(
+ '[概览][ref]\n\n[ref]: "title"\n',
+ linkContext(root, 'docs/guide.zh.md'),
+ ).content).toBe('[概览][ref]\n\n[ref]: "title"\n')
+ })
+
+ it('uses only the first duplicate reference definition', () => {
+ const root = fixture()
+ expect(translationLinkLocaleViolations(
+ '[概览][ref]\n\n[ref]: reference.zh.md\n[ref]: reference.md\n',
+ linkContext(root, 'docs/guide.zh.md'),
+ )).toEqual([])
+ })
+
+ it('does not treat an image-only definition as a document link', () => {
+ const root = fixture()
+ const input = '![preview][asset]\n\n[asset]: reference.zh.md#overview\n'
+ expectUnchangedLinkInput(root, input)
+ })
+
+ it.each([
+ '\n',
+ 'https://example.com/reference.md\n',
+ ])('leaves GFM autolink source unchanged: %s', (input) => {
+ const root = fixture()
+ expectUnchangedLinkInput(root, input)
+ })
+
+ it('normalizes only paired locale paths and retains other bytes', () => {
+ const root = fixture()
+ const english = '[Reference](reference.md#overview) [Outside](../packages/outside.md)\n'
+ const chinese = '[Reference](reference.zh.md#overview) [Outside](../packages/outside.md)\n'
+ expect(normalizeTranslationMarkdownLinks(
+ english,
+ linkContext(root, 'docs/guide.md'),
+ )).toBe(normalizeTranslationMarkdownLinks(
+ chinese,
+ linkContext(root, 'docs/guide.zh.md'),
+ ))
+ })
+
+ it('retains authored query bytes during normalization', () => {
+ const root = fixture()
+ const escaped = '[Reference](reference.md?x=1&y=2#overview)\n'
+ const literal = '[Reference](reference.zh.md?x=1&y=2#overview)\n'
+ expect(normalizeTranslationMarkdownLinks(
+ escaped,
+ linkContext(root, 'docs/guide.md'),
+ )).not.toBe(normalizeTranslationMarkdownLinks(
+ literal,
+ linkContext(root, 'docs/guide.zh.md'),
+ ))
+ })
+})
diff --git a/scripts/translation-links.ts b/scripts/translation-links.ts
new file mode 100644
index 0000000000..af7c991fd4
--- /dev/null
+++ b/scripts/translation-links.ts
@@ -0,0 +1,328 @@
+/** Locale-aware resolution and byte-preserving rewrites for bilingual Markdown links. */
+
+import { existsSync, statSync } from 'node:fs'
+import { posix, resolve } from 'node:path'
+import type { Nodes } from 'mdast'
+import {
+ isExternalOrAbsoluteMarkdownUrl,
+ markdownDestination,
+ parseMarkdown,
+ splitMarkdownUrlTarget,
+ visitMarkdown,
+ type MarkdownDestination,
+} from './markdown.ts'
+
+/** Repository and source document used to resolve one relative link. */
+export interface TranslationLinkContext {
+ /** Absolute repository root. */
+ repoRoot: string
+ /** Repository-relative Markdown source path. */
+ sourcePath: string
+ /** Whether an English Markdown path belongs to the active bilingual corpus. */
+ isTranslationPairSource: (sourcePath: string) => boolean
+ /** Selected content plane; defaults to regular files in the working tree. */
+ repositoryFileExists?: (repoPath: string) => boolean
+}
+
+/** One relative document link whose target uses the wrong locale sibling. */
+export interface TranslationLinkLocaleViolation {
+ sourcePath: string
+ line: number
+ url: string
+ expectedUrl: string
+}
+
+/** Result of rewriting wrong-locale relative document links. */
+export interface TranslationLinkRewriteResult {
+ content: string
+ rewritten: number
+}
+
+interface TranslationPairTarget {
+ source: string
+ zh: string
+}
+
+interface ResolvedTranslationLink {
+ pair: TranslationPairTarget
+ targetPath: string
+ suffix: string
+ expectedPath: string
+ expectedUrl: string
+ locale: 'en' | 'zh'
+}
+
+interface Replacement {
+ start: number
+ end: number
+ value: string
+}
+
+type LinkNode = Extract
+
+/** Offset of the one top-level switcher link immediately following the H1. */
+export function languageSwitcherLinkOffset(
+ tree: Nodes,
+ markdown: string,
+ acceptedTargets: string | readonly string[],
+): number | undefined {
+ if (tree.type !== 'root') return undefined
+ const accepted = new Set(typeof acceptedTargets === 'string' ? [acceptedTargets] : acceptedTargets)
+ const headingIndex = tree.children.findIndex(node => node.type === 'heading' && node.depth === 1)
+ if (headingIndex < 0) return undefined
+ for (const node of tree.children.slice(headingIndex + 1)) {
+ if (node.type === 'heading') return undefined
+ if (node.type !== 'paragraph' || node.position === undefined) continue
+ const start = node.position.start.offset
+ const end = node.position.end.offset
+ if (start === undefined || end === undefined) continue
+ const authored = markdown.slice(start, end)
+ if (!/^(?:English \| \[中文\]\([^\n]+\)|\[English\]\([^\n]+\) \| 中文)$/.test(authored)) continue
+ const links = node.children.filter((child): child is Extract => child.type === 'link')
+ if (links.length === 1 && accepted.has(links[0]?.url ?? '')) {
+ return links[0]?.position?.start.offset
+ }
+ }
+ return undefined
+}
+
+/** Whether the tree carries its canonical top-level language switcher. */
+export function hasLanguageSwitcher(
+ tree: Nodes,
+ markdown: string,
+ acceptedTargets: string | readonly string[],
+): boolean {
+ return languageSwitcherLinkOffset(tree, markdown, acceptedTargets) !== undefined
+}
+
+function decodePath(path: string): string {
+ try {
+ return decodeURIComponent(path)
+ } catch {
+ return path
+ }
+}
+
+function worktreeFileExists(repoRoot: string, repoPath: string): boolean {
+ try {
+ const path = resolve(repoRoot, repoPath)
+ return existsSync(path) && statSync(path).isFile()
+ } catch {
+ return false
+ }
+}
+
+function repositoryFileExists(context: TranslationLinkContext, repoPath: string): boolean {
+ return context.repositoryFileExists?.(repoPath) ?? worktreeFileExists(context.repoRoot, repoPath)
+}
+
+function repositoryRelativePath(path: string): string | undefined {
+ const normalized = posix.normalize(path)
+ if (normalized === '' || normalized === '.' || normalized === '..' || normalized.startsWith('../') || posix.isAbsolute(normalized)) {
+ return undefined
+ }
+ return normalized
+}
+
+function resolveRepositoryTarget(
+ rawPath: string,
+ context: TranslationLinkContext,
+): string | undefined {
+ const decoded = decodePath(rawPath)
+ const exact = repositoryRelativePath(posix.join(posix.dirname(context.sourcePath), decoded))
+ if (exact === undefined) return undefined
+ return repositoryFileExists(context, exact) ? exact : undefined
+}
+
+function translationPairTarget(targetPath: string, context: TranslationLinkContext): TranslationPairTarget | undefined {
+ const source = targetPath.endsWith('.zh.md')
+ ? targetPath.replace(/\.zh\.md$/, '.md')
+ : targetPath.endsWith('.md') ? targetPath : undefined
+ if (source === undefined || !context.isTranslationPairSource(source)) return undefined
+ const zh = source.replace(/\.md$/, '.zh.md')
+ return { source, zh }
+}
+
+function encodePathSegment(segment: string): string {
+ return encodeURIComponent(segment).replace(/[!'()*]/g, character => (
+ `%${character.charCodeAt(0).toString(16).toUpperCase()}`
+ ))
+}
+
+function relativeExpectedPath(
+ context: TranslationLinkContext,
+ expectedPath: string,
+ rawPath: string,
+): string {
+ const relative = posix.relative(posix.dirname(context.sourcePath), expectedPath)
+ const encoded = relative.split('/').map(encodePathSegment).join('/')
+ return rawPath.startsWith('./') && !encoded.startsWith('.') ? `./${encoded}` : encoded
+}
+
+function expectedLocalePath(
+ rawPath: string,
+ locale: 'en' | 'zh',
+ context: TranslationLinkContext,
+ expectedPath: string,
+): string {
+ if (locale === 'zh' && rawPath.endsWith('.md') && !rawPath.endsWith('.zh.md')) {
+ return rawPath.replace(/\.md$/, '.zh.md')
+ }
+ if (locale === 'en' && rawPath.endsWith('.zh.md')) return rawPath.replace(/\.zh\.md$/, '.md')
+ return relativeExpectedPath(context, expectedPath, rawPath)
+}
+
+function resolveTranslationLink(
+ url: string,
+ context: TranslationLinkContext,
+ authoredUrl: string,
+): ResolvedTranslationLink | undefined {
+ if (isExternalOrAbsoluteMarkdownUrl(url)) return undefined
+ const { path } = splitMarkdownUrlTarget(url)
+ const authored = splitMarkdownUrlTarget(authoredUrl)
+ if (path === '') return undefined
+ const targetPath = resolveRepositoryTarget(path, context)
+ if (targetPath === undefined) return undefined
+ const pair = translationPairTarget(targetPath, context)
+ if (pair === undefined) return undefined
+ const locale = context.sourcePath.endsWith('.zh.md') ? 'zh' : 'en'
+ const expectedPath = locale === 'zh' ? pair.zh : pair.source
+ return {
+ pair,
+ targetPath,
+ suffix: authored.suffix,
+ expectedPath,
+ expectedUrl: `${expectedLocalePath(authored.path, locale, context, expectedPath)}${authored.suffix}`,
+ locale,
+ }
+}
+
+function hasExpectedLocale(resolved: ResolvedTranslationLink): boolean {
+ return resolved.targetPath === resolved.expectedPath
+}
+
+function replacementFor(destination: MarkdownDestination, value: string): Replacement {
+ return { start: destination.start, end: destination.end, value }
+}
+
+function authoredExternalTarget(markdown: string, node: LinkNode): string {
+ const start = node.position?.start.offset
+ const end = node.position?.end.offset
+ if (start === undefined || end === undefined) {
+ throw new Error(`translation-links: external link ${JSON.stringify(node.url)} has no source offsets`)
+ }
+ const raw = markdown.slice(start, end)
+ if (node.type === 'definition' || raw.startsWith('[')) return markdownDestination(markdown, node).url
+ if (raw.startsWith('<') && raw.endsWith('>')) return raw.slice(1, -1)
+ return raw
+}
+
+function applyReplacements(markdown: string, replacements: Replacement[]): string {
+ let output = markdown
+ for (const replacement of replacements.sort((left, right) => right.start - left.start)) {
+ output = output.slice(0, replacement.start) + replacement.value + output.slice(replacement.end)
+ }
+ return output
+}
+
+function visitDocumentLinkNodes(
+ markdown: string,
+ skipTargets: readonly string[],
+ visitor: (node: LinkNode) => void,
+): void {
+ const tree = parseMarkdown(markdown)
+ const switcherOffset = languageSwitcherLinkOffset(tree, markdown, skipTargets)
+ const referencedIdentifiers = new Set()
+ const visitedDefinitions = new Set()
+ visitMarkdown(tree, (node) => {
+ if (node.type === 'linkReference') referencedIdentifiers.add(node.identifier)
+ })
+ visitMarkdown(tree, (node) => {
+ if (node.type === 'link' && node.position?.start.offset === switcherOffset) return
+ if (node.type === 'link') {
+ visitor(node)
+ } else if (node.type === 'definition'
+ && referencedIdentifiers.has(node.identifier)
+ && !visitedDefinitions.has(node.identifier)) {
+ visitedDefinitions.add(node.identifier)
+ visitor(node)
+ }
+ })
+}
+
+function visitResolvedDocumentLinks(
+ markdown: string,
+ context: TranslationLinkContext,
+ skipTargets: readonly string[],
+ visitor: (node: LinkNode, destination: MarkdownDestination, resolved: ResolvedTranslationLink) => void,
+): void {
+ visitDocumentLinkNodes(markdown, skipTargets, (node) => {
+ if (isExternalOrAbsoluteMarkdownUrl(node.url)) return
+ const destination = markdownDestination(markdown, node)
+ const resolved = resolveTranslationLink(node.url, context, destination.url)
+ if (resolved !== undefined) visitor(node, destination, resolved)
+ })
+}
+
+/** Return one violation per wrong-locale link or link definition. */
+export function translationLinkLocaleViolations(
+ markdown: string,
+ context: TranslationLinkContext,
+ skipTargets: readonly string[] = [],
+): TranslationLinkLocaleViolation[] {
+ const violations: TranslationLinkLocaleViolation[] = []
+ visitResolvedDocumentLinks(markdown, context, skipTargets, (node, destination, resolved) => {
+ if (hasExpectedLocale(resolved)) return
+ violations.push({
+ sourcePath: context.sourcePath,
+ line: node.position?.start.line ?? 0,
+ url: destination.url,
+ expectedUrl: resolved.expectedUrl,
+ })
+ })
+ return violations
+}
+
+/** Rewrite wrong-locale document links without reserializing surrounding Markdown. */
+export function rewriteTranslationLinkLocales(
+ markdown: string,
+ context: TranslationLinkContext,
+ skipTargets: readonly string[] = [],
+): TranslationLinkRewriteResult {
+ const replacements: Replacement[] = []
+ visitResolvedDocumentLinks(markdown, context, skipTargets, (_node, destination, resolved) => {
+ if (hasExpectedLocale(resolved)) return
+ replacements.push(replacementFor(destination, resolved.expectedUrl))
+ })
+ return { content: applyReplacements(markdown, replacements), rewritten: replacements.length }
+}
+
+/** Normalize only paired-document locale paths while retaining every other byte and URL suffix. */
+export function normalizeTranslationMarkdownLinks(
+ markdown: string,
+ context: TranslationLinkContext,
+ skipTargets: readonly string[] = [],
+): string {
+ const replacements: Replacement[] = []
+ visitResolvedDocumentLinks(markdown, context, skipTargets, (_node, destination, resolved) => {
+ replacements.push(replacementFor(
+ destination,
+ `dsh-translation-target:${resolved.pair.source}${resolved.suffix}`,
+ ))
+ })
+ return applyReplacements(markdown, replacements)
+}
+
+/** Semantic target of one authored inline link or referenced definition. */
+export function semanticTranslationLinkNodeTarget(
+ node: LinkNode,
+ markdown: string,
+ context: TranslationLinkContext,
+): string {
+ if (isExternalOrAbsoluteMarkdownUrl(node.url)) return authoredExternalTarget(markdown, node)
+ const destination = markdownDestination(markdown, node)
+ const resolved = resolveTranslationLink(node.url, context, destination.url)
+ return resolved === undefined
+ ? destination.url
+ : `dsh-translation-target:${resolved.pair.source}${resolved.suffix}`
+}
diff --git a/scripts/translation-pairing-git.ts b/scripts/translation-pairing-git.ts
index 5f1ddd4fc0..e004c935d7 100644
--- a/scripts/translation-pairing-git.ts
+++ b/scripts/translation-pairing-git.ts
@@ -48,6 +48,44 @@ export interface GitIndexBlob {
content: Buffer
}
+/** Every stage-zero path currently present in the Git index. */
+export function gitIndexPaths(root: string): Set {
+ const paths = new Set()
+ const entries = runGit(root, ['ls-files', '--stage', '-z'], 'listing Git index paths')
+ .toString('utf8')
+ .split('\0')
+ .filter(Boolean)
+ for (const entry of entries) {
+ const match = /^\d+ [0-9a-f]+ ([0-3])\t([\s\S]+)$/.exec(entry)
+ if (!match?.[1] || match[2] === undefined) throw new Error('git ls-files --stage returned a malformed entry')
+ if (match[1] === '0') paths.add(match[2])
+ }
+ return paths
+}
+
+/**
+ * Paths visible to a custom merge driver from the current index plus every
+ * merge head Git advertises through `GITHEAD_` environment entries.
+ *
+ * Git invokes custom drivers before it writes clean additions from the other
+ * heads into stage zero. The explicit post-conflict resolver has no GITHEAD
+ * entries and therefore uses the already-merged index alone.
+ */
+export function gitMergeInputPaths(root: string, environment: NodeJS.ProcessEnv = process.env): Set {
+ const paths = gitIndexPaths(root)
+ const heads = Object.keys(environment)
+ .flatMap(key => /^GITHEAD_([0-9a-f]{40})$/.exec(key)?.[1] ?? [])
+ .sort()
+ for (const head of heads) {
+ const files = runGit(root, ['ls-tree', '-r', '--name-only', '-z', head], `listing merge-head ${head} paths`)
+ .toString('utf8')
+ .split('\0')
+ .filter(Boolean)
+ for (const file of files) paths.add(file)
+ }
+ return paths
+}
+
/**
* Read one path from the Git index without consulting working-tree bytes.
*
diff --git a/scripts/translation-pairing-merge.spec.ts b/scripts/translation-pairing-merge.spec.ts
index 32924416a6..7293989c2e 100644
--- a/scripts/translation-pairing-merge.spec.ts
+++ b/scripts/translation-pairing-merge.spec.ts
@@ -6,6 +6,7 @@ import {
mkdtempSync,
mkdirSync,
readFileSync,
+ rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs'
@@ -15,8 +16,8 @@ import { fileURLToPath } from 'node:url'
import { afterEach, describe, expect, it } from 'vitest'
import { gitBlobHash, storeGitBlob } from './translation-pairing-git.ts'
import {
- mergeTranslationPairingRecords,
- resolveTranslationPairingConflicts,
+ mergeTranslationPairingRecords as mergeTranslationPairingRecordsWithScope,
+ resolveTranslationPairingConflicts as resolveTranslationPairingConflictsWithScope,
} from './translation-pairing-merge.ts'
import {
renderTranslationPairingRecord,
@@ -35,6 +36,27 @@ interface Fixture {
root: string
}
+function mergeTranslationPairingRecords(
+ root: string,
+ metaPath: string,
+ ancestorRecord: string,
+ currentRecord: string,
+ otherRecord: string,
+) {
+ return mergeTranslationPairingRecordsWithScope(
+ root,
+ metaPath,
+ ancestorRecord,
+ currentRecord,
+ otherRecord,
+ () => true,
+ )
+}
+
+function resolveTranslationPairingConflicts(root: string): string[] {
+ return resolveTranslationPairingConflictsWithScope(root, () => true)
+}
+
afterEach(() => {
for (const fixture of fixtures.splice(0)) removeFixtureSafely(fixture)
})
@@ -252,6 +274,19 @@ describe('translation pairing merge composition', { timeout: 15_000 }, () => {
)).toThrow('pairing record escapes the repository')
})
+ it('rejects a pairing record excluded from the active corpus', () => {
+ const fixture = createFixture(false)
+
+ expect(() => mergeTranslationPairingRecordsWithScope(
+ fixture.root,
+ 'docs/guide.i18n.yaml',
+ '',
+ '',
+ '',
+ () => false,
+ )).toThrow('docs/guide.i18n.yaml is excluded from the active bilingual documentation corpus')
+ })
+
it('merges the owner blobs named by three valid records', () => {
const fixture = createFixture(false)
git(fixture, ['config', 'merge.default', 'text'])
@@ -271,6 +306,48 @@ describe('translation pairing merge composition', { timeout: 15_000 }, () => {
expect(result.zhHash).toBe(gitBlobHash(Buffer.from(mergedZh)))
})
+ it('accepts locale-specific paths to the same paired document', () => {
+ const fixture = createFixture(false)
+ write(fixture.root, 'docs/reference.md', '# Overview\n')
+ write(fixture.root, 'docs/reference.zh.md', '# 概览\n')
+ git(fixture, ['add', 'docs/reference.md', 'docs/reference.zh.md'])
+ const source = baseSource.replace('Alpha base.', '[Reference](reference.md#overview)')
+ const zh = baseZh.replace('甲基础。', '[参考](reference.zh.md#overview)')
+ const ancestor = record(fixture.root, 'docs/guide.md', source, zh)
+ const current = record(fixture.root, 'docs/guide.md', source, zh)
+ const other = record(fixture.root, 'docs/guide.md', source, zh)
+ rmSync(join(fixture.root, 'docs/reference.md'))
+ rmSync(join(fixture.root, 'docs/reference.zh.md'))
+
+ expect(mergeTranslationPairingRecords(
+ fixture.root,
+ 'docs/guide.i18n.yaml',
+ ancestor,
+ current,
+ other,
+ ).zhContent.toString('utf8')).toBe(zh)
+ })
+
+ it('rejects a clean merge whose Chinese link uses the English sibling', () => {
+ const fixture = createFixture(false)
+ write(fixture.root, 'docs/reference.md', '# Overview\n')
+ write(fixture.root, 'docs/reference.zh.md', '# 概览\n')
+ git(fixture, ['add', 'docs/reference.md', 'docs/reference.zh.md'])
+ const source = baseSource.replace('Alpha base.', '[Reference](reference.md)')
+ const zh = baseZh.replace('甲基础。', '[参考](reference.md)')
+ const ancestor = record(fixture.root, 'docs/guide.md', source, zh)
+ const current = record(fixture.root, 'docs/guide.md', source, zh)
+ const other = record(fixture.root, 'docs/guide.md', source, zh)
+
+ expect(() => mergeTranslationPairingRecords(
+ fixture.root,
+ 'docs/guide.i18n.yaml',
+ ancestor,
+ current,
+ other,
+ )).toThrow('docs/guide.zh.md:5 clean merge uses "reference.md"; expected "reference.zh.md"')
+ })
+
it('merges a generated source without an English language switcher', () => {
const fixture = createFixture(false)
const ancestor = record(fixture.root, 'docs/module-graph.md', generatedBaseSource, generatedBaseZh)
@@ -414,6 +491,49 @@ describe('translation pairing merge composition', { timeout: 15_000 }, () => {
expectMergedPair(fixture)
})
+ it('sees a paired link target added by the other branch', () => {
+ const fixture = createFixture()
+ commitPair(fixture, baseSource, baseZh, 'base')
+ git(fixture, ['switch', '-c', 'current'])
+ commitPair(fixture, currentSource, currentZh, 'current guide')
+ git(fixture, ['switch', 'master'])
+ record(
+ fixture.root,
+ 'docs/guide.md',
+ baseSource.replace('Beta base.', '[Reference](reference.md#overview)'),
+ baseZh.replace('乙基础。', '[参考](reference.zh.md#overview)'),
+ )
+ record(
+ fixture.root,
+ 'docs/reference.md',
+ '# Reference\n\nEnglish | [中文](reference.zh.md)\n\nOverview.\n',
+ '# 参考\n\n[English](reference.md) | 中文\n\n概览。\n',
+ )
+ git(fixture, ['add', '.'])
+ git(fixture, ['commit', '-m', 'other guide and target'])
+ git(fixture, ['switch', 'current'])
+ installFixtureRuntime(fixture.root)
+ git(fixture, [
+ 'config',
+ 'merge.dsh-translation-pairing.driver',
+ 'scripts/merge-translation-pairing-driver.sh %O %A %B %P',
+ ])
+
+ const merge = spawnSync('git', ['-C', fixture.root, 'merge', '--no-edit', 'master'], {
+ encoding: 'utf8',
+ env: fixture.env,
+ })
+
+ expect(merge.status, merge.stderr).toBe(0)
+ expect(git(fixture, ['diff', '--name-only', '--diff-filter=U'])).toBe('')
+ expect(readFileSync(join(fixture.root, 'docs/guide.md'), 'utf8')).toContain(
+ '[Reference](reference.md#overview)',
+ )
+ expect(readFileSync(join(fixture.root, 'docs/guide.zh.md'), 'utf8')).toContain(
+ '[参考](reference.zh.md#overview)',
+ )
+ })
+
it('leaves an ordinary recoverable conflict when the configured runtime is unavailable', () => {
const fixture = createFixture()
const records = createDivergedPair(fixture)
diff --git a/scripts/translation-pairing-merge.ts b/scripts/translation-pairing-merge.ts
index 856c909091..7f0f25114b 100644
--- a/scripts/translation-pairing-merge.ts
+++ b/scripts/translation-pairing-merge.ts
@@ -7,6 +7,7 @@ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path'
import {
GIT_COMMAND_MAX_BUFFER,
gitBlobHash,
+ gitMergeInputPaths,
readGitIndexBlob,
runGit,
storeGitBlob,
@@ -14,12 +15,17 @@ import {
import {
isTranslationScopeFile,
languageSwitcherTargets,
- linksTo,
parseTranslationMarkdown,
+ parseTranslationPairingManifest,
requiresSourceLanguageSwitcher,
+ translationPairSourcePredicate,
translationStructureDiff,
translationStructureSignature,
} from './translation-pairing.ts'
+import {
+ hasLanguageSwitcher,
+ translationLinkLocaleViolations,
+} from './translation-links.ts'
import {
parseTranslationPairingRecord,
renderTranslationPairingRecord,
@@ -162,20 +168,62 @@ function loadRecordOwners(
}
}
-function assertMergedPairStructure(paths: TranslationPairPaths, source: Buffer, zh: Buffer): void {
- const sourceTree = parseTranslationMarkdown(source.toString('utf8'))
- const zhTree = parseTranslationMarkdown(zh.toString('utf8'))
+function assertMergedPairStructure(
+ root: string,
+ paths: TranslationPairPaths,
+ source: Buffer,
+ zh: Buffer,
+ isTranslationPairSource: (sourcePath: string) => boolean,
+): void {
+ const sourceText = source.toString('utf8')
+ const zhText = zh.toString('utf8')
+ const sourceTree = parseTranslationMarkdown(sourceText)
+ const zhTree = parseTranslationMarkdown(zhText)
+ const indexFiles = gitMergeInputPaths(root)
+ const repositoryFileExists = (path: string): boolean => indexFiles.has(path)
const sourceSwitcherTargets = languageSwitcherTargets(paths.source)
const zhSwitcherTargets = languageSwitcherTargets(paths.zh)
- if (requiresSourceLanguageSwitcher(paths.source) && !linksTo(sourceTree, zhSwitcherTargets)) {
+ if (requiresSourceLanguageSwitcher(paths.source)
+ && !hasLanguageSwitcher(sourceTree, sourceText, zhSwitcherTargets)) {
throw new Error(`${paths.source} clean merge lost its language-switcher link to ${basename(paths.zh)}`)
}
- if (!linksTo(zhTree, sourceSwitcherTargets)) {
+ if (!hasLanguageSwitcher(zhTree, zhText, sourceSwitcherTargets)) {
throw new Error(`${paths.zh} clean merge lost its language-switcher link to ${basename(paths.source)}`)
}
+ const localeViolations = [
+ ...translationLinkLocaleViolations(sourceText, {
+ repoRoot: root,
+ sourcePath: paths.source,
+ isTranslationPairSource,
+ repositoryFileExists,
+ }, zhSwitcherTargets),
+ ...translationLinkLocaleViolations(zhText, {
+ repoRoot: root,
+ sourcePath: paths.zh,
+ isTranslationPairSource,
+ repositoryFileExists,
+ }, sourceSwitcherTargets),
+ ]
+ if (localeViolations.length > 0) {
+ const violation = localeViolations[0]
+ if (violation === undefined) throw new Error('translation locale violation disappeared')
+ throw new Error(`${violation.sourcePath}:${violation.line} clean merge uses ${JSON.stringify(violation.url)}; expected ${JSON.stringify(violation.expectedUrl)}`)
+ }
const divergences = translationStructureDiff(
- translationStructureSignature(sourceTree, zhSwitcherTargets),
- translationStructureSignature(zhTree, sourceSwitcherTargets),
+ translationStructureSignature(sourceTree, zhSwitcherTargets, {
+ repoRoot: root,
+ sourcePath: paths.source,
+ isTranslationPairSource,
+ repositoryFileExists,
+ markdown: sourceText,
+ }),
+ translationStructureSignature(zhTree, sourceSwitcherTargets, {
+ repoRoot: root,
+ sourcePath: paths.zh,
+ isTranslationPairSource,
+ repositoryFileExists,
+ markdown: zhText,
+ }),
)
if (divergences.length > 0) {
throw new Error(`${paths.source} and ${paths.zh} clean merges diverge structurally: ${divergences.join('; ')}`)
@@ -212,19 +260,23 @@ export function mergeTranslationPairingRecords(
ancestorRecord: string,
currentRecord: string,
otherRecord: string,
+ isTranslationPairSource: (sourcePath: string) => boolean,
): TranslationPairingMergeResult {
const normalizedMeta = normalizeMetaPath(root, metaPath)
if (!isTranslationScopeFile(normalizedMeta)) {
throw new Error(`${normalizedMeta} is outside the active bilingual documentation corpus`)
}
const paths = translationPairPathsFromMeta(normalizedMeta)
+ if (!isTranslationPairSource(paths.source)) {
+ throw new Error(`${normalizedMeta} is excluded from the active bilingual documentation corpus`)
+ }
assertDefaultTextMerge(root, paths)
const ancestor = loadRecordOwners(root, 'ancestor', ancestorRecord, paths)
const current = loadRecordOwners(root, 'current', currentRecord, paths)
const other = loadRecordOwners(root, 'other', otherRecord, paths)
const sourceContent = mergeBlobTriplet(root, paths.source, ancestor.source, current.source, other.source)
const zhContent = mergeBlobTriplet(root, paths.zh, ancestor.zh, current.zh, other.zh)
- assertMergedPairStructure(paths, sourceContent, zhContent)
+ assertMergedPairStructure(root, paths, sourceContent, zhContent, isTranslationPairSource)
const sourceHash = storeGitBlob(root, sourceContent)
const zhHash = storeGitBlob(root, zhContent)
return {
@@ -236,6 +288,16 @@ export function mergeTranslationPairingRecords(
}
}
+/** Read the repository manifest and return its active bilingual-source predicate. */
+export function repositoryTranslationPairSource(root: string): (sourcePath: string) => boolean {
+ const path = 'scripts/translation-pairing.manifest.json'
+ const content = readGitIndexBlob(root, path)?.content ?? readFileSync(join(root, path))
+ const manifest = parseTranslationPairingManifest(
+ content.toString('utf8'),
+ )
+ return translationPairSourcePredicate(manifest)
+}
+
function unmergedSidecars(root: string): Map {
const output = runGit(root, ['ls-files', '--unmerged', '-z'], 'listing unresolved merge entries').toString('utf8')
const records = new Map()
@@ -289,7 +351,10 @@ function assertUneditedSidecar(
* @param root - Repository root with an in-progress merge-like operation.
* @returns Repository-relative sidecar paths resolved and staged.
*/
-export function resolveTranslationPairingConflicts(root: string): string[] {
+export function resolveTranslationPairingConflicts(
+ root: string,
+ isTranslationPairSource: (sourcePath: string) => boolean,
+): string[] {
const resolutions: { path: string; record: string }[] = []
const failures: { path: string; reason: string }[] = []
for (const [metaPath, stages] of [...unmergedSidecars(root)].sort(([left], [right]) => left.localeCompare(right))) {
@@ -307,6 +372,7 @@ export function resolveTranslationPairingConflicts(root: string): string[] {
ancestorRecord,
currentRecord,
otherRecord,
+ isTranslationPairSource,
)
const paths = translationPairPathsFromMeta(metaPath)
if (readGitIndexBlob(root, paths.source)?.objectId !== result.sourceHash) {
diff --git a/scripts/translation-pairing.spec.ts b/scripts/translation-pairing.spec.ts
index bfb44dc77b..7bc072a953 100644
--- a/scripts/translation-pairing.spec.ts
+++ b/scripts/translation-pairing.spec.ts
@@ -1,11 +1,16 @@
/** Regression tests for bilingual snapshots, corpus scope, and structure. */
import { execFileSync, spawnSync } from 'node:child_process'
-import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
-import { gitBlobHash, readGitIndexBlob, storeGitBlob } from './translation-pairing-git.ts'
+import {
+ gitBlobHash,
+ gitIndexPaths,
+ readGitIndexBlob,
+ storeGitBlob,
+} from './translation-pairing-git.ts'
import {
parseTranslationPairingRecord,
renderTranslationPairingRecord,
@@ -13,21 +18,44 @@ import {
} from './translation-pairing-record.ts'
import {
blobHash,
+ isTranslationPairingManifestExcluded,
isTranslationScopeFile,
languageSwitcherTargets,
- linksTo,
pairAnchorOfArgument,
parseTranslationMarkdown,
parseTranslationPairingCliArgs,
parseTranslationPairingManifest,
partitionGeneratedRegions,
requiresSourceLanguageSwitcher,
+ translationPairSourcePredicate,
translationStructureDiff,
translationStructureSignature,
} from './translation-pairing.ts'
+const fixturePairSource = (): boolean => true
+
function signature(markdown: string) {
- return translationStructureSignature(parseTranslationMarkdown(markdown), 'counterpart.zh.md')
+ return translationStructureSignature(
+ parseTranslationMarkdown(markdown),
+ 'counterpart.zh.md',
+ {
+ repoRoot: process.cwd(), sourcePath: 'counterpart.md',
+ isTranslationPairSource: fixturePairSource, markdown,
+ },
+ )
+}
+
+function fixtureSignature(
+ root: string,
+ sourcePath: string,
+ markdown: string,
+ switcherTarget: string,
+) {
+ return translationStructureSignature(
+ parseTranslationMarkdown(markdown),
+ switcherTarget,
+ { repoRoot: root, sourcePath, isTranslationPairSource: fixturePairSource, markdown },
+ )
}
function gitSupportsObjectFormat(format: 'sha256'): boolean {
@@ -106,6 +134,26 @@ describe('translation pairing snapshots', () => {
}
})
+ it('lists exact index files without treating a directory prefix as one entry', () => {
+ const root = mkdtempSync(join(tmpdir(), 'dsh-translation-pairing-index-'))
+ try {
+ execFileSync('git', ['init', '--quiet', root], {
+ env: { ...process.env, GIT_DEFAULT_HASH: 'sha1' },
+ })
+ mkdirSync(join(root, 'docs'), { recursive: true })
+ writeFileSync(join(root, 'docs/reference.md'), '# Reference\n')
+ writeFileSync(join(root, 'docs/reference.zh.md'), '# 参考\n')
+ execFileSync('git', ['-C', root, 'add', 'docs'])
+
+ expect(gitIndexPaths(root)).toEqual(new Set([
+ 'docs/reference.md',
+ 'docs/reference.zh.md',
+ ]))
+ } finally {
+ rmSync(root, { recursive: true, force: true })
+ }
+ })
+
it.skipIf(!supportsSha256ObjectFormat)('rejects an object format that pairing records cannot represent', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-translation-pairing-'))
try {
@@ -119,11 +167,16 @@ describe('translation pairing snapshots', () => {
describe('translation pairing manifest', () => {
it('accepts an exclusions-only manifest', () => {
- expect(parseTranslationPairingManifest(JSON.stringify({
+ const manifest = parseTranslationPairingManifest(JSON.stringify({
excluded: ['docs/generated/'],
- }))).toEqual({
+ }))
+ expect(manifest).toEqual({
excluded: ['docs/generated/'],
})
+ expect(isTranslationPairingManifestExcluded('docs/generated/page.md', manifest)).toBe(true)
+ expect(translationPairSourcePredicate(manifest)('docs/generated/page.md')).toBe(false)
+ expect(translationPairSourcePredicate(manifest)('docs/guide.md')).toBe(true)
+ expect(translationPairSourcePredicate(manifest)('packages/example/guide.md')).toBe(false)
})
it.each([
@@ -156,16 +209,44 @@ describe('translation pairing switchers', () => {
it('accepts only the canonical public URL for an absolute switcher', () => {
const targets = languageSwitcherTargets('python/sdk/README.zh.md')
- const canonical = parseTranslationMarkdown(
- '[中文](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk/README.zh.md)',
- )
- const wrongPath = parseTranslationMarkdown(
- '[中文](https://github.com/deepseek-ai/deepseek-harness/blob/master/other/README.zh.md)',
- )
+ const canonicalMarkdown = '# README\n\nEnglish | [中文](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk/README.zh.md)\n'
+ const canonical = parseTranslationMarkdown(canonicalMarkdown)
+ const wrongMarkdown = '# README\n\nEnglish | [中文](https://github.com/deepseek-ai/deepseek-harness/blob/master/other/README.zh.md)\n'
+ const wrongPath = parseTranslationMarkdown(wrongMarkdown)
- expect(linksTo(canonical, targets)).toBe(true)
- expect(translationStructureSignature(canonical, targets).links).toEqual([])
- expect(linksTo(wrongPath, targets)).toBe(false)
+ expect(translationStructureSignature(canonical, targets, {
+ repoRoot: process.cwd(),
+ sourcePath: 'python/sdk/README.md',
+ isTranslationPairSource: fixturePairSource,
+ markdown: canonicalMarkdown,
+ }).links).toEqual([])
+ expect(translationStructureSignature(wrongPath, targets, {
+ repoRoot: process.cwd(),
+ sourcePath: 'python/sdk/README.md',
+ isTranslationPairSource: fixturePairSource,
+ markdown: wrongMarkdown,
+ }).links).toEqual([
+ 'https://github.com/deepseek-ai/deepseek-harness/blob/master/other/README.zh.md',
+ ])
+ })
+
+ it('excludes only the header switcher from the structural links', () => {
+ const root = mkdtempSync(join(tmpdir(), 'dsh-translation-switcher-'))
+ try {
+ writeFileSync(join(root, 'guide.md'), '# Guide\n')
+ writeFileSync(join(root, 'guide.zh.md'), '# 指南\n')
+ const markdown = '# 指南\n\n[English](guide.md) | 中文\n\n[正文](guide.md)\n'
+ expect(translationStructureSignature(
+ parseTranslationMarkdown(markdown),
+ languageSwitcherTargets('guide.md'),
+ {
+ repoRoot: root, sourcePath: 'guide.zh.md',
+ isTranslationPairSource: fixturePairSource, markdown,
+ },
+ ).links).toEqual(['dsh-translation-target:guide.md'])
+ } finally {
+ rmSync(root, { recursive: true, force: true })
+ }
})
})
@@ -234,6 +315,81 @@ describe('translation scope discovery', () => {
})
describe('translation structural signature', () => {
+ it('retains external GFM autolinks without parsing inline-link syntax', () => {
+ const markdown = '\n'
+ expect(signature(markdown).links).toEqual(['https://example.com/reference.md'])
+ })
+
+ it('retains exact authored bytes for ordinary external link targets', () => {
+ const escaped = signature('[External](https://example.com/?x=1&y=2)\n')
+ const literal = signature('[External](https://example.com/?x=1&y=2)\n')
+ expect(escaped.links).toEqual(['https://example.com/?x=1&y=2'])
+ expect(translationStructureDiff(escaped, literal)).toEqual([
+ 'link target #1 diverges between the pair: "https://example.com/?x=1&y=2" vs "https://example.com/?x=1&y=2"',
+ ])
+ })
+
+ it('treats target-locale siblings as one semantic link target', () => {
+ const root = mkdtempSync(join(tmpdir(), 'dsh-translation-structure-'))
+ try {
+ writeFileSync(join(root, 'reference.md'), '# Reference\n')
+ writeFileSync(join(root, 'reference.zh.md'), '# 参考\n')
+ const sourceMarkdown = '[Reference](reference.md?view=full#section)\n'
+ const counterpartMarkdown = '[参考](reference.zh.md?view=full#section)\n'
+ const source = fixtureSignature(root, 'guide.md', sourceMarkdown, 'guide.zh.md')
+ const counterpart = fixtureSignature(root, 'guide.zh.md', counterpartMarkdown, 'guide.md')
+ expect(translationStructureDiff(source, counterpart)).toEqual([])
+ } finally {
+ rmSync(root, { recursive: true, force: true })
+ }
+ })
+
+ it('includes reference-style document links but excludes image-only definitions', () => {
+ const root = mkdtempSync(join(tmpdir(), 'dsh-translation-structure-'))
+ try {
+ writeFileSync(join(root, 'reference.md'), '# Reference\n')
+ writeFileSync(join(root, 'reference.zh.md'), '# 参考\n')
+ const markdown = [
+ '[Reference][doc]',
+ '',
+ '![Preview][asset]',
+ '',
+ '[doc]: reference.md',
+ '[asset]: reference.zh.md',
+ '',
+ ].join('\n')
+ expect(translationStructureSignature(
+ parseTranslationMarkdown(markdown),
+ 'guide.zh.md',
+ {
+ repoRoot: root, sourcePath: 'guide.md',
+ isTranslationPairSource: fixturePairSource, markdown,
+ },
+ ).links).toEqual(['dsh-translation-target:reference.md'])
+ } finally {
+ rmSync(root, { recursive: true, force: true })
+ }
+ })
+
+ it('compares the first duplicate reference definition that CommonMark resolves', () => {
+ const root = mkdtempSync(join(tmpdir(), 'dsh-translation-structure-'))
+ try {
+ for (const name of ['reference', 'different', 'other']) {
+ writeFileSync(join(root, `${name}.md`), `# ${name}\n`)
+ writeFileSync(join(root, `${name}.zh.md`), `# ${name} zh\n`)
+ }
+ const sourceMarkdown = '[Reference][ref]\n\n[ref]: reference.md\n[ref]: other.md\n'
+ const counterpartMarkdown = '[参考][ref]\n\n[ref]: different.zh.md\n[ref]: other.zh.md\n'
+ const source = fixtureSignature(root, 'guide.md', sourceMarkdown, 'guide.zh.md')
+ const counterpart = fixtureSignature(root, 'guide.zh.md', counterpartMarkdown, 'guide.md')
+ expect(translationStructureDiff(source, counterpart)).toEqual([
+ 'link target #1 diverges between the pair: "dsh-translation-target:reference.md" vs "dsh-translation-target:different.md"',
+ ])
+ } finally {
+ rmSync(root, { recursive: true, force: true })
+ }
+ })
+
it('accepts matching list kinds, starts, and item counts', () => {
const source = signature('3. One\n4. Two\n\n- A\n- B\n')
const counterpart = signature('3. 一\n4. 二\n\n- 甲\n- 乙\n')
diff --git a/scripts/translation-pairing.ts b/scripts/translation-pairing.ts
index 612baaf193..d4b96a1d4b 100644
--- a/scripts/translation-pairing.ts
+++ b/scripts/translation-pairing.ts
@@ -12,6 +12,11 @@ import { fromMarkdown } from 'mdast-util-from-markdown'
import { gfmFromMarkdown } from 'mdast-util-gfm'
import { gfm } from 'micromark-extension-gfm'
import type { Nodes } from 'mdast'
+import {
+ languageSwitcherLinkOffset,
+ semanticTranslationLinkNodeTarget,
+ type TranslationLinkContext,
+} from './translation-links.ts'
/** Complete opening marker line: `` (slug captured). */
const GENERATED_REGION_BEGIN_LINE = /^$/
@@ -215,6 +220,22 @@ export function parseTranslationPairingManifest(content: string): TranslationPai
return { excluded: excludedField(record) }
}
+/** Whether a manifest entry excludes one exact file or a directory subtree. */
+export function isTranslationPairingManifestExcluded(
+ file: string,
+ manifest: TranslationPairingManifest,
+): boolean {
+ return manifest.excluded.some(entry => (entry.endsWith('/') ? file.startsWith(entry) : file === entry))
+}
+
+/** Build the active bilingual-source predicate shared by every link consumer. */
+export function translationPairSourcePredicate(
+ manifest: TranslationPairingManifest,
+): (sourcePath: string) => boolean {
+ return sourcePath => isTranslationScopeFile(sourcePath)
+ && !isTranslationPairingManifestExcluded(sourcePath, manifest)
+}
+
/**
* Normalize one CLI pair argument to its English anchor path: any of the
* pair's three files (`foo.md`, `foo.zh.md`, `foo.i18n.yaml`) or the bare
@@ -311,18 +332,6 @@ export function languageSwitcherTargets(counterpart: string): string[] {
return [basename(counterpart), `${PUBLIC_REPOSITORY_BLOB_ROOT}${counterpart}`]
}
-/** Whether the tree contains a link to any accepted target. */
-export function linksTo(tree: Nodes, targets: string | readonly string[]): boolean {
- const accepted = new Set(typeof targets === 'string' ? [targets] : targets)
- let found = false
- const visit = (node: Nodes): void => {
- if (node.type === 'link' && accepted.has(node.url)) found = true
- if ('children' in node) for (const child of node.children) visit(child)
- }
- visit(tree)
- return found
-}
-
/** Generated English sources cannot carry a switcher without making their generator stale. */
export function requiresSourceLanguageSwitcher(source: string): boolean {
return ![
@@ -349,11 +358,21 @@ export function requiresSourceLanguageSwitcher(source: string): boolean {
export function translationStructureSignature(
tree: Nodes,
switcherTargets: string | readonly string[],
+ linkContext: TranslationLinkContext & { markdown: string },
): TranslationStructureSignature {
- const acceptedSwitchers = new Set(
- typeof switcherTargets === 'string' ? [switcherTargets] : switcherTargets,
- )
+ const switcherOffset = languageSwitcherLinkOffset(tree, linkContext.markdown, switcherTargets)
const sig: TranslationStructureSignature = { headings: [], code: [], tables: [], lists: [], links: [] }
+ const definitions = new Map>()
+ const collectDefinitions = (node: Nodes): void => {
+ if (node.type === 'definition' && !definitions.has(node.identifier)) {
+ definitions.set(node.identifier, node)
+ }
+ if ('children' in node) for (const child of node.children) collectDefinitions(child)
+ }
+ collectDefinitions(tree)
+ const linkTarget = (node: Extract): string => (
+ semanticTranslationLinkNodeTarget(node, linkContext.markdown, linkContext)
+ )
const visit = (node: Nodes): void => {
switch (node.type) {
case 'heading':
@@ -371,8 +390,17 @@ export function translationStructureSignature(
: `bullet:items=${node.children.length}`)
break
case 'link':
- if (!acceptedSwitchers.has(node.url)) sig.links.push(node.url)
+ if (node.position?.start.offset !== switcherOffset) {
+ sig.links.push(linkTarget(node))
+ }
break
+ case 'linkReference': {
+ const definition = definitions.get(node.identifier)
+ if (definition !== undefined) {
+ sig.links.push(linkTarget(definition))
+ }
+ break
+ }
default:
// Every other node kind is prose or a container, not part of the signature.
break
diff --git a/scripts/translation-prompt.spec.ts b/scripts/translation-prompt.spec.ts
index 65160896c7..96ddc98991 100644
--- a/scripts/translation-prompt.spec.ts
+++ b/scripts/translation-prompt.spec.ts
@@ -58,6 +58,10 @@ describe('translation prompt rendering', () => {
expect(rendered).toContain('Markdown emphasis markers do not create a word boundary')
expect(rendered).toContain('Never invent responsibility merely to avoid a passive construction')
expect(rendered).toContain('Never vary a terminology-table form, defined concept, or contract verb merely for stylistic variety')
+ expect(rendered).toContain('Chinese output uses its `.zh.md` path')
+ expect(rendered).toContain('belongs to the active bilingual corpus')
+ expect(rendered).toContain('a missing counterpart in that corpus is an error')
+ expect(rendered).toContain('exact query/fragment suffix')
expect(rendered).toContain('Return exactly three raw XML sections')
})
diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts
index 31399faf35..02d0e66c0e 100644
--- a/scripts/verify-package-readme-model-experience.ts
+++ b/scripts/verify-package-readme-model-experience.ts
@@ -133,6 +133,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = {
'packages/settings/settings-file': { kind: 'indirect', reason: 'The file provider stores and publishes namespace sections; consumers of ctx.settings own any model-facing behavior.' },
'packages/credentials/credentials': { kind: 'indirect', reason: 'The seam resolves credential references; the consuming adapter owns every model-facing use a value authorizes.' },
'packages/credentials/credentials-local': { kind: 'indirect', reason: 'The file/environment provider stores credential values; consumers of ctx.credentials own any model-facing behavior.' },
+ 'packages/credentials/authorization': { kind: 'none', reason: 'A configuration-time conversation with a human; no flow, notice, or prompt reaches a model request.' },
'packages/util/atomic-write': { kind: 'none', reason: 'Pure filesystem write primitive; registers nothing model-facing.' },
'packages/session/session-telemetry': { kind: 'none', reason: 'The seam observes the session stream and hands redacted copies outward; it registers nothing model-facing.' },
'packages/session/session-telemetry-otel': { kind: 'none', reason: 'The backend forwards seam records into the OTel SDK pipeline and registers nothing model-facing.' },
diff --git a/scripts/verify-translation-pairing.ts b/scripts/verify-translation-pairing.ts
index 9bfc1a1d73..5b2b4052a4 100644
--- a/scripts/verify-translation-pairing.ts
+++ b/scripts/verify-translation-pairing.ts
@@ -10,9 +10,14 @@
* See `docs/i18n/README.md` for the owning contract.
*/
-import { existsSync, globSync, readFileSync, writeFileSync } from 'node:fs'
+import { existsSync, globSync, readFileSync, statSync, writeFileSync } from 'node:fs'
import { basename, join, resolve, sep } from 'node:path'
-import { gitBlobHash, readGitIndexBlob, storeGitBlob } from './translation-pairing-git.ts'
+import {
+ gitBlobHash,
+ gitIndexPaths,
+ readGitIndexBlob,
+ storeGitBlob,
+} from './translation-pairing-git.ts'
import {
parseTranslationPairingRecord,
renderTranslationPairingRecord,
@@ -20,17 +25,23 @@ import {
} from './translation-pairing-record.ts'
import {
languageSwitcherTargets,
- linksTo,
parseTranslationMarkdown,
parseTranslationPairingCliArgs,
parseTranslationPairingManifest,
partitionGeneratedRegions,
requiresSourceLanguageSwitcher,
+ isTranslationPairingManifestExcluded,
isTranslationScopeFile,
TRANSLATION_SCOPE_GLOB_EXCLUDES,
+ translationPairSourcePredicate,
translationStructureDiff,
translationStructureSignature,
} from './translation-pairing.ts'
+import {
+ hasLanguageSwitcher,
+ normalizeTranslationMarkdownLinks,
+ translationLinkLocaleViolations,
+} from './translation-links.ts'
const root = resolve(import.meta.dirname, '..')
let request: ReturnType
@@ -43,6 +54,7 @@ try {
const listMode = request.mode === 'list'
const writeMode = request.mode === 'write'
const indexMode = request.input === 'index'
+const indexFiles = indexMode ? gitIndexPaths(root) : undefined
const contentCache = new Map()
@@ -50,15 +62,17 @@ const contentCache = new Map()
function readRepositoryFile(file: string): Buffer | undefined {
if (contentCache.has(file)) return contentCache.get(file)
const content = indexMode
- ? readGitIndexBlob(root, file)?.content
- : existsSync(join(root, file)) ? readFileSync(join(root, file)) : undefined
+ ? indexFiles?.has(file) ? readGitIndexBlob(root, file)?.content : undefined
+ : existsSync(join(root, file)) && statSync(join(root, file)).isFile()
+ ? readFileSync(join(root, file))
+ : undefined
contentCache.set(file, content)
return content
}
/** Whether one path exists in the selected content plane. */
function repositoryFileExists(file: string): boolean {
- return readRepositoryFile(file) !== undefined
+ return indexMode ? indexFiles?.has(file) === true : readRepositoryFile(file) !== undefined
}
/** Discover source Markdown and pairing sidecars before applying the corpus predicate. */
@@ -74,6 +88,7 @@ if (manifestContent === undefined) {
throw new Error('scripts/translation-pairing.manifest.json is missing from the selected content plane')
}
const manifest = parseTranslationPairingManifest(manifestContent.toString('utf8'))
+const isTranslationPairSource = translationPairSourcePredicate(manifest)
/**
* An excluded entry ending in `/` excludes the whole directory. The trailing
@@ -82,7 +97,7 @@ const manifest = parseTranslationPairingManifest(manifestContent.toString('utf8'
* manifest must keep their trailing slash.
*/
function isExcluded(file: string): boolean {
- return manifest.excluded.some(entry => (entry.endsWith('/') ? file.startsWith(entry) : file === entry))
+ return isTranslationPairingManifestExcluded(file, manifest)
}
// Enumerate the scope once: the whole corpus, or exactly the named pairs'
@@ -230,40 +245,83 @@ for (const source of [...pairAnchors].sort()) {
continue
}
- // Generated regions are language-invariant: the exact same generator output
- // (markers included) must appear in both sides, in the same order. The
- // structural signature below compares the region content again as part of
- // the whole document; this dedicated check exists to name the divergence
- // precisely and to reject a region grammar violation on either side.
+ const sourceText = sourceContent.toString('utf8')
+ const zhText = zhContent.toString('utf8')
+ const sourceSwitcherTargets = languageSwitcherTargets(source)
+ const zhSwitcherTargets = languageSwitcherTargets(zh)
+ for (const violation of [
+ ...translationLinkLocaleViolations(sourceText, {
+ repoRoot: root,
+ sourcePath: source,
+ isTranslationPairSource,
+ repositoryFileExists,
+ }, zhSwitcherTargets),
+ ...translationLinkLocaleViolations(zhText, {
+ repoRoot: root,
+ sourcePath: zh,
+ isTranslationPairSource,
+ repositoryFileExists,
+ }, sourceSwitcherTargets),
+ ]) {
+ errors.push(`${violation.sourcePath}:${violation.line}: link target ${JSON.stringify(violation.url)} uses the wrong locale; expected ${JSON.stringify(violation.expectedUrl)}`)
+ state.set(source, 'out-of-sync')
+ }
+
+ // Generated regions must remain byte-identical after paired document paths
+ // are normalized to one semantic target. The structural signature below
+ // compares their contents again as part of the whole document; this named
+ // check rejects any prose, ordering, code, marker, or non-locale URL drift.
let sourceRegions: { regions: string[]; stripped: string }
let zhRegions: { regions: string[]; stripped: string }
try {
- sourceRegions = partitionGeneratedRegions(sourceContent.toString('utf8'))
- zhRegions = partitionGeneratedRegions(zhContent.toString('utf8'))
+ sourceRegions = partitionGeneratedRegions(sourceText)
+ zhRegions = partitionGeneratedRegions(zhText)
} catch (error) {
errors.push(`${source} ↔ ${zh}: ${error instanceof Error ? error.message : String(error)}`)
state.set(source, 'out-of-sync')
continue
}
- if (sourceRegions.regions.length !== zhRegions.regions.length
- || sourceRegions.regions.some((region, index) => region !== zhRegions.regions[index])) {
- errors.push(`${source} ↔ ${zh}: generated regions differ between the pair — regenerate (the generator writes both sides byte-identically)`)
+ const normalizedSourceRegions = sourceRegions.regions.map(region => normalizeTranslationMarkdownLinks(region, {
+ repoRoot: root,
+ sourcePath: source,
+ isTranslationPairSource,
+ repositoryFileExists,
+ }))
+ const normalizedZhRegions = zhRegions.regions.map(region => normalizeTranslationMarkdownLinks(region, {
+ repoRoot: root,
+ sourcePath: zh,
+ isTranslationPairSource,
+ repositoryFileExists,
+ }))
+ if (normalizedSourceRegions.length !== normalizedZhRegions.length
+ || normalizedSourceRegions.some((region, index) => region !== normalizedZhRegions[index])) {
+ errors.push(`${source} ↔ ${zh}: generated regions differ beyond paired-document locale paths — regenerate both sides`)
state.set(source, 'out-of-sync')
}
- const sourceTree = parseTranslationMarkdown(sourceContent.toString('utf8'))
- const zhTree = parseTranslationMarkdown(zhContent.toString('utf8'))
- const sourceSwitcherTargets = languageSwitcherTargets(source)
- const zhSwitcherTargets = languageSwitcherTargets(zh)
- if (!linksTo(zhTree, sourceSwitcherTargets)) {
+ const sourceTree = parseTranslationMarkdown(sourceText)
+ const zhTree = parseTranslationMarkdown(zhText)
+ if (!hasLanguageSwitcher(zhTree, zhText, sourceSwitcherTargets)) {
errors.push(`${zh}: missing language switcher — no link to ${basename(source)}`)
}
- if (requiresSourceLanguageSwitcher(source) && !linksTo(sourceTree, zhSwitcherTargets)) {
+ if (requiresSourceLanguageSwitcher(source) && !hasLanguageSwitcher(sourceTree, sourceText, zhSwitcherTargets)) {
errors.push(`${source}: missing language switcher — no link back to ${basename(zh)}`)
}
for (const divergence of translationStructureDiff(
- translationStructureSignature(sourceTree, zhSwitcherTargets),
- translationStructureSignature(zhTree, sourceSwitcherTargets),
+ translationStructureSignature(sourceTree, zhSwitcherTargets, {
+ repoRoot: root,
+ sourcePath: source,
+ isTranslationPairSource,
+ repositoryFileExists,
+ markdown: sourceText,
+ }),
+ translationStructureSignature(zhTree, sourceSwitcherTargets, {
+ repoRoot: root,
+ sourcePath: zh,
+ isTranslationPairSource,
+ repositoryFileExists,
+ markdown: zhText,
+ }),
)) {
errors.push(`${source} ↔ ${zh}: ${divergence}`)
}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 91e623e2d1..8d8d5e1433 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -83,6 +83,7 @@
"@deepseek-ai/dsh-commands/types": ["./packages/interaction/commands/src/types.ts"],
"@deepseek-ai/dsh-jobs/brand": ["./packages/jobs/jobs/src/brand.ts"],
"@deepseek-ai/dsh-credentials/types": ["./packages/credentials/credentials/src/types.ts"],
+ "@deepseek-ai/dsh-authorization/types": ["./packages/credentials/authorization/src/types.ts"],
"@deepseek-ai/dsh-settings/types": ["./packages/settings/settings/src/types.ts"],
"@deepseek-ai/dsh-api-remotes/types": ["./packages/api/remotes/src/types.ts"],
"@deepseek-ai/dsh-api-remotes/invariant": ["./packages/api/remotes/src/invariant.ts"],
diff --git a/tsconfig.host.json b/tsconfig.host.json
index 601c74c247..f37b98ea57 100644
--- a/tsconfig.host.json
+++ b/tsconfig.host.json
@@ -51,6 +51,7 @@
"apps/web/tests/message-feedback-layout.e2e.ts",
"apps/web/tests/markdown-images.e2e.ts",
"apps/web/tests/reference-composer.e2e.ts",
+ "apps/web/tests/markdown-wide-table.e2e.ts",
"apps/web/tests/math-rendering.e2e.ts",
"apps/web/tests/markdown-cjk-strong.e2e.ts",
"apps/web/tests/markdown-inline-code-links.e2e.ts",
@@ -153,6 +154,7 @@
{ "path": "./packages/settings/settings-file" },
{ "path": "./packages/credentials/credentials" },
{ "path": "./packages/credentials/credentials-local" },
+ { "path": "./packages/credentials/authorization" },
{ "path": "./packages/session-query/tool-session-query" },
{ "path": "./packages/storage/storage" },
{ "path": "./packages/storage/storage-json" },