Browse Source

Merge origin/master into xtr/trajectory-json-string-wrapping

Resolve the markdown-preview revert and README proofreading against this
PR's trajectory inspection work:

- CodeBlock keeps `showHeader` (the PTC code inspector supplies its own
  toolbar) on top of master's `contentRef` scrollport wrapper.
- ui-primitives README records both `contentRef` and `showHeader`.
- ui-trajectory README keeps master's proofread wording alongside this
  PR's PTC inspection and timing paragraphs.
- Re-record the README.i18n.yaml pairing hashes for the three merged pairs.
_Kerman 1 month ago
parent
commit
b511cd6c2d
100 changed files with 448 additions and 857 deletions
  1. 2 2
      .agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml
  2. 1 1
      .agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md
  3. 1 1
      .agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md
  4. 0 25
      .agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md
  5. 0 25
      .agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md
  6. 2 2
      .agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.i18n.yaml
  7. 1 1
      .agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md
  8. 1 1
      .agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.zh.md
  9. 2 2
      .agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml
  10. 3 3
      .agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md
  11. 3 3
      .agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md
  12. 2 2
      .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml
  13. 2 2
      .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md
  14. 2 2
      .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md
  15. 0 6
      .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml
  16. 0 43
      .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md
  17. 0 43
      .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md
  18. 2 2
      .agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.i18n.yaml
  19. 3 3
      .agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.md
  20. 3 3
      .agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.zh.md
  21. 3 3
      .agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.i18n.yaml
  22. 29 0
      .agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md
  23. 29 0
      .agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md
  24. 1 5
      THIRD_PARTY_NOTICES.md
  25. 1 1
      apps/cli/README.i18n.yaml
  26. 8 8
      apps/cli/README.zh.md
  27. 1 1
      apps/cli/package.json
  28. 2 2
      apps/cli/reference/README.i18n.yaml
  29. 3 3
      apps/cli/reference/README.md
  30. 10 10
      apps/cli/reference/README.zh.md
  31. 1 1
      apps/desktop-host/package.json
  32. 1 1
      apps/desktop/README.i18n.yaml
  33. 18 18
      apps/desktop/README.zh.md
  34. 1 1
      apps/desktop/package.json
  35. 1 1
      apps/web/package.json
  36. 2 2
      apps/web/tests/README.i18n.yaml
  37. 7 31
      apps/web/tests/README.md
  38. 5 21
      apps/web/tests/README.zh.md
  39. 20 9
      apps/web/tests/document-preview.e2e.ts
  40. 0 24
      apps/web/tests/expected/markdown-mermaid/ui.expected.md
  41. 0 23
      apps/web/tests/expected/markdown-mermaid/zh.expected.md
  42. 86 0
      apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md
  43. 5 25
      apps/web/tests/expected/onboarding-deepseek-config/models.expected.md
  44. 0 320
      apps/web/tests/markdown-mermaid.e2e.ts
  45. 34 14
      apps/web/tests/onboarding-deepseek-config.e2e.ts
  46. 4 0
      apps/web/tests/scaffold.ts
  47. 1 1
      apps/web/tests/sidebar-right.e2e.ts
  48. 0 1
      apps/web/tsconfig.json
  49. 1 15
      apps/web/vite.config.ts
  50. 1 1
      benchmarks/active-stream-reconnect/README.i18n.yaml
  51. 2 2
      benchmarks/active-stream-reconnect/README.zh.md
  52. 1 1
      benchmarks/agent-continuation/README.i18n.yaml
  53. 6 6
      benchmarks/agent-continuation/README.zh.md
  54. 1 1
      benchmarks/long-session-browser/README.i18n.yaml
  55. 4 4
      benchmarks/long-session-browser/README.zh.md
  56. 2 2
      docs/config-catalog.i18n.yaml
  57. 2 2
      docs/config-catalog.md
  58. 2 2
      docs/config-catalog.zh.md
  59. 1 1
      docs/i18n/README.i18n.yaml
  60. 1 1
      docs/i18n/README.zh.md
  61. 1 1
      docs/subsystems/README.i18n.yaml
  62. 13 13
      docs/subsystems/README.zh.md
  63. 2 2
      docs/subsystems/sidebar-right.i18n.yaml
  64. 2 2
      docs/subsystems/sidebar-right.md
  65. 2 2
      docs/subsystems/sidebar-right.zh.md
  66. 2 2
      docs/user/guide/providers.i18n.yaml
  67. 1 1
      docs/user/guide/providers.md
  68. 1 1
      docs/user/guide/providers.zh.md
  69. 1 1
      native/system/README.i18n.yaml
  70. 5 5
      native/system/README.zh.md
  71. 1 1
      native/system/packages/darwin-x64/README.i18n.yaml
  72. 1 1
      native/system/packages/darwin-x64/README.zh.md
  73. 1 1
      native/system/packages/entry/README.i18n.yaml
  74. 2 2
      native/system/packages/entry/README.zh.md
  75. 1 1
      native/system/packages/linux-arm64/README.i18n.yaml
  76. 2 2
      native/system/packages/linux-arm64/README.zh.md
  77. 1 1
      native/system/packages/linux-x64/README.i18n.yaml
  78. 2 2
      native/system/packages/linux-x64/README.zh.md
  79. 1 1
      package.json
  80. 1 1
      packages/README.i18n.yaml
  81. 16 16
      packages/README.zh.md
  82. 1 1
      packages/acp/README.i18n.yaml
  83. 4 4
      packages/acp/README.zh.md
  84. 2 2
      packages/acp/acp/README.i18n.yaml
  85. 3 3
      packages/acp/acp/README.md
  86. 11 11
      packages/acp/acp/README.zh.md
  87. 1 1
      packages/acp/acp/package.json
  88. 1 1
      packages/api/README.i18n.yaml
  89. 5 5
      packages/api/README.zh.md
  90. 1 1
      packages/api/gateway/README.i18n.yaml
  91. 12 12
      packages/api/gateway/README.zh.md
  92. 1 1
      packages/api/gateway/package.json
  93. 2 2
      packages/api/remotes/README.i18n.yaml
  94. 2 2
      packages/api/remotes/README.md
  95. 9 9
      packages/api/remotes/README.zh.md
  96. 1 1
      packages/api/remotes/package.json
  97. 1 1
      packages/api/session-controller/README.i18n.yaml
  98. 3 3
      packages/api/session-controller/README.zh.md
  99. 1 1
      packages/api/session-controller/package.json
  100. 1 1
      packages/api/settings-controller/README.i18n.yaml

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.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-09-05-sidebar-tab-types-and-navigation.md
-2026-09-05-sidebar-tab-types-and-navigation.md: a79292eba25828287ce43ad16b7eb917dcdddcb4
-2026-09-05-sidebar-tab-types-and-navigation.zh.md: bf661948036257366714617b192525ac35b0905f
+2026-09-05-sidebar-tab-types-and-navigation.md: 24332c844d13c41e4f966f39aa7d2fe79fd8553f
+2026-09-05-sidebar-tab-types-and-navigation.zh.md: 3aeb38e2ec9c997b9aaf040ae0cb575fa08fa235

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md

@@ -36,7 +36,7 @@ interface SidebarRightTabDefinition {
 
 `priority` is one of three literal bands, spelled as strings so that a type from another package needs no runtime import: `extension` is the band of a type from outside the product and the highest, so a type that declares nothing outranks every viewer shipped here; `builtin` is the ordinary band for shipped types; `fallback` is the plain-content position that anything more specific should beat, which VS Code's text editor holds implicitly and our text preview holds explicitly. `candidates(address)` returns every type whose globs match and whose `canOpen` does not veto, ranked by band, then by the length of the longest pattern that matched, then by registration order. `claim(address, kind?)` takes the best candidate, or the named kind's type in force when the caller overrides (its globs are not consulted; naming the type is the decision), and throws for an address nothing will open — a wiring mistake, not a user error. `get(kind)` returns the type in force; `entries()` and `guide()` list the types and their guide boxes in force; `subscribe` observes changes.
 
-`title(address)` and `guide[].title()` are thunks read on every use, so a language change needs no re-registration. The registry itself is a plain object provided at `apply`'s top level **without** `Service.tracker`: a tracker would rebind `this.ctx` to the caller's context, and a cross-package `register()` would then add its effect to the caller's fiber while that fiber is the active scope, stalling the browser boot with no error.
+`title(address)`, `guide[].title()`, and optional `guide[].description()` are thunks read on every use, so a language change needs no re-registration. [Guide start page and stat pill refinements](../feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the current description visibility and fallback-glyph rules. The registry itself is a plain object provided at `apply`'s top level **without** `Service.tracker`: a tracker would rebind `this.ctx` to the caller's context, and a cross-package `register()` would then add its effect to the caller's fiber while that fiber is the active scope, stalling the browser boot with no error.
 
 ### Bodies and titles: keyed Slot seats under the definition's `id`
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md

@@ -36,7 +36,7 @@ interface SidebarRightTabDefinition {
 
 `priority` 是三个字面量档位之一,写成字符串,好让别的包的类型不需要任何运行时引入:`extension` 是来自产品之外的类型的档位也是最高档,所以什么都不声明的类型压过这里随包交付的每个查看器;`builtin` 是随包类型的常规档;`fallback` 是任何更具体的东西都应压过的纯内容位置,VS Code 的文本编辑器隐含地占据它,我们的文本预览明确地占据它。`candidates(address)` 返回 glob 命中且 `canOpen` 未否决的每个类型,按档位、再按命中的最长 pattern 长度、再按注册顺序排序。`claim(address, kind?)` 取最佳候选,或在调用方指定时取该 kind 生效的类型(不查它的 glob;点名即决定),对无人愿开的地址抛错——这是接线错误,不是用户错误。`get(kind)` 返回生效类型;`entries()` 与 `guide()` 列出生效类型及其引导入口;`subscribe` 观察变化。
 
-`title(address)` 与 `guide[].title()` 是每次使用时重读的 thunk,语言切换无需重新注册。注册表本身是 `apply` 顶层提供的普通对象,**不带** `Service.tracker`:tracker 会把 `this.ctx` 重绑到调用方上下文,跨包 `register()` 就会在调用方 fiber 仍是活动作用域时往它上加 effect,浏览器启动会无声卡死。
+`title(address)`、`guide[].title()` 与可选的 `guide[].description()` 都是每次使用时重读的 thunk,语言切换无需重新注册。[引导起始页与统计 pill 的细化](../feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责当前的描述显示与兜底图标规则。注册表本身是 `apply` 顶层提供的普通对象,**不带** `Service.tracker`:tracker 会把 `this.ctx` 重绑到调用方上下文,跨包 `register()` 就会在调用方 fiber 仍是活动作用域时往它上加 effect,浏览器启动会无声卡死。
 
 ### 正文与标题:按定义 `id` keyed 的 Slot 坑位
 

+ 0 - 25
.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md

@@ -1,25 +0,0 @@
-# Agent Note: Content-sized diagram previews
-
-Status: implemented
-
-English | [中文](2026-09-09-content-sized-diagram-previews.zh.md)
-
-## Problem
-
-Short Graphviz and SVG images leave large empty regions when displayed inside 400px frames. A 59px Graphviz diagram occupies a 400px preview even though its rendered dimensions are already available to the browser.
-
-## Decision
-
-Mermaid, Graphviz, and SVG share the inert image canvas in `SourcePreview`. The browser derives its height from the image, preserves aspect ratio when shrinking to the available width, and adds 16px padding. Source switching retains the image and copying reads the original code. Theme changes regenerate Graphviz colors without introducing a separate sizing observer.
-
-This replaces the diagram-frame choice in [Static Markdown fence previews](../feature/2026-09-09-markdown-static-previews.md); that note continues to own HTML isolation, rendering lifecycle, and license obligations. HTML retains its opaque, script-free, 400px frame. SVG image mode disables scripts, link interaction, and external resource loading without inserting source-controlled markup into the application document.
-
-## Alternatives considered
-
-**Set iframe height to auto.** An iframe does not derive its outer height from its inner document. This still reserves an unrelated viewport.
-
-**Add a frame measurement script or same-origin access.** A diagram already has image dimensions. Extra execution or origin permissions and resize messaging are unnecessary for that content.
-
-## Consequences
-
-Short diagrams occupy only their rendered height and canvas padding. The browser geometry regression compares image and container height in light and dark modes, then verifies proportional scaling in a narrow viewport. Browser security checks include SVG scripts, event handlers, nested HTML, and external images; copy and source toggling remain covered. HTML automatic height remains outside this change because arbitrary document layout has different measurement and isolation requirements.

+ 0 - 25
.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md

@@ -1,25 +0,0 @@
-# Agent Note: 按内容定高的图表预览
-
-Status: implemented
-
-[English](2026-09-09-content-sized-diagram-previews.md) | 中文
-
-## Problem
-
-较矮的 Graphviz 与 SVG 图片显示在 400px iframe 中时,会留下大块空白。浏览器已经知道图片的渲染尺寸,但一张 59px 高的 Graphviz 图表仍占据 400px 的预览区域。
-
-## Decision
-
-Mermaid、Graphviz 与 SVG 共享 `SourcePreview` 的不可执行图片画布。浏览器根据图片决定高度,在可用宽度不足时按比例缩小,并添加 16px 内边距。源码切换保留图片,复制读取原始代码。主题变化重新生成 Graphviz 配色,无需单独的尺寸观察器。
-
-这替代了[静态 Markdown fence 预览](../feature/2026-09-09-markdown-static-previews.zh.md)中的图表 iframe 选择;该记录继续维护 HTML 隔离、渲染生命周期与许可证义务。HTML 保留不透明来源、禁用脚本的 400px iframe。SVG 图片模式禁用脚本、链接交互与外部资源加载,无需把源码控制的标记插入应用文档。
-
-## Alternatives considered
-
-**把 iframe 高度设为 auto。** iframe 不会根据内部文档决定外部高度,仍然会保留与内容无关的视口。
-
-**添加 iframe 测量脚本或同源访问。** 图表已经具备图片尺寸,不需要额外执行权限、来源权限或尺寸消息协议。
-
-## Consequences
-
-较矮图表只占据渲染高度与画布内边距。浏览器几何回归用例在浅色和深色下比较图片与容器高度,再验证窄视口中的等比例缩放。浏览器安全检查包含 SVG 脚本、事件处理器、嵌套 HTML 与外部图片,并保留复制和源码切换覆盖。HTML 自动高度不属于本次改动,因为任意文档布局具有不同的测量与隔离要求。

+ 2 - 2
.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md
-2026-09-02-in-history-system-prompt-replacement.md: b34a0d2b3591c1b62aba16d79963940be787f373
-2026-09-02-in-history-system-prompt-replacement.zh.md: 296932a977ae852c4ef32de1c23b2c5d9e4a1bbf
+2026-09-02-in-history-system-prompt-replacement.md: 8e5fa65ec4649537889cefc75459d2a4958165e5
+2026-09-02-in-history-system-prompt-replacement.zh.md: 73003b04de82f45154f9c673654cc492eb62a4fd

+ 1 - 1
.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md

@@ -18,7 +18,7 @@ For a model route that declares the capability, the loop appends a new `system/m
 
 ### Capability
 
-`dsh-llm` defines `SystemPromptUpdate = 'in-history'` and carries it as an optional sibling field, `systemPromptUpdate`, on `LlmResolvedModelInfo` and `PreparedLlmCall`; `normalizeModelInfo` rejects any other value with an `LlmError` whose code is `INVALID_MODEL_INFO`. The DeepSeek adapter's catalog model (`DeepSeekCatalogModel.systemPromptUpdate`, validated by zod at load) and the replay provider's `ReplayModelConfig.systemPromptUpdate` declare it per model; absence means the model needs message 0 rewritten. No default catalog entry declares it; a deployment enables it through the `models` list in `cordis.yml`, and every `dsh-llm-pi-ai` route keeps the replace behaviour.
+`dsh-llm` defines `SystemPromptUpdate = 'in-history'` and carries it as an optional sibling field, `systemPromptUpdate`, on `LlmResolvedModelInfo` and `PreparedLlmCall`; `normalizeModelInfo` rejects any other value with an `LlmError` whose code is `INVALID_MODEL_INFO`. The DeepSeek adapter's catalog model (`DeepSeekCatalogModel.systemPromptUpdate`, validated by zod at load) and the replay provider's `ReplayModelConfig.systemPromptUpdate` declare it per model; absence means the model needs message 0 rewritten. The sole built-in entry in `dsh-llm-deepseek`, `deepseek-flash`, declares it alongside text and image input. This exact catalog entry records the model capability; names and protocol families do not imply support for other models. A deployment can replace the catalog through the `models` list in `cordis.yml`, and every `dsh-llm-pi-ai` route keeps the replace behaviour.
 
 The loop records the mode in the session: `RequestContext.systemPromptUpdate` joins provider, model, and capacity as a `request/context` field, logged whenever any of them differs from the latest snapshot. Admission reads `PreparedLlmCall.systemPromptUpdate` from the actual call prepared after `agent/request`; the preceding snapshot is not an admission input. First requests, resumed sessions, route changes, and same-route capability changes therefore use the capability of the bound adapter that will serve the call.
 

+ 1 - 1
.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.zh.md

@@ -18,7 +18,7 @@ Status: implemented
 
 ### 能力
 
-`dsh-llm` 定义 `SystemPromptUpdate = 'in-history'`,并把它作为可选的并列字段 `systemPromptUpdate` 放在 `LlmResolvedModelInfo` 与 `PreparedLlmCall` 上;`normalizeModelInfo` 用代码为 `INVALID_MODEL_INFO` 的 `LlmError` 拒绝任何其他值。DeepSeek 适配器的目录模型(`DeepSeekCatalogModel.systemPromptUpdate`,加载时由 zod 校验)与回放提供者的 `ReplayModelConfig.systemPromptUpdate` 逐模型声明它;缺省表示该模型需要重写消息 0。没有默认目录条目声明它;部署方通过 `cordis.yml` 的 `models` 列表启用,所有 `dsh-llm-pi-ai` 路由保持替换行为。
+`dsh-llm` 定义 `SystemPromptUpdate = 'in-history'`,并把它作为可选的并列字段 `systemPromptUpdate` 放在 `LlmResolvedModelInfo` 与 `PreparedLlmCall` 上;`normalizeModelInfo` 用代码为 `INVALID_MODEL_INFO` 的 `LlmError` 拒绝任何其他值。DeepSeek 适配器的目录模型(`DeepSeekCatalogModel.systemPromptUpdate`,加载时由 zod 校验)与回放提供者的 `ReplayModelConfig.systemPromptUpdate` 逐模型声明它;缺省表示该模型需要重写消息 0。`dsh-llm-deepseek` 仅内置 `deepseek-flash` 条目,在该条目上声明它,同时声明文本和图片输入。该精确目录条目记录模型能力;名称和协议类别不能推导其他模型是否支持。部署方可以通过 `cordis.yml` 的 `models` 列表替换目录,所有 `dsh-llm-pi-ai` 路由保持替换行为。
 
 循环把该模式记录进会话:`RequestContext.systemPromptUpdate` 与 provider、model、容量并列成为 `request/context` 的字段,其中任一项与最新快照不同时就记录一次。准入读取 `agent/request` 之后实际准备调用的 `PreparedLlmCall.systemPromptUpdate`;先前快照不是准入输入。因此首次请求、恢复的会话、路由变更以及同一路由的能力变更,都使用将服务该调用的绑定适配器的能力。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md
-2026-09-05-sidebar-text-preview-and-file-tree.md: e25e1c55f5434f2b3b1b2703f886cf3c1e9fe80c
-2026-09-05-sidebar-text-preview-and-file-tree.zh.md: f313d30fa68f9d8afba84a470e5e80902b13d1e3
+2026-09-05-sidebar-text-preview-and-file-tree.md: 0142b325004a1fc018e944ed908d600e75f0ab0d
+2026-09-05-sidebar-text-preview-and-file-tree.zh.md: d40ca8c790c9a7208a2243422c0973fc293a5915

+ 3 - 3
.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md

@@ -20,7 +20,7 @@ Three tab types ship with the Sidebar: the **guide** (`ui-sidebar-right`), the *
 
 The guide is what a pane shows before it holds content. Its registration is `{ id: '@deepseek-ai/dsh-client-ui-sidebar-right/guide', kind: 'guide', priority: 'builtin', title }` with no `patterns`: a guide views nothing, so it is opened by kind through `openTab` and recorded under the page address `sidebar://guide`, which is the registry's bookkeeping and never composed by a caller. The tab's title is `开始` / `Start`, captured into the layout record when the pane is seeded, so a later language change relabels the type and not tabs already open.
 
-The body is a centred column — a lead line (`侧栏用来放你想一直看着的东西。` / `The sidebar holds what you want to keep looking at.`), one line of copy (`会话里的文件和产物会开在这一栏,也可以从下面的入口打开。` / `Files and artifacts from the conversation open in this column; the entries below open more.`), and a grid of entry boxes at most 480px wide, each box at least 160px, filling as many columns as fit. The boxes are projected from every registered type's `guide[]` in `order`, through the registry's observable `guide()` list, so a type registering later appears without the guide knowing it. A box shows the contributing type's glyph, title, and description, and picking it calls `tabActions.openTab(entry.kind, { replaceTab: true })`: the picked type opens in the guide's own tab, and the guide is gone. The guide is a doorway, not a page that stays open beside what it opened.
+The body projects every registered type's `guide[]` in `order` through the registry's observable `guide()` list, so a type registering later appears without the guide knowing it. [Guide start page and stat pill refinements](2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the compass, optional descriptions, fallback glyph, and current capsule layout. Picking a capsule calls `tabActions.openTab(entry.kind, { replaceTab: true })`: the picked type opens in the guide's own tab, and the guide is gone. The guide is a doorway, not a page that stays open beside what it opened.
 
 The body is also the replacement seam. It renders the `sidebar.right.tab.guide` chain with the shipped guide as the chain's fallback, so a product that registers its own entry takes the whole body, and with no entry, or every entry declining, the shipped guide draws. Because the shipped guide is the fallback and not a chain entry, there is always exactly one body and it cannot be outvoted by accident.
 
@@ -42,13 +42,13 @@ Navigation is a `line`. The `read` tool row passes its 1-based `offset` as `open
 
 A changed file is announced, not applied. The body compares the loaded version and the observation captured at read start with later `WorkspaceFileStat.version`; a difference shows the change bar. Reload rereads only this tab through the Preview face and does not mutate shared resource metadata or another tab. A resource failure takes the same bar's place above any content already loaded.
 
-The body's header is one row: the full file path on the left and the matching-renderer menu, conditional wrap toggle, and reload button on the right. The [Document Preview README](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns the current controls and renderer behavior. The preview takes the pane body's full height, and its document body is the scroller beneath the fixed header and change bar.
+The body's header is one row: the full file path on the left and the matching-renderer menu, conditional wrap toggle, and reload button on the right. The [Document Preview README](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns the current controls, renderer behavior, and scrolling. The preview takes the pane body's full height.
 
 A failed read keeps content already shown and adds a localized failure with a retry action. The Preview names actionable file failures and falls back to the carrier message for other codes; `outside-workspace` belongs to directory listing and is not a Preview-specific failure.
 
 ### The file tree
 
-`files` is a page type, not a viewer: it claims no address. Its registration is `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: IconFolderClose16 }] }` — no `patterns`, because nothing navigates *to* a file tree by address; the guide's entry box opens the type itself. `id` is the implementation's identity in the Tab system and doubles as the `key` of the body seat `sidebar.right.pane.tab`, so the same string names the type and the component that draws it. `register()` returns a disposer and goes through `ctx.effect`, as every registration does.
+`files` is a page type, not a viewer: it claims no address. Its registration is `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: FolderSheetGlyph }] }` — no `patterns`, because nothing navigates *to* a file tree by address; the guide's entry box opens the type itself. `FolderSheetGlyph` adapts the shared coloured folder sheet to the guide's requested glyph size; the [guide refinement](2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns that presentation choice. `id` is the implementation's identity in the Tab system and doubles as the `key` of the body seat `sidebar.right.pane.tab`, so the same string names the type and the component that draws it. `register()` returns a disposer and goes through `ctx.effect`, as every registration does.
 
 The root is the session's working directory as the Host reports it in the session list (`useSessions().byId[sessionId].cwd`), labelled by `workspaceTitleOf` from `dsh-util-workspace-path` — the final non-empty path segment — with the root string itself as the label when the path is separator-only. A session without a working directory shows one line (`noWorkspace`) and issues no request. There is no root chooser and no way to browse upward: the Host's `list` refuses paths outside the Session's workspace root, so the one directory the client can list is the one it shows.
 

+ 3 - 3
.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md

@@ -20,7 +20,7 @@ Sidebar 随包交付三个 tab 类型:**引导页**(`ui-sidebar-right`)、
 
 引导页是 pane 承载内容之前显示的东西。它的注册定义是 `{ id: '@deepseek-ai/dsh-client-ui-sidebar-right/guide', kind: 'guide', priority: 'builtin', title }`,没有 `patterns`:引导页不查看任何东西,所以经 `openTab` 按 kind 打开,并记在页地址 `sidebar://guide` 之下——那是注册表自己的记账,调用方从不拼它。tab 标题是 `开始` / `Start`,在 pane 播种时捕获进布局记录,于是之后切换语言只重标类型,不改已开着的 tab。
 
-体是一根居中的列——一句引导语(`侧栏用来放你想一直看着的东西。` / `The sidebar holds what you want to keep looking at.`)、一行文案(`会话里的文件和产物会开在这一栏,也可以从下面的入口打开。` / `Files and artifacts from the conversation open in this column; the entries below open more.`),以及一组最宽 480px 的入口框栅格,每框至少 160px,能放几列放几列。入口框按 `order` 从每个已注册类型的 `guide[]` 投影而来,经注册表可观察的 `guide()` 列表,因此后注册的类型不用引导页知道就能出现。一个框显示贡献类型的图标、标题与说明;点选它调用 `tabActions.openTab(entry.kind, { replaceTab: true })`:被选的类型在引导页自己的 tab 里打开,引导页随之消失。引导页是一扇门,不是留在被打开者旁边的一页。
+正文按 `order` 从每个已注册类型的 `guide[]` 投影入口,并通过注册表可观察的 `guide()` 列表更新,因此后注册的类型无需引导页感知即可出现。[引导起始页与统计 pill 的细化](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责罗盘、可选描述、兜底图标和当前胶囊布局。点选胶囊会调用 `tabActions.openTab(entry.kind, { replaceTab: true })`:被选的类型在引导页自己的 tab 里打开,引导页随之消失。引导页是一扇门,不是留在被打开者旁边的一页。
 
 体同时也是替换接缝。它渲染 `sidebar.right.tab.guide` 链,并以随包交付的引导页作为链的 fallback,于是注册了自己入口的产品接管整个体,而没有入口、或每个入口都拒绝时,随包交付的引导页照常绘制。因为随包交付的引导页是 fallback 而不是链上的一个入口,所以永远恰有一个体,也不可能被意外投掉。
 
@@ -42,13 +42,13 @@ store 是 Slot 标准件:每会话一个独占实例,按 tab id 分桶,持
 
 文件变了只提示,不应用。正文把已加载版本及读取开始时捕获的观察版本与后续 `WorkspaceFileStat.version` 比较;不同则显示变更提示。重新载入只通过 Preview face 重读当前 tab,不修改共享资源元数据或其他 tab。资源失败占用同一个提示位置,已加载内容仍保留在下方。
 
-正文头部为一行:左侧显示完整文件路径,右侧放匹配渲染器菜单、按条件出现的换行开关和重新载入按钮。[Document Preview README](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md)负责当前控件与渲染器行为。预览占满 pane 正文的全部高度,其文档正文是固定头部与变更提示条下方的滚动区域。
+正文头部为一行:左侧显示完整文件路径,右侧放匹配渲染器菜单、按条件出现的换行开关和重新载入按钮。[Document Preview README](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md)负责当前控件、渲染器行为与滚动方式。预览占满 pane 正文的全部高度。
 
 读取失败时保留已显示的内容,并增加本地化失败说明与重试操作。Preview 为可处理的文件错误提供专用文案,其他代码使用载体消息兜底;`outside-workspace` 属于目录列举,不是 Preview 专用失败。
 
 ### 文件树
 
-`files` 是页类型,不是查看器:它不认领任何地址。注册定义是 `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: IconFolderClose16 }] }`——没有 `patterns`,因为没有谁按地址导航*到*一棵文件树;引导页的入口框打开的是类型本身。`id` 是这个实现在 Tab 系统里的唯一键,同时也是体坑位 `sidebar.right.pane.tab` 的 `key`,于是同一个串既命名类型也命名画它的组件。`register()` 返回 disposer 并经 `ctx.effect` 注册,与所有注册一致。
+`files` 是页类型,不是查看器:它不认领任何地址。注册定义是 `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: FolderSheetGlyph }] }`——没有 `patterns`,因为没有谁按地址导航*到*一棵文件树;引导页的入口框打开的是类型本身。`FolderSheetGlyph` 把共享的彩色文件夹页适配到引导页请求的图标尺寸;[引导页细化记录](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责这项展示选择。`id` 是这个实现在 Tab 系统里的唯一键,同时也是体坑位 `sidebar.right.pane.tab` 的 `key`,于是同一个串既命名类型也命名画它的组件。`register()` 返回 disposer 并经 `ctx.effect` 注册,与所有注册一致。
 
 根是 Host 在会话列表里上报的会话工作目录(`useSessions().byId[sessionId].cwd`),标签由 `dsh-util-workspace-path` 的 `workspaceTitleOf` 给出——路径最后一个非空段——路径只有分隔符时用根串本身作标签。没有工作目录的会话只显示一行(`noWorkspace`),不发请求。没有根选择器,也不能往上浏览:Host 的 `list` 拒绝会话工作区根之外的路径,所以客户端能列的那一个目录就是它显示的目录。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md
-2026-09-07-composer-session-stats-pills.md: 818c3c156f72816eb6540d505782be8e03dc6aa0
-2026-09-07-composer-session-stats-pills.zh.md: 8a74cddd3176026d799fb5b4223a51371ee8e253
+2026-09-07-composer-session-stats-pills.md: 541e5a363d78c4333b1b8e3927928a19754822fa
+2026-09-07-composer-session-stats-pills.zh.md: b6ef6da3c37a46d9e07e7ebdf77439e26896fddc

+ 2 - 2
.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md

@@ -12,7 +12,7 @@ The session stats strip under the composer (`StatsLine`, ui-chat, mounted on `co
 
 `StatsPills` (packages/client/ui-chat/src/client/chat/StatsPills.tsx) replaces `StatsLine` on the same `conversation.composer.dock` slot; the losing variant is deleted, its shared helpers (`deriveStats`, `formatDuration`, `cacheHitPercent`, `billedInputTokens`) absorbed into the new module, and the dead `stats.llm`, `stats.toolCall`, `stats.ttftAverage`, `stats.tokensPerSecond`, and `stats.tokens` locale keys removed.
 
-- **Two icon pills, two dialogs.** A gauge pill (new `IconGaugeOutline16`, dial center optically dropped to y=8.75 because the bottom-open arc reads high) shows `{turns} 轮 {steps} 步` plus output TPS and click-opens the 会话统计 dialog (LLM time, tool time, average TTFT, TPS); a log with no timed figure would open an empty dialog, so that pill renders as a static reading instead of a button. A database pill (`IconDatabaseOutline16`) shows the compact billed total plus cache-hit share and click-opens the Token 用量 dialog (cache hit, uncached input, cache read, cache write, output — exact counts). Both dialogs wear the shared `stat-dialog` module (portal panel, anchored placement, outside-dismiss, optionally externally owned open state) extracted for exactly this two-consumer split; the pills row owns one exclusive open slot, so opening either dialog closes the other, and each button carries an explicit `aria-label` that separates with ` · ` the segments the aria-hidden sep glyph joins visually.
+- **Two icon pills, two dialogs.** A gauge pill (new `IconGaugeOutline16`, dial center optically dropped to y=8.75 because the bottom-open arc reads high) shows `{turns} 轮 {steps} 步` plus output TPS and click-opens the 会话统计 dialog (LLM time, tool time, average TTFT, TPS); a log with no timed figure would open an empty dialog, so that pill renders as a static reading instead of a button. A database pill (`IconDatabaseOutline16`) shows the compact billed total plus cache-hit share and click-opens the Token 用量 dialog (cache hit, uncached input, cache read, output, and cache write when non-zero — exact counts). Both dialogs wear the shared `stat-dialog` module (portal panel, anchored placement, outside-dismiss, optionally externally owned open state) extracted for exactly this two-consumer split; the pills row owns one exclusive open slot, so opening either dialog closes the other, and each button carries an explicit `aria-label` that separates with ` · ` the segments the aria-hidden sep glyph joins visually. [Guide start page and stat pill refinements](2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the zero cache-write omission.
 - **Data sourcing is unchanged in architecture.** Counts and times prefer the durable `sessionStats` projection with the window fold as the assembly-without-the-unit fallback ([whole-session counts](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md)); token figures ride `tokenUsage` only, so an absent projection drops the usage pill rather than showing window-derived billing. Cache writes stay in the billed total and the cache-hit denominator ([projection decision](../architecture/2026-07-29-projected-token-usage-and-request-context.md)). Context occupancy stays on the composer's ContextMeter ring, where it already lived beside `StatsLine` — the strip never carried it.
 - **Render discipline.** The row folds settled nodes only (`chat.legacy.nodes` identity), so streaming chunk frames cause zero rerenders — pinned by a render-count unit test. A session with no closed step and no billed tokens renders nothing.
 - **`data-composer-stats` is a cross-package attribute contract.** The pills' root carries it; ui-conversation's `InputBar.module.css` `:has([data-composer-stats])` rule tightens the composer's bottom clearance to 4px when the row is mounted. The producer side pins the attribute in unit tests, following the `data-trigger-menu` precedent.
@@ -21,7 +21,7 @@ The session stats strip under the composer (`StatsLine`, ui-chat, mounted on `co
 
 - **The single-line variant (StatsLine, the A/B loser).** All figures resident in one text row, with a hover tooltip restating the full line when it truncated. Lost on crowding and reach: exact token counts appeared nowhere (the line and its tooltip both carried compact totals only), and one row gave time and billing figures no visual grouping.
 - **Three resident groups with one shared dialog.** An intermediate iteration kept counts, time, and tokens as three inline groups. Two pills won because the time/usage split matches the two underlying projections one-to-one and each pill's icon telegraphs its dialog.
-- **Extracting the dl bucket rows shared with `TurnUsagePanel`.** The session-total dialog and the per-turn panel render the same skin but different contracts (all buckets always present vs optional per-turn fields plus model routes); a shared component would be conditionals around nine lines. The mirror is marked `jscpd:ignore` with the reason inline.
+- **Extracting the dl bucket rows shared with `TurnUsagePanel`.** The session-total dialog and the per-turn panel render the same skin but different contracts (required session input, cache-read, and output rows plus a non-zero cache-write row, versus optional per-turn fields and model routes); a shared component would be conditionals around nine lines. The mirror is marked `jscpd:ignore` with the reason inline.
 
 ## Consequences
 

+ 2 - 2
.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 `StatsPills`(packages/client/ui-chat/src/client/chat/StatsPills.tsx)在同一 `conversation.composer.dock` 插槽上取代 `StatsLine`;落选变体已删除,其共享工具函数(`deriveStats`、`formatDuration`、`cacheHitPercent`、`billedInputTokens`)并入新模块,废弃的 `stats.llm`、`stats.toolCall`、`stats.ttftAverage`、`stats.tokensPerSecond`、`stats.tokens` 文案键一并移除。
 
-- **两个图标 pill、两个弹层。** 仪表盘 pill(新增 `IconGaugeOutline16`,因下开口圆弧视觉偏高而把表盘中心光学下移到 y=8.75)展示 `{turns} 轮 {steps} 步` 加输出 TPS,点击打开「会话统计」弹层(模型用时、工具调用用时、首 token 平均、输出速度);日志里没有任何计时数字时弹层会是空的,此时该 pill 渲染为静态读数而非按钮。数据库 pill(`IconDatabaseOutline16`)展示紧凑计费总量加缓存命中率,点击打开「Token 用量」弹层(缓存命中、未缓存输入、缓存读取、缓存写入、输出——精确计数)。两个弹层共用为这两处消费者抽出的 `stat-dialog` 模块(portal 面板、锚定定位、点击外部关闭、可选的外部持有开合状态);pill 行持有唯一的互斥开合槽位,打开任一弹层即关闭另一个,且每个按钮携带显式 `aria-label`,用 ` · ` 分隔 aria-hidden 分隔符在视觉上连接的两段文本。
+- **两个图标 pill、两个弹层。** 仪表盘 pill(新增 `IconGaugeOutline16`,因下开口圆弧视觉偏高而把表盘中心光学下移到 y=8.75)展示 `{turns} 轮 {steps} 步` 加输出 TPS,点击打开「会话统计」弹层(模型用时、工具调用用时、首 token 平均、输出速度);日志里没有任何计时数字时弹层会是空的,此时该 pill 渲染为静态读数而非按钮。数据库 pill(`IconDatabaseOutline16`)展示紧凑计费总量加缓存命中率,点击打开「Token 用量」弹层(缓存命中、未缓存输入、缓存读取、输出,以及非零时的缓存写入——精确计数)。两个弹层共用为这两处消费者抽出的 `stat-dialog` 模块(portal 面板、锚定定位、点击外部关闭、可选的外部持有开合状态);pill 行持有唯一的互斥开合槽位,打开任一弹层即关闭另一个,且每个按钮携带显式 `aria-label`,用 ` · ` 分隔 aria-hidden 分隔符在视觉上连接的两段文本。[引导起始页与统计 pill 的细化](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责缓存写入为零时省略该行的规则。
 - **数据来源架构不变。** 计数与用时优先读取持久的 `sessionStats` 投影,窗口折叠仅作无该单元装配时的回退([全会话计数](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md));token 数字只走 `tokenUsage`,投影缺席时直接不渲染用量 pill,而非展示窗口推算的计费。缓存写入仍计入计费总量与缓存命中分母([投影决定](../architecture/2026-07-29-projected-token-usage-and-request-context.zh.md))。上下文占用仍在输入框旁的 ContextMeter 圆环上,`StatsLine` 时代它就在那里——统计条从未承载过它。
 - **渲染纪律。** 该行只折叠已定稿节点(`chat.legacy.nodes` 身份),流式 chunk 帧零重渲染——由渲染计数单测钉住。无已完成步且无计费 token 的会话什么都不渲染。
 - **`data-composer-stats` 是跨包属性契约。** pill 行根元素携带它;ui-conversation 的 `InputBar.module.css` 用 `:has([data-composer-stats])` 在该行挂载时把输入框底部留白收紧到 4px。生产方在单测里钉住该属性,沿用 `data-trigger-menu` 先例。
@@ -21,7 +21,7 @@ Status: implemented
 
 - **单行变体(StatsLine,A/B 落选方)。** 全部数字常驻一行文本,悬停提示只在截断时复述整行。败在拥挤与可达性:精确 token 计数无处可看(行内和提示里都只有紧凑总量),单行也无法给时间与计费数字分组。
 - **三个常驻分组共享一个弹层。** 中间迭代曾把计数、时间、token 保持为三个内联分组。双 pill 胜出是因为时间/用量的切分与两个底层投影一一对应,且每个 pill 的图标直接预告其弹层内容。
-- **抽出与 `TurnUsagePanel` 共享的 dl 分桶行。** 会话总量弹层与逐轮面板皮肤相同但契约不同(此处所有分桶恒在;逐轮字段可选且含模型路由);共享组件只是给九行代码包一层条件。该镜像以 `jscpd:ignore` 标注并内联说明理由。
+- **抽出与 `TurnUsagePanel` 共享的 dl 分桶行。** 会话总量弹层与逐轮面板皮肤相同但约定不同(会话输入、缓存读取与输出行始终存在,缓存写入行仅在非零时出现;逐轮字段可选且含模型路由);共享组件只是给九行代码包一层条件。该镜像以 `jscpd:ignore` 标注并内联说明理由。
 
 ## 后果
 

+ 0 - 6
.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.i18n.yaml

@@ -1,6 +0,0 @@
-# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
-# side as of the last confirmed-consistent state. Both languages carry equal authority;
-# after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md
-2026-09-09-markdown-static-previews.md: f3a0363e3d7c4cb19ca67dfc5011e936f7c024cf
-2026-09-09-markdown-static-previews.zh.md: 24f87cd73504c1fbd274e1420a3692b3ca55ed5e

+ 0 - 43
.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.md

@@ -1,43 +0,0 @@
-# Agent Note: Static Markdown fence previews
-
-Status: implemented
-
-English | [中文](2026-09-09-markdown-static-previews.zh.md)
-
-## Problem
-
-Readers need to see Mermaid and DOT diagrams, SVG artwork, and HTML examples directly in Assistant replies without interpreting source or copying it to another renderer. These sources are untrusted, and a renderer's npm license field may omit compiled components with separate distribution obligations.
-
-## Decision
-
-The shared Markdown renderer enables settled `mermaid`, `graphviz`/`dot`, `svg`, and `html` fences through localized `MarkdownLabels.preview` and the parsed fence language. The [UI primitives package](../../../../packages/client/ui-primitives/README.md) owns `SourcePreview`, which accepts a renderer, source, and labels without Session, file, or Cordis dependencies. Mermaid and Graphviz each supply a `.ts` renderer to that same component. Consumers without preview labels and streaming messages retain code.
-
-`CodeBlock.preview` owns default visualization, source switching, and verbatim copying. Previews omit the language banner and expose compact icon actions on hover or keyboard focus; devices with any touch input keep actions visible below the diagram. Switching to source hides the mounted preview, preserving completed rendering and pending work. Both views use the same focused toggle button, and copying always reads the source prop.
-
-`SourcePreview` owns loading, failure, and cancellation of stale result publication. Source replacement and unmounting cancel publication; cancellation before runtime loading completes skips layout. Failures show a localized error and the original source, and replacing invalid source with valid input recovers the preview.
-
-Mermaid loads on demand. A shared queue serializes theme initialization with diagram work, and each call removes its temporary measurement DOM in `finally`. Strict security, disabled HTML labels, the application palette, and error-rendering policy cannot be overridden by diagram configuration. Generated SVG is displayed as an image without installing links or scripts. Intrinsic dimensions come from the SVG viewBox; large diagrams shrink to fit, and the canvas follows the code-block background. Mounted previews observe document theme attributes and regenerate only when resolved colors change; obsolete renders cannot publish. Graphviz default colors follow the same palette, while authored DOT, SVG, and HTML colors remain intact.
-
-HTML uses DOMPurify with navigation attributes and document-loading elements forbidden, followed by an opaque `sandbox=""` iframe. A trusted CSP precedes source markup and permits only inline CSS, data images, and data fonts. Scripts, external resources, child frames, form submission, and same-origin access are unavailable. SVG is parsed as XML and displayed with Graphviz output as inert images in a [content-sized canvas](../bug-fix/2026-09-09-content-sized-diagram-previews.md), so SVG scripts and links never become active. HTML frames have a fixed scrollable viewport: measuring their content would require additional origin access or trusted frame scripts.
-
-Graphviz uses the pinned, unmodified `@viz-js/viz` 3.30.0 WebAssembly distribution with the `dot` layout engine. Its npm MIT declaration covers the wrapper; its build provenance identifies Graphviz 16.0.0 (EPL-2.0), Expat 2.8.4 (MIT), and Emscripten 5.0.7 (MIT/NCSA). Distribution retains these component terms and the exact Graphviz source download under EPL-2.0 section 3.1. [Full preview notices](../../../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt) also retain Mermaid's MIT text and select Apache-2.0 for DOMPurify. The UI primitives package ships the notices and the Web build emits the same bytes alongside its assets. License review covers embedded payloads separately from the general permissive npm-metadata policy.
-
-## Alternatives considered
-
-**Keep the code banner above diagrams.** A language label and permanent text controls distract from the diagram. Overlay actions retain source access without reserving a title row.
-
-**Render each streamed chunk.** Incomplete diagrams are frequently invalid, and repeated layout work competes with text streaming. Message settlement provides complete source.
-
-**Put rendering inside Chat or add a general preview registry.** A shared primitive with plain props satisfies reuse without another registry or feature-plugin dependency. This follows the [shared-control rule](../architecture/2026-09-05-shared-client-control-primitives.md).
-
-**Execute HTML scripts in a frame.** Static chat examples do not need script execution. An empty sandbox plus sanitization and CSP gives the preview fewer capabilities and avoids introducing a frame messaging protocol.
-
-**Insert rendered markup into Chat.** The requested preview needs diagram display and source access; HTML styles and executable SVG would share the application document. Iframes isolate layout and origin; SVG image mode additionally disables SVG behavior.
-
-**Treat Viz.js as MIT-only or use a remote Graphviz service.** The compiled Graphviz license still applies locally; a remote renderer would send conversation content off-device. The bundled renderer retains source availability and legal notices without network rendering.
-
-## Consequences
-
-The feature changes presentation without changing persisted messages, provider requests, tools, or Host APIs. Inline HTML styles work, while interactive scripts, external assets, and links do not. Mermaid and Graphviz add lazy browser assets and run layout on the browser thread. Cancellation cannot preempt active layout; Mermaid finishes and releases its measurement DOM even when its result cannot be published. The previews have no editing, export, zoom controls, or interactive diagram links.
-
-Component tests cover source copying, default preview, delayed completion, stale success and failure, unmounting, fallback, cancellation, runtime loading failure, and recovery. The keyless [browser scenario](../../../../apps/web/tests/markdown-mermaid.e2e.ts) verifies Chinese Mermaid flowcharts, sequence diagrams, malformed source, configuration overrides, opaque frames, real image decoding, source toggling, blocked script/navigation/resource attempts, English/Chinese UI snapshots, and served license text. Notices checks pin the reviewed wrapper and native provenance so upgrades require renewed review.

+ 0 - 43
.agents/notes/implemented/feature/2026-09-09-markdown-static-previews.zh.md

@@ -1,43 +0,0 @@
-# Agent Note: 静态 Markdown fence 预览
-
-Status: implemented
-
-[English](2026-09-09-markdown-static-previews.md) | 中文
-
-## Problem
-
-读者需要直接在 Assistant 回复中查看 Mermaid 和 DOT 图表、SVG 图像和 HTML 示例,而不必解读源码或复制到其他渲染器。这些源码不可信,而渲染器的 npm 许可证字段可能遗漏编译组件各自的分发义务。
-
-## Decision
-
-共享 Markdown 渲染器通过本地化的 `MarkdownLabels.preview` 和解析后的 fence 语言启用定稿后的 `mermaid`、`graphviz`/`dot`、`svg` 和 `html` fence。[UI primitives 包](../../../../packages/client/ui-primitives/README.zh.md)拥有 `SourcePreview`,它接收渲染器、源码和文案,不依赖 Session、文件或 Cordis。Mermaid 与 Graphviz 各自向同一组件提供 `.ts` 渲染器。未提供预览文案的调用方与流式消息保留代码显示。
-
-`CodeBlock.preview` 负责默认可视化、源码切换与原样复制。预览省略语言横幅,在悬停或键盘聚焦时显示紧凑的图标操作;具有任何触控输入的设备始终在图表下方显示操作。切换到源码会隐藏已挂载的预览,保留完成的渲染和待完成的工作。两种视图使用同一个保持焦点的切换按钮,复制始终读取源码属性。
-
-`SourcePreview` 负责加载、失败和取消过期结果发布。替换源码和卸载组件会取消结果发布;在运行时加载完成前取消会跳过布局。失败时显示本地化错误与原始源码,用有效输入替换无效源码后可以恢复预览。
-
-Mermaid 按需加载。共享队列将主题初始化与图表渲染一起串行执行,每次调用都在 `finally` 中删除临时测量 DOM。图表配置无法覆盖严格安全模式、禁用 HTML 标签、应用配色和错误渲染策略。生成的 SVG 作为图片显示,不安装链接或脚本。固有尺寸来自 SVG viewBox;大图缩小以适应宽度,画布使用代码块背景。已挂载的预览观察文档主题属性,仅在解析后的配色变化时重新生成;过期渲染不能发布结果。Graphviz 的默认颜色采用同一配色,DOT、SVG 和 HTML 中明确指定的颜色保持原样。
-
-HTML 先由 DOMPurify 清理,禁止导航属性与文档加载元素,再进入不透明来源的 `sandbox=""` iframe。可信 CSP 位于源码标记之前,仅允许内联 CSS、data 图片与 data 字体。脚本、外部资源、子 iframe、表单提交与同源访问均不可用。SVG 按 XML 解析后,与 Graphviz 输出一同作为不可执行图片显示在[按内容定高的画布](../bug-fix/2026-09-09-content-sized-diagram-previews.zh.md)中,因此 SVG 脚本与链接不会激活。HTML iframe 使用固定的可滚动视口:测量内容需要额外的来源访问权限或可信 iframe 脚本。
-
-Graphviz 使用固定且未修改的 `@viz-js/viz` 3.30.0 WebAssembly 发布包,布局引擎为 `dot`。其 npm MIT 声明覆盖包装层;构建来源记录标明 Graphviz 16.0.0(EPL-2.0)、Expat 2.8.4(MIT)与 Emscripten 5.0.7(MIT/NCSA)。分发保留各组件条款,并按 EPL-2.0 第 3.1 节提供准确的 Graphviz 源码下载地址。[完整预览声明](../../../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt)还保留 Mermaid 的 MIT 文本,并为 DOMPurify 选择 Apache-2.0。UI primitives 包携带该声明,Web 构建在资源旁输出相同字节。内嵌产物的许可证检查独立于通用的宽松 npm 元数据策略。
-
-## Alternatives considered
-
-**在图表上方保留代码横幅。** 语言标签和常驻文字控件会分散对图表的注意力。浮层操作保留源码访问,不占用标题行。
-
-**渲染每个流式片段。** 未完成的图表通常无效,反复布局会与文本流式输出争用资源。消息定稿可提供完整源码。
-
-**在 Chat 内部渲染,或添加通用预览注册表。** 使用普通属性的共享基础组件即可满足复用,无需另加注册表或功能插件依赖。这遵循[共享控件规则](../architecture/2026-09-05-shared-client-control-primitives.zh.md)。
-
-**在 iframe 内执行 HTML 脚本。** 静态聊天示例不需要脚本执行。空 sandbox、清理与 CSP 降低预览权限,也避免引入 iframe 消息协议。
-
-**把渲染标记插入 Chat。** 所需预览只需显示图表和访问源码;HTML 样式和可执行 SVG 会与应用共享文档。iframe 隔离布局与来源,SVG 图片模式额外禁用 SVG 行为。
-
-**将 Viz.js 视为仅使用 MIT,或使用远程 Graphviz 服务。** 本地编译的 Graphviz 仍受其许可证约束;远程渲染会把对话内容发送到设备之外。内置渲染器保留源码可获取性与法律声明,无需网络渲染。
-
-## Consequences
-
-该功能改变呈现,不改变持久化消息、provider 请求、工具或 Host API。HTML 内联样式可用,交互脚本、外部资源和链接不可用。Mermaid 和 Graphviz 增加按需加载的浏览器资源,并在浏览器线程上执行布局。取消无法抢占进行中的布局;即使结果无法发布,Mermaid 仍会完成并释放测量 DOM。预览不提供编辑、导出、缩放控件或交互式图表链接。
-
-组件测试覆盖源码复制、默认预览、延迟完成、过期成功与失败、卸载、回退、取消、运行时加载失败与恢复。无密钥[浏览器场景](../../../../apps/web/tests/markdown-mermaid.e2e.ts)验证中文 Mermaid 流程图、时序图、无效源码、配置覆盖、不透明来源 iframe、真实图片解码、源码切换、脚本/导航/资源请求被阻止、中英文 UI 快照和已提供的许可证文本。声明检查固定已审查的包装层与原生构建来源,升级时须重新审查。

+ 2 - 2
.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.md
-2026-09-09-sidebar-and-preview-interaction-polish.md: df0e768aa1aea913344c356d84c2a7d3386ccc6b
-2026-09-09-sidebar-and-preview-interaction-polish.zh.md: d3f8b00fd86ec12d867d48a917cd4a65cab18ba9
+2026-09-09-sidebar-and-preview-interaction-polish.md: eba8bd3a2314c7eaf18cd9982aa4e599665dbc62
+2026-09-09-sidebar-and-preview-interaction-polish.zh.md: 93d88d41e99f50621c80d69d615e39852b715c89

+ 3 - 3
.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.md

@@ -18,7 +18,7 @@ Five small interaction defects around the right Sidebar and the document preview
 
 **Menus close on focus entering an iframe.** [Menu.tsx](../../../../packages/client/ui-primitives/src/Menu.tsx) adds a window `blur` listener gated on `document.activeElement instanceof HTMLIFrameElement` — the focus move is the only signal a pointerdown inside a cross-origin iframe leaves, and the gate keeps app or tab switches from closing the list.
 
-**The code preview pins its banner and drops the card fill.** With wrap off, [CodeBody.module.css](../../../../packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.module.css) sizes the renderer `max-content` so the sticky banner has the full scroll width to ride, and pins the banner `sticky; left: 0; width: 100cqw` against the document scroller (`container-type: inline-size` on the preview body). The shared CodeBlock's fill is routed through a new `--dsl-code-block-background` variable (default unchanged, so chat keeps its gray card) and the shared banner carries an inert `data-code-block-banner` hook; the preview sets the variable to `transparent` so code sits on the pane's own background.
+**The code preview separates its banner from scrolling source and drops the card fill.** The shared CodeBlock wraps its rendered source in a stable `data-code-block-content` node that defaults to `display: contents`, so existing consumers keep their layout. [CodeBody.module.css](../../../../packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.module.css) materializes that node as a full-height inner scrollport below the banner; the code renderer reports it through a callback ref, so the document owner follows Slot replacement for position restoration, paging, and line navigation. The shared CodeBlock's fill is routed through `--dsl-code-block-background` (default unchanged, so chat keeps its gray card), while the preview sets it to `transparent` so code sits on the pane's own background.
 
 ## Alternatives considered
 
@@ -28,8 +28,8 @@ Five small interaction defects around the right Sidebar and the document preview
 
 **A bare window-blur close for menus.** Closes the list on every app or tab switch; the `activeElement` gate scopes the close to the one case the document cannot see.
 
-**An inner code scroller for the horizontal axis.** Restoring `overflow-x: auto` on the `pre` keeps the banner still, but puts the horizontal scrollbar at the bottom of the whole block — unreachable in a long file — and both axes deliberately live in the document owner's scroller.
+**Keep code in the shared document scroller.** A child banner cannot cover its parent's native scrollbar. Giving the stable source wrapper both scroll axes keeps the scrollbar at the visible viewport edge below the adjacent banner, rather than at the end of a long code block.
 
 ## Consequences
 
-`planDropTab`'s factory parameter is new kit API any embedder may pass; `planSettle` already accepted an absent factory, which now also names the Sidebar's collapsed-state behavior. The `--dsl-code-block-background` variable and `data-code-block-banner` attribute are the code block's owner-styling seam; no shared stylesheet rule targets the attribute. Kit planner specs cover the backfilled self-split and its focus order; Sidebar store, service, and seat specs cover lazy seeding, pane-scoped page merges, and the empty collapsed layout; a Menu spec covers the gated blur close. The `ui-sidebar-right`, `ui-dockkit`, and `ui-sidebar-documentpreview` READMEs restate the rules.
+`planDropTab`'s factory parameter is new kit API any embedder may pass; `planSettle` already accepted an absent factory, which now also names the Sidebar's collapsed-state behavior. The `--dsl-code-block-background` variable keeps chat's default gray card while the preview uses the pane background, and `data-code-block-content` lets an owner materialize a dedicated source viewport without changing other CodeBlock layouts. Kit planner specs cover the backfilled self-split and its focus order; Sidebar store, service, and seat specs cover lazy seeding, pane-scoped page merges, and the empty collapsed layout; document-preview specs cover inner scrolling and line navigation; a Menu spec covers the gated blur close. The `ui-sidebar-right`, `ui-dockkit`, and `ui-sidebar-documentpreview` READMEs restate the rules.

+ 3 - 3
.agents/notes/implemented/feature/2026-09-09-sidebar-and-preview-interaction-polish.zh.md

@@ -18,7 +18,7 @@ Status: implemented
 
 **焦点进入 iframe 时关闭菜单。**[Menu.tsx](../../../../packages/client/ui-primitives/src/Menu.tsx) 增加 window `blur` 监听,以 `document.activeElement instanceof HTMLIFrameElement` 为门:焦点移动是跨源 iframe 内 pointerdown 留下的唯一信号,这道门也让应用或标签页切换不会误关列表。
 
-**代码预览钉住复制条并去掉卡片填充。**关闭折行时,[CodeBody.module.css](../../../../packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.module.css) 把渲染器设为 `max-content`,让吸附的复制条拥有完整滚动宽度可骑行,并以 `sticky; left: 0; width: 100cqw` 把它钉在文档滚动区上(预览正文设 `container-type: inline-size`)。共享 CodeBlock 的填充改经新变量 `--dsl-code-block-background`(默认值不变,会话保持灰色卡片),共享复制条带上惰性的 `data-code-block-banner` 钩子;预览把变量设为 `transparent`,代码于是坐在分栏自身的背景上。
+**代码预览把复制条与滚动源码分开,并去掉卡片填充。**共享 CodeBlock 用稳定的 `data-code-block-content` 节点包裹渲染后的源码;该节点默认使用 `display: contents`,因此既有消费者保持原布局。[CodeBody.module.css](../../../../packages/client/ui-sidebar-documentpreview/src/client/code/CodeBody.module.css) 将该节点实体化为复制条下方占满剩余高度的内部滚动区;代码渲染器通过 callback ref 报告该节点,使文档 owner 在 Slot 替换后仍能用当前节点恢复位置、分页和跳转代码行。共享 CodeBlock 的填充通过 `--dsl-code-block-background` 设置(默认值不变,会话保持灰色卡片),预览将它设为 `transparent`,代码因此直接使用分栏背景。
 
 ## Alternatives considered
 
@@ -28,8 +28,8 @@ Status: implemented
 
 **菜单用裸的 window blur 关闭。**每次应用或标签页切换都会关掉列表;`activeElement` 门把关闭收窄到父文档看不见的那一种情形。
 
-**代码横轴用内层滚动。**在 `pre` 上恢复 `overflow-x: auto` 能让复制条不动,但横向滚动条会落在整个代码块底部——长文件里够不着——而且两个轴本就有意放在文档 owner 的滚动区里。
+**让代码继续使用共享文档滚动区。**子级复制条无法覆盖父级的原生滚动条。让稳定的源码包装节点同时承载两个滚动轴,滚动条会停在复制条下方的可见视口边缘,而不是长代码块的末端。
 
 ## Consequences
 
-`planDropTab` 的工厂参数是任何嵌入方都可传的新套件 API;`planSettle` 本就接受缺省工厂,如今它同时命名了侧边栏的折叠态行为。`--dsl-code-block-background` 变量与 `data-code-block-banner` 属性是代码块的 owner 定制接缝;共享样式表没有任何规则指向该属性。套件 planner 规格覆盖带回填的本格分栏及其聚焦顺序;侧边栏 store、service 与 seat 规格覆盖惰性播种、格内页合并与折叠后的空布局;一条 Menu 规格覆盖带门的 blur 关闭。`ui-sidebar-right`、`ui-dockkit` 与 `ui-sidebar-documentpreview` 的 README 重述了这些规则。
+`planDropTab` 的工厂参数是任何嵌入方都可传的新套件 API;`planSettle` 本就接受缺省工厂,如今它同时命名了侧边栏的折叠态行为。`--dsl-code-block-background` 变量保留 Chat 默认的灰色卡片,Preview 则使用分栏背景;`data-code-block-content` 允许 owner 实体化专用源码视口,而不改变其它 CodeBlock 布局。套件 planner 规格覆盖带回填的本格分栏及其聚焦顺序;侧边栏 store、service 与 seat 规格覆盖惰性播种、格内页合并与折叠后的空布局;文档预览规格覆盖内部滚动与行跳转;一条 Menu 规格覆盖带门的 blur 关闭。`ui-sidebar-right`、`ui-dockkit` 与 `ui-sidebar-documentpreview` 的 README 重述了这些规则。

+ 3 - 3
.agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml → .agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.i18n.yaml

@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-09-content-sized-diagram-previews.md
-2026-09-09-content-sized-diagram-previews.md: ceb29238eec09f2dde312835dbf376855998a507
-2026-09-09-content-sized-diagram-previews.zh.md: 2689602bfe3469f53e532367877f7e49d52d3d5d
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md
+2026-09-10-guide-start-page-and-stat-pill-refinements.md: 987e46fd597b11046843a943c60187fdb1a769d0
+2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md: 590f18a2963507a8918a0aca30997aac23c6e477

+ 29 - 0
.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md

@@ -0,0 +1,29 @@
+# Agent Note: Guide start page and stat pill refinements
+
+Status: implemented
+
+English | [中文](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)
+
+## Problem
+
+The right Sidebar's guide tab was a bare list of entry capsules: no visual anchor above them, a capsule could only say its title, and an entry whose type registered no glyph rendered with no icon at all, so a mixed list read as broken rather than sparse. Separately, the session token-usage dialog under the composer printed a `Cache write 0 tok` row for sessions that never wrote cache.
+
+## Decision
+
+**The guide is a compass over self-describing capsules.** This refines the guide contracts in [Right Sidebar tab types and navigation](../architecture/2026-09-05-sidebar-tab-types-and-navigation.md) and [Sidebar text preview and file tree](2026-09-05-sidebar-text-preview-and-file-tree.md). [GuideBody.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx) draws a muted 56px compass hero over the entry capsules and no heading, as a browser start page shows its doors without a caption. [`SidebarRightGuideEntry`](../../../../packages/client/ui-sidebar-right/src/client/tab-registry.ts) gains an optional thunked `description` — read fresh on every render like `title`, so language changes need no re-registration. A capsule shows its description under the title only while the guide lists at most `MAX_DESCRIBED_ENTRIES` (4) entries; a longer list drops every description to stay light, so a type must stand on its title. The glyph rides the capsule's height: 22px beside a bare title, 26px beside two lines.
+
+**Icon-less entries fall back to a shipped cube placeholder.** The fallback is decided at the render site (`entry.icon ?? CubeGlyph`), not at registration, so every contributor — builtin or extension — gets it uniformly and a chain replacement of the body replaces the rule with it. `CubeGlyph` lives beside `CompassGlyph` in [GuideTitle.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx): an isometric box in 1.1px straight strokes with rounded joins on `currentColor`, drawn on `--dsw-alias-label-tertiary` — one step quieter than a registered glyph's ink — to mark the slot as unclaimed. The files type registers a description and the shared folder glyph in [definition.tsx](../../../../packages/client/ui-sidebar-files/src/client/definition.tsx).
+
+**The session usage dialog drops the zero cache-write row.** This refines [Composer session stats](2026-09-07-composer-session-stats-pills.md). [StatsPills.tsx](../../../../packages/client/ui-chat/src/client/chat/StatsPills.tsx) renders the `Cache write` row only when `cacheWriteTokens !== 0`, as the per-turn panel already drops its absent optional fields; the always-present buckets (input, cache read, output) keep their rows.
+
+## Alternatives considered
+
+**Register the cube in `ui-primitives`.** Its `icons/index.tsx` is the imported figma `ic_ds_*` set, and the cube has a single consumer; `CompassGlyph` set the precedent of package-local guide glyphs.
+
+**Default the icon at registration time.** A `?? default` inside the registry would hide the fallback from the body and make "registered no glyph" undetectable, losing the quieter placeholder ink; explicit render-site fallback keeps registrations honest.
+
+**Show `Cache write 0`.** A session on a provider that never writes cache would carry the row forever; zero here means "not a thing", not a measurement.
+
+## Consequences
+
+`description` is new pre-stable registry API; every consumer was updated (the files entry registers one). The 4-entry threshold is a shipped constant of the guide body, not configuration. Guide-body specs cover the placeholder (size and ink), the description threshold on both sides, and the registered-glyph path; chat-stats specs cover the dropped and present cache-write row. The `ui-sidebar-right` and `ui-sidebar-files` READMEs restate the guide rules.

+ 29 - 0
.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md

@@ -0,0 +1,29 @@
+# Agent Note: 引导起始页与统计 pill 的细化
+
+Status: implemented
+
+[English](2026-09-10-guide-start-page-and-stat-pill-refinements.md) | 中文
+
+## Problem
+
+右侧边栏的引导 tab 只是一列光秃秃的入口胶囊:上方没有视觉锚点,胶囊只能显示标题,而类型没注册图标的入口干脆不画图标,混合列表看起来像坏了而不是稀疏。另外,输入框下方的会话 token 用量弹窗会给从未写入缓存的会话打印一行 `Cache write 0 tok`。
+
+## Decision
+
+**引导页是罗盘压着能自我说明的胶囊。** 本决议细化[右侧 Sidebar tab 类型与导航](../architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md)和 [Sidebar 文本预览与文件树](2026-09-05-sidebar-text-preview-and-file-tree.zh.md)中的引导页约定。[GuideBody.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx) 在入口胶囊上方画一枚弱化的 56px 罗盘,不加标题,如同浏览器起始页不给它的入口配说明文字。[`SidebarRightGuideEntry`](../../../../packages/client/ui-sidebar-right/src/client/tab-registry.ts) 新增可选的 thunk 化 `description`——与 `title` 一样每次渲染重新读取,语言切换无需重新注册。仅当引导页列出的入口不超过 `MAX_DESCRIBED_ENTRIES`(4)个时,胶囊才在标题下显示描述;更长的列表去掉所有描述以保持轻盈,所以类型必须靠标题立得住。图标随胶囊高度变化:单行标题旁 22px,两行旁 26px。
+
+**没有图标的入口回退到内置的立方体占位符。** 回退在渲染点决定(`entry.icon ?? CubeGlyph`)而不在注册时,因此每个贡献者——内置或扩展——得到统一的占位符,链式替换 body 时规则随之整体替换。`CubeGlyph` 与 `CompassGlyph` 一起放在 [GuideTitle.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx):一只等距视角的盒子,1.1px 直线描边、圆角拼接、走 `currentColor`,用 `--dsw-alias-label-tertiary` 着色——比注册图标的墨色浅一档——标记这个槽位无人认领。files 类型在 [definition.tsx](../../../../packages/client/ui-sidebar-files/src/client/definition.tsx) 注册了描述和共享的文件夹图标。
+
+**会话用量弹窗去掉为零的缓存写入行。** 本决议细化[输入框下的会话统计](2026-09-07-composer-session-stats-pills.zh.md)。[StatsPills.tsx](../../../../packages/client/ui-chat/src/client/chat/StatsPills.tsx) 仅当 `cacheWriteTokens !== 0` 时渲染 `Cache write` 行,与 per-turn 面板去掉缺失可选字段的做法一致;始终存在的桶(输入、缓存读取、输出)保留各自的行。
+
+## Alternatives considered
+
+**把立方体注册进 `ui-primitives`。** 其 `icons/index.tsx` 是导入的 figma `ic_ds_*` 集合,而立方体只有一个消费者;`CompassGlyph` 已开了引导图标包内自持的先例。
+
+**在注册时默认图标。** 注册表内部的 `?? default` 会对 body 隐藏回退,让"没注册图标"无法辨认,浅色占位墨色随之丢失;显式的渲染点回退让注册保持诚实。
+
+**显示 `Cache write 0`。** 用不写缓存的 provider 的会话会永远挂着这一行;这里的零意味着"没有这回事",不是一次测量。
+
+## Consequences
+
+`description` 是新的 pre-stable 注册表 API;所有消费者已同步更新(files 入口注册了一个)。4 个入口的阈值是引导 body 的内置常量,不是配置。guide-body 用例覆盖占位符(尺寸与墨色)、描述阈值两侧和已注册图标路径;chat-stats 用例覆盖缓存写入行的去除与保留。`ui-sidebar-right` 与 `ui-sidebar-files` 的 README 重述了引导页规则。

+ 1 - 5
THIRD_PARTY_NOTICES.md

@@ -55,7 +55,6 @@ External packages installed for runtime use or distributed inside the prebuilt b
 | [`@shikijs/langs`](https://github.com/shikijs/shiki) | MIT |
 | [`@standard-schema/spec`](https://github.com/standard-schema/standard-schema) | MIT |
 | [`@tanstack/react-virtual`](https://github.com/TanStack/virtual) | MIT |
-| [`@viz-js/viz`](https://github.com/mdaines/viz-js) | MIT |
 | [`@vscode/ripgrep`](https://github.com/microsoft/vscode-ripgrep) | MIT |
 | [`@xterm/headless`](https://github.com/xtermjs/xterm.js) | MIT |
 | [`@yarnpkg/parsers`](https://github.com/yarnpkg/berry) | BSD-2-Clause |
@@ -67,7 +66,6 @@ External packages installed for runtime use or distributed inside the prebuilt b
 | [`commander`](https://github.com/tj/commander.js) | MIT |
 | [`compression`](https://github.com/expressjs/compression) | MIT |
 | [`diff`](https://github.com/kpdecker/jsdiff) | BSD-3-Clause |
-| [`dompurify`](https://github.com/cure53/DOMPurify) | (MPL-2.0 OR Apache-2.0) |
 | [`e2b`](https://github.com/e2b-dev/e2b) | MIT |
 | [`electron-updater`](https://github.com/electron-userland/electron-builder) | MIT |
 | [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT |
@@ -81,7 +79,6 @@ External packages installed for runtime use or distributed inside the prebuilt b
 | [`mdast-util-from-markdown`](https://github.com/syntax-tree/mdast-util-from-markdown) | MIT |
 | [`mdast-util-gfm`](https://github.com/syntax-tree/mdast-util-gfm) | MIT |
 | [`mdast-util-math`](https://github.com/syntax-tree/mdast-util-math) | MIT |
-| [`mermaid`](https://github.com/mermaid-js/mermaid) | MIT |
 | [`micromark-core-commonmark`](https://github.com/micromark/micromark/tree/main/packages/micromark-core-commonmark) | MIT |
 | [`micromark-extension-gfm`](https://github.com/micromark/micromark-extension-gfm) | MIT |
 | [`micromark-extension-math`](https://github.com/micromark/micromark-extension-math) | MIT |
@@ -115,8 +112,6 @@ External packages installed for runtime use or distributed inside the prebuilt b
 | [`zod`](https://github.com/colinhacks/zod) | MIT |
 | [`zustand`](https://github.com/pmndrs/zustand) | MIT |
 
-The Markdown preview distribution also contains Graphviz 16.0.0 (EPL-2.0), Expat 2.8.4 (MIT), and Emscripten 5.0.7 runtime code (MIT/NCSA) inside `@viz-js/viz` 3.30.0. The wrapper's MIT metadata does not relicense these components. [Preview notices](packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt) preserve their full license texts and Graphviz source availability, together with Mermaid's MIT and DOMPurify's selected Apache-2.0 terms. The UI primitives npm package includes this file; the Web build emits it as `preview-third-party-notices.txt`.
-
 pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification:
 
 - `@yao-pkg/pkg@6.21.0` — [`patches/@yao-pkg__pkg@6.21.0.patch`](patches/@yao-pkg__pkg@6.21.0.patch)
@@ -194,6 +189,7 @@ External packages **directly declared** for development, tests, types, or toolin
 | [`jsdom`](https://github.com/jsdom/jsdom) | MIT |
 | [`lefthook`](https://github.com/evilmartians/lefthook) | MIT |
 | [`lightningcss`](https://github.com/parcel-bundler/lightningcss) | MPL-2.0 |
+| [`mermaid`](https://github.com/mermaid-js/mermaid) | MIT |
 | [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT |
 | [`msgpackr`](http://github.com/kriszyp/msgpackr) | MIT |
 | [`oxlint`](https://github.com/oxc-project/oxc) | MIT |

+ 1 - 1
apps/cli/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 apps/cli/README.md
 README.md: 2e74ef68cf0b8487083a2e2af0f5175c78ec5212
-README.zh.md: 85a75f4c025e1bfe1461b9ffdaca533d308318ba
+README.zh.md: 553f28f3af78b18658701449678de80b086dbb15

+ 8 - 8
apps/cli/README.zh.md

@@ -2,7 +2,7 @@
 
 [English](README.md) | 中文
 
-`dsh` 是唯一受支持的 Node 应用启动器;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。SDK 与 ACP 都是 profile,而不是独立的公开 bin。Python 运行时 wheel 会打包同一个命令;SDK 默认使用 `sdk`,极简示例选择 `sdk-minimal`。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。
+`dsh` 是唯一受支持的 Node 应用启动器;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。SDK 与 ACP(Agent Client Protocol)都是 profile,而不是独立的公开可执行命令。Python 运行时 wheel 包中也包含同一个命令;SDK 默认使用 `sdk`,极简示例选择 `sdk-minimal`。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。
 
 ## 入口模式
 
@@ -10,14 +10,14 @@
 |---|---|
 | `dsh --profile <name>` | 启动位于 `$DSH_HOME/profiles/<name>` 的指定 profile。 |
 | `dsh --profile <name> --from-default-profile <template>` | 从随附模板创建新的自定义 profile,然后启动它。 |
-| `dsh --profile acp` | 通过 ACP stdio 为自动化 client 提供服务,直至断开连接。 |
+| `dsh --profile acp` | 通过 ACP stdio 为自动化客户端提供服务,直至断开连接。 |
 | `dsh --profile headless "job"` | 运行一个全新的持久化会话,打印最终答案并退出。 |
-| `dsh --profile sdk` | 通过 JSON-RPC stdio 为 SDK client 提供服务,直至关闭或断开连接。 |
-| `dsh --profile sdk-minimal` | 以独立极简 agent 配置树为 SDK client 提供服务。 |
+| `dsh --profile sdk` | 通过 JSON-RPC stdio 为 SDK 客户端提供服务,直至关闭或断开连接。 |
+| `dsh --profile sdk-minimal` | 以独立极简 agent(智能体)配置树为 SDK 客户端提供服务。 |
 | `dsh web` | `--profile web` 的别名。 |
 | `dsh plugin --profile <name> <pnpm args>` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 |
 
-运行命令时所在的目录将作为默认 workspace 根目录。`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 在首次使用时会从随附模板自动初始化。使用 `--from-default-profile` 可以基于这些模板之一,在尚未使用的非内置名称处创建其他 profile;通过 `dsh plugin` 则可以初始化一个以 base 为基础的 profile。`desktop` 名称保留给 Electron 持有的 profile,因此 CLI 会拒绝针对它的启动、配置 dump 和插件管理请求。
+运行命令时所在的目录将作为默认 workspace 根目录。`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 在首次使用时会从随附模板自动初始化。使用 `--from-default-profile` 可以基于这些模板之一,在尚未使用的非内置名称处创建其他 profile;通过 `dsh plugin` 则可以初始化一个以 base 为基础的 profile。`desktop` 名称保留给 Electron 持有的 profile,因此 CLI(命令行界面)会拒绝针对它的启动、配置 dump 和插件管理请求。
 
 ## 应用参数
 
@@ -45,11 +45,11 @@ profile 目录包含一个 `package.json`,其中记录树外插件依赖,以
 
 使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。
 
-层的确切优先级、flag、关闭行为、部署默认值和源码执行方式,以 [CLI(命令行界面)行为参考](reference/README.zh.md)为准。
+层的确切优先级、flag、关闭行为、部署默认值和源码执行方式,以 [CLI 行为参考](reference/README.zh.md)为准。
 
-## 可选 Overlay
+## 可选覆盖层
 
-`config/examples/` 交付 GitHub 评审 webhook、会话内 Schedule、记忆 MCP 服务与运行时 Cordis 工具的可选 overlay。它们绝不属于默认 profile;安装与安全说明由[用户指南](../../docs/user/guide/index.zh.md)和[开发实战指南](../../docs/user/develop/practice/index.zh.md)负责。
+`config/examples/` 交付 GitHub 评审 webhook、会话内 Schedule、记忆 MCP 服务器与运行时 Cordis 工具的可选覆盖层。它们绝不属于默认 profile;设置与安全说明由[用户指南](../../docs/user/guide/index.zh.md)和[开发实战指南](../../docs/user/develop/practice/index.zh.md)负责。
 
 ## 开发
 

+ 1 - 1
apps/cli/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh",
   "description": "dsh CLI: profile boot, plugin management, and the browser UI alias",
-  "version": "0.1.5-alpha.1",
+  "version": "0.1.5-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 2 - 2
apps/cli/reference/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 apps/cli/reference/README.md
-README.md: 99ef8310cd1316e9ee4ade55fcac00766ae1e821
-README.zh.md: aa446d539a5b666e7745f459b5a014b22e72f90e
+README.md: 5a92052eb24c8af432c2ede371a2ff2bc84f57c0
+README.zh.md: a9268ad4920a7d5a72a4b0ead9b2672d22e10805

+ 3 - 3
apps/cli/reference/README.md

@@ -10,7 +10,7 @@ This reference defines the profile, web-alias, plugin-management, and config-dum
 
 Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-sdk-minimal`, `@deepseek-ai/dsh-acp-app`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules`. Plain Node installations place one healed symlink there per dependency-closure package. A pkg executable instead places a real ESM proxy that mirrors explicit exports and re-exports the virtual package URL, because operating-system symlinks cannot enter pkg's `/snapshot` filesystem. Every launch also links packages carried only by selected external bundles through a dsh-owned directory into the current profile's `node_modules`; existing pnpm entries win, and each profile owns its links independently.
 
-The `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` profiles auto-initialize from shipped templates on first use (`web`: base + web-app with live patches; `headless`: base + headless with startup-only patches; `sdk`: base + sdk-app with startup-only patches; `sdk-minimal`: its standalone bundle with startup-only patches; `acp`: base + acp-app with startup-only patches). Any other missing profile fails loud with a hint to run `dsh plugin --profile <name> add <package>`.
+The `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` profiles auto-initialize from shipped templates on first use (`web`: base + web-app with live patches; `headless`: base + headless with startup-only patches; `sdk`: base + sdk-app with startup-only patches; `sdk-minimal`: its standalone bundle with startup-only patches; `acp`: base + acp-app with startup-only patches). Any other missing profile fails loudly with a hint to run `dsh plugin --profile <name> add <package>`.
 
 `dsh --profile <name> --from-default-profile <template>` initializes a new custom target from one of those five shipped templates before boot. The target name cannot be a shipped profile name, and its complete profile directory must not exist. The launcher claims that directory exclusively, so residual files and another concurrent creator are rejected without modification. It copies the template's current bundle list and `patchReload` value into a new manifest with empty dependencies and an empty user patch. It does not read the local profile named by `<template>`, copy its dependencies or patch, or persist an inheritance field; later template-list changes do not rewrite the new profile. The in-box bundles named by that copied list still resolve from the current dsh installation. A successful initialization adds no launcher output.
 
@@ -37,7 +37,7 @@ The shipped apps own these command lines:
 | `headless` | the task text, as the positional argument |
 | `sdk` | no options; stdio carries the JSON-RPC protocol |
 | `sdk-minimal` | no options; stdio carries the same JSON-RPC protocol |
-| `acp` | no options; stdio carries Agent Client Protocol |
+| `acp` | no options; stdio carries ACP (Agent Client Protocol) |
 
 A one-shot task (`dsh --profile headless "run the tests"`) creates one fresh persisted Agent through the core registry, submits the task, waits for quiescence, and flushes the Session before deriving the last non-empty assistant text and final `turn/end` reason from its durable interval. It streams non-empty provider reasoning deltas to stderr under a `dsh: reasoning:` heading, prints only the final text on stdout, and exits 0 for `completed`, else 1; a successful response with no reasoning leaves stderr empty. An invocation with no task is a usage error from that app. The shipped headless profile mounts no browser Connection, HTTP server, Web runtime, or browser client, and opens no listening port.
 
@@ -100,7 +100,7 @@ New sessions in base-backed profiles default to the `workspace-write` permission
 
 The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search` and `web_fetch`, the public-only HTTP fetch provider, opt-in DeepSeek session-log upload, and feedback-gated OTel upload for all users. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`. Enabled fetch calls run in every sandbox and approval mode without per-call confirmation; the provider rejects non-public destinations before connecting. The Web app disables the base tool row and exposes the same tools through its `cordis`, `ptc`, and `standard` agent presets.
 
-Feedback is recorded in the Session log without starting model work. Enable the [DeepSeek session-log contributor](../../../packages/session/session-log-deepseek/README.md) to send complete unaccepted log suffixes with subsequent DeepSeek requests, including configured gateways. [OTel session upload](../../../packages/session/session-telemetry-otel/README.md) applies to all users and providers, including `deepseek-official`, without requiring a request header. The base defaults to `FEEDBACK_ONLY`: new own text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including stored context; later records wait for the next explicit feedback. Inherited parent feedback does not authorize a fork. Requests, restoration, mount, and HMR do not trigger capture. SDK batching may finish an authorized upload without further interaction or model work. `DSH_TELEMETRY_MODE=DISABLED` disables OTel delivery; `FULL` is rejected, and any non-empty `DSH_TELEMETRY_DISABLED` disables its row. `DSH_TELEMETRY_OTLP_URL` selects the collector. Handoff is best-effort, not collector acceptance; no durable outbox or retry guarantee is provided. These OTel settings do not enable or disable the DeepSeek contribution. Neither path changes model input, but exports can include message text, tool arguments and results, and workspace paths.
+Feedback is recorded in the Session log without starting model work. Enable the [DeepSeek session-log contributor](../../../packages/session/session-log-deepseek/README.md) to send complete unaccepted log suffixes with subsequent DeepSeek requests, including requests sent through configured gateways. [OTel session upload](../../../packages/session/session-telemetry-otel/README.md) applies to all users and providers, including `deepseek-official`, without requiring a request header. The base defaults to `FEEDBACK_ONLY`: new own text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including stored context; later records wait for the next explicit feedback. Inherited parent feedback does not authorize a fork. Requests, restoration, mount, and HMR do not trigger capture. SDK batching may finish an authorized upload without further interaction or model work. `DSH_TELEMETRY_MODE=DISABLED` disables OTel delivery; `FULL` is rejected, and any non-empty `DSH_TELEMETRY_DISABLED` disables its row. `DSH_TELEMETRY_OTLP_URL` selects the collector. Handoff is best-effort, not collector acceptance; no durable outbox or retry guarantee is provided. These OTel settings do not enable or disable the DeepSeek contribution. Neither path changes model input, but exports can include message text, tool arguments and results, and workspace paths.
 
 Install external plugin bundles through `dsh plugin --profile <name> add <package-or-git-spec>`. The installed package owns its dependencies and contributes its declared `cordis.patch.yml` layer. The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox.
 

+ 10 - 10
apps/cli/reference/README.zh.md

@@ -10,13 +10,13 @@
 
 `dsh --profile <name>` 启动位于 `$DSH_HOME/profiles/<name>` 的 profile。生效配置树以空根节点为起点,依次叠加 profile manifest(元数据清单)的 `dsh.profile.bundles` 列表中指定的各组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(这是各 profile 共享的机器本地偏好,因此优先于逐 profile 配置层),以及按 argv 顺序指定的各个 `--patch <path>` 覆盖层。对同一配置行,后应用的层优先。patch 会替换目标行的整个 `config` 值,而不是深度合并其中的键;patch 也可以插入新行。`dsh.profile.patchReload` 可选择 `live` patch 文件监视或 `startup` 单次加载;自定义 profile 省略该值时默认使用 `live`。配置解析、schema 校验、模块解析或插件启动失败时,系统会报告错误并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。
 
-组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-sdk-minimal`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。普通 Node 安装会为依赖闭包中的每个包放置并修复一个符号链接。pkg 可执行程序则放置真实 ESM 代理,镜像显式 exports 并重新导出虚拟包 URL,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统。每次启动还会把仅由所选外部 bundle 携带的包经 dsh 自有目录链接到当前 profile 的 `node_modules`;已有 pnpm 条目优先,且每个 profile 独立拥有自己的链接。
+组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-sdk-minimal`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。普通 Node 安装会为依赖闭包中的每个包放置并修复一个符号链接。pkg 可执行程序则放置真实 ESM 代理,镜像显式 exports 并重新导出虚拟包 URL,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统。每次启动还会把仅由所选外部组合包携带的包经 dsh 自有目录链接到当前 profile 的 `node_modules`;已有 pnpm 条目优先,且每个 profile 独立拥有自己的链接。
 
 `web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app,实时应用 patch;`headless`:base + headless,只在启动时应用 patch;`sdk`:base + sdk-app,只在启动时应用 patch;`sdk-minimal`:独立组合包,只在启动时应用 patch;`acp`:base + acp-app,只在启动时应用 patch)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile <name> add <package>`。
 
-`dsh --profile <name> --from-default-profile <template>` 会在启动前,从上述五个随附模板之一初始化新的自定义目标。目标名称不能是随附 profile 名称,并且完整的目标 profile 目录必须不存在。launcher 会以独占方式领取该目录,因此残留文件和另一个并发创建者都会在不作修改的情况下被拒绝。它把模板当前的 bundle 列表和 `patchReload` 值复制进一份依赖为空、用户 patch 为空的新 manifest。它不会读取 `<template>` 指定的本地同名 profile,不会复制其依赖或 patch,也不会持久化继承字段;模板列表之后的变化不会改写新 profile。复制列表中指名的内置 bundle 仍从当前 dsh 安装目录解析。初始化成功不会增加 launcher 输出。
+`dsh --profile <name> --from-default-profile <template>` 会在启动前,从上述五个随附模板之一初始化新的自定义目标。目标名称不能是随附 profile 名称,并且完整的目标 profile 目录必须不存在。launcher 会以独占方式领取该目录,因此残留文件和另一个并发创建者都会在不作修改的情况下被拒绝。它把模板当前的组合包列表和 `patchReload` 值复制进一份依赖为空、用户 patch 为空的新 manifest。它不会读取 `<template>` 指定的本地同名 profile,不会复制其依赖或 patch,也不会持久化继承字段;模板列表之后的变化不会改写新 profile。复制列表中指名的内置组合包仍从当前 dsh 安装目录解析。初始化成功不会增加 launcher 输出。
 
-profile 已经存在时,`--from-default-profile` 会被拒绝,且不会修改或启动它;去掉该选项即可使用它。残留的目标目录同样会被原样保留,此时必须改用另一个 profile 名称。未知模板或随附目标名称会在创建目标之前失败;未知模板的诊断会列出有效模板。初始化在 bundle 解析和应用启动之前提交,因此后续失败仍会把新 profile 留在磁盘上,重试时需要去掉创建选项。`--dump-config` 和 `--dump-default-config` 接受该选项:它们初始化目标并打印所请求的配置树,但不启动应用。
+profile 已经存在时,`--from-default-profile` 会被拒绝,且不会修改或启动它;去掉该选项即可使用它。残留的目标目录同样会被原样保留,此时必须改用另一个 profile 名称。未知模板或随附目标名称会在创建目标之前失败;未知模板的诊断会列出有效模板。初始化在组合包解析和应用启动之前提交,因此后续失败仍会把新 profile 留在磁盘上,重试时需要去掉创建选项。`--dump-config` 和 `--dump-default-config` 接受该选项:它们初始化目标并打印所请求的配置树,但不启动应用。
 
 ```sh
 dsh --profile rescue --from-default-profile web
@@ -39,9 +39,9 @@ dsh --profile rescue
 | `headless` | 任务文本,作为位置参数 |
 | `sdk` | 无选项;stdio 携带 JSON-RPC 协议 |
 | `sdk-minimal` | 无选项;stdio 携带相同的 JSON-RPC 协议 |
-| `acp` | 无选项;stdio 携带 Agent Client Protocol |
+| `acp` | 无选项;stdio 携带 ACP(Agent Client Protocol) |
 
-一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent(智能体),提交任务、等待完全停稳并对会话执行 flush,再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 `dsh: reasoning:` 标题下将非空的提供方推理分片流式写入 stderr,只在 stdout 打印最终文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出;没有推理内容的成功响应会保持 stderr 为空。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载浏览器 Connection、HTTP 服务器、Web 运行时或浏览器客户端,也不会打开监听端口。
+一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent(智能体),提交任务、等待完全停稳并对会话执行 flush,再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 `dsh: reasoning:` 标题下将非空的提供方推理(reasoning)增量流式写入 stderr,只在 stdout 打印最终文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出;没有推理内容的成功响应会保持 stderr 为空。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载浏览器 Connection、HTTP 服务器、Web 运行时或浏览器客户端,也不会打开监听端口。
 
 可在不启动的情况下检查组合出的配置树:
 
@@ -56,7 +56,7 @@ dsh --profile web --patch ./extra.yml --dump-config
 
 `dsh plugin --profile <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,系统都会根据当前安装状态更新 `dsh.profile.bundles`:如果某项依赖解析到的包在 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,该依赖就会加入配置层栈;如果某项依赖在 `update` 后获得该声明,也会随即激活。没有组合包声明的依赖仍作为普通依赖保留,并显示一次性警告;已移除的依赖则从配置层栈中删除。
 
-Codex 与 Claude Code subagent provider 是两个彼此独立的可选 Bundle。可以只添加一个包、在同一命令中添加两个包,或独立移除任一包:
+Codex 与 Claude Code subagent 提供方是两个彼此独立的可选组合包。可以只添加一个包、在同一命令中添加两个包,或独立移除任一包:
 
 ```sh
 dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
@@ -66,7 +66,7 @@ dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
 dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-claude-code
 ```
 
-pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑通过热重载生效。下一次启动时,每个已安装 Bundle 只注册自己的休眠 Host provider;还须在复制出的 Preset 中单独启用对应工具行,新 Agent 才能看到该工具。[Codex provider README](../../../packages/subagent/subagent-codex/README.zh.md)与 [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.zh.md)负责可执行文件、身份验证、载荷与失败细节;[base Bundle 参考](../../../packages/bundle/base/README.zh.md)负责默认依赖闭包。
+pnpm 操作成功后会改变磁盘上的 Profile manifest 与组合包列表;正在运行的 Profile 会保留本次启动时的组合包集合。添加、移除或更新组合包后须重启该 Profile。这个启动边界只适用于组合包成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑通过热重载生效。下一次启动时,每个已安装组合包只注册自己的休眠 Host 提供方;还须在复制出的 Preset 中单独启用对应工具行,新 Agent 才能看到该工具。[Codex provider README](../../../packages/subagent/subagent-codex/README.zh.md) 与 [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.zh.md) 负责可执行文件、身份验证、载荷与失败细节;[base 组合包参考](../../../packages/bundle/base/README.zh.md) 负责默认依赖闭包。
 
 ```sh
 dsh plugin --profile tui add github:deepseek-harness/turtle-ui
@@ -92,7 +92,7 @@ dsh web --help
 
 进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT` 或 `SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。
 
-基于 base 的模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。独立的 `sdk-minimal` profile 把运行命令时所在的目录作为沙箱策略根目录,但刻意省略文件系统工具、指令发现与 SQLite。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose(资源释放)所有实时监视器。
+基于 base 的模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。独立的 `sdk-minimal` profile 把运行命令时所在的目录作为沙箱策略根目录,但刻意省略文件系统工具、指令发现与 SQLite。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose 所有实时监视器。
 
 基于 base 的 profile 中,新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。独立的 `sdk-minimal` 配置树则固定为 `danger-full-access`,且不挂载 approval 或权限 settings 服务。
 
@@ -102,9 +102,9 @@ dsh web --help
 
 基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和 `web_fetch`、仅限公网的 HTTP fetch 提供方,需主动开启的 DeepSeek 会话日志上传,以及面向所有用户的反馈门控 OTel 上传。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`。已启用的抓取调用会在所有 sandbox 与审批模式下执行,无需逐次确认;提供方会在连接前拒绝非公开目的地址。Web app 会禁用 base 工具配置项,再通过 `cordis`、`ptc` 与 `standard` agent preset 暴露相同工具。
 
-反馈记录在会话日志中,不会启动模型工作。开启 [DeepSeek 会话日志贡献器](../../../packages/session/session-log-deepseek/README.zh.md)后,后续 DeepSeek 请求会发送尚未确认的完整日志后缀,包括发往配置网关的请求。[OTel 会话上传](../../../packages/session/session-telemetry-otel/README.zh.md)适用于所有用户和提供方,包括 `deepseek-official`,无需请求头。基础配置默认使用 `FEEDBACK_ONLY`:新的自身文本反馈、消息评分、编辑与撤回会释放截至该事件的完整权威日志前缀,包含存储的上下文;后续记录等待下一次显式反馈。继承的父级反馈不构成 fork 的授权。请求、恢复、挂载和 HMR 不触发捕获。SDK 批处理可完成已授权上传,无需进一步交互或模型工作。`DSH_TELEMETRY_MODE=DISABLED` 禁止 OTel 投递;`FULL` 被拒绝,任何非空的 `DSH_TELEMETRY_DISABLED` 都会禁用其配置行。`DSH_TELEMETRY_OTLP_URL` 选择采集端。交接尽力而为,不代表采集端接受;不提供持久化 outbox 或重试保证。这些 OTel 设置不会开启或关闭 DeepSeek 贡献。两条路径都不改变模型输入,但导出可能包含消息文本、工具参数和结果,以及工作区路径。
+反馈记录在会话日志中,不会启动模型工作。开启 [DeepSeek 会话日志贡献器](../../../packages/session/session-log-deepseek/README.zh.md)后,后续 DeepSeek 请求会发送尚未确认接收的完整日志后缀,包括经已配置网关发送的请求。[OTel 会话上传](../../../packages/session/session-telemetry-otel/README.zh.md)适用于所有用户和提供方,包括 `deepseek-official`,无需请求头。基础配置默认使用 `FEEDBACK_ONLY`:新的自身文本反馈、消息评分、编辑与撤回会释放截至该事件的完整规范日志前缀,包含存储的上下文;后续记录等待下一次显式反馈。继承的父级反馈不构成 fork 的授权。请求、恢复、挂载和 HMR 不触发捕获。SDK 批处理可完成已授权上传,无需进一步交互或模型工作。`DSH_TELEMETRY_MODE=DISABLED` 禁止 OTel 投递;`FULL` 被拒绝,任何非空的 `DSH_TELEMETRY_DISABLED` 都会禁用其配置行。`DSH_TELEMETRY_OTLP_URL` 选择采集端。交接尽力而为,不代表采集端接受;不提供持久化 outbox 或重试保证。这些 OTel 设置不会开启或关闭 DeepSeek 贡献。两条路径都不改变模型输入,但导出可能包含消息文本、工具参数和结果,以及工作区路径。
 
-通过 `dsh plugin --profile <name> add <package-or-git-spec>` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml` 层。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent(智能体)沙箱之外的受信任可执行代码。
+通过 `dsh plugin --profile <name> add <package-or-git-spec>` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml` 层。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent 沙箱之外的受信任可执行代码。
 
 <a id="source-execution"></a>
 ## 源码执行

+ 1 - 1
apps/desktop-host/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-desktop-host",
   "description": "Private upstream-Node host process for the Electron desktop application",
-  "version": "0.1.5-alpha.1",
+  "version": "0.1.5-rc.1",
   "private": true,
   "license": "MIT",
   "type": "module",

+ 1 - 1
apps/desktop/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 apps/desktop/README.md
 README.md: cf350a9d5e9577f5cf52c4371b32f16e278133f9
-README.zh.md: df48eb5903e21cae9ff041626c28d41d12e5c632
+README.zh.md: aa870c2a74f3b74d3f6de921c4bc5ef04fbfad3f

File diff suppressed because it is too large
+ 18 - 18
apps/desktop/README.zh.md


+ 1 - 1
apps/desktop/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-desktop",
   "description": "Electron desktop shell for an isolated pnpm-installed dsh runtime",
-  "version": "0.1.5-alpha.1",
+  "version": "0.1.5-rc.1",
   "private": true,
   "license": "MIT",
   "type": "module",

+ 1 - 1
apps/web/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-web-frontend",
   "description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web",
-  "version": "0.1.5-alpha.1",
+  "version": "0.1.5-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 2 - 2
apps/web/tests/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 apps/web/tests/README.md
-README.md: a3503e66d780e62562348b02830517381d21df92
-README.zh.md: a9039bbd0444a0786cd419c1e6aba0c3504e26d3
+README.md: 883d93d746afa6bac0b67f2c2d6bc6c03b31553a
+README.zh.md: 7a232329b890a669240272aada3a5bafff9efd99

+ 7 - 31
apps/web/tests/README.md

@@ -2,11 +2,7 @@
 
 English | [中文](README.zh.md)
 
-These tests boot the real web composition in-process and drive it with a real
-Chromium over real HTTP. The lane's mechanics — modes, fixtures, goldens, and
-the deliberate composition divergences from `dsh web` — are documented in
-[`scaffold.ts`](scaffold.ts) and the
-[browser e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md).
+These tests boot the real web composition in-process and drive it with a real Chromium over real HTTP. The lane's mechanics — modes, fixtures, goldens, and the deliberate composition divergences from `dsh web` — are documented in [`scaffold.ts`](scaffold.ts) and the [browser e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md).
 
 ## Completion observations
 
@@ -14,34 +10,14 @@ State-sensitive cases use Workspace, admission, attachment, and model-stream bar
 
 ## These are Host-face tests
 
-They type-check in the root `tsconfig.host.json`, not in the Client aggregate,
-because they read Host services directly: `ctx.connection`, the Host
-`SessionStore`, and `ctx.sessionProjectionCache`. Driving a browser at runtime does
-not make a file part of the Client program — the two faces merge cordis
-`Context` under the same keys with different services, so one program cannot see
-both. Moving these files into the Client aggregate makes every Host-service
-access fail to compile.
+They type-check in the root `tsconfig.host.json`, not in the Client aggregate, because they read Host services directly: `ctx.connection`, the Host `SessionStore`, and `ctx.sessionProjectionCache`. Driving a browser at runtime does not make a file part of the Client program — the two faces merge Cordis `Context` under the same keys with different services, so one program cannot see both. Moving these files into the Client aggregate makes every Host-service access fail to compile.
 
 ## Do not import `@deepseek-ai/dsh-client-*` here
 
-Importing a Client package — a value or a type — pulls its whole TypeScript
-project, and every project it references, into the **Host build graph**. That has
-bitten this lane once already: four Client consumer packages reference
-`api/remotes`' Client face, which cannot compile until Host tsdown has generated
-`@deepseek-ai/dsh-goal/remote`, so the Host build phase ended up waiting on an
-artifact it produces itself.
-
-When a scenario needs a Client-owned constant or pure function, mirror it here
-instead, next to the commented-out import that names the source module. A drift
-then surfaces as a missed selector or a stale mirrored value — a loud failure,
-never a silent pass. `scaffold.ts` follows this rule for the welcome-notice
-namespace, acknowledgement field, version, and asserted Chinese copy.
-
-One kind of Client import stands. `assembled-boot.ts` drives the shell itself, so
-it imports `AppWebEntry` from `@deepseek-ai/dsh-client-web` and the boot-manifest
-type from `@deepseek-ai/dsh-client-modules/client`: booting the real shell is what
-that harness is for, and both packages are already in the Host graph. The chat
-scenarios mirror `conversationContextKey` in `support.ts` instead of importing
-its Client owner.
+Importing a Client package — a value or a type — pulls its whole TypeScript project, and every project it references, into the **Host build graph**. That has bitten this lane once already: four Client consumer packages reference `api/remotes`' Client face, which cannot compile until Host tsdown has generated `@deepseek-ai/dsh-goal/remote`, so the Host build phase ended up waiting on an artifact it produces itself.
+
+When a scenario needs a Client-owned constant or pure function, mirror it here instead, next to the commented-out import that names the source module. A drift then surfaces as a missed selector or a stale mirrored value — a loud failure, never a silent pass. `scaffold.ts` follows this rule for the welcome-notice namespace, acknowledgement field, version, and asserted Chinese copy.
+
+One kind of Client import stands. `assembled-boot.ts` drives the shell itself, so it imports `AppWebEntry` from `@deepseek-ai/dsh-client-web` and the boot-manifest type from `@deepseek-ai/dsh-client-modules/client`: booting the real shell is what that harness is for, and both packages are already in the Host graph. The chat scenarios mirror `conversationContextKey` in `support.ts` instead of importing its Client owner.
 
 Nothing mechanically enforces this rule; keep it in review.

+ 5 - 21
apps/web/tests/README.zh.md

@@ -2,10 +2,7 @@
 
 [English](README.md) | 中文
 
-这些测试在进程内启动真实的 web 组合,并用真实 Chromium 通过真实 HTTP 驱动它。该 lane
-的运行机制——模式、fixture、golden,以及与 `dsh web` 之间刻意保留的组合差异——记录在
-[`scaffold.ts`](scaffold.ts) 和
-[浏览器 e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md)中。
+这些测试在进程内启动真实的 web 组合,并用真实 Chromium 通过真实 HTTP 驱动它。该 lane 的运行机制——模式、fixture(测试前置数据)、golden,以及与 `dsh web` 之间刻意保留的组合差异——记录在 [`scaffold.ts`](scaffold.ts) 和 [浏览器 e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md) 中。
 
 ## 完成状态观察
 
@@ -13,27 +10,14 @@
 
 ## 这些是 Host 面的测试
 
-它们在根 `tsconfig.host.json` 中做类型检查,而不在 Client aggregate 中,因为它们直接读取
-Host 服务:`ctx.connection`、Host 侧 `SessionStore` 与 `ctx.sessionProjectionCache`。运行时驱动
-浏览器并不使一个文件成为 Client 程序的一部分——两个 face 在相同的键上以不同服务合并 cordis
-`Context`,因此单个程序无法同时看见两者。把这些文件挪进 Client aggregate 会让每一处
-Host 服务访问都无法编译。
+它们在根 `tsconfig.host.json` 中做类型检查,而不在 Client aggregate 中,因为它们直接读取 Host 服务:`ctx.connection`、Host 侧 `SessionStore` 与 `ctx.sessionProjectionCache`。运行时驱动浏览器并不使一个文件成为 Client 程序的一部分——两个 face 在相同的键上以不同服务合并 Cordis `Context`,因此单个程序无法同时看见两者。把这些文件挪进 Client aggregate 会让每一处 Host 服务访问都无法编译。
 
 ## 不要在此 import `@deepseek-ai/dsh-client-*`
 
-import 一个 Client 包——无论值还是类型——都会把它整个 TypeScript 工程、以及它引用的每个工程
-拉进 **Host 构建图**。这已经坑过本 lane 一次:四个 Client 消费方包引用了 `api/remotes` 的
-Client face,而该 face 必须等 Host tsdown 生成 `@deepseek-ai/dsh-goal/remote` 之后才能编译,
-于是 Host 构建阶段变成在等一个由它自己产出的产物。
+import 一个 Client 包——无论值还是类型——都会把它整个 TypeScript 工程、以及它引用的每个工程拉进 **Host 构建图**。这已经坑过本 lane 一次:四个 Client 消费方包引用了 `api/remotes` 的 Client face,而该 face 必须等 Host tsdown 生成 `@deepseek-ai/dsh-goal/remote` 之后才能编译,于是 Host 构建阶段变成在等一个由它自己产出的产物。
 
-当某个场景需要 Client 持有的常量或纯函数时,改为在此处镜像一份,并紧挨着一条注释掉的
-import 点明源模块。这样漂移会表现为选择器未命中或镜像值过期——是响亮的失败,绝不会是静默
-通过。`scaffold.ts` 按此规则镜像欢迎声明的 namespace、确认字段、版本和被断言的中文文案。
+当某个场景需要 Client 持有的常量或纯函数时,改为在此处镜像一份,并紧挨着一条注释掉的 import 点明源模块。这样漂移会表现为选择器未命中或镜像值陈旧——是响亮的失败,绝不会是静默通过。`scaffold.ts` 按此规则镜像 welcome-notice 的 namespace、确认字段、版本和被断言的中文文案。
 
-有一类 Client import 是长期成立的。`assembled-boot.ts` 驱动 shell 本身,因此它从
-`@deepseek-ai/dsh-client-web` import `AppWebEntry`、从
-`@deepseek-ai/dsh-client-modules/client` import boot manifest 类型:启动真实 shell 正是该
-harness 的用途,且这两个包本来就在 Host 图中。chat 场景则在 `support.ts` 中镜像
-`conversationContextKey`,而不 import 其 Client owner。
+有一类 Client import 是长期成立的。`assembled-boot.ts` 驱动 shell 本身,因此它从 `@deepseek-ai/dsh-client-web` import `AppWebEntry`、从 `@deepseek-ai/dsh-client-modules/client` import boot manifest(元数据清单)类型:启动真实 shell 正是该 harness 的用途,且这两个包本来就在 Host 图中。chat 场景则在 `support.ts` 中镜像 `conversationContextKey`,而不 import 其 Client owner。
 
 没有任何机制强制这条规则;靠 review 守住它。

+ 20 - 9
apps/web/tests/document-preview.e2e.ts

@@ -240,6 +240,18 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', ()
     const iframe = preview.locator('[data-html-preview]')
     await iframe.waitFor({ timeout: 15_000 })
     expect(await iframe.getAttribute('sandbox')).toBe('allow-scripts')
+    expect(await iframe.evaluate((node) => {
+      const host = node.closest('[data-textpreview-body]')
+      if (!(host instanceof HTMLElement)) throw new Error('HTML preview body is unavailable')
+      const outer = host.getBoundingClientRect()
+      const frame = node.getBoundingClientRect()
+      return {
+        top: Math.round(frame.top - outer.top),
+        right: Math.round(outer.right - frame.right),
+        bottom: Math.round(outer.bottom - frame.bottom),
+        left: Math.round(frame.left - outer.left),
+      }
+    })).toEqual({ top: 0, right: 0, bottom: 0, left: 0 })
     const html = page.frameLocator('[data-html-preview]')
     await html.getByRole('heading', { name: 'HTML smoke', exact: true }).waitFor({ timeout: 15_000 })
     await expect.poll(() => html.locator('#result').innerText()).toBe('INLINE_OK')
@@ -379,6 +391,7 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', ()
     const highlightedLines = preview.locator('.shiki .line')
     await expect.poll(() => highlightedLines.count(), { timeout: 15_000 }).toBe(PAGE_LINES)
     const codeBlock = preview.locator('.md-code-block')
+    const codeScrollport = preview.locator('[data-code-block-content]')
     expect(await codeBlock.getAttribute('data-line-numbers')).toBe('true')
     await expect.poll(() => highlightedLines.first().evaluate(node => getComputedStyle(node, '::before').content))
       .not.toMatch(/^(?:none|normal)$/u)
@@ -397,33 +410,31 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', ()
     const prefix = await highlightedLines.allTextContents()
     expect(prefix).toEqual(codeLines.slice(0, PAGE_LINES))
     await expect.poll(() => preview.locator('[data-textpreview-more]').isEnabled()).toBe(true)
-    await scrollForNextPage(body)
+    await scrollForNextPage(codeScrollport)
     await expect.poll(() => highlightedLines.count(), { timeout: 15_000 }).toBe(codeLines.length)
     const completed = await highlightedLines.allTextContents()
     expect(completed).toEqual(codeLines)
     await expect.poll(() => preview.locator('[data-textpreview-more]').count()).toBe(0)
-    const scrollTop = await body.evaluate((node) => {
+    const scrollTop = await codeScrollport.evaluate((node) => {
       const target = Math.floor((node.scrollHeight - node.clientHeight) / 2)
       if (target <= 0) throw new Error('code fixture does not overflow the document body')
       node.scrollTop = target
       return target
     })
-    await expect.poll(() => body.evaluate((node) => {
-      const banner = node.querySelector('.md-code-block')?.firstElementChild
+    await expect.poll(() => codeScrollport.evaluate((node) => {
+      const codeBlock = node.parentElement
+      const banner = codeBlock?.firstElementChild
       const firstLine = node.querySelector('.shiki .line')
       if (!(banner instanceof HTMLElement) || firstLine === null) throw new Error('missing rendered code banner or source line')
       const bounds = node.getBoundingClientRect()
       const clipTop = bounds.top + node.clientTop
       const bannerBounds = banner.getBoundingClientRect()
-      const hit = document.elementFromPoint(bounds.left + node.clientLeft + node.clientWidth / 2, clipTop + 1)
       return {
         scrollTop: node.scrollTop,
-        position: getComputedStyle(banner).position,
-        topGap: bannerBounds.top - clipTop,
+        scrollportBelowBanner: Math.abs(bannerBounds.bottom - bounds.top) < 1,
         firstLineAbove: firstLine.getBoundingClientRect().top < clipTop,
-        topCoveredByBanner: hit !== null && banner.contains(hit),
       }
-    })).toEqual({ scrollTop, position: 'sticky', topGap: 0, firstLineAbove: true, topCoveredByBanner: true })
+    })).toEqual({ scrollTop, scrollportBelowBanner: true, firstLineAbove: true })
     await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], { origin: new URL(page.url()).origin })
     await page.evaluate(() => navigator.clipboard.writeText(''))
     await codeBlock.getByRole('button', { name: 'Copy', exact: true }).click()

+ 0 - 24
apps/web/tests/expected/markdown-mermaid/ui.expected.md

@@ -1,24 +0,0 @@
-- heading "Mermaid previews" [level=1]
-- button "Source"
-- button "Copy"
-- tooltip "Copy"
-- img "Mermaid diagram"
-- button "Source"
-- button "Copy"
-- img "Mermaid diagram"
-- button "Source"
-- button "Copy"
-- status: Unable to render this diagram. The source is shown below.
-- code: flowchart LR A[unfinished
-- button "Source"
-- button "Copy"
-- img "Mermaid diagram"
-- button "Source"
-- button "Copy"
-- img "Graphviz diagram"
-- button "Source"
-- button "Copy"
-- img "SVG preview"
-- button "Source"
-- button "Copy"
-- iframe

+ 0 - 23
apps/web/tests/expected/markdown-mermaid/zh.expected.md

@@ -1,23 +0,0 @@
-- heading "Mermaid previews" [level=1]
-- button "源码"
-- button "复制"
-- img "Mermaid 图表"
-- button "源码"
-- button "复制"
-- img "Mermaid 图表"
-- button "源码"
-- button "复制"
-- status: 无法渲染此图表,源码如下。
-- code: flowchart LR A[unfinished
-- button "源码"
-- button "复制"
-- img "Mermaid 图表"
-- button "源码"
-- button "复制"
-- img "Graphviz 图表"
-- button "源码"
-- button "复制"
-- img "SVG 预览"
-- button "源码"
-- button "复制"
-- iframe

+ 86 - 0
apps/web/tests/expected/onboarding-deepseek-config/default-models.expected.md

@@ -0,0 +1,86 @@
+- dialog "设置":
+  - navigation:
+    - text: 设置
+    - button "通用设置":
+      - img
+      - text: 通用设置
+    - button "模型":
+      - img
+      - text: 模型
+    - button "插件":
+      - img
+      - text: 插件
+    - button "Agent 预设":
+      - img
+      - text: Agent 预设
+  - button "打开配置文件"
+  - button "关闭":
+    - img
+    - text: 关闭
+  - heading "模型" [level=2]
+  - paragraph: 填入各提供方的 API 密钥即可使用其模型。
+  - list:
+    - listitem:
+      - text: DeepSeek
+      - img "API 密钥已配置"
+      - button "编辑 DeepSeek (deepseek-official)": 编辑
+      - text: DeepSeek deepseek-official API 密钥
+      - textbox "API 密钥":
+        - /placeholder: 已配置——输入新值可替换
+      - group:
+        - text: 自定义设置 API 地址
+        - textbox "API 地址":
+          - /placeholder: https://api.deepseek.com
+        - region "模型目录":
+          - text: 模型目录 正在使用适配器默认模型
+          - textbox "模型 ID 1":
+            - /placeholder: 模型 ID
+            - text: deepseek-flash
+          - textbox "显示名称 1":
+            - /placeholder: 显示名称
+            - text: DeepSeek-V41-Flash
+          - button "容量 1":
+            - img
+          - button "删除模型 1":
+            - img
+          - textbox "模型 ID 2":
+            - /placeholder: 模型 ID
+            - text: deepseek-v4-flash
+          - textbox "显示名称 2":
+            - /placeholder: 显示名称
+            - text: DeepSeek-V4-Flash
+          - button "容量 2":
+            - img
+          - button "删除模型 2":
+            - img
+          - textbox "模型 ID 3":
+            - /placeholder: 模型 ID
+            - text: deepseek-v4-pro
+          - textbox "显示名称 3":
+            - /placeholder: 显示名称
+            - text: DeepSeek-V4-Pro
+          - button "容量 3":
+            - img
+          - button "删除模型 3":
+            - img
+          - textbox "模型 ID 4":
+            - /placeholder: 模型 ID
+            - text: deepseek-v4-flash-vision-exp
+          - textbox "显示名称 4":
+            - /placeholder: 显示名称
+            - text: DeepSeek-V4-Flash-Vision-Exp
+          - button "容量 4":
+            - img
+          - button "删除模型 4":
+            - img
+          - button "添加模型":
+            - img
+            - text: 添加模型
+      - button "取消"
+      - button "保存"
+  - button "添加提供方":
+    - img
+    - text: 添加提供方
+  - button "添加自定义提供方":
+    - img
+    - text: 添加自定义提供方

+ 5 - 25
apps/web/tests/expected/onboarding-deepseek-config/models.expected.md

@@ -35,41 +35,21 @@
           - text: 模型目录 已自定义模型目录
           - button "恢复默认模型"
           - textbox "模型 ID 1":
-            - /placeholder: 模型 ID
-            - text: deepseek-v4-pro
-          - textbox "显示名称 1":
-            - /placeholder: 显示名称
-            - text: DeepSeek-V4-Pro
-          - button "容量 1":
-            - img
-          - button "删除模型 1":
-            - img
-          - textbox "模型 ID 2":
-            - /placeholder: 模型 ID
-            - text: deepseek-v4-flash-vision-exp
-          - textbox "显示名称 2":
-            - /placeholder: 显示名称
-            - text: DeepSeek-V4-Flash-Vision-Exp
-          - button "容量 2":
-            - img
-          - button "删除模型 2":
-            - img
-          - textbox "模型 ID 3":
             - /placeholder: 模型 ID
             - text: private-preview
-          - textbox "显示名称 3":
+          - textbox "显示名称 1":
             - /placeholder: 显示名称
             - text: Private Preview
-          - button "容量 3" [expanded]:
+          - button "容量 1" [expanded]:
             - img
-          - button "删除模型 3":
+          - button "删除模型 1":
             - img
           - text: 上下文窗口
-          - textbox "上下文窗口 3":
+          - textbox "上下文窗口 1":
             - /placeholder: 1M
             - text: "131072"
           - text: 最大输出 token 数
-          - textbox "最大输出 token 数 3":
+          - textbox "最大输出 token 数 1":
             - /placeholder: 256K
             - text: 64K
           - button "添加模型":

+ 0 - 320
apps/web/tests/markdown-mermaid.e2e.ts

@@ -1,320 +0,0 @@
-import { fileURLToPath } from 'node:url'
-import { join } from 'node:path'
-import type { Browser, Page } from 'playwright'
-import { chromium } from 'playwright'
-import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
-import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
-import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session'
-import type {} from '@deepseek-ai/dsh-session-title'
-import {
-  assertFixtureInventory, captureStableAria, compareOrRefreshGolden, launchWebScaffold,
-  seedSession, watchConsole, webSnapshotMode, type WebScaffold,
-} from './scaffold.ts'
-import { newEnglishPage, saveFailureShot } from './support.ts'
-
-const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/markdown-mermaid', import.meta.url))
-const MODE = webSnapshotMode()
-const SEED_ID = 'markdown-mermaid-web-e2e'
-const DIAGRAM_MESSAGE = '[class*="markdown"]:has(.md-code-block)'
-const FLOW = 'flowchart LR\n  A[输入] --> B[共享渲染器] --> C[图形预览]'
-const SEQUENCE = 'sequenceDiagram\n  participant U as User\n  participant R as Renderer\n  U->>R: Mermaid source\n  R-->>U: Diagram'
-const INVALID = 'flowchart LR\n  A[unfinished'
-const UNTRUSTED = [
-  '%%{init: {"securityLevel":"loose","htmlLabels":true,"themeCSS":"body {display:none!important}"}}%%',
-  'flowchart LR',
-  '  A["<img src=x onerror=alert(1)>"] --> B[Safe]',
-  '  click B "javascript:alert(1)"',
-].join('\n')
-
-const DOT = 'digraph { rankdir=LR; Input -> Preview }'
-const SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="460" height="100" onload="parent.document.body.dataset.previewEscaped='yes'">
-<rect width="460" height="100" fill="lightblue"/><text x="20" y="55">SVG preview</text>
-<script>parent.document.body.dataset.previewEscaped='yes';alert('unsafe SVG')</script>
-<image href="https://preview.invalid/svg-image" width="1" height="1"/>
-<foreignObject width="1" height="1"><div xmlns="http://www.w3.org/1999/xhtml"><script>alert('unsafe HTML')</script></div></foreignObject>
-</svg>`
-const HTML = `<style>h2 { color: green }</style><h2>Static HTML</h2>
-<script>parent.document.body.dataset.previewEscaped = 'yes'; alert('unsafe')</script>
-<meta http-equiv="refresh" content="0;url=https://preview.invalid/refresh">
-<a href="https://preview.invalid/navigate">Disabled navigation</a>
-<img src="https://preview.invalid/image" onerror="alert('unsafe')">
-<iframe src="https://preview.invalid/frame"></iframe>`
-
-function fixture(): string {
-  const session = Session.create(SessionId('markdown-mermaid-source'))
-  session.append('turn/start', { turn: 1 })
-  const user = session.append('user/message', createUserMessage({
-    content: [{ type: 'text', text: 'Preview these Mermaid diagrams.' }], source: { kind: 'user' },
-  }), { surfaceOp: 'append' })
-  session.append('session/title', { title: 'Mermaid previews', messageSeqs: [user.seq], source: { kind: 'fallback' } })
-  session.append('step/start', { turn: 1, step: 1 })
-  session.append('assistant/message', {
-    stream: [], turn: 1, step: 1,
-    message: createMessage({
-      role: 'assistant',
-      content: [{ type: 'text', text: [
-        '# Mermaid previews',
-        ...[FLOW, SEQUENCE, INVALID, UNTRUSTED].map(code => `\`\`\`mermaid\n${code}\n\`\`\``),
-        ...[['dot', DOT], ['svg', SVG], ['html', HTML]].map(([lang, code]) => `\`\`\`${lang}\n${code}\n\`\`\``),
-      ].join('\n\n') }],
-      source: { kind: 'model', provider: 'fixture', model: 'fixture' },
-    }),
-  }, { surfaceOp: 'append' })
-  session.append('step/end', { turn: 1, step: 1 })
-  session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
-  const noon = new Date().setHours(12, 0, 0, 0)
-  return [
-    JSON.stringify({
-      type: 'session', version: SESSION_FORMAT_VERSION, id: '{{sessionId}}', createdAt: 0,
-      cwd: '{{cwd}}', isSeeded: false, delegationDepth: 0,
-    }),
-    ...session.snapshotEvents().map(event => JSON.stringify({ ...event, time: noon + event.seq * 1000 })),
-    '',
-  ].join('\n')
-}
-
-async function openConversation(page: Page, scaffold: WebScaffold): Promise<void> {
-  await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
-  await page.getByRole('treeitem').first().click({ timeout: 30_000 })
-  await page.getByRole('treeitem').nth(1).click()
-  await page.getByRole('heading', { name: 'Mermaid previews' }).waitFor()
-}
-
-describe('web e2e: Mermaid chat previews', () => {
-  let scaffold: WebScaffold
-  let browser: Browser
-  beforeAll(async () => {
-    scaffold = await launchWebScaffold({})
-    await seedSession(scaffold, fixture(), SEED_ID)
-    browser = await chromium.launch()
-  }, 120_000)
-  afterAll(async () => {
-    await browser?.close()
-    await scaffold?.close()
-  })
-
-  it.skipIf(MODE === 'record').each(['light', 'dark'] as const)(
-    'tightly wraps short diagram previews in %s mode', async (scheme) => {
-      const page = await newEnglishPage(browser)
-      try {
-        await page.emulateMedia({ colorScheme: scheme })
-        await openConversation(page, scaffold)
-        await expect.poll(() => page.evaluate(() => getComputedStyle(document.documentElement).colorScheme)).toBe(scheme)
-
-        const mermaid = page.getByRole('img', { name: 'Mermaid diagram' }).first()
-        await mermaid.evaluate(async (node: HTMLImageElement) => { await node.decode() })
-        const mermaidHeight = await mermaid.evaluate(node => node.getBoundingClientRect().height)
-        const canvasHeight = await mermaid.evaluate(node => node.parentElement!.getBoundingClientRect().height)
-        // Compact diagrams need at most the Mermaid canvas's 16px padding on each side.
-        expect(canvasHeight - mermaidHeight).toBeLessThanOrEqual(32)
-
-        for (const title of ['Graphviz diagram', 'SVG preview']) {
-          const image = page.getByRole('img', { name: title, exact: true })
-          await image.evaluate(async (node: HTMLImageElement) => { await node.decode() })
-          const imageHeight = await image.evaluate(node => node.getBoundingClientRect().height)
-          const containerHeight = await image.evaluate(node => node.parentElement!.getBoundingClientRect().height)
-          expect(imageHeight).toBeGreaterThan(0)
-          expect(containerHeight).toBeGreaterThanOrEqual(imageHeight)
-          expect.soft(containerHeight - imageHeight,
-            `${scheme} ${title}: ${containerHeight}px container around ${imageHeight}px content`,
-          ).toBeLessThanOrEqual(32)
-        }
-        await page.setViewportSize({ width: 360, height: 800 })
-        const svg = page.getByRole('img', { name: 'SVG preview', exact: true })
-        await expect.poll(() => svg.evaluate((node) => {
-          const { width, height } = node.getBoundingClientRect()
-          return width > 0 && height > 0 && width < 460
-        })).toBe(true)
-        const compact = await svg.evaluate((node: HTMLImageElement) => ({
-          width: node.getBoundingClientRect().width,
-          height: node.getBoundingClientRect().height,
-          canvasHeight: node.parentElement!.getBoundingClientRect().height,
-          ratio: node.naturalWidth / node.naturalHeight,
-        }))
-        expect(compact.width / compact.height).toBeCloseTo(compact.ratio, 1)
-        expect(compact.canvasHeight - compact.height).toBeLessThanOrEqual(32)
-      } finally {
-        await page.close()
-      }
-    },
-  )
-
-  it.skipIf(MODE === 'record')('updates existing diagrams when the document switches between light and dark', async () => {
-    const page = await newEnglishPage(browser)
-    try {
-      await openConversation(page, scaffold)
-      const image = page.getByRole('img', { name: 'Mermaid diagram' }).first()
-      await image.waitFor()
-      let previous = await image.getAttribute('src')
-      for (const scheme of ['dark', 'light'] as const) {
-        await page.evaluate((value) => {
-          document.documentElement.style.colorScheme = value
-          document.body.toggleAttribute('data-ds-dark-theme', value === 'dark')
-        }, scheme)
-        await expect.poll(() => image.getAttribute('src')).not.toBe(previous)
-        await expect.poll(() => image.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0)
-        const colors = await image.evaluate((node) => {
-          const reference = document.createElement('span')
-          reference.style.backgroundColor = 'var(--dsw-alias-markdown-code-block)'
-          document.body.append(reference)
-          try {
-            return {
-              canvas: getComputedStyle(node.parentElement!).backgroundColor,
-              expected: getComputedStyle(reference).backgroundColor,
-            }
-          } finally {
-            reference.remove()
-          }
-        })
-        expect(colors.canvas).toBe(colors.expected)
-        await expect.poll(() => page.getByTitle('HTML preview', { exact: true }).getAttribute('srcdoc'))
-          .toContain(`color-scheme:${scheme}`)
-        previous = await image.getAttribute('src')
-      }
-    } finally {
-      await page.close()
-    }
-  })
-
-  it.skipIf(MODE === 'record')('renders diagrams, switches to source, copies source, and contains malformed content', async () => {
-    const page = await newEnglishPage(browser)
-    onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-mermaid'))
-    const tripwire = watchConsole(page)
-    const dialogs: string[] = []
-    page.on('dialog', (dialog) => { dialogs.push(dialog.message()); void dialog.dismiss() })
-    await page.context().grantPermissions(['clipboard-read', 'clipboard-write'])
-    await openConversation(page, scaffold)
-    const images = page.getByRole('img', { name: 'Mermaid diagram' })
-    await expect.poll(() => images.count(), { timeout: 20_000 }).toBe(3)
-    await expect.poll(() => images.evaluateAll(nodes => nodes.every(node => (node as HTMLImageElement).naturalWidth > 0))).toBe(true)
-    expect(await images.evaluateAll(nodes => nodes.every(node =>
-      node.getBoundingClientRect().width <= (node as HTMLImageElement).naturalWidth))).toBe(true)
-    expect(await page.getByText('Unable to render this diagram. The source is shown below.', { exact: true }).count()).toBe(1)
-    expect(await page.locator('pre code').allTextContents()).toContain(INVALID)
-    expect(await images.first().evaluate(node => decodeURIComponent((node as HTMLImageElement).src))).toContain('共享渲染器')
-    const first = page.locator('.md-code-block').first()
-    const diagram = await first.getByRole('img').elementHandle()
-    const controls = first.locator('[class*="bannerWrap"]')
-    expect(await first.getByText('mermaid', { exact: true }).count()).toBe(0)
-    await page.mouse.move(0, 0)
-    expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('0')
-    await first.hover()
-    expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1')
-    await first.getByRole('button', { name: 'Copy', exact: true }).click()
-    expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(FLOW)
-    await page.mouse.move(0, 0)
-    await first.getByRole('button', { name: 'Source', exact: true }).focus()
-    expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1')
-    await page.keyboard.press('Enter')
-    expect(await first.getByRole('button', { name: 'Preview', exact: true })
-      .evaluate(node => node === document.activeElement)).toBe(true)
-    expect(await first.locator('pre code').textContent()).toBe(FLOW)
-    expect(await diagram!.evaluate(node => node.isConnected)).toBe(true)
-    expect(await diagram!.isVisible()).toBe(false)
-    await first.getByRole('button', { name: 'Preview', exact: true }).click()
-    expect(await diagram!.isVisible()).toBe(true)
-    await first.getByRole('button', { name: 'Copy', exact: true }).waitFor()
-    expect(await page.locator('body').evaluate(node => getComputedStyle(node).display)).not.toBe('none')
-    expect(await page.locator('[id^="dsh-mermaid-"]').count()).toBe(0)
-    expect(dialogs).toEqual([])
-    expect(tripwire.pageErrors).toEqual([])
-    expect(tripwire.warnings).toEqual([])
-    await page.getByRole('heading', { name: 'Mermaid previews' }).click()
-    await page.mouse.move(0, 0)
-    const snapshot = await captureStableAria(page, DIAGRAM_MESSAGE, scaffold.workspaceCwd)
-    await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'ui.expected.md'), snapshot, MODE)
-    await page.close()
-  }, 60_000)
-
-  it.skipIf(MODE === 'record')('localizes the preview and failure states in Chinese', async () => {
-    const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'zh-CN', timezoneId: 'Asia/Shanghai', hasTouch: true })
-    await openConversation(page, scaffold)
-    await expect.poll(() => page.getByRole('img', { name: 'Mermaid 图表' }).count(), { timeout: 20_000 }).toBe(3)
-    expect(await page.getByRole('button', { name: '源码', exact: true }).count()).toBe(7)
-    const controls = page.locator('.md-code-block').first().locator('[class*="bannerWrap"]')
-    expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1')
-    expect(await page.getByRole('button', { name: '源码', exact: true }).first()
-      .evaluate(node => node.getBoundingClientRect().width)).toBe(44)
-    expect(await page.getByText('无法渲染此图表,源码如下。', { exact: true }).count()).toBe(1)
-    const firstImage = page.getByRole('img', { name: 'Mermaid 图表' }).first()
-    await expect.poll(() => firstImage.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0)
-    expect(await controls.evaluate(node => node.getBoundingClientRect().top))
-      .toBeGreaterThanOrEqual(await firstImage.evaluate(node => node.getBoundingClientRect().bottom))
-    const snapshot = await captureStableAria(page, DIAGRAM_MESSAGE, scaffold.workspaceCwd)
-    await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'zh.expected.md'), snapshot, MODE)
-    await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md', 'zh.expected.md'])
-    await page.close()
-  }, 60_000)
-
-  it.skipIf(MODE === 'record')('previews inert DOT and SVG images and isolates static HTML', async () => {
-    const page = await newEnglishPage(browser)
-    const requests: string[] = []
-    const dialogs: string[] = []
-    // Routing observes requests eligible for transport; Chromium also emits 'request' for CSP-blocked URLs.
-    await page.route('https://preview.invalid/**', async (route) => {
-      requests.push(route.request().url())
-      await route.abort()
-    })
-    page.on('dialog', (dialog) => { dialogs.push(dialog.message()); void dialog.dismiss() })
-    await page.context().grantPermissions(['clipboard-read', 'clipboard-write'])
-    await openConversation(page, scaffold)
-    for (const [title, code] of [['Graphviz diagram', DOT], ['SVG preview', SVG], ['HTML preview', HTML]] as const) {
-      const preview = title === 'HTML preview'
-        ? page.getByTitle(title, { exact: true })
-        : page.getByRole('img', { name: title, exact: true, includeHidden: true })
-      await preview.waitFor()
-      if (title === 'HTML preview') {
-        expect(await preview.getAttribute('sandbox')).toBe('')
-        const body = preview.contentFrame()
-        await body.getByRole('heading', { name: 'Static HTML' }).waitFor()
-        expect(await body.getByRole('heading').evaluate(node => getComputedStyle(node).color)).toBe('rgb(0, 128, 0)')
-        await body.getByText('Disabled navigation').click()
-        expect(await preview.evaluate(node => (node as HTMLIFrameElement).contentDocument)).toBeNull()
-      } else {
-        await expect.poll(() => preview.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0)
-      }
-      const block = page.locator('.md-code-block').filter({ has: preview })
-      await block.hover()
-      await block.getByRole('button', { name: 'Copy', exact: true }).click()
-      expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(code)
-      await block.getByRole('button', { name: 'Source', exact: true }).click()
-      expect(await block.locator('pre code').textContent()).toBe(code)
-      await block.getByRole('button', { name: 'Preview', exact: true }).click()
-      expect(await preview.isVisible()).toBe(true)
-    }
-    expect(await page.locator('body').getAttribute('data-preview-escaped')).toBeNull()
-    expect(dialogs).toEqual([])
-    expect(requests).toEqual([])
-    const notices = await page.request.get(new URL('/preview-third-party-notices.txt', scaffold.authenticatedUrl).href)
-    expect(notices.ok()).toBe(true)
-    expect(await notices.text()).toContain('Eclipse Public License - v 2.0')
-    await page.close()
-  })
-
-  it.skipIf(MODE === 'record')('keeps actions visible when a mouse and touchscreen are both available', async () => {
-    const hybridBrowser = await chromium.launch({
-      args: ['--blink-settings=availablePointerTypes=6,primaryPointerType=4,availableHoverTypes=2,primaryHoverType=2'],
-    })
-    try {
-      const page = await newEnglishPage(hybridBrowser)
-      expect(await page.evaluate(() =>
-        matchMedia('(hover: hover) and (pointer: fine) and (any-pointer: coarse)').matches)).toBe(true)
-      await openConversation(page, scaffold)
-      const first = page.locator('.md-code-block').first()
-      const diagram = first.getByRole('img', { name: 'Mermaid diagram' })
-      await expect.poll(() => diagram.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0)
-      await page.mouse.move(0, 0)
-      const controls = first.locator('[class*="bannerWrap"]')
-      expect(await controls.evaluate(node => getComputedStyle(node).opacity)).toBe('1')
-      expect(await controls.evaluate(node => getComputedStyle(node).pointerEvents)).toBe('auto')
-      expect(await first.getByRole('button', { name: 'Source', exact: true })
-        .evaluate(node => node.getBoundingClientRect().width)).toBe(44)
-      expect(await controls.evaluate(node => node.getBoundingClientRect().top))
-        .toBeGreaterThanOrEqual(await diagram.evaluate(node => node.getBoundingClientRect().bottom))
-      await first.getByRole('button', { name: 'Source', exact: true }).click()
-      expect(await first.locator('pre code').textContent()).toBe(FLOW)
-    } finally {
-      await hybridBrowser.close()
-    }
-  }, 60_000)
-})

+ 34 - 14
apps/web/tests/onboarding-deepseek-config.e2e.ts

@@ -21,6 +21,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/onboarding-deepseek-confi
 const WELCOME_EXPECTED = join(SNAPSHOT_DIR, 'welcome.expected.md')
 const MISSING_EXPECTED = join(SNAPSHOT_DIR, 'missing.expected.md')
 const MODELS_EXPECTED = join(SNAPSHOT_DIR, 'models.expected.md')
+const DEFAULT_MODELS_EXPECTED = join(SNAPSHOT_DIR, 'default-models.expected.md')
 const MODE = webSnapshotMode()
 
 describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup', () => {
@@ -202,15 +203,39 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
     await deepSeek.waitFor({ timeout: 10_000 })
     await deepSeek.locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click()
     await settings.getByText('自定义设置').click()
-    await settings.getByRole('button', { name: /删除模型/ }).first().click()
+    expect(await settings.getByLabel('模型 ID 1').inputValue()).toBe('deepseek-flash')
+    expect(await settings.getByLabel('显示名称 1').inputValue()).toBe('DeepSeek-V41-Flash')
+    expect(await settings.getByLabel('模型 ID 2').inputValue()).toBe('deepseek-v4-flash')
+    expect(await settings.getByLabel('模型 ID 3').inputValue()).toBe('deepseek-v4-pro')
+    expect(await settings.getByLabel('模型 ID 4').inputValue()).toBe('deepseek-v4-flash-vision-exp')
+    expect(await settings.getByRole('button', { name: /删除模型/ }).count()).toBe(4)
+    const defaultModels = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
+    await compareOrRefreshGolden(DEFAULT_MODELS_EXPECTED, defaultModels, MODE)
+    await settings.getByLabel('显示名称 1').fill('Configured Flash')
+    await settings.getByRole('button', { name: '保存', exact: true }).click()
+    await settings.getByLabel('模型 ID 1').waitFor({ state: 'detached', timeout: 15_000 })
+    const savedDefaults = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+    expect(savedDefaults).toContain('id: deepseek-flash')
+    expect(savedDefaults).toContain('inputModalities:')
+    expect(savedDefaults).toContain('- text')
+    expect(savedDefaults).toContain('- image')
+    expect(savedDefaults).toContain('systemPromptUpdate: in-history')
+    await expect(scaffold.ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-flash')).resolves.toMatchObject({
+      name: 'Configured Flash', inputModalities: ['text', 'image'], systemPromptUpdate: 'in-history',
+    })
+    await deepSeek.locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click()
+    await settings.getByText('自定义设置').click()
+    for (let index = 0; index < 4; index++) {
+      await settings.getByRole('button', { name: /删除模型/ }).first().click()
+    }
     await settings.getByRole('button', { name: '添加模型' }).click()
-    const customModelId = settings.getByLabel('模型 ID 3')
+    const customModelId = settings.getByLabel('模型 ID 1')
     await customModelId.fill('private-preview')
-    await settings.getByLabel('显示名称 3').fill('Private Preview')
+    await settings.getByLabel('显示名称 1').fill('Private Preview')
     // Capacities live behind the row's own disclosure, as in the pi-ai form.
-    await settings.getByRole('button', { name: '容量 3' }).click()
-    await settings.getByLabel('上下文窗口 3').fill('131072')
-    await settings.getByLabel('最大输出 token 数 3').fill('64K')
+    await settings.getByRole('button', { name: '容量 1' }).click()
+    await settings.getByLabel('上下文窗口 1').fill('131072')
+    await settings.getByLabel('最大输出 token 数 1').fill('64K')
 
     await expect.poll(
       () => settings.getByLabel('API 密钥', { exact: true }).getAttribute('placeholder'),
@@ -222,15 +247,11 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
     await customModelId.waitFor({ state: 'detached', timeout: 15_000 })
 
     const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
-    expect(document).toContain('id: deepseek-v4-pro')
-    expect(document).toContain('id: deepseek-v4-flash-vision-exp')
-    expect(document).toContain('inputModalities:')
-    expect(document).toContain('- image')
     expect(document).toContain('id: private-preview')
     expect(document).toContain('name: Private Preview')
     expect(document).toContain('contextWindow: 131072')
     expect(document).toContain('maxTokens: 64000')
-    expect(document).not.toMatch(/^\s*- id: deepseek-v4-flash$/m)
+    expect(document).not.toContain('id: deepseek-flash')
 
     await page.keyboard.press('Escape')
     // A connected Workspace is what puts a live composer — and its model
@@ -241,8 +262,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
     await modelTrigger.waitFor({ timeout: 10_000 })
     await modelTrigger.click()
     await page.getByRole('menuitem', { name: /模型/ }).click()
-    expect(await page.getByText('deepseek-v4-flash', { exact: true }).count()).toBe(0)
-    await page.getByRole('menuitemradio', { name: 'DeepSeek-V4-Flash-Vision-Exp' }).waitFor({ timeout: 10_000 })
+    expect(await page.getByText('Configured Flash', { exact: true }).count()).toBe(0)
     await page.getByRole('menuitemradio', { name: 'Private Preview' }).waitFor({ timeout: 10_000 })
     expect(tripwire.warnings).toEqual([])
     expect(tripwire.pageErrors).toEqual([])
@@ -251,7 +271,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
   it('keeps the fixture inventory closed', async () => {
     await assertFixtureInventory(
       SNAPSHOT_DIR,
-      ['welcome.expected.md', 'missing.expected.md', 'models.expected.md'],
+      ['welcome.expected.md', 'missing.expected.md', 'models.expected.md', 'default-models.expected.md'],
     )
   })
 })

+ 4 - 0
apps/web/tests/scaffold.ts

@@ -515,6 +515,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
   const patches: PatchOptions[] = [
     ...basePatches,
     ...surfacePatches,
+    // Keyless scenarios retain the recorded default; explicit scenario overlays win.
+    ...mode === 'record' || options.deepSeekMissingCredential === true
+      ? []
+      : [{ id: 'agent-default-model', config: { provider: 'deepseek-official', model: 'deepseek-v4-flash' } }],
     ...extraOverlayPatches,
     // The roster's shipped presets are the plugin's own, bundled inside
     // `dsh-agent-presets` and prepended by it. Pin only the machine-local

+ 1 - 1
apps/web/tests/sidebar-right.e2e.ts

@@ -1061,7 +1061,7 @@ describe('web e2e: shipped right Sidebar', () => {
         expect(await width(column)).toBeGreaterThan(300)
         await expect.poll(async () => await tabTitles(column)).toEqual(['文件', '开始'])
         await expect.poll(async () => await guide.locator('[data-sidebar-right-guide-entry="files"]').innerText())
-          .toBe('工作区文件')
+          .toBe('工作区文件\n浏览会话工作区的文件')
         await shot(zhPage, '05-guide-copy-zh')
 
         expect(zhTripwire.pageErrors).toEqual([])

+ 0 - 1
apps/web/tsconfig.json

@@ -79,7 +79,6 @@
     "tests/math-rendering.e2e.ts",
     "tests/markdown-cjk-strong.e2e.ts",
     "tests/markdown-inline-code-links.e2e.ts",
-    "tests/markdown-mermaid.e2e.ts",
     "tests/clickable-links-gallery.e2e.ts",
     "tests/queue-actions.e2e.ts",
     "tests/queue-image.e2e.ts",

+ 1 - 15
apps/web/vite.config.ts

@@ -27,20 +27,6 @@ function clientDocumentTitle(): Plugin {
   }
 }
 
-/** Preserve preview licenses and Graphviz source availability in every browser distribution. */
-function previewNotices(): Plugin {
-  return {
-    name: 'dsh-preview-notices',
-    async generateBundle() {
-      this.emitFile({
-        type: 'asset',
-        fileName: 'preview-third-party-notices.txt',
-        source: await readFile(src('../../packages/client/ui-primitives/THIRD_PARTY_PREVIEW_NOTICES.txt'), 'utf8'),
-      })
-    },
-  }
-}
-
 /** Fail before a Vite dev or preview server can expose the boot-manifest-free shell. */
 function rejectStandaloneServe(): Plugin {
   return {
@@ -161,7 +147,7 @@ export default defineConfig({
   // Relative asset URLs: preview.html mounts the same output under any base
   // directory, and the served index resolves identically from the site root.
   base: './',
-  plugins: [previewNotices(), rejectStandaloneServe(), clientDocumentTitle(), react(), emitPreviewPage()],
+  plugins: [rejectStandaloneServe(), clientDocumentTitle(), react(), emitPreviewPage()],
   build: {
     // The worker bootstrap holds its page at top-level await; Vite's default
     // `modules` target (es2020-era) rejects that syntax.

+ 1 - 1
benchmarks/active-stream-reconnect/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 benchmarks/active-stream-reconnect/README.md
 README.md: e75a41eba3952bb4db343c2018e217e6f5e88f96
-README.zh.md: acf0260f53855f9f4e643b2e72c67aa7a8afd6bb
+README.zh.md: 3d831208144e3810d353fb64fc5ae4adae766bdd

+ 2 - 2
benchmarks/active-stream-reconnect/README.zh.md

@@ -2,6 +2,6 @@
 
 [English](README.md) | 中文
 
-[reconnect.bench.client.ts](reconnect.bench.client.ts) 测量重连携带未完成的 100,000 个 reasoning delta 前缀时,生产 Client 的折叠成本。编译后的私有适配器调用 `ClientAssistantStream.replace()`,不增加产品导出。三个全新纯 Node worker 在计时前合成紧凑 baseline;替换时间与强制 GC 后的保留 heap 分别执行中位数预算检查。下一个稠密序号的实时 frame 仍须被接受。标准托管 CI 使用 50 ms 替换预期及共享的 1.25× 余量(向上取整为 63 ms);保留 heap 预算仍为 30 MiB。记录样本和合成回归对照使用与 worker 判定相同的时间断言。
+[reconnect.bench.client.ts](reconnect.bench.client.ts) 测量重连携带一个包含 100,000 个 delta、尚未完成的 reasoning 前缀时,生产 Client 的折叠成本。编译后的私有适配器调用 `ClientAssistantStream.replace()`,不增加产品导出。三个全新的纯 Node worker 在计时前合成紧凑基线;替换时间与强制 GC 后的保留堆分别执行中位数预算检查。下一个稠密序号的实时 frame 仍须被接受。标准托管 CI 使用 50 ms 替换预期及共享的 1.25× 余量(上限为 63 ms);保留 heap 预算仍为 30 MiB。实测样本和合成回归对照使用与 worker 判定相同的时间断言。
 
-通过 `pnpm run build:bench` 构建,再在 `vitest.bench.config.ts` 中选择 `benchmarks/active-stream-reconnect`。该聚焦 Node workload 既不构建也不测量浏览器渲染。[前端性能预算](../../.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md)记录校准与排除项。
+通过 `pnpm run build:bench` 构建,再在 `vitest.bench.config.ts` 中选择 `benchmarks/active-stream-reconnect`。这项针对 Node 的工作负载既不构建也不测量浏览器渲染。[前端性能预算](../../.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md)记录校准与排除项。

+ 1 - 1
benchmarks/agent-continuation/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 benchmarks/agent-continuation/README.md
 README.md: 489939499af8d98922df2cbbdd6be793bf6e3796
-README.zh.md: e99760cfd1aba0ca41e78243f333fe18446474fe
+README.zh.md: e51fac2d0df005de9d609cb634778f30b537ae41

+ 6 - 6
benchmarks/agent-continuation/README.zh.md

@@ -2,15 +2,15 @@
 
 [English](README.md) | 中文
 
-## Summary
+## 概述
 
-在不使用网络服务或录制用户数据的情况下,测量长历史请求处理、冷工具密集续聊和重复发现非活动 fork 子会话。SDK 变体通过已发布 sdk-minimal profile 和显式 editor patch 执行 100 个轮次和 800 次真实文件读取;其他用例隔离后端服务成本。所有用例均不渲染浏览器。
+在不使用网络服务或所记录的用户数据的情况下,测量长历史请求处理、冷启动的工具密集型续聊和重复发现非活动 fork 子会话。SDK 变体通过已发布 sdk-minimal profile 和显式 editor patch 执行 100 个轮次和 800 次真实文件读取;其他用例隔离后端服务成本。所有用例均不渲染浏览器。
 
-## Table of Contents
+## 目录
 
 - [运行](#run)
 - [测量](#measurements)
-- [Dev Note](#dev-note)
+- [开发备注](#dev-note)
 
 <a id="run"></a>
 
@@ -24,8 +24,8 @@
 
 ## 测量
 
-[workload.ts](workload.ts)拥有合成维度。其当前代历史在首个 step 的用户输入之前保留空 system 头节点,因此续聊提示会替换该头节点而不移动历史消息。[Agent Note](../../.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md)拥有计时终点、校准证据、内存解释和排除项。模型适配器不执行服务商序列化或网络调用;集成用例通过真实工具执行管线运行合成工具体,SDK profile 变体则执行真实文件读取。
+[workload.ts](workload.ts) 负责定义合成维度。其当前代历史在首个步骤的用户输入之前保留空 system 头节点,因此续聊提示词会替换该头节点而不移动历史消息。[Agent Note](../../.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md) 负责说明计时终点、校准证据、内存解释和排除项。模型适配器不执行提供方序列化或网络调用;集成用例通过真实工具执行流水线运行合成工具体,SDK profile 变体则执行真实文件读取。
 
-## Dev Note
+## 开发备注
 
 无。

+ 1 - 1
benchmarks/long-session-browser/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 benchmarks/long-session-browser/README.md
 README.md: 009a7a61d9a1a0d4b198ae017def5d3126b713a2
-README.zh.md: 2dd0c52b0f1cc04aac8a78d847401067201afa13
+README.zh.md: df1da8fd76c2f4fa926b6348fa631530fb98181a

+ 4 - 4
benchmarks/long-session-browser/README.zh.md

@@ -1,8 +1,8 @@
-# 长 Session 浏览器基准
+# 长会话浏览器基准
 
 [English](README.md) | 中文
 
-[long-session.bench.ts](long-session.bench.ts) 中必需的 Chromium 工作流测量打开合成的 240 轮 Session、加载所有更早的分页、访问 Trajectory,以及在有节奏的流式回复期间输入下一条草稿。随产品维护的 Web scaffold 拥有隔离的主目录、持久化、重放适配器和回环监听器;Chromium 加载构建后的 Web 产物,而非替代开发服务器。
+[long-session.bench.ts](long-session.bench.ts) 中必需的 Chromium 工作流测量打开包含 240 个轮次的合成会话、加载所有较早页面、访问 Trajectory,以及在有节奏的流式回复期间输入下一条草稿。随产品维护的 Web scaffold 拥有隔离的主目录、持久化、回放适配器和回环监听器;Chromium 加载构建后的 Web 产物,而非替代开发服务器。
 
 ## 运行
 
@@ -10,8 +10,8 @@
 
 ## 测量
 
-三个全新浏览器进程与 scaffold 环境产生原始样本及中位数判定。打开和分页在预期对话状态出现且经过两次动画帧后结束;这包含一次渲染机会,而非硬件显示时间戳。分页报告每一页,并对各样本最慢分页时间的中位数执行预算检查。流式报告首段可见回复、真实草稿键入、完整回复壁钟时间和 Chromium 主线程任务时间。Enter 从已聚焦的输入框提交;草稿键入保留该焦点,不执行鼠标点击。回复标记查找与输入事件文本证据仅读取最新 Assistant step,避免重复扫描全部历史文本与无障碍属性。输入观察器在提交前安装,首个标记可见后立即开始草稿键入,不额外等待输入前动画帧。诊断报告首段可见等待后的标记状态与焦点,以及首个输入事件的浏览器时钟时间戳与焦点。诊断不会暂停重放;首段观察或输入延迟仍可能导致重叠失败。实际首个输入事件必须观察到未完成的回复;完成测量在 Host 结算后等待新 turn-tail 渲染。测量后,在 DONE 之后发送的真实按键必须无法通过同一个重叠断言。打开、最慢更早分页和首次 Trajectory 使用标准托管预期 900/700/500 ms。共享的 1.25× 余量分别产生 1125/875/625 ms 上限;流式终点的额外开销预算不变。强制 GC 后的 heap 与 DOM 数量仅供诊断,不作为泄漏预算。
+三个全新浏览器进程与 scaffold 环境产生原始样本及中位数判定。打开和分页在预期 transcript(文本记录)状态出现且经过两次动画帧后结束;这包含一次渲染机会,而非硬件显示时间戳。分页报告每一页,并对各样本最慢分页时间的中位数执行预算检查。流式报告首次可见回复、受信任的草稿键入、完整回复壁钟时间和 Chromium 主线程任务时间。Enter 从已聚焦的输入框提交;草稿键入保留该焦点,不执行鼠标点击。回复标记查找与输入事件文本观察器仅读取最新 Assistant 步骤,避免重复进行全量历史文本扫描和无障碍扫描。输入事件文本观察器在提交前安装,首个标记可见后立即开始草稿键入,不额外等待输入前动画帧。诊断报告等待首次可见回复后的标记状态与焦点,以及首个输入事件的浏览器时钟时间戳与焦点。诊断不会暂停回放;首次观察或输入延迟仍可能导致重叠失败。实际首个输入事件必须观察到未完成的回复;完整回复的测量会在 Host 结算后等待新 turn-tail 渲染完成。测量后,在 DONE 之后发送的受信任按键必须无法通过同一个重叠断言。打开、最慢的较早页面和首次 Trajectory 使用标准托管预期 900/700/500 ms。共享的 1.25× 余量分别产生 1125/875/625 ms 上限;流式终点的额外开销预算不变。强制 GC 后的 heap 与 DOM 数量仅供诊断,不作为泄漏预算。
 
-fixture(测试前置数据)在首条用户消息前保留空 system 头节点,每条用户消息都位于其 step 内。它包含混合语言提示、正文、推理、20 个代码块和 40 个合成工具结果。每条历史 Assistant 都含紧凑 stream,由生产 accumulator 从匹配的推理、文本、工具参数、usage 和 finish chunk 构建。其内容不来自模型、工具、外部网络、录制 Session 或私有 Harness 主目录。流式回复以 16 ms 重放间隔发送 120 个文本 delta,经过真实输入框、agent loop(智能体循环)、传输与持久化。
+fixture(测试前置数据)在首条用户消息前保留空 system 头节点,每条用户消息都位于其步骤内。它包含混合语言提示词、正文、推理(reasoning)、20 个围栏代码块和 40 个合成工具结果。每条历史 Assistant 都含紧凑流,由生产 accumulator 从匹配的推理、文本、工具参数、usage 和 finish 分片构建。其内容不来自模型、工具、外部网络、录制会话或私有 Harness 主目录。流式回复以 16 ms 回放间隔发送 120 个文本 delta,经过真实输入框、agent loop(智能体循环)、传输与持久化。
 
 [决策记录](../../.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md)拥有校准、排除项与替代方案。更大规模的[手动诊断](../../apps/web/tests/complex-history.perf.ts)保持独立。

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: 9adf44b995601196b602e9219e53c52208039051
-config-catalog.zh.md: e2f6c6babc18bfa689f6830c81231012b4d9de18
+config-catalog.md: 79878fc8daaa5ba3e64d4ffb27d279ff172b6749
+config-catalog.zh.md: f254241618d280acaf738cc011ba1f7c5a2b6b21

+ 2 - 2
docs/config-catalog.md

@@ -1037,7 +1037,7 @@ export interface Config {
   maxTokens?: number
   /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
   defaultContextWindow?: number
-  /** Advisory models shown by discovery consumers; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp. */
+  /** Advisory models shown by discovery consumers; defaults to V41 Flash, V4 Flash, V4 Pro, and V4 Flash Vision Exp. */
   models?: DeepSeekCatalogModel[]
   /** Maximum provider idle time while one stream read is outstanding (default five minutes). */
   streamIdleTimeoutMs?: number
@@ -1094,7 +1094,7 @@ export interface DeepSeekCatalogModel {
 
 Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts)
 
-Source: [`packages/llm/llm-deepseek/src/index.ts:125`](../packages/llm/llm-deepseek/src/index.ts)
+Source: [`packages/llm/llm-deepseek/src/index.ts:134`](../packages/llm/llm-deepseek/src/index.ts)
 
 <a id="deepseek-aidsh-llm-pi-ai"></a>
 

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

@@ -1039,7 +1039,7 @@ export interface Config {
   maxTokens?: number
   /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
   defaultContextWindow?: number
-  /** Advisory models shown by discovery consumers; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp. */
+  /** Advisory models shown by discovery consumers; defaults to V41 Flash, V4 Flash, V4 Pro, and V4 Flash Vision Exp. */
   models?: DeepSeekCatalogModel[]
   /** Maximum provider idle time while one stream read is outstanding (default five minutes). */
   streamIdleTimeoutMs?: number
@@ -1096,7 +1096,7 @@ export interface DeepSeekCatalogModel {
 
 依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts)
 
-来源:[`packages/llm/llm-deepseek/src/index.ts:125`](../packages/llm/llm-deepseek/src/index.ts)
+来源:[`packages/llm/llm-deepseek/src/index.ts:134`](../packages/llm/llm-deepseek/src/index.ts)
 
 <a id="deepseek-aidsh-llm-pi-ai"></a>
 

+ 1 - 1
docs/i18n/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 docs/i18n/README.md
 README.md: 2ff8fc62f21d58a4d31b8aadd80c7a0c14556e6d
-README.zh.md: 73da4445bc35e779a990d4cd4aef605cc9078a06
+README.zh.md: 2e0c183f6f9374c08d698fbd7109eb4e59f87c10

+ 1 - 1
docs/i18n/README.zh.md

@@ -33,7 +33,7 @@
 
 面向源码的代码门禁会把精确的 `.zh.md` 围栏序列视为其无后缀兄弟文件的派生内容,而不会再次编译相同代码或在 manifest(元数据清单)中重复登记。该序列必须在长度、顺序、围栏类型和按字节精确的正文上一致;否则两份副本仍会独立受检,配对门禁也会报告结构不匹配。
 
-`pnpm run verify-translation-pairing --list` 打印范围内每篇文档的当前配对状态(missing、out-of-sync 或 ok)。它从不失败;其中 missing 与 out-of-sync 行指出普通检查会拒绝的违规。
+`pnpm run verify-translation-pairing --list` 打印范围内每篇文档的当前配对状态(missing、out-of-sync 或 ok)。它从不失败;其中 `missing` 与 `out-of-sync` 行指出普通检查会拒绝的违规。
 
 `pnpm run verify-translation-pairing <pair...>` 只检查被点名的配对——配对的三个文件中的任意一个(或其裸词干)都能点名它——因此更新循环几秒内就能验证自己的配对,而不必重新扫描全语料。`doc-sync` 与 CI 运行的是无参数的全语料形式;限定范围的绿灯在 PR 层面永远不能替代它。
 

+ 1 - 1
docs/subsystems/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 docs/subsystems/README.md
 README.md: 9afbdda338ed797279b719eaf577be45a84349e2
-README.zh.md: ee3c89830a76bc1cddb83e0a50507f3e4439800d
+README.zh.md: 78f799d99221d8458ad1639dffbdda69b6783ef6

+ 13 - 13
docs/subsystems/README.zh.md

@@ -6,21 +6,21 @@
 
 | 页面 | 负责内容 |
 |---|---|
-| [core.md](core.zh.md) | `packages/core` 如何控制 agent loop(智能体循环):逐包的循环说明、agent 创建与所有权(`AgentHandle`)、`Agent` 句柄的投递/取消/拦截约定,以及全仓通用类型模式(`…Map → 派生联合`、品牌化 id) |
+| [core.md](core.zh.md) | `packages/core` 如何控制 agent loop(智能体循环):逐包的循环说明、agent 创建与所有权(`AgentHandle`)、`Agent` 句柄的投递/取消/拦截约定,以及全仓通用类型模式(`…Map → derived-union`、品牌化 id) |
 | [llm-streaming.md](llm-streaming.zh.md) | `packages/llm` 的对话类型——`Message`/`ContentBlock`、组装完成的模型请求、`StreamChunk` wire protocol 和适配器约定(adapter contract)、`BlockAssembler`,以及 `LlmAdapter` 提供方约定 |
 | [token-meter.md](token-meter.zh.md) | 不可变的标量与位置回放度量,附带已消费日志修订号 |
 | [scope.md](scope.zh.md) | 作用域注册标识、dispatch 载体,以及拥有的 `Scope` 上下文 |
 | [typert.md](typert.zh.md) | 远程调用描述符、lookup/Context 声明、Typert 注册表,以及 Host Gateway/Client API 边界 |
 | [goal.md](goal.zh.md) | 持久 goal 标识、生命周期快照、激活、变更记录与 Round 归属 |
-| [schedule.md](schedule.zh.md) | 仅限 Session 内的提醒记录、持久转换、活动视图与普通对话交付 |
-| [todo.md](todo.zh.md) | todo 包的整列表条目类型、持久事件所有权、投影和开放轮次不变量 |
+| [schedule.md](schedule.zh.md) | 仅限会话内的提醒记录、持久转换、活动视图与普通对话交付 |
+| [todo.md](todo.zh.md) | todo 包的整列表条目类型、持久事件所有权、投影和未结束轮次不变式 |
 | [commands.md](commands.zh.md) | 人类命令注册表服务:定义、适配器发现、直接调用、结果与解析视图 |
 | [session.md](session.zh.md) | 完整的 `SessionEventMap` 变体目录、`TurnEndReason`、`deriveMessages()`、执行封闭与独立事件 |
-| [persistence.md](persistence.zh.md) | 持久性 seam:`SessionPersistence`、JSONL provider、`session/flush`、崩溃恢复、`SessionHeader` |
+| [persistence.md](persistence.zh.md) | 持久性 seam:`SessionPersistence`、JSONL 提供方、`session/flush`、崩溃恢复、`SessionHeader` |
 | [settings.md](settings.zh.md) | 用户设置 seam:`SettingsNamespace` 注册、分层解析(默认值 → 组合 `base` → 用户文档)、owner scope、热提交 |
 | [credentials.md](credentials.zh.md) | 凭据 seam:配置中的 `CredentialRef` 引用(绝不含值)、按操作解析、对 UI 安全的 `CredentialInfo`、提供方来源层 |
 | [session-query.md](session-query.zh.md) | 逻辑记录、有界精确事件读取、关系追踪、语义筛选器/文档与全文检索结果页 |
-| [feedback.md](feedback.zh.md) | 绑定生命周期的逐消息反馈记录、乐观版本、伴随记录持久化与 Host Remote 契约 |
+| [feedback.md](feedback.zh.md) | 绑定生命周期的逐消息反馈记录、乐观版本、伴随记录持久化与 Host Remote 约定 |
 | [session-title.md](session-title.zh.md) | 持久标题快照、被引用的来源消息 seq 与异步提供方约定 |
 | [session-reference.md](session-reference.zh.md) | 结构化跨会话引用:`SessionReferenceInput`/`Candidate`、prepared 消息上下文、稳定错误分类 |
 | [system-prompt.md](system-prompt.zh.md) | 逐次组装的上下文、工具提供方结果、提示词段落与协作式组装 |
@@ -33,7 +33,7 @@
 | [terminal.md](terminal.zh.md) | 持久化终端 ID、后端/会话约定、发送就绪状态、有界读取与 owner 可见快照 |
 | [sandbox.md](sandbox.zh.md) | 每会话策略解析与进程约束 seam:文件效果模式、执行/提供方策略、`ConfinedArgv`、强制执行与故障关闭错误 |
 | [code-runtime.md](code-runtime.zh.md) | 代码执行 seam:`CodeRunRequest`/`Result`、绑定命名空间、捕获日志、`CodeRunFailure` 分类体系 |
-| [extensions.md](extensions.zh.md) | 带版本的动态 Cordis Plugin 与 Package、Host/Client 激活、审批、运行时检查和生命周期撤销 |
+| [extensions.md](extensions.zh.md) | 带版本的动态 Cordis 插件与包、Host/Client 激活、审批、运行时检查和生命周期清理 |
 | [filesystem.md](filesystem.zh.md) | 文件系统 seam:`FsTarget`、读/写/编辑结果、观测到的文件状态、`FsErrorCode` |
 | [lsp.md](lsp.zh.md) | LSP 导航 seam:`LspQueryRequest`/`Result`、`LspProvider`/`Service`、四种操作、`LspError` |
 | [skills.md](skills.zh.md) | skill(技能)服务:发现优先级、`SkillSummary`/`SkillDefinition`、会话前缀目录、面向模型的 `skill` 加载 |
@@ -48,16 +48,16 @@
 | [plan.md](plan.zh.md) | 计划模式:仅记日志的 `plan/mode` 状态、待定选择的冲刷、`PlanModeConfig`、`exit_plan_mode` 审阅流程 |
 | [invariants.md](invariants.zh.md) | 运行时不变式注册表:选择配置 `Config`、`InvariantInstaller`/`InvariantFailure`、空配套插件约定 |
 | [web-server.md](web-server.zh.md) | HTTP 载体:`WebRouteKind`/`WebRoute`、匹配顺序、可认领的回退席位、index 渲染挂接点 |
-| [webhook.md](webhook.zh.md) | 通过身份验证的提供方交付、任意程序化规则,以及 fire-and-forget 的 Workspace Session 创建 |
+| [webhook.md](webhook.zh.md) | 通过身份验证的提供方交付、任意程序化规则,以及发起 Workspace 会话创建后不等待结果 |
 | [storage.md](storage.zh.md) | 存储子系统:后端约定(`StorageBackend`)、`StorageForms`、`DomainSpec`/`Domain`、`domain/changed` |
 | [workspace.md](workspace.zh.md) | 工作区注册表:`Workspace`/`WorkspaceId`、注册与解析、与会话 `cwd` 的关系 |
-| [web-client.md](web-client.zh.md) | 浏览器架构:启动、Remote 通信、配对的 Client model、UI adapter、Conversation 组装、Slots 与重连语义 |
-| [client-modules.md](client-modules.zh.md) | Web 插件表:`dsh.client` 声明、`WebBootGraph` 线上组合、bundle 路由与 index 转换 |
-| [slots.md](slots.zh.md) | 类型化 Web UI 组合:声明所有权、cardinality 与 scope、框架与功能注入、props 推导及当前层级 |
-| [client-resources.md](client-resources.zh.md) | 客户端资源模型:`dsh-resource://<type>/…` 地址、协议提供方与 `ResourceProtocolMap`、`useResource` 全局 hook 及其状态、钉住与释放 |
+| [web-client.md](web-client.zh.md) | 浏览器架构:启动、Remote 通信、配对的 Client model、UI 适配器、Conversation 组装、slot 与重连语义 |
+| [client-modules.md](client-modules.zh.md) | Web 插件表:`dsh.client` 声明、`WebBootGraph` 协议格式组合、bundle 路由与 index 挂接点 |
+| [slots.md](slots.zh.md) | 类型化 Web UI 组合:声明所有权、cardinality 与 scope、框架与功能注入、props 推导及已交付的层级结构 |
+| [client-resources.md](client-resources.zh.md) | 客户端资源模型:`dsh-resource://<type>/…` 地址、协议提供方与 `ResourceProtocolMap`、`useResource` 全局钩子及其状态、钉住与释放 |
 | [sidebar-right.md](sidebar-right.zh.md) | 右侧 Sidebar:资源地址与导航地址、tab 类型注册与路由、`ctx.sidebarRight` 导航服务、pane-tab slot 与 owner props、资源模型及 Workspace Files 服务 |
-| [conversation.md](conversation.zh.md) | target-neutral Session event 组装:Context identity、Location data、replay 路径、view builder 与 target 自有 render node |
+| [conversation.md](conversation.zh.md) | 目标无关的会话事件组装:上下文标识、位置数据、回放路径、视图构建器与目标自有的渲染节点 |
 | [session-projection.md](session-projection.zh.md) | 投影 seam:`SessionProjectionMap`、纯函数 `ProjectionDefinition` 单元、`ProjectionSnapshot` 的一致切面、变更馈送 |
-| [session-telemetry.md](session-telemetry.zh.md) | 对外会话上报能力 seam:`SessionTelemetryRecord`/`SessionTelemetrySeverity`、`SessionTelemetrySink` 约定和 `session-telemetry/record` 脱敏 waterfall |
+| [session-telemetry.md](session-telemetry.zh.md) | 对外会话上报能力 seam:`SessionTelemetryRecord`/`SessionTelemetrySeverity`、`SessionTelemetrySink` 约定和 `session-telemetry/record` 脱敏 waterfall(瀑布式事件) |
 
 > 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.zh.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务与事件使用每页生成的 **Cordis API** 小节。

+ 2 - 2
docs/subsystems/sidebar-right.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/sidebar-right.md
-sidebar-right.md: 807edc5873c97a256cb66923e776b0573af3186f
-sidebar-right.zh.md: a56ebe819e8fbc63f5e6aa7697eb08e03b8d1c46
+sidebar-right.md: 3b6e5eb08fc2c0a0bac74369bde4a48b2ee0fdb6
+sidebar-right.zh.md: 680e2ae16f8f8aeeea57b11172be8c8c9e359122

+ 2 - 2
docs/subsystems/sidebar-right.md

@@ -43,7 +43,7 @@ Tab identity is the pair `(kind, address)`: the registry's claim uses the addres
 | `priority` | One of three literal bands: `extension` (the default and the highest: a type from outside the product outranks every shipped viewer), `builtin` (types shipped with the product), `fallback` (plain-content viewers anything more specific should beat). |
 | `canOpen(address)` | Optional synchronous veto of a glob match; it runs on every routing decision. |
 | `title(address)` | The chip's text, captured into the layout record when the tab opens and never rewritten. |
-| `guide` | Optional entry boxes for the guide page: `{ order, title(), description(), icon? }`. Picking a box opens the contributing type as a page; omit to stay off the page. |
+| `guide` | Optional entry boxes for the guide page: `{ order, title(), description?(), icon? }`. Picking a box opens the contributing type as a page; omit to stay off the page. |
 
 Routing is a ranked claim. `candidates(address)` ranks the types whose patterns match and whose `canOpen` does not veto: by band, then by the length of the longest matched pattern, then by registration order. `claim(address, kind?)` picks the first candidate, or the named `kind` outright — its globs are skipped, its `canOpen` still applies — and returns `{ kind, contentId: address, title }`. An address no type claims throws: it is a wiring mistake, not a user error.
 
@@ -130,7 +130,7 @@ The Host `ctx.workspaceFiles` service and generated `workspaceFiles` Remote name
 
 ## Shipped types
 
-- **`guide`** — `builtin`, opened as `openTab('guide')`. A centred title, one line, and one entry box per `guide` entry the registered types contributed, in `order`; picking a box opens the contributing type as a page in the guide tab's place. A pane holds at most one guide tab, and the strip's add control appears only while its pane has none. A new pane receives the registered default page: the sole guide entry directly, or the guide when the entry count is not one ([guide](../../packages/client/ui-sidebar-right/README.md#the-guide)).
+- **`guide`** — `builtin`, opened as `openTab('guide')`. A muted compass sits above one capsule per contributed `guide` entry, in `order`; short lists show registered descriptions, and every missing icon uses the shipped placeholder. Picking a capsule opens the contributing type as a page in the guide tab's place. A pane holds at most one guide tab, and the strip's add control appears only while its pane has none. A new pane receives the registered default page: the sole guide entry directly, or the guide when the entry count is not one ([guide](../../packages/client/ui-sidebar-right/README.md#the-guide)).
 - **`text`** — `fallback`, `dsh-resource://file/**`, claiming Session addresses only. Document Preview observes metadata through `useResource<'file'>`, loads content through Remote callbacks, and owns renderer selection, the toolbar, per-tab refresh, scroll, and source navigation; unknown extensions render as plain text ([README](../../packages/client/ui-sidebar-documentpreview/README.md)).
 - **`files`** — `builtin`, opened as `openTab('files')`. The workspace directory tree, listed lazily through `list`, opening a file with `tab.actions.openResource(fileAddressFor(sessionId, root, path))` into its own pane ([README](../../packages/client/ui-sidebar-files/README.md)).
 

+ 2 - 2
docs/subsystems/sidebar-right.zh.md

@@ -43,7 +43,7 @@ tab 身份是 `(kind, address)` 二元组:注册表的认领把地址原文用
 | `priority` | 三档字面量之一:`extension`(缺省且最高:产品之外的类型压过所有内置查看器)、`builtin`(随产品发布的类型)、`fallback`(任何更具体的类型都应压过的纯内容查看器)。 |
 | `canOpen(address)` | 可选的同步否决,对 glob 命中生效;每次路由决策都会调用。 |
 | `title(address)` | chip 文本,在 tab 打开时捕获进布局记录,之后不再改写。 |
-| `guide` | 可选的引导页入口框:`{ order, title(), description(), icon? }`。点一框即把贡献它的类型作为页面打开;省略即不上引导页。 |
+| `guide` | 可选的引导页入口框:`{ order, title(), description?(), icon? }`。点一框即把贡献它的类型作为页面打开;省略即不上引导页。 |
 
 路由是一次排序认领。`candidates(address)` 对模式命中且未被 `canOpen` 否决的类型排序:先按档,再按最长命中模式的长度,最后按注册顺序。`claim(address, kind?)` 取第一个候选,或直接用点名的 `kind`——跳过它的 glob,但 `canOpen` 仍生效——返回 `{ kind, contentId: address, title }`。没有任何类型认领的地址会抛错:这是接线错误,不是用户错误。
 
@@ -130,7 +130,7 @@ Host 的 `ctx.workspaceFiles` 服务与生成的 `workspaceFiles` Remote 命名
 
 ## 内置类型
 
-- **`guide`**——`builtin`,以 `openTab('guide')` 打开。居中标题、一行说明,以及已注册类型贡献的每个 `guide` 入口一框、按 `order` 排列;点一框即在引导 tab 的位置把贡献它的类型作为页面打开。每个 pane 最多一个引导 tab,tab 条的新增控件只在本 pane 没有引导时出现。新 pane 使用已注册的默认页:只有一个引导入口时直接使用该入口,否则使用引导页([引导](../../packages/client/ui-sidebar-right/README.zh.md#the-guide))。
+- **`guide`**——`builtin`,以 `openTab('guide')` 打开。一枚弱化的罗盘位于各类型按 `order` 贡献的入口胶囊上方;入口较少时显示已注册的描述,未提供图标的入口统一使用内置占位符。点选胶囊即在引导 tab 的位置把贡献它的类型作为页面打开。每个 pane 最多一个引导 tab,tab 条的新增控件只在本 pane 没有引导时出现。新 pane 使用已注册的默认页:只有一个引导入口时直接使用该入口,否则使用引导页([引导](../../packages/client/ui-sidebar-right/README.zh.md#the-guide))。
 - **`text`**——`fallback`,`dsh-resource://file/**`,只认领 Session 地址。Document Preview 通过 `useResource<'file'>` 观察元数据,经 Remote 回调加载内容,并拥有渲染器选择、工具栏、逐 tab 刷新、滚动与源码定位;未知扩展名按纯文本渲染([README](../../packages/client/ui-sidebar-documentpreview/README.zh.md))。
 - **`files`**——`builtin`,以 `openTab('files')` 打开。工作区目录树,经 `list` 懒加载,用 `tab.actions.openResource(fileAddressFor(sessionId, root, path))` 在自己所在 pane 打开文件([README](../../packages/client/ui-sidebar-files/README.zh.md))。
 

+ 2 - 2
docs/user/guide/providers.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/user/guide/providers.md
-providers.md: 0db3280760c487c8469399bc9668b0685213e375
-providers.zh.md: 3af9096f5a77ee40afe2ee788aa22a0758dd0e86
+providers.md: 90d3bb2c96a80185fbc5e3e70f1ff577f6923acc
+providers.zh.md: 2937d676253ee45dad48655398736d36fc37dff2

+ 1 - 1
docs/user/guide/providers.md

@@ -186,5 +186,5 @@ Every switch, its accepted values, and the protocols that take it are listed und
 - **The Effort menu does not appear for a model you entered by hand** — It declares no levels. Add `reasoningEfforts` to the model in `settings.yaml`.
 - **`off` does not stop a DeepSeek model from thinking** — An empty `off` sends no reasoning field at all, and an endpoint that thinks by default keeps thinking. Set `compat.thinkingFormat: deepseek` on the model or the route.
 - **A compat switch is refused as having no value** — A key written with nothing after the colon. Give it a value, or remove the key to keep the installed catalog's.
-- **An image is refused before sending** — The model declares no image modality. Give a custom provider's model `input: [text, image]`; on DeepSeek's own route, select `deepseek-v4-flash-vision-exp`, the model that declares images.
+- **An image is refused before sending** — The model declares no image modality. Give a custom provider's model `input: [text, image]`; on DeepSeek's own route, select an image-capable entry from the configured catalog (`deepseek-flash` by default) and confirm that your gateway serves that model with image input.
 - **The provider rejects a request carrying an image** — The model declares images its endpoint does not actually serve. Remove `image` from whichever list granted it — the model's `input`, or the route's `defaultInput` — then start a new session: the attached image stays in the session log, so the same request repeats until the session moves off it.

+ 1 - 1
docs/user/guide/providers.zh.md

@@ -186,5 +186,5 @@ llm-pi-ai:
 - **手动录入的模型没有推理等级菜单**:该模型没有声明任何等级。在 `settings.yaml` 中给该模型加上 `reasoningEfforts`。
 - **`off` 无法让 DeepSeek 模型停止思考**:留空的 `off` 不发送任何推理字段,默认思考的端点就继续思考。请在模型或路由上设置 `compat.thinkingFormat: deepseek`。
 - **某个 compat 开关因没有值而被拒绝**:冒号后什么都没写。给它一个值,或删掉该键以沿用已安装 catalog 的值。
-- **图片在发送前被拒绝**:该模型未声明图片模态。请给自定义提供方的模型加上 `input: [text, image]`;在 DeepSeek 自身的路由上,请选择声明了图片能力的模型 `deepseek-v4-flash-vision-exp`。
+- **图片在发送前被拒绝**:该模型未声明图片模态。请给自定义提供方的模型加上 `input: [text, image]`;在 DeepSeek 自身的路由上,请从配置的目录中选择支持图片的条目(默认为 `deepseek-flash`),并确认网关提供该模型且支持图片输入。
 - **提供方拒绝了带图片的请求**:该模型声明了其端点实际并不提供的图片能力。请从授予它图片能力的那个列表中移除 `image`——可能是模型的 `input`,也可能是路由的 `defaultInput`——然后开启新会话:附加的图片会留在会话日志里,因此在会话离开它之前,同一个请求会不断重复。

+ 1 - 1
native/system/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 native/system/README.md
 README.md: 5a01754cb96fabbdfd2dbf4f068f19addd7a2347
-README.zh.md: dabd00cecd05ded13a50d2efd000985afed713d4
+README.zh.md: 27d0ca70e28e68e46cb121b8e6b63c56ba5c99d3

+ 5 - 5
native/system/README.zh.md

@@ -1,5 +1,5 @@
 ---
-description: "为 Linux 进程隔离与 POSIX Session 写锁提供预编译系统原语。"
+description: "为 Linux 进程隔离与 POSIX 会话写锁提供预编译系统原语。"
 kind: "package-library"
 ---
 # @deepseek-ai/node-addon-system
@@ -8,7 +8,7 @@ kind: "package-library"
 
 ## Summary
 
-使用 Linux `landlock-run` 可执行文件限制子进程,或通过 `./flock` 入口获取 POSIX 写锁。平台包包含预编译二进制;用户安装时不会构建原生代码。Landlock 策略与 Session 生命周期仍由调用方负责。
+使用 Linux `landlock-run` 可执行文件限制子进程,或通过 `./flock` 入口获取 POSIX 写锁。平台包包含预编译二进制;消费方安装时不会构建原生代码。Landlock 策略与会话生命周期仍由调用方负责。
 
 ## Table of Contents
 
@@ -22,9 +22,9 @@ kind: "package-library"
 
 [flock 行为约定](docs/flock-contract.md) 将描述符、进程和咨询式锁语义对应到独立原生测试。
 
-`@deepseek-ai/node-addon-system/flock` 导出 `tryLockExclusive(fd): Promise<void>`。在调用完成前保持描述符打开。获取操作使用非阻塞独占 flock;竞争以 `EAGAIN` 或 `EWOULDBLOCK` 拒绝,关闭该打开文件描述的最后一个描述符即释放锁。参见[入口 README](packages/entry/README.zh.md)。
+`@deepseek-ai/node-addon-system/flock` 导出 `tryLockExclusive(fd): Promise<void>`。在调用完成前保持描述符打开。获取操作使用非阻塞独占 flock;发生竞争时,返回的 Promise 会以 `EAGAIN` 或 `EWOULDBLOCK` 拒绝,关闭该打开文件描述的最后一个描述符即释放锁。参见[入口 README](packages/entry/README.zh.md)。
 
-导入任一入口都不会加载 addon。Landlock 可执行文件缺失时探测为不可用;flock 绑定缺失时拒绝获取。两条路径都不会编译或静默授予不受支持的行为。
+导入任一入口都不会加载 addon。Landlock 可执行文件缺失时探测为不可用;flock 绑定缺失时拒绝获取。两条路径都不会进行编译,也不会静默允许不受支持的行为。
 
 ## Support
 
@@ -32,7 +32,7 @@ Linux x64/arm64 包包含静态 Landlock 可执行文件,以及分别用于 gl
 
 ## Development
 
-在本目录运行 `pnpm build:ts` 构建入口、`pnpm build:native` 构建当前宿主声明的原生产物、`pnpm build:test-oracle` 构建独立的 flock 系统调用 fixture。随后用 `pnpm test` 验证入口、锁、打包及可用的内核行为。Linux 完整构建需要 musl-gcc;macOS 使用 cc。根目录 `pnpm run build:native-system` 只构建源码测试所需的当前宿主 addon。
+在本目录运行 `pnpm build:ts` 构建入口、`pnpm build:native` 构建当前宿主声明的原生产物、`pnpm build:test-oracle` 构建独立的 flock 系统调用 fixture(测试前置数据)。随后用 `pnpm test` 验证入口、锁、打包及可用的内核行为。Linux 完整构建需要 musl-gcc;macOS 使用 cc。根目录 `pnpm run build:native-system` 只构建源码测试所需的当前宿主 addon。
 
 [架构](docs/architecture.md)、[打包](docs/packaging.md)和[发布流程](docs/release.md)分别负责实现与发布细节。
 

+ 1 - 1
native/system/packages/darwin-x64/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 native/system/packages/darwin-x64/README.md
 README.md: 1740788eef943373591a895baeb7207635972a46
-README.zh.md: 644165743486a29e48bee7af33429ebf4f5d8fe2
+README.zh.md: 88053b85150c94048a4ffc32b506d365ff13c15f

+ 1 - 1
native/system/packages/darwin-x64/README.zh.md

@@ -6,4 +6,4 @@ kind: "package-library"
 
 [English](README.md) | 中文
 
-此平台包提供 `bin/system.node`,供 `@deepseek-ai/node-addon-system/flock` 使用的稳定 Node-API v8 addon。它不包含 Landlock 可执行文件、JavaScript 加载器或安装构建脚本。Native 工作流在 macOS x64 上构建它,并负责验证安装后的产物。
+此平台包提供 `bin/system.node`,这是一个供 `@deepseek-ai/node-addon-system/flock` 使用的稳定 Node-API v8 addon。它不包含 Landlock 可执行文件、JavaScript 加载器或安装构建脚本。Native 工作流在 macOS x64 上构建它,并负责验证安装后的产物。

+ 1 - 1
native/system/packages/entry/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 native/system/packages/entry/README.md
 README.md: 8f6a4d341c8f9e4c104e5706e08d9a7d45c065e1
-README.zh.md: 152257916df0cf40511b9743d389ae753ab6c85c
+README.zh.md: e401fc2ec71b1afb664d8e73bf3e9372054957a7

+ 2 - 2
native/system/packages/entry/README.zh.md

@@ -8,8 +8,8 @@ kind: "package-library"
 
 `./landlock-run` 入口导出 Landlock 启动器路径、强制执行探测、授权参数和协议常量。独立的 `./flock` 入口导出 `tryLockExclusive(fd): Promise<void>`;导入任一入口都不会加载 `system.node`。包不提供根导出。
 
-锁操作异步尝试 `LOCK_EX | LOCK_NB`。在完成前保持调用方拥有的描述符打开;竞争以 `EAGAIN`/`EWOULDBLOCK` 拒绝,其他系统调用失败也会拒绝,错误携带 code、正 errno 和 `syscall: 'flock'`。原生调用准备阶段的错误也会拒绝同一个 promise。关闭该打开文件描述的最后一个描述符即释放锁。绑定不打开、复制、关闭或显式解锁描述符。
+锁操作异步尝试 `LOCK_EX | LOCK_NB`。在完成前保持调用方拥有的描述符打开;竞争以 `EAGAIN`/`EWOULDBLOCK` 拒绝,其他系统调用失败也会拒绝,错误携带 code、值为正数的 errno 和 `syscall: 'flock'`。原生调用准备阶段的错误也会拒绝同一个 promise。关闭指向该打开文件描述的最后一个描述符即释放锁。绑定不打开、复制、关闭或显式解锁描述符。
 
-可选操作系统/CPU 平台包携带二进制。Linux 包含 `bin/landlock-run` 和分别用于两种 libc 的 `bin/glibc/system.node` / `bin/musl/system.node`;macOS 包含 `bin/system.node`。flock 绑定缺失或无法加载时拒绝获取,不在安装时编译。Landlock 仍是遵循既有失败关闭协议的独立可执行文件;不支持的内核或平台探测为不可用。
+可选操作系统/CPU 平台包携带二进制。Linux 包含 `bin/landlock-run` 和分别用于两种 libc 的 `bin/glibc/system.node` / `bin/musl/system.node`;macOS 包含 `bin/system.node`。flock 绑定缺失或无法加载时,锁获取请求会被拒绝,不在安装时编译。Landlock 仍是遵循既有失败关闭协议的独立可执行文件;不支持的内核或平台探测结果为不可用。
 
 两个 C 源文件随包分发以供审计。参见工作区[架构](../../docs/architecture.md)、[支持矩阵](../../docs/support-matrix.md)和 [CLI 约定](../../docs/cli-contract.md)。

+ 1 - 1
native/system/packages/linux-arm64/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 native/system/packages/linux-arm64/README.md
 README.md: b1e97481a3f59b5b7cf85e6565cd3d078e42cd7f
-README.zh.md: 79f3b75e16570c4dff8d7249bcd75481f11cc85c
+README.zh.md: 68efb95c60f41b2b95d1e963d9b4f0c8c130a80c

+ 2 - 2
native/system/packages/linux-arm64/README.zh.md

@@ -6,6 +6,6 @@ kind: "package-library"
 
 [English](README.md) | 中文
 
-此平台包包含静态 musl 可执行文件 `bin/landlock-run`,以及 Node-API v8 addon `bin/glibc/system.node` 和 `bin/musl/system.node`。入口包按运行 Node 进程的 libc 选择 addon;Landlock 可执行文件在两种 libc 系统上共用。
+此平台包包含静态 musl 可执行文件 `bin/landlock-run`,以及 Node-API v8 addon `bin/glibc/system.node` 和 `bin/musl/system.node`。入口会选择与正在运行的 Node 进程所用 libc 匹配的 addon;Landlock 可执行文件在两种 libc 系统上共用。
 
-包中没有 JavaScript 或安装编译脚本。平台 prepack 检查完整产物、ELF 架构、Node-API 导出和启动器可执行权限;安装演练核对字节并执行原生行为。参见工作区[支持矩阵](../../docs/support-matrix.md)。
+包中没有 JavaScript 或安装编译脚本。平台 prepack 检查产物完整性、ELF 架构、Node-API 导出和启动器可执行权限;已安装产物演练核对字节并执行原生行为。参见工作区的[支持矩阵](../../docs/support-matrix.md)。

+ 1 - 1
native/system/packages/linux-x64/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 native/system/packages/linux-x64/README.md
 README.md: caebb5d8a888a69a952fc89afb3edfb8ece489fc
-README.zh.md: 6ad1dcf6fe26007f76cdeb98936a94262ece4d7c
+README.zh.md: bf5edf59222a5675dbaa63d02fcac115bec7b58d

+ 2 - 2
native/system/packages/linux-x64/README.zh.md

@@ -6,6 +6,6 @@ kind: "package-library"
 
 [English](README.md) | 中文
 
-此平台包包含静态 musl 可执行文件 `bin/landlock-run`,以及 Node-API v8 addon `bin/glibc/system.node` 和 `bin/musl/system.node`。入口包按运行 Node 进程的 libc 选择 addon;Landlock 可执行文件在两种 libc 系统上共用。
+此平台包包含静态 musl 可执行文件 `bin/landlock-run`,以及 Node-API v8 addon `bin/glibc/system.node` 和 `bin/musl/system.node`。入口按运行 Node 进程的 libc 选择 addon;Landlock 可执行文件在两种 libc 系统上共用。
 
-包中没有 JavaScript 或安装编译脚本。平台 prepack 检查完整产物、ELF 架构、Node-API 导出和启动器可执行权限;安装演练核对字节并执行原生行为。参见工作区[支持矩阵](../../docs/support-matrix.md)。
+包中没有 JavaScript 或安装编译脚本。平台 prepack 检查完整产物、ELF 架构、Node-API 导出和启动器可执行权限;已安装产物演练核对字节并执行原生行为。参见工作区[支持矩阵](../../docs/support-matrix.md)。

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "@deepseek-ai/dsh-root",
-  "version": "0.1.5-alpha.1",
+  "version": "0.1.5-rc.1",
   "license": "MIT",
   "private": true,
   "type": "module",

+ 1 - 1
packages/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/README.md
 README.md: 9c298d3f2426bab92a6d602495d836ef8ee9fb44
-README.zh.md: 309a0e678968927b79d527991447847d0fa45f83
+README.zh.md: db8977a279713ac7a2f86144323a54125743a46a

+ 16 - 16
packages/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-group"
 
 ## 概述
 
-harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会话与 agent 循环、面向模型的工具、shell 与文件系统执行、Web 访问、subagent 等等。把本页当作顶层地图使用:先找到拥有某能力的组,再打开其 README 查看包列表。每个包都以 `@deepseek-ai/dsh-*` 为作用域、只属于一个组;每个组的 README 都是该能力系列的权威包映射。
+harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会话与 agent loop(智能体循环)、面向模型的工具、shell 与文件系统执行、Web 访问、subagent 等等。把本页当作顶层地图使用:先找到拥有某能力的组,再打开其 README 查看包列表。每个包都以 `@deepseek-ai/dsh-*` 为作用域、只属于一个组;每个组的 README 都是该能力系列的权威包映射。
 
 ## 目录
 
@@ -35,24 +35,24 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会
 | [`schedule/`](schedule/README.zh.md) | 仅限会话内的定时后续操作 |
 | [`feedback/`](feedback/README.zh.md) | 人类反馈的采集与命令 |
 | [`identity/`](identity/README.zh.md) | 共享匿名身份 |
-| [`llm/`](llm/README.zh.md) | LLM 能力系列:抽象服务 + 提供方适配器 |
+| [`llm/`](llm/README.zh.md) | LLM(大语言模型)能力系列:抽象服务 + 提供方适配器 |
 | [`e2b/`](e2b/README.zh.md) | E2B 远程运行时提供方 |
 | [`subprocess/`](subprocess/README.zh.md) | 子进程能力系列:Service Definition + 本地进程树提供方 |
 | [`shell/`](shell/README.zh.md) | Bash 能力系列:执行器 seam、本地实现、面向模型的工具 |
 | [`terminal/`](terminal/README.zh.md) | 持久 PTY 能力系列:限定所有者范围的会话、本地实现、面向模型的工具 |
 | [`code-runtime/`](code-runtime/README.zh.md) | 代码执行能力系列:Service Definition + worker 线程提供方 + PTC mode Consumer |
-| [`sandbox/`](sandbox/README.zh.md) | 进程限制 seam;bwrap/Landlock/Seatbelt 后端 |
+| [`sandbox/`](sandbox/README.zh.md) | 进程限制 seam;bwrap、Landlock、Seatbelt 后端 |
 | [`fs/`](fs/README.zh.md) | 文件系统能力系列:seam、本地实现、面向模型的文件工具、发现工具 |
 | [`lsp/`](lsp/README.zh.md) | LSP 能力系列:seam、通用 stdio 提供方和 `lsp` 工具 |
-| [`skill/`](skill/README.zh.md) | skill 能力系列:提供方注册表、本地提供方、面向模型的目录/loader |
-| [`compaction/`](compaction/README.zh.md) | 压缩能力系列:Service Definition + 基础提供方 + 命令 Consumer |
+| [`skill/`](skill/README.zh.md) | skill(技能)能力系列:提供方注册表、本地提供方、面向模型的目录/loader |
+| [`compaction/`](compaction/README.zh.md) | 压缩(compaction)能力系列:Service Definition + 基础提供方 + 命令 Consumer |
 | [`context/`](context/README.zh.md) | 模型可见请求上下文:workspace 指令、时间上下文、引用 |
 | [`subagent/`](subagent/README.zh.md) | subagent 能力系列:提供方注册表约定和面向模型的委托工具 |
 | [`jobs/`](jobs/README.zh.md) | 通用后台任务运行时和面向模型的作业控制工具 |
 | [`experimental/`](experimental/README.zh.md) | 私有原型与内部专用插件 |
-| [`workflow/`](workflow/README.zh.md) | 工作流 seam、worker 线程引擎、面向模型的 `workflow`/`ralph` 工具 |
-| [`webhook/`](webhook/README.zh.md) | 已验证外部事件、受信规则与即发即弃 Workspace Session |
-| [`web/`](web/README.zh.md) | Web 能力系列:seam、搜索/获取提供方、面向模型的 Web 工具 |
+| [`workflow/`](workflow/README.zh.md) | 工作流 seam、worker 线程引擎、面向模型的 `workflow`/`ralph` 工具 |
+| [`webhook/`](webhook/README.zh.md) | 已验证外部事件、受信规则与即发即弃 Workspace 会话 |
+| [`web/`](web/README.zh.md) | Web 能力系列:seam、搜索/获取提供方、面向模型的 Web 工具 |
 | [`attachment/`](attachment/README.zh.md) | 持久附件标识、校验、本地内容寻址存储 |
 | [`spill/`](spill/README.zh.md) | spill 能力系列:存储 seam、本地实现、工具结果 spill 策略 |
 | [`todo/`](todo/README.zh.md) | 面向模型的 `todo_write` 工具 |
@@ -60,30 +60,30 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会
 | [`preset/`](preset/README.zh.md) | 由 preset `cordis.yml` 按会话组装 agent |
 | [`guard/`](guard/README.zh.md) | 循环卫生守卫:建议性重复调用提醒 + `tools/execute` 截止时间强制执行器 |
 | [`bundle/`](bundle/README.zh.md) | 可安装的 `dsh --profile` 补丁层 |
-| [`extensions/`](extensions/README.zh.md) | agent 运行时自修改:实时插件/服务检查与模型所写挂载/卸载 |
-| [`hooks/`](hooks/README.zh.md) | 钩子桥接 + 共享的 Claude Code / Codex 线协议库 |
+| [`extensions/`](extensions/README.zh.md) | agent 运行时自修改:实时插件/服务检查与模型所写挂载/卸载 |
+| [`hooks/`](hooks/README.zh.md) | 钩子桥接 + 共享的 Claude Code/Codex 线协议库 |
 | [`session/`](session/README.zh.md) | 持久会话数据平面:持久化 seam + 后端、投影 seam、基于日志的标题、会话上报 |
 | [`session-query/`](session-query/README.zh.md) | 会话检索系列:逻辑语料库、有界读取、血缘、语义过滤、SQLite 全文搜索 |
 | [`settings/`](settings/README.zh.md) | 用户设置 seam + 基于文件的提供方 |
-| [`credentials/`](credentials/README.zh.md) | 凭据引用/记录 seam + 环境变量优先于 `.env` 的提供方 + 询问人类的授权 flow |
+| [`credentials/`](credentials/README.zh.md) | 凭据引用与凭据记录 seam + 环境变量优先于 `.env` 的提供方 + 需要向人询问的授权流程 |
 | [`storage/`](storage/README.zh.md) | 非会话存储中枢 + 后端 + 领域形式 |
 | [`workspace/`](workspace/README.zh.md) | Workspace 实体 |
 | [`sdk/`](sdk/README.zh.md) | 进程外 SDK:JSON-RPC 协议与 TypeScript 客户端/服务器 |
-| [`acp/`](acp/README.zh.md) | 仅面向自动化的 Agent Client Protocol 服务器 |
-| [`interaction/`](interaction/README.zh.md) | 人机协作平面:批准/交互 seam、权限预设、命令、询问用户的工具 |
+| [`acp/`](acp/README.zh.md) | 仅面向自动化的 ACP(Agent Client Protocol)服务器 |
+| [`interaction/`](interaction/README.zh.md) | 人机协作平面:批准/交互 seam、权限预设、命令、询问用户的工具 |
 | [`boot/`](boot/README.zh.md) | 共享的 app bin 启动粘合层 |
 | [`host/`](host/README.zh.md) | web GUI 宿主半侧:API 网关 + HTTP 路由服务器 |
 | [`client/`](client/README.zh.md) | web GUI 浏览器半侧:shell、协议层、对象服务、slot、`ui-*` 插件 |
 | [`test-support/`](test-support/README.zh.md) | 支持基础设施(testkit、不变式、回放、Loader 冒烟测试) |
 | [`runtime-diagnostics/`](runtime-diagnostics/README.zh.md) | 运行时诊断:按包归属的运行时不变式检查与报告 |
-| [`util/`](util/README.zh.md) | 组间共享的低层零依赖工具(`Branded<B>`、home/路径辅助函数、超时、留存) |
+| [`util/`](util/README.zh.md) | 组间共享的低层零依赖工具(`Branded<B>`、home/路径辅助函数、超时、留存) |
 
 -----
 
 <a id="release-expectations"></a>
 ## 发布预期
 
-大多数组是产品——稳定 API。例外:`e2b/` 是 POC,`experimental/` 不发布,`test-support/`、`runtime-diagnostics/` 与 `util/` 是兼容性预期较低的支持组。
+大多数组属于产品组,提供稳定 API。例外:`e2b/` 是 POC,`experimental/` 不发布,`test-support/`、`runtime-diagnostics/` 与 `util/` 是兼容性预期较低的支持组。
 
 -----
 
@@ -92,7 +92,7 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会
 
 依赖图由工具生成:[docs/module-graph.md](../docs/module-graph.zh.md)(`pnpm run gen-module-graph`,CI 中有新鲜度门禁)。
 
-**扩展插件依赖 Service Definition,绝不依赖具体提供方。** `dsh-agent-loop` 可替换;UI、钩子和工具插件使用 `dsh-agent`。组合包可以依赖主干插件。能力在需要独立演进时分离 Service Definition / Service Provider / Consumer 角色;详见[能力 seam](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)。
+**扩展插件依赖 Service Definition,绝不依赖具体提供方。** `dsh-agent-loop` 可替换;UI、钩子和工具插件使用 `dsh-agent`。组合包可以依赖主干插件。能力在需要独立演进时分离 Service Definition/Service Provider/Consumer 角色;详见[能力 seam](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)。
 
 -----
 

+ 1 - 1
packages/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/acp/README.md
 README.md: 710a77335eec233c6fdef36091742fb61e95f30a
-README.zh.md: eb2e6847aeb5926d276e2c77d7a0482f55217287
+README.zh.md: a40fb43ef64a92a1467faf2cfeb9fd884e8890a1

+ 4 - 4
packages/acp/README.zh.md

@@ -1,15 +1,15 @@
 ---
-description: "ACP(Agent Client Protocol)包组:通过 JSON-RPC stdio 将全新 harness agent 暴露给程序化客户端的仅自动化服务器。"
+description: "ACP(Agent Client Protocol)包组:通过 JSON-RPC stdio 将全新 harness agent(智能体)暴露给程序化客户端的仅面向自动化的服务器。"
 kind: "package-group"
 ---
 
-# acp/ — Agent Client Protocol 自动化
+# acp/:Agent Client Protocol 自动化
 
 [English](README.md) | 中文
 
 ## 概述
 
-acp 组提供一个包:一台服务器,让程序与自动化可以通过标准 Agent Client Protocol 运行持久 DeepSeek Harness agent。客户端可以创建、列出、恢复与关闭会话,挂载标准 MCP 服务器,选择模型选项,发送文本与图片提示词,接收语义更新,响应权限提示并取消工作——无需人类参与。从另一个 harness 启动这种服务器的配套客户端位于 `subagent/subagent-acp`。本页是组的映射;包 README 负责各自的包级约定。
+acp 组提供一个包:一个服务器,让程序与自动化流程可以通过标准 Agent Client Protocol 运行持久 DeepSeek Harness agent。客户端可以创建、列出、恢复与关闭会话,挂载标准 MCP 服务器,选择模型选项,发送文本与图片提示词,接收语义更新,响应权限提示并取消工作——无需人类参与。从另一个 harness 启动这种服务器的配套客户端位于 `subagent/subagent-acp`。本页概述该包组;各包的具体约定由其 README 规定。
 
 ## 目录
 
@@ -24,7 +24,7 @@ acp 组提供一个包:一台服务器,让程序与自动化可以通过标
 
 | 包 | 职责 |
 |---|---|
-| [`acp/`](acp/README.zh.md) | 让程序通过 ACP 管理持久 agent、挂载 MCP 服务器、选择模型选项、发送或取消工作并接收语义更新 |
+| [`acp/`](acp/README.zh.md) | 让程序通过 ACP 管理持久 agent、挂载 MCP 服务器、选择模型选项、发送提示词、取消工作并接收语义更新 |
 
 -----
 

+ 2 - 2
packages/acp/acp/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/acp/acp/README.md
-README.md: 635e0e6993a65f403a5cc89f2c9d147b48308a04
-README.zh.md: 65d7d3b2f9f66701c801c910048a501f565b0a46
+README.md: a30396733026108fdeaff67713fbe7aefb8c5101
+README.zh.md: 7f7f2f43ac6fa3059a6d5400e6c2c8d7227197d9

+ 3 - 3
packages/acp/acp/README.md

@@ -1,5 +1,5 @@
 ---
-description: "Automation-only Agent Client Protocol server for programmatic clients and maintainers driving DeepSeek Harness agents over JSON-RPC stdio."
+description: "Automation-only ACP (Agent Client Protocol) server for programmatic clients and maintainers driving DeepSeek Harness agents over JSON-RPC stdio."
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-acp` lets trusted programs automate persistent DeepSeek Harness agents through the standard [Agent Client Protocol](https://agentclientprotocol.com): create or resume sessions, select a model and reasoning effort, attach MCP servers, submit or cancel work, receive semantic updates, and close sessions independently. Choose it for out-of-process subagents, test runners, and scripted controllers; it intentionally omits DSH-specific presentation data and interactive UI features. Persistence supports listing, resuming, and closing sessions across process restarts, but deletion, forks, transcript replay, and additional directories are unsupported. Run `pnpm dsh --profile acp` to start the server; use `dsh-subagent-acp` as the repository client.
+`dsh-acp` lets trusted programs automate persistent DeepSeek Harness agents through the standard [ACP](https://agentclientprotocol.com): create or resume sessions, select a model and reasoning effort, attach MCP servers, submit or cancel work, receive semantic updates, and close sessions independently. Choose it for out-of-process subagents, test runners, and scripted controllers; it intentionally omits DSH-specific presentation data and interactive UI features. Persistence supports listing, resuming, and closing sessions across process restarts, but deletion, forks, transcript replay, and additional directories are unsupported. Run `pnpm dsh --profile acp` to start the server; use `dsh-subagent-acp` as the repository client.
 
 ## Table of Contents
 
@@ -73,7 +73,7 @@ One connection can run several sessions at once, each independent. The calls a c
 | `session/update` | Committed assistant messages and thoughts, generic tool lifecycle, configuration changes, and context usage, serialized per session. |
 | `session/request_permission` | A permission prompt with one-shot allow/reject choices; your client can answer automatically. |
 
-Session configuration offers opaque provider/model choices from the live LLM service catalog and a `reasoning_effort` selector when the exact model declares one. A prompt snapshots that selection before asynchronous image admission and pins it across every model step in that turn; a concurrent option change applies to the next turn. ACP clients are trusted controllers: stdio MCP entries authorize their absolute commands and environment, HTTP entries authorize their absolute HTTP(S) URLs and headers, and any initial connection or discovery failure rolls back the unpublished Agent. Unsupported surfaces are omitted or reject: `session/load`, deletion, fork, additional directories, SSE or ACP-transport MCP, modes, commands, plans, terminals, client filesystem operations, and elicitation.
+Session configuration offers opaque provider/model choices from the live LLM service catalog and a `reasoning_effort` selector when the exact model declares one. A prompt snapshots that selection before asynchronous image admission and pins it across every model step in that turn; a concurrent option change applies to the next turn. ACP clients are trusted controllers: stdio MCP entries authorize their absolute commands and environment, HTTP entries authorize their absolute HTTP(S) URLs and headers, and any initial connection or discovery failure rolls back the unpublished Agent. Unsupported surfaces are omitted or rejected: `session/load`, deletion, fork, additional directories, SSE or ACP-transport MCP, modes, commands, plans, terminals, client filesystem operations, and elicitation.
 
 -----
 

+ 11 - 11
packages/acp/acp/README.zh.md

@@ -1,5 +1,5 @@
 ---
-description: "面向程序化客户端与维护者的仅自动化 Agent Client Protocol 服务器,用于通过 JSON-RPC stdio 驱动 DeepSeek Harness agent。"
+description: "面向程序化客户端与维护者的仅自动化 ACP(Agent Client Protocol)服务器,用于通过 JSON-RPC stdio 驱动 DeepSeek Harness agent(智能体)。"
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-acp` 让受信程序通过标准 [Agent Client Protocol(ACP)](https://agentclientprotocol.com) 自动操作持久 DeepSeek Harness agent:创建或恢复会话、选择模型与推理强度、挂载 MCP 服务器、提交或取消工作、接收语义更新,并独立关闭会话。进程外 subagent、测试运行器与脚本化控制器适合选择它;它刻意不提供 DSH 专用呈现数据与交互式 UI 功能。持久化支持跨进程重启列出、恢复与关闭会话,但不支持删除、fork、转录回放与附加目录。运行 `pnpm dsh --profile acp` 可启动服务器;仓库客户端使用 `dsh-subagent-acp`。
+`dsh-acp` 让受信程序通过标准 [ACP](https://agentclientprotocol.com) 自动操作持久 DeepSeek Harness agent:创建或恢复会话、选择模型与推理强度、挂载 MCP 服务器、提交或取消工作、接收语义更新,并独立关闭会话。进程外 subagent、测试运行器与脚本化控制器适合选择它;它刻意不提供 DSH 专用呈现数据与交互式 UI 功能。持久化支持跨进程重启列出、恢复与关闭会话,但不支持删除、fork、transcript(文本记录)回放与附加目录。运行 `pnpm dsh --profile acp` 可启动服务器;仓库客户端使用 `dsh-subagent-acp`。
 
 ## 目录
 
@@ -64,8 +64,8 @@ kind: "package-reference"
 | `initialize` | 稳定 ACP v1,以及 `session/list`、`session/resume`、`session/close` 与 Streamable HTTP MCP 支持;图片提示词只在持久附件存储和配置的确切路由支持时公布。 |
 | `authenticate` | 立即成功;服务器不需要身份验证。 |
 | `session/new` | 全新持久 agent;其绝对工作区与 stdio 或 HTTP MCP 服务器会在发布前通过校验,并返回完整配置选项状态。 |
-| `session/list` | 按确定的新到旧顺序分页返回已持久、可恢复的根会话;可选绝对 `cwd` 筛选会尽可能使用物理目录标识。 |
-| `session/resume` | 恢复一个已持久且非活跃的会话;组合前校验其规范工作区,并恢复日志但不回放旧更新。 |
+| `session/list` | 按确定的新到旧顺序分页返回已持久化、可恢复的根会话;可选绝对 `cwd` 筛选会尽可能使用物理目录标识。 |
+| `session/resume` | 恢复一个已持久化且非活跃的会话;组合前校验其规范工作区,并恢复日志但不回放旧更新。 |
 | `session/close` | 停稳式取消、更新 drain、后代释放、持久化 flush,并且只释放指定 Agent 作用域。 |
 | `session/set_config_option` | 串行更新公布的 `model` 或 `reasoning_effort`,并返回完整结果状态。 |
 | `session/prompt` | 有序文本、资源链接与受支持图片,每个会话一次一个提示词;Agent 空闲且有序更新交付后才结算。 |
@@ -73,7 +73,7 @@ kind: "package-reference"
 | `session/update` | 已提交 assistant 消息与 thought、通用工具生命周期、配置变化与上下文用量,按会话串行交付。 |
 | `session/request_permission` | 带一次性允许/拒绝选项的权限提示;你的客户端可以自动回答。 |
 
-会话配置从实时 LLM 服务目录提供不透明的提供方/模型选项,并在确切模型声明推理选项时提供 `reasoning_effort`。提示词会在异步图片准入前快照该选择,并在该轮的每个模型步骤中固定它;并发选项变更从下一轮开始生效。ACP 客户端是受信控制器:stdio MCP 条目授权其绝对命令与环境,HTTP 条目授权其绝对 HTTP(S) URL 与 header;初始连接或发现失败会回滚尚未发布的 Agent。不支持的界面会被省略或拒绝:`session/load`、删除、fork、附加目录、SSE 或 ACP 传输 MCP、mode、命令、计划、终端、客户端文件系统操作与 elicitation。
+会话配置从实时 LLM(大语言模型)服务目录提供不透明的提供方/模型选项,并在确切模型声明推理选项时提供 `reasoning_effort`。提示词会在异步图片准入前快照该选择,并在该轮次的每个模型步骤中固定它;并发选项变更从下一轮次开始生效。ACP 客户端是受信控制器:stdio MCP 条目授权其绝对命令与环境,HTTP 条目授权其绝对 HTTP(S) URL 与 header;初始连接或发现失败会回滚尚未发布的 Agent。不支持的界面会被省略或拒绝:`session/load`、删除、fork、附加目录、SSE(Server-Sent Events)或 ACP 传输 MCP、mode、命令、计划、终端、客户端文件系统操作与 elicitation。
 
 -----
 
@@ -102,15 +102,15 @@ kind: "package-reference"
 | [`src/index.ts`](src/index.ts) | 插件入口:`Config` schema、`AgentSideConnection` 接线、按会话记录、准入与结算、清理 |
 | [`src/content.ts`](src/content.ts) | 协议内容准入与投影:图片校验、路由重查、提示词重建、assistant 块转换 |
 | [`src/codec.ts`](src/codec.ts) | 轮次结束到 ACP `stopReason` 的纯映射 |
-| — | 不发布运行时不变式伴生入口;本传输不拥有持久包内事件流。 |
+| — | 不发布运行时不变式伴生入口;本传输不拥有持久包内事件流;协议与生命周期测试覆盖其映射。 |
 
 ### 准入与提示词结算
 
-每个会话只允许一个正在处理的提示词。准入先校验整个提示词批次、快照所选路由、重新检查 Agent 是否为同一对象与图片能力、持久化图片附件,然后才把用户消息入队——赢得准入的取消绝不会入队迟到的轮次。入队后,会话模块把该快照与 inbox 消息关联到认领时刻,并在提示词变量与该轮的每个模型步骤中固定相同的提供方、模型与推理强度。按会话更新会串行交付;已提交图片会重新读取并验证完整性,因此图片缺失或损坏会让关联提示词失败,而不是发出占位符。结算优先级依次为显式取消、已提交输出失败、区间内 Agent 失败、关联轮次结束。
+每个会话只允许一个正在处理的提示词。准入先校验整个提示词批次、快照所选路由、重新检查 Agent 是否为同一对象与图片能力、持久化图片附件,然后才把用户消息入队——先于准入完成的取消绝不会让迟到的轮次入队。入队后,会话模块会将该快照与 inbox 消息关联起来,直至该消息被认领,并在提示词变量与该轮次的每个模型步骤中固定相同的提供方、模型与推理强度。按会话更新会串行交付;已提交图片会重新读取并验证完整性,因此图片缺失或损坏会让关联提示词失败,而不是发出占位符。结算优先级依次为显式取消、已提交输出失败、区间内 Agent 失败、关联轮次结束。
 
 ### 清理与连接归属
 
-每个会话模块拥有其 Agent 句柄、MCP 挂载、未来与轮次固定的模型选择、提示词槽位、更新链和记忆化关闭操作。显式关闭、客户端断开与 Cordis 释放使用同一停稳式清理流程:停止新工作、取消提示词准入与 Agent 活动、drain 已提交更新、按子优先顺序释放可继续后代、flush 持久化并释放所拥有的 Agent 作用域。会话关闭后,持久状态仍可供列出与恢复;共享上下文的其他会话或前端不受影响。
+每个会话模块拥有其 Agent 句柄、MCP 挂载、未来与轮次固定的模型选择、提示词槽位、更新链和记忆化关闭操作。显式关闭、客户端断开与 Cordis 释放使用同一停稳式清理流程:停止新工作、取消提示词准入与 Agent 活动、drain 已提交更新、按子优先顺序释放可继续后代、flush 持久化并释放所拥有的 Agent 作用域。会话关闭后,已持久化的状态仍可供列出与恢复;共享上下文的其他会话或前端不受影响。
 
 </details>
 
@@ -139,11 +139,11 @@ kind: "package-reference"
 
 #### Token 影响
 
-提示词内容、工具调用/结果和持久图片引用会保留在该会话中直到 compaction。并发会话保留独立上下文。
+提示词内容、工具调用/结果和持久图片引用会保留在该会话中直到上下文压缩(context compaction)。并发会话保留独立上下文。
 
 #### KV Cache 影响
 
-仅追加;新用户消息位于可复用请求前缀之后,不会使先前缓存条目失效。
+只要所选路由与组装后的前缀保持不变,就仅追加。模型变更会使下一个 ACP 轮次改用新路由。
 
 ### 权限决策
 
@@ -169,7 +169,7 @@ kind: "package-reference"
 - **仅一个主 workspace**——附加目录仍不支持。
 - **仅光栅提示词图片**——PNG、JPEG、WebP 与 GIF 要求持久附件存储及确切的图片能力路由。
 - **仅 MCP 工具**——MCP resource 与 prompt 没有 DSH 消费方。
-- **没有转录回放或交互式扩展**——会话删除、fork、`session/load`、mode、命令、计划、终端、客户端文件系统操作与 elicitation 仍不属于此自动化界面。
+- **没有 transcript 回放或交互式扩展**——会话删除、fork、`session/load`、mode、命令、计划、终端、客户端文件系统操作与 elicitation 仍不属于此自动化界面。
 
 <a id="dev-note"></a>
 ### 开发备注

+ 1 - 1
packages/acp/acp/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-acp",
   "description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio",
-  "version": "0.1.5-alpha.1",
+  "version": "0.1.5-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/api/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/api/README.md
 README.md: 5bc878fa53b9ad0c3795c13e9442274b98820190
-README.zh.md: 814113f405b3d5c40f74b529bff57d0aa2a9c77f
+README.zh.md: 8aa364850e2378c7361807ae1065cfdd6eeaa9cc

+ 5 - 5
packages/api/README.zh.md

@@ -22,18 +22,18 @@ kind: "package-group"
 <a id="packages"></a>
 ## 包
 
-下面这些包共同提供 Remote 层;穷尽式约定以各包 README 为准。
+下面这些包共同提供 Remote 层;完整约定以各包 README 为准。
 
 | 包 | 职责 | ctx key |
 |---|---|---|
 | [`remotes/`](remotes/README.zh.md) | 决定 Client 可以消费哪些 Host 能力与事件。 | — |
-| [`gateway/`](gateway/README.zh.md) | 承载带类型的单次调用、多路复用 stream 与转发的 Host 事件。 | `ctx.typertGateway` / `ctx.remote` |
-| [`session-controller/`](session-controller/README.zh.md) | 拥有 Session 命令、历史 stream、实时控制状态与 Agent/Session 身份策略。 | `ctx.sessionController` / `ctx.remote.session` |
+| [`gateway/`](gateway/README.zh.md) | 承载类型化一元调用、多路复用流与转发的 Host 事件。 | `ctx.typertGateway` / `ctx.remote` |
+| [`session-controller/`](session-controller/README.zh.md) | 拥有会话命令、历史记录流、实时控制状态与 Agent/Session 身份策略。 | `ctx.sessionController` / `ctx.remote.session` |
 | [`settings-controller/`](settings-controller/README.zh.md) | 拥有 settings 域各 seam 之上的配置界面读写。 | `ctx.settingsController`、`ctx.credentialsController` / `ctx.remote.settings`、`ctx.remote.credentials` |
 | [`workspace-controller/`](workspace-controller/README.zh.md) | 拥有 Workspace 变更与完整 Client Workspace 投影。 | `ctx.workspaceController` / `ctx.remote.workspace` |
-| [`workspace-files/`](workspace-files/README.zh.md) | 拥有有界的工作区文件访问——`stat`、分页 `read`、`list` 与已埋点操作的 `changes` 流——以及其上的 Client `file` 资源提供者。 | `ctx.workspaceFiles` / `ctx.remote.workspaceFiles` |
+| [`workspace-files/`](workspace-files/README.zh.md) | 拥有有界的工作区文件访问——`stat`、分页 `read`、`list` 与已埋点操作的 `changes` 流——以及其上的 Client `file` 资源提供方。 | `ctx.workspaceFiles` / `ctx.remote.workspaceFiles` |
 
-Remote 调用沿 Client → Host 方向运行在应用共享的 Connection 之上。API Gateway 拥有 Remote 传输,各 controller 包分别拥有 Session、配置界面与 Workspace 行为。流式下载等不适合 Remote 调用的响应由功能包注册精确的 Connection Fetch 路由。
+Remote 调用沿 Client → Host 方向运行在应用共享的 Connection 之上。API Gateway 拥有 Remote 传输,各控制器包分别拥有 Session、配置界面与 Workspace 行为。流式下载等不适合 Remote 调用的响应由功能包注册精确的 Connection Fetch 路由。
 
 -----
 

+ 1 - 1
packages/api/gateway/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/api/gateway/README.md
 README.md: a234189cfff09bae894de14dada607a357b398fd
-README.zh.md: 067adbdbe06db8fb4caec323db0361d13f6309a7
+README.zh.md: 7a6e66724724efd83173835095a409c527faccef

+ 12 - 12
packages/api/gateway/README.zh.md

@@ -1,5 +1,5 @@
 ---
-description: "带类型的 Client 到 Host 调用与 stream:分派、校验、取消、重连与转发的 Host 事件。"
+description: "带类型的 Client 到 Host 调用与流:分派、校验、取消、重连与转发的 Host 事件。"
 kind: "package-reference"
 ---
 
@@ -26,15 +26,15 @@ kind: "package-reference"
 
 每次调用时,`ctx.typertGateway.invoke()` 都会解析当前的描述符和 Cordis 服务,校验具名参数是否完全匹配,解析已注册的对象或 Context 身份标识,调用公开的业务方法,并校验其结果。业务服务继承 [`dsh-typert-protocol`](../../typert/protocol/README.zh.md) 的 `TypertRemoteService`,并用 `@Remote` 或 `@RemoteScope` 标记方法;已有其他基类时仍可改用 `bindTypertRemote()`。
 
-严格模式从 `ctx.typert.local` 读取生成的调用描述符。查找参数使用 `ctx.typert.lookups` 中当前有效的 resolver:业务包注册稳定声明与默认策略,Host 组合可用 effect-scoped `configure()` 覆盖解析行为;`@RemoteScope` 则通过已注册的 Host Context adapter 解析其接收者。SRC 模式是开发阶段的回退路径,适用于从未具备严格定义的端点;它解析简单参数名,并且只允许非查找参数使用可安全表示为 JSON 的值。已观测到的严格定义一旦撤回,系统会直接报错,而不会降低校验强度。
+严格模式从 `ctx.typert.local` 读取生成的调用描述符。查找参数使用 `ctx.typert.lookups` 中当前有效的解析器:业务包注册稳定声明与默认策略,Host 组合可用 effect-scoped `configure()` 覆盖解析行为;`@RemoteScope` 则通过已注册的 Host Context 适配器解析其接收者。SRC 模式是开发阶段的回退路径,适用于从未具备严格定义的端点;它解析简单参数名,并且只允许非查找参数使用可安全表示为 JSON 的值。已观测到的严格定义一旦撤回,系统会直接报错,而不会降低校验强度。
 
-Connection 可用时,Host 入口会在 Connection 共享的 `/api` FetchHandler 上注册 trusted-host interceptor。Connection 把这个复合 handler 交给 HTTP bridge;handler 将已认领 endpoint 分发给 Gateway,未认领且没有精确 Fetch 路由负责的请求返回 404。直接调用 `invoke()` 会保留业务错误;`TypertGatewayError` 是 `RemoteError` 的子类,其 `gateway/*` 码命名了分发、绑定、提供方、查找、Context、参数和编解码器各自负责的故障。因策略而拒绝的 resolver——冷恢复失败或 ownership fence——抛出自己的 `RemoteError`,它选定的码原样到达调用方。
+Connection 可用时,Host 入口会在 Connection 共享的 `/api` FetchHandler 上注册 trusted-host interceptor。Connection 把这个复合 handler 交给 HTTP bridge;handler 将已认领 endpoint 分发给 Gateway,未认领且没有精确 Fetch 路由负责的请求返回 404。直接调用 `invoke()` 会保留业务错误;`TypertGatewayError` 是 `RemoteError` 的子类,其 `gateway/*` 码命名了分发、绑定、提供方、查找、Context、参数和编解码器各自负责的故障。因策略而拒绝的解析器——冷恢复失败或 ownership fence——抛出自己的 `RemoteError`,它选定的码原样到达调用方。
 
 支持取消的 Remote 方法会把 `signal: AbortSignal` 声明为最后一个 Host 参数。signal 是 descriptor 元数据,而不是 wire 参数:Connection 将它提供给 Gateway,Gateway 则在已解码的业务参数之后注入它。SRC 识别这个保留的末位参数名,严格生成还要求它具有全局 `AbortSignal` 类型。
 
-流式 Remote 使用 `@Remote({ mode: 'stream' })` 并返回 `Iterable` 或 `AsyncIterable`。`ctx.typertGateway.stream()` 执行与一元调用相同的 endpoint、参数、lookup 和取消校验,再用生成的 result codec 校验每个产出项。Client 插件激活时打开 Gateway 自有的 `/api/remote.mux` WebSocket,并让它在空闲时保持连接。Connection 拥有重试调度;每次 retry 前,它要求 mux 取消候选或活动 socket,并且只做一次全新的物理连接尝试。Host 按配置的 `websocketHeartbeatIntervalMs` 间隔(默认 2 秒)发送 Ping 控制帧,浏览器在 WebSocket 协议层自动回复 Pong,使空闲网络中间层持续看到流量,而不新增 Remote stream frame。若 socket 尚未回复上一次 Ping,Host 会在下一间隔终止它。可独立取消的逻辑流共享这条连接;进程内 Connection 载体直接提供等价的流,不打开该 WebSocket。
+流式 Remote 使用 `@Remote({ mode: 'stream' })` 并返回 `Iterable` 或 `AsyncIterable`。`ctx.typertGateway.stream()` 执行与一元调用相同的 endpoint、参数、lookup 和取消校验,再用生成的 result codec 校验每个产出项。Client 插件激活时打开 Gateway 自有的 `/api/remote.mux` WebSocket,并让它在空闲时保持连接。Connection 拥有重试调度;每次 retry 前,它要求 mux 取消候选或活动 socket,并且只做一次全新的物理连接尝试。Host 按配置的 `websocketHeartbeatIntervalMs` 间隔(默认 2 秒)发送 Ping 控制帧,浏览器在 WebSocket 协议层自动回复 Pong,使空闲网络中间层持续看到流量,而不新增 Remote 流帧。若 socket 尚未回复上一次 Ping,Host 会在下一间隔终止它。可独立取消的逻辑流共享这条连接;进程内 Connection 载体直接提供等价的流,不打开该 WebSocket。
 
-Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source。Gateway 为它保留内部 `$events` logical endpoint,只接受空 `args`,并在 source 撤回时中止该注册打开的 stream。事件名单、参数校验、每 Client 队列及 opening `{ type: 'ready', clientId, host: { home } }` frame 中的 Host home 由 API Remotes 拥有。source factory 在返回 iterable 前同步挂好增量 listener,因此 Client 只在增量投递就绪后发布 generation 并开始 baseline 读取。
+Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source。Gateway 为它保留内部 `$events` logical endpoint,只接受空 `args`,并在 source 撤回时中止该注册打开的流。事件名单、参数校验、每个 Client 的队列及 opening `{ type: 'ready', clientId, host: { home } }` frame 中的 Host home 由 API Remotes 拥有。source factory 在返回 iterable 前同步挂好增量 listener,因此 Client 只在增量投递就绪后发布 generation 并开始 baseline 读取。
 
 <a id="client-service-clientremote-ctx-key-remote"></a>
 ## Client 服务:`ClientRemote`(ctx key:`remote`)
@@ -43,13 +43,13 @@ Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source
 
 每次一元调用都会校验位置参数,构造与描述符完全匹配的具名 `args`,再通过 `ctx.connection.rpc.call('/api', endpoint, ...)` 发送。生成的流方法返回 `AsyncIterable`,并在进程内 Connection 载体可用时通过它打开逻辑流,否则通过共享的 Gateway WebSocket 打开。生成的支持取消的方法接受最后一个可选 `AbortSignal`;Client 会在调用载体前将它与贡献项的挂载生命周期合并。一元结果和每个流项都经过校验后才会交给应用代码。撤回贡献项会同时移除其描述符和方法、中止正在进行的调用与流,并使外部仍持有的方法句柄在调用时返回拒绝。
 
-每次一元调用都解析为 `RemoteResult<T>`——`{ ok: true, value }` 或 `{ ok: false, error }`——且绝不因载体问题 reject:本面把断线载体折入错误分支,调用方 signal 中止时答以 `gateway/cancelled`,因此没有消费方需要包一层来兜载体失败。只有装配故障仍会 reject:参数个数不符、方法未挂载、贡献已撤下、缺少 Context adapter。`error` 是活的 `RemoteError` 实例,所以 `throw result.error` 保持 throw 语义;而 `isRemoteFailure(value)` 是消费方唯一需要的谓词——它认下的捕获值带着 Host 码,它拒绝的一律是本地故障,调用方应当让其崩掉。
+每次一元调用都解析为 `RemoteResult<T>`——`{ ok: true, value }` 或 `{ ok: false, error }`——且绝不因载体问题 reject:本面把断线载体折入错误分支,调用方 signal 中止时答以 `gateway/cancelled`,因此没有消费方需要包一层来兜载体失败。只有装配故障仍会 reject:参数个数不符、方法未挂载、贡献已撤下、缺少 Context 适配器。`error` 是活的 `RemoteError` 实例,所以 `throw result.error` 保持 throw 语义;而 `isRemoteFailure(value)` 是消费方唯一需要的谓词——它认下的捕获值带着 Host 码,它拒绝的一律是本地故障,调用方应当让其崩掉。
 
-`ctx.remote.$host` 以普通值读取固定的 Host 事实:`home`(首个 ready 帧之前为 undefined)与 `isLoopback`。它不是 store——没有订阅、没有代次计数——所以需要响应重连的消费方去监听 `connection/reset`,而不是轮询它。
+`ctx.remote.$host` 以普通值读取固定的 Host 事实:`home`(首个 ready 帧之前为 undefined)与 `isLoopback`。它不是存储——没有订阅、没有代次计数——所以需要响应重连的消费方去监听 `connection/reset`,而不是轮询它。
 
-`ctx.remote.$stream()` 返回跨越多个物理载体代次的单消费方 `RemoteStream`。Host 仍在线时,它允许一次立即重试;Host 离线时,它等待下一代连接,并为每个流项标注物理代次。领域消费方校验并接受各代次的 opening value;业务与协议错误仍然终止流。一切终态失败离开本面时都是 `RemoteError`,包括重试耗尽和在 opening value 之前就结束的代次,因此流消费方与一元调用方用同一种方式判别。`RemoteStreamCarrierError` 命名的是可重试的物理丢失,它只作为 `carrierFailed` 回调参数到达领域,绝不作为终态结果。`RemoteSnapshotStream` 在此之上规定每代由一个 opening snapshot 和后续 delta 组成。`RemoteJournalStream` 基于领域提供的 entry 闭区间提供 follow-before-page、分页、重连追赶与缺口修复;它丢弃完整重复项,并拒绝缺口、倒置区间和部分重叠。领域还可以携带无 cursor 的通知:通知绝不推进或修复持久 cursor,在缺口修复期间收到的通知只会在 replacement page 提交后发布。若更新代次取代该修复,旧代次 held notification 会与其 page 一同丢弃。dispose 任一种 stream 都会取消其请求,并在活动 iterator 完全停止后完成。
+`ctx.remote.$stream()` 返回跨越多个物理载体代次的单消费方 `RemoteStream`。Host 仍在线时,它允许一次立即重试;Host 离线时,它等待下一代连接,并为每个流项标注物理代次。领域消费方校验并接受各代次的 opening value;业务与协议错误仍然终止流。一切终态失败离开本面时都是 `RemoteError`,包括重试耗尽和在 opening value 之前就结束的代次,因此流消费方与一元调用方用同一种方式判别。`RemoteStreamCarrierError` 命名的是可重试的物理丢失,它只作为 `carrierFailed` 回调参数到达领域,绝不作为终态结果。`RemoteSnapshotStream` 在此之上规定每代由一个初始快照和后续 delta 组成。`RemoteJournalStream` 基于领域提供的 entry 闭区间提供 follow-before-page、分页、重连追赶与缺口修复;它丢弃完整重复项,并拒绝缺口、倒置区间和部分重叠。领域还可以携带无 cursor 的通知:通知绝不推进或修复持久 cursor,在缺口修复期间收到的通知只会在 replacement page 提交后发布。若更新代次取代该修复,旧代次 held notification 会与其 page 一同丢弃。对任一种流执行 dispose(资源释放)时,系统会取消该流的请求,并在活动 iterator 完全停止后完成资源释放。
 
-`ctx.remote.$on()` 订阅一条被转发的 Host 事件。它的合法键恰好等于 Host 装配声明的转发选择,listener 类型就是事件所属包自己的 Cordis `Events` 声明,因此不存在会与之漂移的第二份签名。每个订阅归属调用方 fiber,并随该 fiber 一起消失。Client Remote 服务激活时就把 `$events` pump 注册为 Connection generation source,因此即使当前无 `$on` 订阅,它也会在 Connection 循环启动时打开。浏览器使用 Remote mux,进程内组合使用 `connection.rpc.open`;opening `ready` 项建立 Connection generation 并提供 Host 信息。物理 carrier 失败、Remote stream error、意外正常结束、非 ready 首项或畸形事件项都会终止该 generation,由 Connection 按持续且间隔封顶的带抖动指数退避重开。普通通知按注册顺序运行并隔离 listener 失败;Agent-scoped waterfall 允许 listener 返回结果、调用 `next()` 或拒绝,Gateway 再通过现有 HTTP 一元载体回送该结果。
+`ctx.remote.$on()` 订阅一条被转发的 Host 事件。它的合法键恰好等于 Host 装配声明的转发选择,listener 类型就是事件所属包自己的 Cordis `Events` 声明,因此不存在会与之漂移的第二份签名。每个订阅归属调用方 fiber,并随该 fiber 一起消失。Client Remote 服务激活时就把 `$events` pump 注册为 Connection generation source,无论当前是否存在 `$on` listener。浏览器使用 Remote mux,进程内组合使用 `connection.rpc.open`;opening `ready` 项建立 Connection generation 并提供 Host 信息。物理 carrier 失败、Remote 流故障、意外正常结束、非 ready 首项或畸形事件项都会终止该 generation,由 Connection 按持续且间隔封顶的带抖动指数退避重开。普通通知按注册顺序运行并隔离 listener 失败;Agent-scoped waterfall(瀑布式事件)允许 listener 返回结果、调用 `next()` 或拒绝,Gateway 再通过现有 HTTP 一元载体回送该结果。
 
 `ctx.remote` 不暴露 Connection 生命周期控制。只有职责包含恢复的消费方才直接读取 `ctx.connection.state` 并调用 `ctx.connection.reconnect()`;普通 Remote 消费方仍只使用生成的 namespace 与 `$stream()`。
 
@@ -71,8 +71,8 @@ Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source
 - Connection 适配器对分发故障与未归类异常答以 `gateway/internal`,且不附带详细信息;拥有方或 Gateway 自己抛出的 `RemoteError` 带着自有码、message 与 details 过线。其 `cause` 链与 `TypertGatewayError` 子类身份只对同进程调用方留存。
 - SRC 模式仅支持名称唯一的标识符参数,不支持解构、默认值或剩余参数。它只校验值能否安全表示为 JSON,不校验生成的业务类型,也绝不会推断可选字段。
 - Client 侧只能挂载严格模式生成的贡献项。SRC 标记不具备 Client 编解码器或类型投影。
-- `$stream()` 监督载体替换,但不推断回放语义;各领域自行拥有恢复 cursor 或替换 baseline 的校验,以及正常结束的分类。Connection generation 会重开内部 `$events`;单向通知不会重放,仍处于 pending 的 scoped waterfall 则沿用同一个 event id 重放。
-- lookup resolver 按 key 配置;当前无法让单个 Remote 参数或 endpoint 在同一 `agent`/`session` key 下选择 live-only 策略。
+- `$stream()` 监督载体替换,但不推断回放语义;各领域自行拥有恢复 cursor 或替换 baseline 的校验,以及正常结束的分类。Connection generation 会重开内部 `$events` 流;单向通知不会重放,仍处于 pending 的 scoped waterfall 则沿用同一个 event id 重放。
+- lookup 解析器按 key 配置;当前无法让单个 Remote 参数或 endpoint 在同一 `agent`/`session` key 下选择 live-only 策略。
 - 被转发的事件到达 `$on` 时不做业务载荷投影或脱敏。普通通知在重连后不重放;Agent-scoped waterfall 只投影选择 Client Context 所需的顶层 Agent 身份,并自行携带 pending 生命周期。
 - `websocketHeartbeatIntervalMs` 同时是 Ping 周期和 Pong 截止时间。对端未在下一周期前回复时,Host 会终止连接;如果部署的事件循环或网络可能停顿超过该间隔,必须调大此配置。
 
@@ -87,4 +87,4 @@ Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source
 
 </details>
 
-**运行时不变式:** 不发布伴生入口。Host 调用会重新读取权威的 Cordis 与 Typert 状态,Client 方法、描述与 `$on` 订阅则在同一个 effect 中完成变更。
+**运行时不变式:** 不发布伴生入口。Host 调用会重新读取权威的 Cordis 与 Typert 状态,Client 方法、描述符与 `$on` 订阅的变更则统一归属同一个 effect。

+ 1 - 1
packages/api/gateway/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-api-gateway",
   "description": "Typert Remote Host dispatcher and Client API endpoint",
-  "version": "0.1.5-alpha.1",
+  "version": "0.1.5-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 2 - 2
packages/api/remotes/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/api/remotes/README.md
-README.md: 52ef6223f4d7770224df1b1c5d783962c55f73f6
-README.zh.md: eab5d2c6642a7899fac60d8c666ee55057eaea05
+README.md: 88b7b0fd023049be1169122f0f613aa740ccbc37
+README.zh.md: 9bfdc0a05920363ac523657e71ca02f728d7ee03

+ 2 - 2
packages/api/remotes/README.md

@@ -29,7 +29,7 @@ Two-sided BFF for Host Remote capabilities selected by this application. The Hos
 
 The Client assembly mounts Commands, credentials, settings, Goal, dynamic Cordis, file and Session references, read-only Host plugin inventory, message feedback, Session Controller, and Workspace Controller contributions. Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, streams, and cancellation. The Client entry consumes the shared `TypertClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation.
 
-This facade is also the front door for the wire type vocabulary a Client package names. It re-exports, type-only, the Remote failure vocabulary (`RemoteResult`, `RemoteFailure`, `RemoteErrorCode`, `RemoteErrorDetailsMap`), the Host facts (`RemoteHostFacts`), and each selected domain's client-safe payload types, so a Client feature package imports one specifier instead of reaching into `dsh-typert-protocol`, the Gateway, or an owner's Host entry. Two kinds of package deliberately skip this door: the api-layer packages this assembly itself selects — importing it back would close a dependency cycle — and their tests, which take the failure vocabulary from `dsh-typert-protocol` directly. A UI package's tests instead take the `RemoteError` constructor from [`dsh-client-test-runtime`](../../test-support/client-runtime/README.md).
+This facade is also the front door for the wire type vocabulary a Client package names. It re-exports, type-only, the Remote failure vocabulary (`RemoteResult`, `RemoteFailure`, `RemoteErrorCode`, `RemoteErrorDetailsMap`), the Host facts (`RemoteHostFacts`), and each selected domain's client-safe payload types, so a Client feature package imports one specifier instead of reaching into `dsh-typert-protocol`, the Gateway, or an owner's Host entry. Two kinds of package deliberately skip this door: the API-layer packages this assembly itself selects — importing it back would close a dependency cycle — and their tests, which take the failure vocabulary from `dsh-typert-protocol` directly. A UI package's tests instead take the `RemoteError` constructor from [`dsh-client-test-runtime`](../../test-support/client-runtime/README.md).
 
 This package owns no physical transport or Host service discovery. It projects the application selection into generated Remote contributions and an independent Host event source per Client; API Gateway owns endpoints, carriers, cancellation, and reconnection. Its Client face can be reused by Web or a future TUI that provides the same React-free `ctx.remote` contract.
 
@@ -70,7 +70,7 @@ No direct effect; mounted Host capabilities own any model-visible behavior they
 
 - The capability set is fixed by explicit build-time value imports; the Client does not discover the Host's active Services or Remote definitions at runtime.
 - Additional capabilities require an explicit `/remote` value import and mount in this assembly.
-- Ordinary forwarded events are not replayed; state that requires reliable recovery needs an owner-provided query, cursor, or opening baseline.
+- Only scoped waterfalls that are still pending are replayed after reconnection; ordinary one-way notifications remain isolated best-effort deliveries and are not replayed. State that requires reliable recovery needs an owner-provided query, cursor, or opening baseline.
 
 
 <a id="dev-note"></a>

+ 9 - 9
packages/api/remotes/README.zh.md

@@ -25,24 +25,24 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-[`@deepseek-ai/dsh-api-session-controller`](../session-controller/README.zh.md) 拥有 Agent 与 Session 身份策略,包括供其他 namespace 使用的 Typert lookup resolver。本包只选择并挂载生成的 Session contribution,不复制激活策略。
+[`@deepseek-ai/dsh-api-session-controller`](../session-controller/README.zh.md) 拥有 agent(智能体)与会话身份策略,包括供其他 namespace 使用的 Typert lookup 解析器。本包只选择并挂载生成的会话 contribution,不复制激活策略。
 
-Client 组合挂载 Commands、凭据、settings、Goal、动态 Cordis、文件与 Session 引用、只读 Host 插件清单、消息反馈、Session Controller 和 Workspace Controller contribution。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪 namespace Service、直接与作用域方法、调用、流与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
+Client 组合挂载 Commands、凭据、settings、Goal、动态 Cordis、文件与会话引用、只读 Host 插件清单、消息反馈、会话控制器和 Workspace 控制器 contribution。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪的 namespace 服务、直接与作用域方法、调用、流与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
 
-本 facade 同时是 Client 包指称 wire 类型词汇的正门。它以 type-only 方式转出 Remote 失败词汇(`RemoteResult`、`RemoteFailure`、`RemoteErrorCode`、`RemoteErrorDetailsMap`)、Host 事实(`RemoteHostFacts`),以及各已选领域的浏览器安全载荷类型,因此 Client 功能包只 import 一个 specifier,不必伸手进 `dsh-typert-protocol`、Gateway 或某个拥有方的 Host 入口。有两类包刻意不走这道门:本装配自己选中的 api 层包——反向 import 会形成依赖环——以及它们的测试,后者直接从 `dsh-typert-protocol` 取失败词汇。UI 包的测试则从 [`dsh-client-test-runtime`](../../test-support/client-runtime/README.zh.md) 取 `RemoteError` 构造器。
+本 facade 同时是 Client 包指称 wire 类型词汇的正门。它以 type-only 方式转出 Remote 失败词汇(`RemoteResult`、`RemoteFailure`、`RemoteErrorCode`、`RemoteErrorDetailsMap`)、Host 事实(`RemoteHostFacts`),以及各已选领域对 Client 安全的载荷类型,因此 Client 功能包只 import 一个 specifier,不必伸手进 `dsh-typert-protocol`、Gateway 或某个拥有方的 Host 入口。有两类包刻意不走这道门:本装配自己选中的 API 层包——反向 import 会形成依赖环——以及它们的测试,后者直接从 `dsh-typert-protocol` 取失败词汇。UI 包的测试则从 [`dsh-client-test-runtime`](../../test-support/client-runtime/README.zh.md) 取 `RemoteError` 构造器。
 
-本包不拥有物理传输或 Host 服务发现。它只把应用选择投影为生成的 Remote contribution 和唯一的 Host Cordis event source;API Gateway 负责 endpoint、carrier、取消与重连。Web 或未来的 TUI 只要提供同一份不依赖 React 的 `ctx.remote` 约定,均可复用其 Client face。
+本包不拥有物理传输或 Host 服务发现。它只把应用选择投影为生成的 Remote contribution,以及每个 Client 各自独立的 Host 事件源;API Gateway 负责 endpoint、carrier、取消与重连。Web 或未来的 TUI 只要提供同一份不依赖 React 的 `ctx.remote` 约定,均可复用其 Client face。
 
 -----
 
 <a id="forwarded-host-events"></a>
 ## 转发的 Host 事件
 
-`src/remote-events.ts` 持有 `API_REMOTE_FORWARDED_EVENTS`,即本应用不改名转发给消费端的 Host Cordis 事件名单;每个条目还会选择普通发送或 Agent-scoped waterfall 投递。该名单同时就是 `ctx.remote.$on` 的合法键集,只含类型的 `src/types.ts` 派生其选择面。多转发一个事件只需在该数组里加一项:类型投影、消费端键面与 Host 转发循环全部由它派生。
+`src/remote-events.ts` 持有 `API_REMOTE_FORWARDED_EVENTS`,即本应用不改名转发给消费端的 Host Cordis 事件名单;每个条目还会选择普通发送或 agent-scoped waterfall(瀑布式事件)投递。该名单同时就是 `ctx.remote.$on` 的合法键集,只含类型的 `src/types.ts` 派生其选择面。多转发一个事件只需在该数组里加一项:类型投影、消费端键面与 Host 转发循环全部由它派生。
 
-监听器签名不在此处重写。名单内每条事件的 Cordis `Events` 声明都住在其 owner 包 client-safe 的 `./types` 出口,本包两个 face 都把那些声明纳入编译面。Host face 还会把每个条目断言给 `TypertForwardableEventEntry`:`emit` 条目必须是已声明的单向事件,`waterfall` 条目则必须是已声明的 Agent-scoped waterfall,且其最后一个参数是返回相同结果类型的 `next()` 回调。
+监听器签名不在此处重写。名单内每条事件的 Cordis `Events` 声明都住在其 owner 包 client-safe 的 `./types` 导出,本包两个 face 都把那些声明纳入编译面。Host face 还会把每个条目断言给 `TypertForwardableEventEntry`:`emit` 条目必须是已声明的单向事件,`waterfall` 条目则必须是已声明的 agent-scoped waterfall,且其最后一个参数是返回相同结果类型的 `next()` 回调。
 
-Host entry 为每条 Client stream 独立注册 allowlist listener 和队列,并在普通事件入队前拒绝非 JSON 参数。对于 waterfall,它只投影顶层 Agent 身份与 JSON 请求字段;Client 结果也必须能无损表示为 JSON,而 `next()` 会委托给后续 Host listener。每个作用域 waterfall 请求都必须以 `request.agent` 直接携带路由所用的 Agent;Host 会在转发前拒绝缺失或不匹配的身份。该 source 在 `ctx.typertGateway.registerRemoteEvents()` 暴露 Gateway 内部的 `$events` logical stream 前同步挂好所有 listener,因此首个 `ready` 项既能证明增量投递已就绪,也会携带供 Client 显示路径的 Host home。撤回注册会中止活动 stream。
+Host entry 为每条 Client 流独立注册一组 allowlist listener 和一个队列,并在普通事件入队前拒绝非 JSON 参数。对于 waterfall,它只投影顶层 agent 身份与 JSON 请求字段;Client 结果也必须能无损表示为 JSON,而 `next()` 会委托给后续 Host listener。每个作用域 waterfall 请求都必须以 `request.agent` 直接携带路由所用的 agent;Host 会在转发前拒绝缺失或不匹配的身份。该 source 在 `ctx.typertGateway.registerRemoteEvents()` 暴露 Gateway 内部的 `$events` 逻辑流前同步挂好所有 listener,因此首个 `ready` 项既能证明增量投递已就绪,也会携带供 Client 显示路径的 Host home。撤回注册会中止活动流。
 
 <a id="build-boundary"></a>
 ## 构建边界
@@ -70,7 +70,7 @@ Host entry 为每条 Client stream 独立注册 allowlist listener 和队列,
 
 - 能力集合由构建时显式导入的值固定确定;Client 不会在运行时发现 Host 中已启用的服务或 Remote 定义。
 - 若要增加能力,必须显式导入相应的 `/remote` 值并在此组合中挂载。
-- 只有仍在等待的作用域 waterfall 会在重连后重放;单向通知仍是相互隔离的 best-effort 投递。
+- 只有仍在等待的作用域 waterfall 会在重连后回放;单向通知仍是相互隔离的 best-effort 投递,不会回放。需要可靠恢复的状态必须由拥有方提供查询、游标或初始基线。
 
 
 <a id="dev-note"></a>
@@ -83,4 +83,4 @@ Host entry 为每条 Client stream 独立注册 allowlist listener 和队列,
 
 </details>
 
-**运行时不变式:** 不发布伴生入口。被观察的关系由 Typert 以及 Agent、Session 注册表负责。
+**运行时不变式:** 不发布伴生入口。被观察的关系由 Typert、agent 注册表和会话注册表负责。

+ 1 - 1
packages/api/remotes/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-api-remotes",
   "description": "Remote BFF assembly for application-selected Host capabilities",
-  "version": "0.1.5-alpha.1",
+  "version": "0.1.5-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/api/session-controller/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/api/session-controller/README.md
 README.md: 24cfbba626ddec510452e3b6c8bc4333c03876bd
-README.zh.md: c90357c60eab9427f5656b48c4ea99252d87e2bf
+README.zh.md: 883fa2343693e6512638fa437a3f8bd0db09310c

File diff suppressed because it is too large
+ 3 - 3
packages/api/session-controller/README.zh.md


+ 1 - 1
packages/api/session-controller/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-api-session-controller",
   "description": "Session Remote commands, cold reads, and live control transport",
-  "version": "0.1.5-alpha.1",
+  "version": "0.1.5-rc.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/api/settings-controller/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/api/settings-controller/README.md
 README.md: 761e4c43751675eec141a2efcf85caf52cc91590
-README.zh.md: 541c9deb3dbc35914e4e95abdda2af5cdce2bbd0
+README.zh.md: c5a59f3eebfd1e6702c2abcd2407bc2ef4e725bd

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