Explorar el Código

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

# Conflicts:
#	packages/client/ui-deliverables/package.json
#	snapshots/web/present-svg/ui.expected.md
creatixchu hace 4 semanas
padre
commit
1ea80b7b69
Se han modificado 100 ficheros con 1424 adiciones y 149 borrados
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.i18n.yaml
  2. 3 3
      .agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.md
  3. 3 3
      .agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.zh.md
  7. 2 2
      .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml
  8. 2 2
      .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
  9. 2 2
      .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md
  10. 2 2
      .agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.i18n.yaml
  11. 6 2
      .agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md
  12. 6 2
      .agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md
  13. 2 2
      .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml
  14. 1 1
      .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md
  15. 1 1
      .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md
  16. 2 2
      .agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml
  17. 4 4
      .agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md
  18. 4 4
      .agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md
  19. 2 2
      .agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.i18n.yaml
  20. 1 1
      .agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.md
  21. 1 1
      .agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.zh.md
  22. 2 2
      .agents/notes/implemented/architecture/2026-09-05-canonical-feedback-log.i18n.yaml
  23. 2 2
      .agents/notes/implemented/architecture/2026-09-05-canonical-feedback-log.md
  24. 2 2
      .agents/notes/implemented/architecture/2026-09-05-canonical-feedback-log.zh.md
  25. 2 2
      .agents/notes/implemented/architecture/2026-09-05-nonofficial-feedback-otel.i18n.yaml
  26. 2 2
      .agents/notes/implemented/architecture/2026-09-05-nonofficial-feedback-otel.md
  27. 2 2
      .agents/notes/implemented/architecture/2026-09-05-nonofficial-feedback-otel.zh.md
  28. 2 2
      .agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.i18n.yaml
  29. 1 1
      .agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.md
  30. 1 1
      .agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.zh.md
  31. 2 2
      .agents/notes/implemented/architecture/2026-09-07-sidebar-responsive-tab-info.i18n.yaml
  32. 1 1
      .agents/notes/implemented/architecture/2026-09-07-sidebar-responsive-tab-info.md
  33. 1 1
      .agents/notes/implemented/architecture/2026-09-07-sidebar-responsive-tab-info.zh.md
  34. 2 2
      .agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml
  35. 3 3
      .agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md
  36. 3 3
      .agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md
  37. 6 0
      .agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.i18n.yaml
  38. 118 0
      .agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md
  39. 118 0
      .agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md
  40. 6 0
      .agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.i18n.yaml
  41. 28 0
      .agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.md
  42. 28 0
      .agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.zh.md
  43. 6 0
      .agents/notes/implemented/architecture/2026-09-14-sidebar-layout-provider-recovery.i18n.yaml
  44. 35 0
      .agents/notes/implemented/architecture/2026-09-14-sidebar-layout-provider-recovery.md
  45. 35 0
      .agents/notes/implemented/architecture/2026-09-14-sidebar-layout-provider-recovery.zh.md
  46. 6 0
      .agents/notes/implemented/bug-fix/2026-09-14-chat-presentation-defaults.i18n.yaml
  47. 25 0
      .agents/notes/implemented/bug-fix/2026-09-14-chat-presentation-defaults.md
  48. 25 0
      .agents/notes/implemented/bug-fix/2026-09-14-chat-presentation-defaults.zh.md
  49. 6 0
      .agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.i18n.yaml
  50. 33 0
      .agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.md
  51. 33 0
      .agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.zh.md
  52. 6 0
      .agents/notes/implemented/bug-fix/2026-09-14-web-diff-context.i18n.yaml
  53. 33 0
      .agents/notes/implemented/bug-fix/2026-09-14-web-diff-context.md
  54. 33 0
      .agents/notes/implemented/bug-fix/2026-09-14-web-diff-context.zh.md
  55. 6 0
      .agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.i18n.yaml
  56. 41 0
      .agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.md
  57. 41 0
      .agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.zh.md
  58. 2 2
      .agents/notes/implemented/feature/2026-09-04-right-sidebar-docking-infrastructure.i18n.yaml
  59. 2 2
      .agents/notes/implemented/feature/2026-09-04-right-sidebar-docking-infrastructure.md
  60. 2 2
      .agents/notes/implemented/feature/2026-09-04-right-sidebar-docking-infrastructure.zh.md
  61. 2 2
      .agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.i18n.yaml
  62. 3 1
      .agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md
  63. 3 1
      .agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.zh.md
  64. 2 2
      .agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.i18n.yaml
  65. 10 6
      .agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md
  66. 10 6
      .agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.zh.md
  67. 6 0
      .agents/notes/implemented/feature/2026-09-11-sidebar-document-preview-polish.i18n.yaml
  68. 35 0
      .agents/notes/implemented/feature/2026-09-11-sidebar-document-preview-polish.md
  69. 35 0
      .agents/notes/implemented/feature/2026-09-11-sidebar-document-preview-polish.zh.md
  70. 6 0
      .agents/notes/implemented/feature/2026-09-14-docs-mermaid-viewer.i18n.yaml
  71. 33 0
      .agents/notes/implemented/feature/2026-09-14-docs-mermaid-viewer.md
  72. 33 0
      .agents/notes/implemented/feature/2026-09-14-docs-mermaid-viewer.zh.md
  73. 6 0
      .agents/notes/implemented/feature/2026-09-14-unattended-browser-terminal-reclamation.i18n.yaml
  74. 56 0
      .agents/notes/implemented/feature/2026-09-14-unattended-browser-terminal-reclamation.md
  75. 56 0
      .agents/notes/implemented/feature/2026-09-14-unattended-browser-terminal-reclamation.zh.md
  76. 2 2
      .agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml
  77. 2 0
      .agents/notes/implemented/process/2026-08-10-npm-release-sequences.md
  78. 2 0
      .agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md
  79. 6 0
      .agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.i18n.yaml
  80. 116 0
      .agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.md
  81. 116 0
      .agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.zh.md
  82. 2 0
      THIRD_PARTY_NOTICES.md
  83. 2 2
      apps/cli/package.json
  84. 2 2
      apps/cli/reference/README.i18n.yaml
  85. 3 3
      apps/cli/reference/README.md
  86. 3 3
      apps/cli/reference/README.zh.md
  87. 24 4
      apps/cli/src/profile-boot.ts
  88. 1 0
      apps/cli/tests/built-bin.e2e.ts
  89. 4 0
      apps/cli/tests/profiles/headless/goal-snapshot.patch.yml
  90. 4 0
      apps/cli/tests/profiles/headless/semantic-checkpoint-snapshot.patch.yml
  91. 4 0
      apps/cli/tests/profiles/headless/subagent-diagnostic-snapshot.patch.yml
  92. 4 0
      apps/cli/tests/profiles/headless/subagent-inheritance-snapshot.patch.yml
  93. 4 0
      apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml
  94. 4 0
      apps/cli/tests/profiles/headless/workspace-context-resume-snapshot.patch.yml
  95. 20 8
      apps/cli/tests/web-agent-presets.e2e.ts
  96. 2 2
      apps/desktop-host/package.json
  97. 28 11
      apps/desktop-host/src/index.ts
  98. 9 4
      apps/desktop/electron-builder.config.d.mts
  99. 11 13
      apps/desktop/electron-builder.config.mjs
  100. 1 1
      apps/desktop/package.json

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.md
-2026-07-27-compiler-independent-typert-model.md: c9e7ec0e471c9deabcffda69346077a2a10ac42a
-2026-07-27-compiler-independent-typert-model.zh.md: 9bd419f39c9d8a490dfb0baf12a9852fdbb817a8
+2026-07-27-compiler-independent-typert-model.md: 5083c55176b2f4d342d14d0c1cf2a7f0c2c14b3c
+2026-07-27-compiler-independent-typert-model.zh.md: 70d6954fae0fcb48e76137231168d5f2417668bf

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.md

@@ -20,7 +20,7 @@ Each face independently owns a PackageModel and TypeGraph. Direct project refere
 
 PackageModel recognizes Cordis services, events, `@typert object` reference objects, and `@typert schema` data roots. Services and objects expose only public instance members, excluding constructors and static, private, and protected members; inheritance edges remain in TypeGraph instead of being copied into flattened members. When a public property, parameter, or return type lacks an annotation, `check` mode reports an error, while `write` mode writes the checker-inferred result, rebuilds the project, and analyzes it again in strict mode.
 
-[`dsh-typert-registry`](../../../../packages/typert/registry/README.md) provides `ctx.typert` and handles runtime registration only: one contribution atomically carries package-face reflection and an optional Zod schema, and Cordis effect disposal revokes it. The registry neither analyzes TypeScript nor merges the two faces. JSON Schema is an on-demand projection of registered Zod schemas.
+[`dsh-typert-registry`](../../../../packages/typert/registry/README.md) provides `ctx.typert` and handles runtime registration only: one contribution atomically carries package-face reflection and optional Zod schema factories, and Cordis effect disposal revokes it. The registry validates factories without invoking them, then materializes and caches each schema on its first `get()`, `resolve()`, `list()`, or JSON Schema projection. It neither analyzes TypeScript nor merges the two faces.
 
 Package artifact publication remains explicit opt-in through package exports. When invoked, `WorkspaceTypertGenerator` validates that each requested host face exposes the user-facing subpath `package/typert` from the root artifact `package/lib/typert.host.{js,d.ts}`, or that each requested client face exposes `package/client/typert` from `package/lib/typert.client.{js,d.ts}`; it never edits those exports. The later [Typert Remote design](2026-08-02-typert-remote-method-calls.md) adds a whole-workspace Host contract pass to root build, typecheck, lint, and documentation typecheck. For opted-in Host packages, that pass emits both local reflection and strict Host-for-Client `/remote` contracts before consumers resolve them. Generated local declarations keep `TYPERT` typed as `unknown`, so business packages do not depend on the registry.
 
@@ -34,7 +34,7 @@ For every property in `SyntaxZoo`, the TypeScript printer normalizes the source
 
 Boundary cases pin explicit package imports within and across faces, cross-face named re-exports, exact export aliases, qualified `import()` links, and the External classification of global `@types` declarations; they reject TypeScript diagnostics originating in package-owned files, relative-path boundary crossings, references outside `package.json#exports`, and cross-face namespace re-exports without a model target. Interface declaration merging explicitly preserves every authored part; other merges that cannot be represented losslessly fail.
 
-For each supported node kind and literal category, Zod emitter tests run both successful and failing parses; for each unsupported kind, they assert an explicit `TypertEmitError`. Emitter fixtures snapshot generated Zod JavaScript and `.d.ts` text, execute the JavaScript, and typecheck the declarations. `dsh-typert-registry` tests pin atomic registration, queries, JSON Schema, and effect disposal; `dsh-typert-loader` tests also prove delayed mounting, unloading, and disposal while a dynamic import remains pending. A real `dsh-tools` vertical slice generates a contribution from the model, loads it through the runtime registry, and compares its service, event, and related-type records with the committed static `SERVICE_API`, `EVENT_API`, and `TYPE_API`. A full-workspace projector test regenerates the two Cordis catalog documents and the `tool-cordis` API catalog and requires all three texts to be byte-for-byte identical to the committed artifacts.
+For each supported node kind and literal category, Zod emitter tests run both successful and failing parses; for each unsupported kind, they assert an explicit `TypertEmitError`. Emitter fixtures snapshot generated Zod JavaScript and `.d.ts` text, execute each schema factory, and typecheck the declarations. `dsh-typert-registry` tests pin atomic registration, first-use materialization, successful-result caching, retry after factory failure, queries, JSON Schema, and effect disposal; `dsh-typert-loader` tests also prove delayed mounting, unloading, and disposal while a dynamic import remains pending. A real `dsh-tools` vertical slice generates a contribution from the model, loads it through the runtime registry, and compares its service, event, and related-type records with the committed static `SERVICE_API`, `EVENT_API`, and `TYPE_API`. A full-workspace projector test regenerates the two Cordis catalog documents and the `tool-cordis` API catalog and requires all three texts to be byte-for-byte identical to the committed artifacts.
 
 ## Alternatives considered
 
@@ -50,4 +50,4 @@ For each supported node kind and literal category, Zod emitter tests run both su
 
 New generation targets and static checks can reuse the same TypeGraph, and business categories can extend PackageModel without parsing the AST again. Preserving pre-evaluation types and independent faces makes the model more complex than a flattened schema; emitters must explicitly declare their supported scope and fail on missing capabilities.
 
-Explicit package opt-in keeps artifact publication and exports under package ownership. Repository orchestration may still run the whole-workspace Host contract pass for every opted-in package; that pass remains owned by the later Remote Gateway Agent Note. The static Cordis catalogs remain reproducible from the canonical model without coupling `tool-cordis` to runtime registry state. `ctx.typert` reflects only artifacts mounted in the current runtime, and unloading does not control Zod instances that consumers retain after importing them directly.
+Explicit package opt-in keeps artifact publication and exports under package ownership. Repository orchestration may still run the whole-workspace Host contract pass for every opted-in package; that pass remains owned by the later Remote Gateway Agent Note. The static Cordis catalogs remain reproducible from the canonical model without coupling `tool-cordis` to runtime registry state. `ctx.typert` reflects only artifacts mounted in the current runtime, and unloading does not control Zod instances that consumers retain after materializing them.

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.zh.md

@@ -20,7 +20,7 @@ TypeGraph 保存开发者写下的计算前类型结构,包括泛型参数与
 
 PackageModel 识别 Cordis service、event、`@typert object` 引用对象和 `@typert schema` 数据根。service 与 object 只暴露 public instance member,排除 constructor、static、private 和 protected;继承边保留在 TypeGraph 中,不复制为扁平成员。缺少 public property、parameter 或 return 类型标注时,`check` 模式报错,`write` 模式写入 checker 推断结果后重建 project 并再次以严格模式分析。
 
-[`dsh-typert-registry`](../../../../packages/typert/registry/README.zh.md) 提供 `ctx.typert`,且只负责运行时注册:一个 contribution 原子携带 package-face reflection 与可选 Zod schema,并随 Cordis effect 撤销。注册表不分析 TypeScript,也不合并两个 face。JSON Schema 是对已注册 Zod schema 的按需投影。
+[`dsh-typert-registry`](../../../../packages/typert/registry/README.zh.md) 提供 `ctx.typert`,且只负责运行时注册:一个 contribution 原子携带 package-face reflection 与可选 Zod schema factory,并随 Cordis effect 撤销。注册表校验 factory 时不会调用它;首次 `get()`、`resolve()`、`list()` 或 JSON Schema 投影才会物化并缓存各 schema。注册表不分析 TypeScript,也不合并两个 face。
 
 包产物发布仍通过 package exports 采用显式 opt-in。`WorkspaceTypertGenerator` 仅在被调用时校验所请求 face 的根目录产物协议:host face 必须通过面向用户的 subpath `package/typert` 暴露 `package/lib/typert.host.{js,d.ts}`,client face 必须通过 `package/client/typert` 暴露 `package/lib/typert.client.{js,d.ts}`;它不会修改这些 exports。后续的 [Typert Remote 设计](2026-08-02-typert-remote-method-calls.zh.md) 为根目录 build、typecheck、lint 与文档类型检查增加了全仓 Host 约定 pass。对于已 opt-in 的 Host 包,该 pass 会在消费方解析两者之前生成本地反射产物与严格的 Host-for-Client `/remote` 约定。生成的本地声明将 `TYPERT` 类型保持为 `unknown`,因此业务包不依赖注册表。
 
@@ -34,7 +34,7 @@ PackageModel 识别 Cordis service、event、`@typert object` 引用对象和 `@
 
 边界用例固定同 face 与跨 face 的显式包导入、跨 face 命名 re-export、精确 export alias、qualified `import()` link 和全局 `@types` External 归属,并拒绝 package 自有 TypeScript 诊断、相对路径越界、`package.json#exports` 之外的引用,以及尚无模型 target 的跨 face namespace re-export。interface declaration merging 显式保留每个 authored part,无法无损表示的其他 merge 失败。
 
-Zod emitter 对支持的节点和各类 literal 逐类执行成功与失败 parse,对不支持的节点逐类断言明确的 `TypertEmitError`。Emitter fixture 对生成的 Zod JavaScript 与 `.d.ts` 文本做快照,执行 JavaScript,并对声明做类型检查。`dsh-typert-registry` 测试固定原子注册、查询、JSON Schema 和 effect 撤销,`dsh-typert-loader` 测试还证明延迟挂载、卸载及未完成 dynamic import 的释放行为。真实 `dsh-tools` 纵切从模型生成 contribution,经运行时注册表加载后,将其服务、事件与关联类型记录同已提交的静态 `SERVICE_API`、`EVENT_API` 和 `TYPE_API` 对照。全仓 projector 测试重新生成两份 Cordis catalog 文档与 `tool-cordis` API catalog,并要求三份文本同已提交产物逐字节一致。
+Zod emitter 对支持的节点和各类 literal 逐类执行成功与失败 parse,对不支持的节点逐类断言明确的 `TypertEmitError`。Emitter fixture 对生成的 Zod JavaScript 与 `.d.ts` 文本做快照,执行每个 schema factory,并对声明做类型检查。`dsh-typert-registry` 测试固定原子注册、首次使用物化、成功结果缓存、factory 失败后重试、查询、JSON Schema 和 effect 撤销,`dsh-typert-loader` 测试还证明延迟挂载、卸载及未完成 dynamic import 的释放行为。真实 `dsh-tools` 纵切从模型生成 contribution,经运行时注册表加载后,将其服务、事件与关联类型记录同已提交的静态 `SERVICE_API`、`EVENT_API` 和 `TYPE_API` 对照。全仓 projector 测试重新生成两份 Cordis catalog 文档与 `tool-cordis` API catalog,并要求三份文本同已提交产物逐字节一致。
 
 ## Alternatives considered
 
@@ -50,4 +50,4 @@ Zod emitter 对支持的节点和各类 literal 逐类执行成功与失败 pars
 
 新增生成目标或静态检查可复用同一 TypeGraph,业务类目也可在 PackageModel 上扩展,而无需再次解析 AST。保留计算前类型和独立 face 的代价是模型比打平后的 schema 更复杂,emitter 必须显式声明支持范围并对缺失能力失败。
 
-包级显式 opt-in 使产物发布与 exports 由各包自行管理。仓库编排仍可为每个已 opt-in 的包运行全仓 Host 约定 pass;该 pass 仍由后续 Remote Gateway Agent Note 负责说明。静态 Cordis catalog 可从标准模型复现,同时不把 `tool-cordis` 与运行时注册表状态耦合。`ctx.typert` 只反映当前运行时中已挂载的产物;对于消费方直接导入后仍持有的 Zod 实例,卸载流程无法控制。
+包级显式 opt-in 使产物发布与 exports 由各包自行管理。仓库编排仍可为每个已 opt-in 的包运行全仓 Host 约定 pass;该 pass 仍由后续 Remote Gateway Agent Note 负责说明。静态 Cordis catalog 可从标准模型复现,同时不把 `tool-cordis` 与运行时注册表状态耦合。`ctx.typert` 只反映当前运行时中已挂载的产物;对于消费方物化后仍持有的 Zod 实例,卸载流程无法控制。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md
-2026-08-02-typert-remote-method-calls.md: 73ab996d408c71ab70d25058677d0d02efe05804
-2026-08-02-typert-remote-method-calls.zh.md: 06b3f9ad454ca905d33e8d08dde51e6c4e99427e
+2026-08-02-typert-remote-method-calls.md: b6551e1c7f8c94fb02a788aa62cb4acef1addffe
+2026-08-02-typert-remote-method-calls.zh.md: 058ec47e6749ee7576fd84fdcacfda350eec3fb8

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md

@@ -147,7 +147,7 @@ The strict generator writes `scope` only when a direct method has exactly one lo
 
 Parameter order comes from the method signature. HTTP fields come from parameter names or lookup declarations. A cancellation descriptor reserves only the final `signal` position and keeps it outside named `args`; Connection or a direct Gateway caller supplies the actual signal. The Gateway does not infer optional fields, Context types, lookup types, or missing arguments from request contents, and it does not synthesize business defaults.
 
-A LIB codec contains a Zod schema and a canonical `typeSymbol` consisting of "package + public subpath + export name." An SRC codec is marked only as `src-json`. When the Host and consumer run in different JavaScript realms, each holds its own Zod instances, but both sets are generated from the same Typert model and symbol keys.
+A LIB codec contains a success-cached Zod schema factory and a canonical `typeSymbol` consisting of "package + public subpath + export name." Host and Client gateways invoke the factory only when that boundary first encodes or decodes a value. An SRC codec is marked only as `src-json`. When the Host and consumer run in different JavaScript realms, each holds its own Zod instances, but both sets are generated from the same Typert model and symbol keys.
 
 Descriptors exist only in the local registry on each side. The wire carries only the `/api` channel, endpoint, and `{ args }` payload. The Host uses its descriptor to decode and invoke the method, while the Client uses its corresponding descriptor to encode arguments and validate the result.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.zh.md

@@ -147,7 +147,7 @@ InvocationDescriptor {
 
 参数顺序来自方法签名,HTTP 字段来自参数名或 lookup 声明。取消 descriptor 只保留最后一个 `signal` 位置,并使其不进入具名 `args`;实际 signal 由 Connection 或直接调用 Gateway 的调用方提供。Gateway 不根据请求内容推断可选字段、Context 类型、lookup 类型或缺失参数,也不会合成业务默认值。
 
-LIB codec 带有 Zod schema 和「package + 公共 subpath + export name」的规范 `typeSymbol`;SRC codec 只标记 `src-json`。Host 和消费端运行在不同 JavaScript realm 时会各自持有 Zod 实例,但这些实例由同一 Typert 模型和 symbol key 生成。
+LIB codec 带有只缓存成功结果的 Zod schema factory 和「package + 公共 subpath + export name」的规范 `typeSymbol`;Host 与 Client gateway 只在该边界首次编码或解码值时调用 factory。SRC codec 只标记 `src-json`。Host 和消费端运行在不同 JavaScript realm 时会各自持有 Zod 实例,但这些实例由同一 Typert 模型和 symbol key 生成。
 
 descriptor 只存在于两端本地 registry。wire 上只有 `/api` channel、endpoint 和 `{ args }` payload;Host 用自己的 descriptor 解码和调用,Client 用自己的对应 descriptor 编码参数和验证结果。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
-2026-08-05-profile-plugin-bundles.md: 7e51345e7eba8a58db63807e31d4a11481e3ffea
-2026-08-05-profile-plugin-bundles.zh.md: b2631603737ea9412eb97029ff01d751d8084cec
+2026-08-05-profile-plugin-bundles.md: 48786a9c1ccaceb5f16c9eefed01e556cf759e6d
+2026-08-05-profile-plugin-bundles.zh.md: c88cbf98e4276549a6fae6a5b1253d3d838a64ca

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md

@@ -14,7 +14,7 @@ Everything becomes a **profile**: a directory `$DSH_HOME/profiles/<name>` with a
 
 The default Profile templates use `@deepseek-ai/dsh-base` as the shared core for `web`, `headless`, `sdk`, and `acp`, with one mode bundle above it. The [standalone `sdk-minimal` profile](../../../../packages/bundle/sdk-minimal/README.md) instead lists one bundle that owns its complete explicit tree. Generic `dsh --profile <name>` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, headless owns its task positional, and the protocol profiles accept no app options. Patch overlays use launcher-owned `--patch`. A new, non-shipped target can use `--from-default-profile <template>` to copy one default template's bundle list and patch-reload policy before boot or config dump. This creates an independent profile with empty dependencies and an empty user patch: it neither reads a local profile named by the template nor records an inheritance relationship. The launcher claims the complete target directory exclusively, so existing state and concurrent creators fail without modification. `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes a base-backed profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](../../archived/architecture/2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract.
 
-Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory — so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them — while bare plugin names in patch rows resolve through the profile directory's Node parent-walk into the maintained flat fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch).
+Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory, so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them. Bare plugin names in patch rows use the [immutable profile resolution generation](2026-09-09-profile-resolution-generations.md), which applies the same installation-first and ordered-bundle rules in memory; retained link and dual modes can materialize the same result.
 
 Two supporting refactors: the webserver's built-in static dist serving became the single-owner **fallback seat** (`registerFallback`/`applyIndexTaps`), with the SPA server extracted to `@deepseek-ai/dsh-host-frontend-static` so the web bundle owns its dist as composition, not launcher code; and the personal-overlay machinery of the [dsh CLI personal-config decision](../../archived/feature/2026-07-20-dsh-cli-personal-config.md) (`loadPersonalPatches`, `$DSH_HOME/config.yaml`) was retargeted to the per-profile and home-level `cordis.patch.yml` layers (`loadOptionalPatches`, `watchUserPatches` taking a filename), superseding that note's entry modes and file location while keeping its Harness-home root, patch semantics, and fail-loud parsing.
 
@@ -31,5 +31,5 @@ Two supporting refactors: the webserver's built-in static dist serving became th
 - New composition surfaces (a TUI, provider packs) ship as ordinary npm packages installable per profile, without a repository row for every deployment shape.
 - Users can start an independent custom profile from any shipped application template without copying machine-local profile state.
 - `apps/cli` shrank to argv parsing, profile machinery consumption, and the pnpm forwarder; `AppCLIEntry` and the per-surface boot paths are gone.
-- The keyless web e2e scaffold boots the same bundle layers over the same empty-root shape as production, including the profiles module fallback, so composition drift between test and product fails loudly.
+- The keyless web e2e scaffold boots the same bundle layers over the same empty-root shape as production and exercises the same profile package-selection rules, so composition drift between test and product fails loudly.
 - Under the pre-release stance, backends carry no compatibility behavior for old on-disk configuration; `$DSH_HOME/config.yaml` is ignored.

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md

@@ -14,7 +14,7 @@ Status: implemented
 
 默认 Profile 模板为 `web`、`headless`、`sdk` 与 `acp` 使用 `@deepseek-ai/dsh-base` 作为共享核心,并在其上叠加一个模式组合包。[独立 `sdk-minimal` profile](../../../../packages/bundle/sdk-minimal/README.zh.md)则只列出一个拥有完整显式配置树的组合包。通用的 `dsh --profile <name>` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 持有任务位置参数,协议 profile 不接受应用选项。patch overlay 使用启动器持有的 `--patch`。新的非内置目标可以使用 `--from-default-profile <template>`,在启动或配置 dump 之前复制一个默认模板的 bundle 列表与 patch 重载策略。这会创建依赖为空、用户 patch 为空的独立 profile:它既不读取与模板同名的本地 profile,也不记录继承关系。launcher 会以独占方式领取完整的目标目录,因此既有状态和并发创建者都会在不作修改的情况下失败。`dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化一个以 base 为基础的 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](../../archived/architecture/2026-08-09-headless-direct-core-entry-point.md)负责 headless 组合约定。
 
-解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 `$DSH_HOME/profiles/node_modules`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。
+解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析,因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们。patch 行中的裸插件名称使用[不可变 profile resolution generation](2026-09-09-profile-resolution-generations.zh.md),在内存中应用相同的安装优先与有序 bundle 规则;保留的 link 与 dual 模式可以物化同一结果。
 
 两项配套重构:webserver 内置的静态 dist 服务改为单一所有者的**回退席位**(`registerFallback`/`applyIndexTaps`),SPA 服务器提取到 `@deepseek-ai/dsh-host-frontend-static`,使 web 组合包以组合的方式持有自己的 dist,而不是靠启动器代码;[dsh CLI 个人配置决策](../../archived/feature/2026-07-20-dsh-cli-personal-config.md)的个人 overlay 机制(`loadPersonalPatches`、`$DSH_HOME/config.yaml`)改为面向逐 profile 与 home 级的 `cordis.patch.yml` 层(`loadOptionalPatches`、接受文件名的 `watchUserPatches`),取代该笔记的各入口模式与文件位置,同时保留其 Harness home 根目录、patch 语义与响亮失败的解析。
 
@@ -31,5 +31,5 @@ Status: implemented
 - 新的组合表层(TUI、提供方扩展包)以普通 npm 包形式交付,可按 profile 安装,无需在仓库中为每种部署形态各留一行。
 - 用户可以从任意随附应用模板启动一个独立的自定义 profile,而不会复制机器本地的 profile 状态。
 - `apps/cli` 收缩为 argv 解析、profile 机制的消费方和 pnpm 转发器;`AppCLIEntry` 与各表层专属的启动路径全部移除。
-- 无密钥 web e2e 脚手架以与生产相同的空根形态启动相同的组合包层,包括 profiles 模块回退,因此测试与产品之间的组合漂移会响亮失败。
+- 无密钥 web e2e 脚手架以与生产相同的空根形态启动相同的组合包层,并执行相同的 profile 包选择规则,因此测试与产品之间的组合漂移会响亮失败。
 - 按发布前姿态,后端不携带旧磁盘配置的兼容行为;`$DSH_HOME/config.yaml` 会被忽略。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md
-2026-08-15-client-shells-and-dynamic-packages.md: 89f89784512b86b70ee1b9460850e6f2f951a082
-2026-08-15-client-shells-and-dynamic-packages.zh.md: 5512d9262e979a94a65c25a6c1271b030e23ec70
+2026-08-15-client-shells-and-dynamic-packages.md: ad2bb26b84253e859e567775eec3b06d73b0003b
+2026-08-15-client-shells-and-dynamic-packages.zh.md: 70563962b3c613caa611c61a0c690d2e0e777def

+ 6 - 2
.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md

@@ -46,13 +46,17 @@ There is no general `dsh.client.provide` alias mechanism. Dynamic rows and stati
 The modules Node half injects the startup protocol into the served HTML in this order:
 
 1. Install `window.__ModuleLoader__` in queue mode with `pendingQueue`, `load()`, and `create()`.
-2. Start preloading every content-addressed application combo URL containing the rows other than modules.
+2. Start preloading every revisioned application combo URL containing the rows other than modules.
 3. Execute every blocking bootstrap combo URL; these currently contain the ordinary modules factory registration.
 4. Assign `window.__DSH_BOOT__`, including all scheduling descriptors and every row's one-resource HMR combo URL.
 5. Execute the Vite main module.
 
 The bootstrap combo currently registers only the modules factory. The startup kernel passes the raw graph and shell seeds to `__ModuleLoader__.create()`. The facade removes the modules registration, materializes it with a `require` function that rejects every external, and invokes its `createClientModuleSystem` export. The modules bundle parses the graph, constructs and returns `ClientModuleSystem`, caches its own exports as the modules row, and switches the same facade to live mode. The kernel installs that instance as its Loader's `internal`, and the modules plugin reads it there when it provides `ctx.modules`. The modules client face consequently has a zero-external bootstrap requirement and no module-global system identity.
 
+The Host publishes graph and combo descriptors without concatenating response bodies. Each script URL shares one lazy Promise that concatenates its captured bundle bytes on first `GET` and appends the corresponding map URL; each map URL has a separate lazy Promise that reads and composes source maps only on its first `GET`. `HEAD` requests trigger neither body. The Web URL remains gated by Loader settlement and the required-entry audit, but that readiness point does not materialize combo bodies; an index request reads the current graph.
+
+The theme Host contribution is prepended to index collection. CSS in the head selects the initial document canvas palette, using `prefers-color-scheme` directly for the `system` preference; a body script applies the existing palette attribute and font-size variable before the loading page and application module.
+
 After the `immediately` tier has registered its factories, the kernel creates all Loader entries, awaits Cordis quiescence, and requires every fiber to be ACTIVE. It then calls `ctx.uiRenderer.mount(container)`. The dynamic `ui-renderer` package owns React, slot rendering, hydration of the existing boot DOM, and the React root lifecycle; the startup kernel and failure page remain React-free.
 
 ### Dependency declarations
@@ -79,7 +83,7 @@ Ordinary installed libraries remain `dependencies`: a dynamic build may bundle a
 
 Bundle contents stay stable when an internal DSH relationship is development-only, because each build face declares externality directly. Static libraries remain host-assembled, while dynamic packages retain uniform artifacts and lifecycle governance. The shipped profile owns the complete Client package roster, so individual Client packages do not ask npm to solve the same graph again through peer placement.
 
-The startup protocol depends on the modules package id, and modules must remain self-contained at runtime. Combo generation preserves its ordinary package artifact and gives every other row one shared initial transport; HMR uses the same route with that row as its sole resource. A missing bootstrap registration fails before Cordis starts; later plugin import, apply, and service-wait failures remain visible through the boot page's ACTIVE scan.
+The startup protocol depends on the modules package id, and modules must remain self-contained at runtime. Combo generation preserves its ordinary package artifact and gives every other row one shared initial transport; HMR uses the same route with that row as its sole resource. Deferring response bodies moves concatenation to first access, while separately deferring maps keeps debugger-only work off script delivery. A missing bootstrap registration fails before Cordis starts; later plugin import, apply, and service-wait failures remain visible through the boot page's ACTIVE scan.
 
 The shell consumes built `lib/` products, so source and browser artifacts can drift until the relevant build or watcher runs. Typechecking source alone does not prove the served application uses the same code.
 

+ 6 - 2
.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md

@@ -46,13 +46,17 @@ Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundl
 Modules Node 半按以下顺序向实际返回的 HTML 注入启动协议:
 
 1. 以 queue 模式安装 `window.__ModuleLoader__`,包含 `pendingQueue`、`load()` 与 `create()`。
-2. 开始预加载所有按内容寻址的 application combo URL,其中包含 modules 之外的 row。
+2. 开始预加载所有带 revision 的 application combo URL,其中包含 modules 之外的 row。
 3. 执行所有阻塞式 bootstrap combo URL;当前其中包含普通的 modules factory registration。
 4. 赋值 `window.__DSH_BOOT__`,其中包含全部调度描述及每个 row 的单资源 HMR combo URL。
 5. 执行 Vite 主模块。
 
 Bootstrap combo 当前只登记 modules factory。启动内核把原始图与外壳 seed 传给 `__ModuleLoader__.create()`。Facade 移除 modules registration,用拒绝全部 external 的 `require` 函数将其物化,再调用其 `createClientModuleSystem` 导出。Modules bundle 解析图、构造并返回 `ClientModuleSystem`、把自身 exports 缓存为 modules row,并把同一 facade 切换到 live 模式。内核把该实例装成自身 Loader 的 `internal`,modules 插件从这里读取并提供 `ctx.modules`。因此 modules client face 保持零 external 的自举要求,也没有模块级系统身份。
 
+Host 发布 graph 与 combo descriptor 时不会拼接响应 body。每个脚本 URL 共用一个惰性 Promise,在首次 `GET` 时拼接捕获的 bundle 字节并追加对应的 map URL;每个 map URL 使用另一个惰性 Promise,只在首次 `GET` 时读取并组合 source map。`HEAD` 不触发任一 body。Web URL 仍由 Loader 结算和 required-entry audit 控制,但这个就绪点不会物化 combo body;index 请求读取当时的最新 graph。
+
+Theme 的 Host 贡献会前置到 index 收集顺序。head 中的 CSS 选择初始文档画布调色板,`system` 偏好直接使用 `prefers-color-scheme`;body 脚本在加载页面和应用模块之前应用既有的调色板属性与字号变量。
+
 `immediately` 层级完成 factory 注册后,内核创建全部 Loader entry,等待 Cordis 静止,并要求每个 fiber 都进入 ACTIVE。随后调用 `ctx.uiRenderer.mount(container)`。动态 `ui-renderer` 包拥有 React、slot 渲染、已有启动 DOM 的 hydrate 和 React root 生命周期;启动内核与失败页保持 React-free。
 
 ### 依赖声明
@@ -79,7 +83,7 @@ Bootstrap combo 当前只登记 modules factory。启动内核把原始图与外
 
 内部 DSH 关系仅放在开发区段时,bundle 内容仍保持稳定,因为每个构建 face 都直接声明 external。静态库继续由宿主装配,动态包则保留统一产物与生命周期治理。发布 profile 拥有完整 Client 包名册,因此各 Client 包不再要求 npm 通过 peer placement 重复求解同一张图。
 
-启动协议依赖 modules 的 package id,modules 还必须保持运行期自包含。Combo 生成保留其普通 package 产物,并为其他全部 row 提供一条共享初始传输;HMR 使用同一条路由,并只把该 row 作为资源。缺少 bootstrap registration 会在 Cordis 启动前失败;后续插件 import、apply 与 service 等待失败仍由启动页的 ACTIVE 扫描呈现。
+启动协议依赖 modules 的 package id,modules 还必须保持运行期自包含。Combo 生成保留其普通 package 产物,并为其他全部 row 提供一条共享初始传输;HMR 使用同一条路由,并只把该 row 作为资源。响应 body 的延迟生成会把拼接移到首次访问,而 map 的独立延迟生成会让仅供调试器使用的工作不进入脚本交付路径。缺少 bootstrap registration 会在 Cordis 启动前失败;后续插件 import、apply 与 service 等待失败仍由启动页的 ACTIVE 扫描呈现。
 
 外壳消费已构建 `lib/` 产品,因此在相关 build 或 watcher 运行前,源码与浏览器产物可能漂移。仅源码 typecheck 通过不能证明实际服务的应用使用同一份代码。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md
-2026-08-18-experimental-agent-teams-packages.md: 2a00b651e0073434a5a68df13b9716adcab5fccf
-2026-08-18-experimental-agent-teams-packages.zh.md: df73ee5c05fd3a25bf843bee10b06535a1512783
+2026-08-18-experimental-agent-teams-packages.md: 4a78a60c2ad7463c06b670e6ec664579ffd767b9
+2026-08-18-experimental-agent-teams-packages.zh.md: 8056cd8194695e8ce41acb359e51c040d4ed3aec

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

@@ -20,7 +20,7 @@ The generic caller-reserved continuable child identity and selective direct-chil
 
 The published Host-side Agent Teams profile bundle depends on the Team packages and applies after `dsh-base`. It inserts the Team rows and disables the global continuable-child controls whose model-visible names overlap the Team tools. The separate published Web profile applies after `dsh-web-app` and the Host profile; it inserts the Team UI, which mounts the Remote contribution generated by the Team package. Both layers remain opt-in and leave the shipped base, CLI, Web, and Python runtime dependency graphs unchanged.
 
-Profile installation resolves each published bundle and its dependencies through the profile's package manager. The generic profile launcher then applies the selected layers without adding them to any shipped profile or changing another profile's resolution.
+Profile startup resolves selected bundles before computing the [immutable profile resolution generation](2026-09-09-profile-resolution-generations.md). The generation retains installation-first precedence, traverses each explicit bundle root completely in profile order, and keeps pnpm-managed profile packages authoritative. Runtime mode enforces the result in memory; retained link and dual modes materialize the same result as shared and profile-owned projections. A private profile layer can therefore carry experimental plugin rows without adding those plugins to a release app, requiring profile users to install transitive packages directly, weakening packaged-runtime module identity, or changing another profile's resolution.
 
 Experimental status changes compatibility and support expectations, not publication for these five packages. They retain the repository's ordinary documentation, invariant, lifecycle, security, unit, real-composition, and snapshot requirements. Promotion still requires review of the public contracts, limitations, test evidence, runtime dependents, and a named owner accepting stable-package obligations.
 

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

@@ -20,7 +20,7 @@ dsh 打包与发布集合以及本地基线发布器包含这五个 Agent Teams
 
 公开发布的 Host 侧 Agent Teams profile bundle 依赖 Team 包,并在 `dsh-base` 之后应用。它会插入 Team 配置行,并禁用模型可见名称与 Team 工具重叠的全局 continuable-child control。独立公开发布的 Web profile 在 `dsh-web-app` 与 Host profile 之后应用;它会插入 Team UI,后者挂载 Team package 生成的 Remote contribution。两个层都保持显式启用,不改变随附 base、CLI、Web 与 Python runtime 的依赖图。
 
-profile 安装通过自身 package manager 解析每个公开 bundle 及其依赖。通用 profile launcher 随后应用所选层,不会把它们加入任何随附 profile,也不会改变其他 profile 的解析结果。
+profile 启动会先解析所选 bundle,再计算[不可变 profile resolution generation](2026-09-09-profile-resolution-generations.zh.md)。generation 保留安装优先顺序,按 profile 顺序完整遍历每个显式 bundle 根,并让 pnpm 管理的 profile 包保持优先。runtime 模式在内存中强制该结果;保留的 link 与 dual 模式把同一结果物化为共享和 profile 自有投影。因此,私有 profile 层可以携带实验性 plugin 配置行,而无需把这些 plugin 加入发布 app、要求 profile 用户直接安装传递依赖、破坏 packaged-runtime 的模块身份,或改变其他 profile 的解析结果。
 
 对这五个包而言,实验性状态改变兼容性与支持预期,而不阻止发布。这些包仍须满足仓库的一般文档、不变式、生命周期、安全、单元测试、真实组合测试和快照要求。promotion 前仍须评审公开约定、限制、测试证据、运行时依赖方,并由一名具名 owner 接受稳定包义务。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md
-2026-08-21-deepseek-llm-api-request-extensions.md: a0e38c1da5af32b0ba5e07df632ac4e25148419e
-2026-08-21-deepseek-llm-api-request-extensions.zh.md: 763f418c7bd6029c08027cd5f4705bffd984b9ee
+2026-08-21-deepseek-llm-api-request-extensions.md: 7a3609a3f7d8b2aa6edb634643ef234b7e3e8c07
+2026-08-21-deepseek-llm-api-request-extensions.zh.md: 69426df3cc85851e448b98dc51af79555b8cbafc

+ 4 - 4
.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md

@@ -14,13 +14,13 @@ Both values belong only on the official DeepSeek adapter path. Adding them to `G
 
 ## Decision
 
-`@deepseek-ai/dsh-deepseek-llm-api-extensions` registers `ctx.deepseekLlmApiExtensions`, an additive registry of top-level fields for `deepseek-official` request bodies. A contributor claims one declaration-merged field with `register()`. The adapter invokes `prepare()` after serializing the exact wire messages, passes the request cancellation signal, rejects preparation or base-field collision before HTTP, merges the detached fields, and calls the captured `accept()` transaction after HTTP 2xx. The registry stops awaiting preparation after cancellation even if a contributor ignores the signal. Acceptance failures remain request failures under `REQUEST_EXTENSION`; transport and non-2xx failures never accept a contribution. A composition without the registry retains the reusable base adapter. Shipped compositions mount the registry and both contributors: package metadata is enabled by default, while Session-log upload is disabled by default and requires `session-log-deepseek.enabled: true`. Keyless `deepseek-official` replay invokes preparation with a synthetic empty base body and the same acceptance transaction before its first recorded chunk, preserving post-2xx extension side effects rather than field bytes.
+`@deepseek-ai/dsh-deepseek-llm-api-extensions` registers `ctx.deepseekLlmApiExtensions`, an additive registry of top-level fields for `deepseek-official` request bodies. A contributor claims one declaration-merged field with `register()`. The adapter invokes `prepare()` after serializing the exact wire messages, passes the request cancellation signal, rejects preparation or base-field collision before HTTP, merges the detached fields, and calls the captured `accept()` transaction after HTTP 2xx. The registry stops awaiting preparation after cancellation even if a contributor ignores the signal. Acceptance failures remain request failures under `REQUEST_EXTENSION`; transport and non-2xx failures never accept a contribution. A composition without the registry retains the reusable base adapter. Shipped compositions mount the registry and both contributors: both package metadata and Session-log upload are enabled by default; `session-log-deepseek.enabled: false` disables log upload under the [default-upload decision](2026-09-14-session-log-upload-default.md). Keyless `deepseek-official` replay invokes preparation with a synthetic empty base body and the same acceptance transaction before its first recorded chunk, preserving post-2xx extension side effects rather than field bytes.
 
 The provider-neutral `llm` package and `llm-pi-ai` contain no extension type, service lookup, field merge, or acceptance call.
 
 ## Incremental session-log field
 
-`@deepseek-ai/dsh-session-log-deepseek` owns `dsh_session_log` as an explicit opt-in. When enabled, each request carrying a live Session id sends the contiguous canonical event suffix after the greatest durable `session-log-deepseek/delivery-accepted` watermark for that same Session identity. The field includes the immutable Session header and complete event envelopes. A 2xx appends a new watermark for the transmitted `throughSeq`; that event enters the following request's suffix. Forked logs retain parent watermark ids, so a child starts from sequence zero under its own identity. Concurrent acceptances may arrive out of order, and the maximum watermark remains authoritative. A process-local fold scans each Session event once and incrementally consumes later appends; a new Session object or HMR generation rebuilds the fold from durable history.
+`@deepseek-ai/dsh-session-log-deepseek` owns the default-on `dsh_session_log` field. When enabled, each request carrying a live Session id sends the contiguous canonical event suffix after the greatest durable `session-log-deepseek/delivery-accepted` watermark for that same Session identity. The field includes the immutable Session header and complete event envelopes. A 2xx appends a new watermark for the transmitted `throughSeq`; that event enters the following request's suffix. Forked logs retain parent watermark ids, so a child starts from sequence zero under its own identity. Concurrent acceptances may arrive out of order, and the maximum watermark remains authoritative. A process-local fold scans each Session event once and incrementally consumes later appends; a new Session object or HMR generation rebuilds the fold from durable history.
 
 The failure direction is at least once. A transport or provider rejection records no watermark. A crash after remote acceptance but before the watermark persists causes replay after resume, never a skipped sequence. Existing session checkpoints persist the event; the upload plugin owns no second store.
 
@@ -50,7 +50,7 @@ The process-lifetime manifest-identity cache remains separate because in-process
 
 ## Verification
 
-Registry tests pin duplicate ownership, effect-scoped disposal, detached field values, concurrent and abortable preparation, receiver-preserving acceptance, one acceptance settlement, and failure aggregation. Session tests pin the default-off policy, explicit full-first/suffix-later delivery, direct complete event envelopes independent of base-body messages, incremental watermark folding, persisted restart recovery, fork identity fencing, out-of-order acceptance, and late invariant loading. Package-inventory tests pin default-on and explicit-off policies, host and standing-preset discovery, conflicting Loader resolution bases, manifest resolution, lifecycle filtering, and exact name/version ordering. The direct adapter mock proves pre-HTTP preparation failure, cancellation, non-2xx non-acceptance, 2xx acceptance before a later stream failure, and field collision. Keyless replay pins post-2xx extension acceptance, and the TypeScript JSON-RPC plus Python packaged-runtime snapshots project the acceptance event through both SDKs. Real Loader composition pins default package metadata plus opt-in Session upload, one real-API request mounts both shipped extensions and proves the official endpoint accepts them, and pi-ai tests retain their unchanged wire requests.
+Registry tests pin duplicate ownership, effect-scoped disposal, detached field values, concurrent and abortable preparation, receiver-preserving acceptance, one acceptance settlement, and failure aggregation. Session tests pin default-on and explicit-off policies, full-first/suffix-later delivery, direct complete event envelopes independent of base-body messages, incremental watermark folding, persisted restart recovery, fork identity fencing, out-of-order acceptance, and late invariant loading. Package-inventory tests pin default-on and explicit-off policies, host and standing-preset discovery, conflicting Loader resolution bases, manifest resolution, lifecycle filtering, and exact name/version ordering. The direct adapter mock proves pre-HTTP preparation failure, cancellation, non-2xx non-acceptance, 2xx acceptance before a later stream failure, and field collision. Keyless replay pins post-2xx extension acceptance, and the TypeScript JSON-RPC plus Python packaged-runtime snapshots project the acceptance event through both SDKs. Real Loader composition pins default package metadata and Session upload plus explicit upload disablement, one real-API request mounts both shipped extensions and proves the official endpoint accepts them, and pi-ai tests retain their unchanged wire requests.
 
 ## Alternatives considered
 
@@ -85,7 +85,7 @@ About 98% of the measured v1 real-session events were `assistant/chunk`. Omittin
 
 ## Consequences
 
-Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. An explicit Session-log opt-in also carries the complete newly unaccepted Session suffix. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata.
+Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. Unless Session-log upload is disabled, each eligible request also carries the complete newly unaccepted Session suffix. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata.
 
 The `delivery-accepted` event becomes part of the canonical log and is itself delivered on a later request. Crash recovery can duplicate a suffix but does not infer acceptance from assistant output or create a second local cursor store. Direct calls without a live Session omit the session field; host package inventory remains available.
 

+ 4 - 4
.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md

@@ -14,13 +14,13 @@ Status: implemented
 
 ## 决策
 
-`@deepseek-ai/dsh-deepseek-llm-api-extensions` 注册 `ctx.deepseekLlmApiExtensions`,即 `deepseek-official` 请求正文顶层字段的增量注册表。贡献方通过 `register()` 认领一个经声明合并的字段。适配器在序列化确切协议消息后调用 `prepare()`、传入请求取消信号,在 HTTP 前拒绝准备失败或基础字段冲突,合并分离字段,并在 HTTP 2xx 后调用捕获的 `accept()` 事务。即使贡献方忽略信号,注册表也会在取消后停止等待准备。接受失败仍以 `REQUEST_EXTENSION` 使请求失败;传输失败与非 2xx 失败绝不会接受贡献。未挂载注册表的组合会保留可复用基础适配器。随附组合会挂载注册表与两个贡献方:插件包元数据默认开启,会话日志上传默认关闭,需要设置 `session-log-deepseek.enabled: true`。无密钥 `deepseek-official` 回放会使用合成的空基础正文执行准备,并在第一个已记录分片前调用同一接受事务;它保持的是 2xx 后扩展副作用,而非字段字节。
+`@deepseek-ai/dsh-deepseek-llm-api-extensions` 注册 `ctx.deepseekLlmApiExtensions`,即 `deepseek-official` 请求正文顶层字段的增量注册表。贡献方通过 `register()` 认领一个经声明合并的字段。适配器在序列化确切协议消息后调用 `prepare()`、传入请求取消信号,在 HTTP 前拒绝准备失败或基础字段冲突,合并分离字段,并在 HTTP 2xx 后调用捕获的 `accept()` 事务。即使贡献方忽略信号,注册表也会在取消后停止等待准备。接受失败仍以 `REQUEST_EXTENSION` 使请求失败;传输失败与非 2xx 失败绝不会接受贡献。未挂载注册表的组合会保留可复用基础适配器。随附组合会挂载注册表与两个贡献方:插件包元数据和会话日志上传均默认开启;按[默认上传决策](2026-09-14-session-log-upload-default.zh.md),设置 `session-log-deepseek.enabled: false` 可关闭日志上传。无密钥 `deepseek-official` 回放会使用合成的空基础正文执行准备,并在第一个已记录分片前调用同一接受事务;它保持的是 2xx 后扩展副作用,而非字段字节。
 
 提供方无关的 `llm` 包与 `llm-pi-ai` 不包含任何扩展类型、服务查找、字段合并或接受调用。
 
 ## 增量会话日志字段
 
-`@deepseek-ai/dsh-session-log-deepseek` 以显式选择启用的方式拥有 `dsh_session_log`。启用后,每个携带存活会话 id 的请求都会发送该确切会话身份最大持久 `session-log-deepseek/delivery-accepted` 水位之后的连续权威事件后缀。该字段包含不可变会话 header 与完整事件信封。2xx 会为已发送的 `throughSeq` 追加新水位;该事件会进入下一次请求的后缀。Fork 日志会保留父级水位 id,因此子会话会在自己的身份下从序列零开始。并发接受可能乱序到达,最大水位仍保持权威。进程内 fold 会让每条会话事件只被扫描一次,并增量消费后续追加;新的会话对象或 HMR generation 会从持久历史重建该 fold。
+`@deepseek-ai/dsh-session-log-deepseek` 拥有默认开启的 `dsh_session_log` 字段。启用后,每个携带存活会话 id 的请求都会发送该确切会话身份最大持久 `session-log-deepseek/delivery-accepted` 水位之后的连续权威事件后缀。该字段包含不可变会话 header 与完整事件信封。2xx 会为已发送的 `throughSeq` 追加新水位;该事件会进入下一次请求的后缀。Fork 日志会保留父级水位 id,因此子会话会在自己的身份下从序列零开始。并发接受可能乱序到达,最大水位仍保持权威。进程内 fold 会让每条会话事件只被扫描一次,并增量消费后续追加;新的会话对象或 HMR generation 会从持久历史重建该 fold。
 
 失败方向为至少一次。传输失败或提供方拒绝不会记录水位。远端接受后、水位持久化前发生崩溃,会在恢复后触发重放,绝不会跳过序列。现有会话检查点会持久化该事件;上传插件不拥有第二份存储。
 
@@ -50,7 +50,7 @@ Status: implemented
 
 ## 验证
 
-注册表测试固定重复所有权、effect 作用域 dispose(资源释放)、分离字段值、并发且可取消的准备、保留接收者的接受操作、单次接受结算与失败聚合。会话测试固定默认关闭策略、显式启用后的首次完整/后续后缀交付、与基础正文消息无关的直接完整事件信封、增量水位 fold、持久化重启恢复、fork 身份围栏、乱序接受与 invariant 延迟加载。插件包清单测试固定默认开启与显式关闭策略、宿主与 standing preset 发现、冲突的 Loader 解析基址、manifest 解析、生命周期过滤及确切名称/版本排序。直接适配器 mock 测试证明 HTTP 前准备失败、取消、非 2xx 不接受、2xx 在后续流失败前接受,以及字段冲突。无密钥回放会固定 2xx 后扩展接受,TypeScript JSON-RPC 与 Python 打包运行时快照则通过两套 SDK 投影接受事件。真实 Loader 组合会固定默认包元数据与显式启用的会话上传,一个真实 API 请求会挂载两个随附扩展并证明官方端点接受它们;pi-ai 测试保持其协议请求不变。
+注册表测试固定重复所有权、effect 作用域 dispose(资源释放)、分离字段值、并发且可取消的准备、保留接收者的接受操作、单次接受结算与失败聚合。会话测试固定默认开启与显式关闭策略、首次完整/后续后缀交付、与基础正文消息无关的直接完整事件信封、增量水位 fold、持久化重启恢复、fork 身份围栏、乱序接受与 invariant 延迟加载。插件包清单测试固定默认开启与显式关闭策略、宿主与 standing preset 发现、冲突的 Loader 解析基址、manifest 解析、生命周期过滤及确切名称/版本排序。直接适配器 mock 测试证明 HTTP 前准备失败、取消、非 2xx 不接受、2xx 在后续流失败前接受,以及字段冲突。无密钥回放会固定 2xx 后扩展接受,TypeScript JSON-RPC 与 Python 打包运行时快照则通过两套 SDK 投影接受事件。真实 Loader 组合会固定默认包元数据、默认会话上传与显式关闭上传,一个真实 API 请求会挂载两个随附扩展并证明官方端点接受它们;pi-ai 测试保持其协议请求不变。
 
 ## 考虑过的替代方案
 
@@ -85,7 +85,7 @@ Status: implemented
 
 ## 后果
 
-DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。显式选择启用会话日志后,请求还会携带完整的未接受会话新后缀。这些字段对模型不可见,不增加提示词 token,也不改变 KV Cache,但可能显著增大 HTTP 正文。Manifest 解析、字段冲突、接受记录或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。
+DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。除非关闭会话日志上传,否则符合条件的请求还会携带完整的未接受会话新后缀。这些字段对模型不可见,不增加提示词 token,也不改变 KV Cache,但可能显著增大 HTTP 正文。Manifest 解析、字段冲突、接受记录或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。
 
 `delivery-accepted` 事件会成为权威日志的一部分,并在后续请求中自行交付。崩溃恢复可能重复后缀,但不会根据 assistant 输出推断接受,也不会创建第二份本地游标存储。缺少存活会话的直接调用会省略会话字段;宿主包清单仍然可用。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.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-01-v2-embedded-assistant-streams.md
-2026-09-01-v2-embedded-assistant-streams.md: 219879600f8435a6ecc9593661dd3f463ed1158c
-2026-09-01-v2-embedded-assistant-streams.zh.md: d38c180b1f1de689620f2e0533d2b02f4a71e466
+2026-09-01-v2-embedded-assistant-streams.md: 89b494959fcc10a2c9847908adcf13d5eedcd749
+2026-09-01-v2-embedded-assistant-streams.zh.md: 63cb425740cd2243361ec00a6dadad82c38f6f0f

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.md

@@ -31,7 +31,7 @@ The migration publication verifier and frozen v2 fixture validator require the e
 
 The Web follow adapter opts into these process-local frames and adds the last durable sequence observed at each start. It presents chunks as Client-only `assistant/live-chunk` updates between durable cursors, stages only a later matching settlement until the committed end, and reopens follow on a revision gap. A committed end publishes a named settlement delta that removes the attempt's transient matches, adds the durable entry, and replays only affected Conversation Contexts; an abandoned end publishes the same delta without an entry. A reconnect baseline carries the active attempt's durable start cursor and compact prefix.
 
-The Client event source passes durable settlements through unchanged. The Chat and Trajectory Assistant nodes fold `assistant/live-chunk` while an attempt is active, build settled output directly from `assistant/message`, and do not replay an `assistant/attempt` stream for presentation. Cold settled presentation therefore does not reconstruct per-token timing; other consumers may expand the durable stream when they require its exact evidence.
+The Client event source passes durable settlements through unchanged. Chat and Trajectory fold `assistant/live-chunk` while an attempt is active and build settled output directly from `assistant/message`. Chat does not reconstruct first-token timing after settlement retires the transient chunks. Trajectory reads timing from the [compact stream records](2026-09-06-embedded-stream-record-readers.md) in `assistant/message` and `assistant/attempt`, including when opening history. Neither target expands settled streams into per-delta objects for presentation; other consumers may expand the durable stream when they require its exact evidence.
 
 ### Released v1 to v2 migration
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.zh.md

@@ -31,7 +31,7 @@ Migration publication verifier 与冻结的 v2 fixture validator 要求嵌入式
 
 Web follow adapter 显式选择接收这些进程本地 frame,并为每个 start 补充当时观察到的最后一个持久序号。它把 chunk 呈现为持久 cursor 之间的 Client-only `assistant/live-chunk` update,只暂存 start 之后匹配的 settlement,并在 revision 缺口时重新打开 follow。committed end 会发布具名 settlement delta,删除该 attempt 的 transient match、加入持久 entry,并只重放受影响的 Conversation Context;abandoned end 会发布不含 entry 的同类 delta。重连 baseline 携带活跃 attempt 的持久起始 cursor 与紧凑前缀。
 
-Client event source 原样传递持久 settlement。Chat 与 Trajectory 的 Assistant node 在 attempt 活跃期间折叠 `assistant/live-chunk`,直接从 `assistant/message` 构建 settled output,并且不为展示重放 `assistant/attempt` stream。因此冷恢复的 settled presentation 不会重建逐 token timing;其他消费方需要精确证据时仍可展开持久 stream。
+Client event source 原样传递持久 settlement。Chat 与 Trajectory 在 attempt 活跃时折叠 `assistant/live-chunk`,直接从 `assistant/message` 构造 settled output。结算移除临时 chunk 后,Chat 不会重建首 token 计时。Trajectory 从 `assistant/message` 与 `assistant/attempt` 中的[紧凑流记录](2026-09-06-embedded-stream-record-readers.zh.md)读取计时,包括打开历史时。两个目标都不会为展示将已结算流展开为逐 delta 对象;其他消费方需要精确证据时仍可展开持久 stream。
 
 ### 已发布 v1 到 v2 迁移
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-05-canonical-feedback-log.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-canonical-feedback-log.md
-2026-09-05-canonical-feedback-log.md: c890064817ac0604fa4cc2b4073850174195e511
-2026-09-05-canonical-feedback-log.zh.md: e7e8563df8ad37d7aba641e58da4fc8872ec10b9
+2026-09-05-canonical-feedback-log.md: ce7b705b24cb0a4f7432215e19589095a9afc719
+2026-09-05-canonical-feedback-log.zh.md: 7d970ff7aea4962ea52c764fec5b6c07e3669099

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-05-canonical-feedback-log.md

@@ -14,7 +14,7 @@ The canonical Session log owns feedback. Session-level remarks use `feedback/rec
 
 Live message-feedback mutations append through the owning Session and await its durability checkpoint; cold mutations hold a persistence write handle across read, comparison, append, and flush without creating a Session or Agent. A matching no-op appends nothing but still awaits persistence. Failures propagate, and a failed live flush can leave an observable in-memory item for retry. Per-item versions prevent unrelated message edits from conflicting; strict stale-write rejection prevents ABA overwrites even when the desired value matches. Target validation binds a judgment to a sent assistant message, and forks keep independent judgments. These choices retain rationale recorded in the [archived sidecar decision](../../archived/architecture/2026-08-10-message-feedback-sidecar.md), whose storage and commit mechanism is superseded.
 
-The existing opt-in [session-log-deepseek contribution](../../../../packages/session/session-log-deepseek/README.md) includes feedback in the ordinary `dsh_session_log` suffix on a subsequent eligible request. It uses the existing DeepSeek destination selection and acceptance watermark. There is no separate `dsh_feedback` uploader, feedback-triggered LLM request, or model-input field. The [explicit-feedback OTel decision](2026-09-05-nonofficial-feedback-otel.md) owns the independent feedback-triggered upload for all users and providers.
+The existing default-on [session-log-deepseek contribution](../../../../packages/session/session-log-deepseek/README.md) includes feedback in the ordinary `dsh_session_log` suffix on a subsequent eligible request. It uses the existing DeepSeek destination selection and acceptance watermark. There is no separate `dsh_feedback` uploader, feedback-triggered LLM request, or model-input field. The [explicit-feedback OTel decision](2026-09-05-nonofficial-feedback-otel.md) owns the independent feedback-triggered upload for all users and providers.
 
 The command confirms recording with the Session and anonymous user ids, without depending on telemetry or disclosing its policy. Its append remains unflushed. This supersedes the command-copy decision in the [archived sharing disclosure note](../../archived/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.md). The [telemetry service's policy API](../../../../packages/session/session-telemetry/README.md#the-sharing-disclosure) remains independently available: a backend discloses its policy, not delivery or retention, and the optional OTel package does not own that vocabulary.
 
@@ -24,7 +24,7 @@ The command confirms recording with the Session and anonymous user ids, without
 
 **Reuse `feedback/record` for message edits.** A free-text Session remark does not identify an item mutation. Distinct events preserve message identity and deletion semantics; upload policy remains consumer-owned.
 
-**Add a dedicated feedback uploader or immediate LLM request.** The opt-in log contribution carries canonical events on eligible requests. The existing OTel pipeline independently handles explicit-feedback uploads for all providers, without a custom feedback uploader or another model request.
+**Add a dedicated feedback uploader or immediate LLM request.** The default-on log contribution carries canonical events on eligible requests. The existing OTel pipeline independently handles explicit-feedback uploads for all providers, without a custom feedback uploader or another model request.
 
 ## Consequences
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-05-canonical-feedback-log.zh.md

@@ -14,7 +14,7 @@ Status: implemented
 
 live 消息反馈变更通过所属 Session 追加,并等待其持久化检查点;cold 变更在读取、比较、追加和 flush 期间持有持久化写句柄,不创建 Session 或 Agent。匹配版本的无变更操作不追加事件,但仍等待持久化。故障会原样传播,live flush 失败可能留下可观测的内存条目以供重试。逐条版本避免不同消息的编辑互相冲突;严格拒绝陈旧写入避免 ABA 覆盖,即使期望值已经匹配也不例外。目标校验把判断绑定到已发送的 assistant 消息,fork 保持独立判断。这些选择保留[已归档伴随记录决策](../../archived/architecture/2026-08-10-message-feedback-sidecar.md)记载的理由,但其存储与提交机制已被取代。
 
-现有需显式启用的 [session-log-deepseek 贡献](../../../../packages/session/session-log-deepseek/README.zh.md)会在后续符合条件的请求中,把反馈纳入普通 `dsh_session_log` 后缀。它使用现有的 DeepSeek 目标选择和接受水位。没有独立的 `dsh_feedback` 上传器、反馈触发的 LLM 请求或模型输入字段。[显式反馈 OTel 决策](2026-09-05-nonofficial-feedback-otel.zh.md)负责面向所有用户和提供方的独立反馈触发上传。
+现有默认开启的 [session-log-deepseek 贡献](../../../../packages/session/session-log-deepseek/README.zh.md)会在后续符合条件的请求中,把反馈纳入普通 `dsh_session_log` 后缀。它使用现有的 DeepSeek 目标选择和接受水位。没有独立的 `dsh_feedback` 上传器、反馈触发的 LLM 请求或模型输入字段。[显式反馈 OTel 决策](2026-09-05-nonofficial-feedback-otel.zh.md)负责面向所有用户和提供方的独立反馈触发上传。
 
 命令用 Session 与匿名用户 id 确认记录,不依赖遥测,也不披露其策略。其追加仍不执行 flush。这取代[已归档共享披露记录](../../archived/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.md)中的命令文案决策。[遥测服务的策略 API](../../../../packages/session/session-telemetry/README.zh.md#the-sharing-disclosure) 仍可独立使用:后端披露策略,而不保证投递或保留,可选 OTel 包不拥有这套词汇。
 
@@ -24,7 +24,7 @@ live 消息反馈变更通过所属 Session 追加,并等待其持久化检查
 
 **对消息编辑复用 `feedback/record`。** 自由文本的 Session 备注不能标识条目变更。独立事件保留消息身份和删除语义;上传策略仍由消费方负责。
 
-**增加专用反馈上传器或立即发起 LLM 请求。** 需显式启用的日志贡献在符合条件的请求上传送权威事件。现有 OTel 流水线独立处理所有提供方的显式反馈上传,无需自定义反馈上传器或另一个模型请求。
+**增加专用反馈上传器或立即发起 LLM 请求。** 默认开启的日志贡献在符合条件的请求上传送权威事件。现有 OTel 流水线独立处理所有提供方的显式反馈上传,无需自定义反馈上传器或另一个模型请求。
 
 ## 后果
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-05-nonofficial-feedback-otel.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-nonofficial-feedback-otel.md
-2026-09-05-nonofficial-feedback-otel.md: bbc947455f7a84a41af6651223eb62129fa8d452
-2026-09-05-nonofficial-feedback-otel.zh.md: de9b0322861118cdc1163a0faefb172fc1062776
+2026-09-05-nonofficial-feedback-otel.md: 8225d8bea030c4f09b60fd1a228f0425e9489d39
+2026-09-05-nonofficial-feedback-otel.zh.md: e8ea48b3fe2f228ef57ae1a20f92100dfc333f9c

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-05-nonofficial-feedback-otel.md

@@ -16,7 +16,7 @@ An authorized prefix includes all unhanded canonical context from seq 0 through
 
 The backend uses on-demand capture with complete history and the existing redaction waterfall. `DISABLED` constructs no transport. `FULL` is rejected rather than aliased. Direct `ctx.sessionTelemetry.emit()` calls are no-ops, so callers cannot bypass feedback authorization. SDK scheduled flush and shutdown may finish previously authorized batches but never capture new records. Sending after submission needs no further user interaction or model call.
 
-The [canonical-feedback decision](2026-09-05-canonical-feedback-log.md) owns storage, versions, deletion, and plain command confirmation. The [opt-in DeepSeek contribution](../../../../packages/session/session-log-deepseek/README.md) remains independent, with its existing destination and acceptance behavior.
+The [canonical-feedback decision](2026-09-05-canonical-feedback-log.md) owns storage, versions, deletion, and plain command confirmation. The [default-on DeepSeek contribution](../../../../packages/session/session-log-deepseek/README.md) remains independent, with its existing destination and acceptance behavior.
 
 ## Alternatives considered
 
@@ -28,6 +28,6 @@ The [canonical-feedback decision](2026-09-05-canonical-feedback-log.md) owns sto
 
 ## Consequences
 
-Handoff is best-effort, not collector acceptance. Same-object cursors suppress repeated capture, but fresh cold snapshots and new feedback after restart can repeat prefixes; receivers deduplicate on `(session.id, session.format_version, event.seq)`. There is no durable OTel outbox, delivery watermark, or harness HTTP retry promise. SDK batching and loss behavior apply after enqueue. OTel and the opt-in DeepSeek path can overlap. Withdrawal exports a deletion event, not remote erasure.
+Handoff is best-effort, not collector acceptance. Same-object cursors suppress repeated capture, but fresh cold snapshots and new feedback after restart can repeat prefixes; receivers deduplicate on `(session.id, session.format_version, event.seq)`. There is no durable OTel outbox, delivery watermark, or harness HTTP retry promise. SDK batching and loss behavior apply after enqueue. OTel and the default-on DeepSeek path can overlap. Withdrawal exports a deletion event, not remote erasure.
 
 [OTel tests](../../../../packages/session/session-telemetry-otel/tests/otel.spec.ts) cover explicit-feedback capture, provider-independent behavior, lifecycle silence, fork consent, cold commits, and direct-call denial. [Coordinator tests](../../../../packages/session/session-telemetry/tests/telemetry.spec.ts) cover history capture; [base tests](../../../../packages/bundle/base/tests/base.spec.ts) pin the mounted default.

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-05-nonofficial-feedback-otel.zh.md

@@ -16,7 +16,7 @@ Status: implemented
 
 后端使用包含完整历史的按需捕获与现有脱敏 waterfall(瀑布式事件)。`DISABLED` 不构造传输。`FULL` 被拒绝,不作为别名。直接调用 `ctx.sessionTelemetry.emit()` 是空操作,因此调用方不能绕过反馈授权。SDK 定时刷新和关闭可以完成先前已授权的批次,但绝不捕获新记录。提交后的发送无需进一步用户交互或模型调用。
 
-[权威反馈决策](2026-09-05-canonical-feedback-log.zh.md)负责存储、版本、删除与纯命令确认。[需主动开启的 DeepSeek 贡献](../../../../packages/session/session-log-deepseek/README.zh.md)保持独立,保留现有目标与接受行为。
+[权威反馈决策](2026-09-05-canonical-feedback-log.zh.md)负责存储、版本、删除与纯命令确认。[默认开启的 DeepSeek 贡献](../../../../packages/session/session-log-deepseek/README.zh.md)保持独立,保留现有目标与接受行为。
 
 ## 考虑过的替代方案
 
@@ -28,6 +28,6 @@ Status: implemented
 
 ## 后果
 
-交接尽力而为,不代表采集端接受。同对象游标抑制重复捕获,但新冷快照和重启后的新反馈可能重复前缀;接收方按 `(session.id, session.format_version, event.seq)` 去重。没有持久化 OTel outbox、投递水位或 harness HTTP 重试承诺。入队后适用 SDK 批处理与丢失行为。OTel 与需主动开启的 DeepSeek 路径可能重叠。撤回导出删除事件,不是远端擦除。
+交接尽力而为,不代表采集端接受。同对象游标抑制重复捕获,但新冷快照和重启后的新反馈可能重复前缀;接收方按 `(session.id, session.format_version, event.seq)` 去重。没有持久化 OTel outbox、投递水位或 harness HTTP 重试承诺。入队后适用 SDK 批处理与丢失行为。OTel 与默认开启的 DeepSeek 路径可能重叠。撤回导出删除事件,不是远端擦除。
 
 [OTel 测试](../../../../packages/session/session-telemetry-otel/tests/otel.spec.ts)覆盖显式反馈捕获、提供方无关行为、生命周期静默、fork 同意、冷会话提交与直接调用拒绝。[协调器测试](../../../../packages/session/session-telemetry/tests/telemetry.spec.ts)覆盖历史捕获;[基础配置测试](../../../../packages/bundle/base/tests/base.spec.ts)固定挂载默认值。

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.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-06-embedded-stream-record-readers.md
-2026-09-06-embedded-stream-record-readers.md: 76e109de577d070093bb7123aa50d9a4ae7fcbe5
-2026-09-06-embedded-stream-record-readers.zh.md: 33f4b31117197c47e7f2ff1637bc13fa2cbf1a8c
+2026-09-06-embedded-stream-record-readers.md: efad86a4f940bf3b56d4b141a8fe1e3659c8dd9c
+2026-09-06-embedded-stream-record-readers.zh.md: f7131a27171163954bca8b7013dc6d90f907c552

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.md

@@ -20,7 +20,7 @@ After v2 embedded streams settlement widened with the message content and Chat a
 - Run readers: `runFirstTokenTime` and `runFirstVisibleTime` reconstruct the first qualifying member's time from `time0` and the `dt` gaps and stop scanning there; a name-bearing Tool-call run yields `time0` without reading a fragment.
 - Stream readers: `assistantStreamFirstTokenTime`, `assistantStreamHasVisibleContent`, `assistantStreamHasVisibleText`, `lastAssistantStreamChunk(stream, type)` (backward scan), `assistantStreamChunks(stream, type)`, `joinAssistantStreamText`, and `assembleAssistantStream`, which feeds a `BlockAssembler` one joined delta per run (assembly only concatenates, so blocks, usage, finish, and replay state equal the per-member result). `RawStreamChunkType` excludes the delta types, so a raw-chunk lookup can never silently skip packed members.
 
-Session Stats, Chat, and Trajectory read `assistantStreamFirstTokenTime` from both `assistant/attempt` and `assistant/message`, retaining the Step's first token across retries. Chat and Trajectory settle content from the assembled message while reading timing independently, so reopening history retains TTFT and decoding metrics without expanding streams. The token meter reads `lastAssistantStreamChunk(stream, 'usage')` and assembles provider output through `assembleAssistantStream`; the subagent output fold appends `joinAssistantStreamText`; the Session Controller scans `assistantStreamChunks(stream, 'block-end')` for images.
+Session Stats and Trajectory read `assistantStreamFirstTokenTime` from both `assistant/attempt` and `assistant/message`, retaining the Step's first token across retries. Trajectory settles content from the assembled message while reading timing independently, so reopening history retains TTFT and decoding metrics without expanding streams. Chat follows its [settled-reply timing policy](../bug-fix/2026-09-14-chat-presentation-defaults.md). The token meter reads `lastAssistantStreamChunk(stream, 'usage')` and assembles provider output through `assembleAssistantStream`; the subagent output fold appends `joinAssistantStreamText`; the Session Controller scans `assistantStreamChunks(stream, 'block-end')` for images.
 
 `expandAssistantStream` keeps its strict validation and its remaining callers, which need every member or validate the stream at a durable boundary: Session restore validation, the v1-to-v2 migration validator and publication Worker replay, the reconnect baseline, and test support.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-06-embedded-stream-record-readers.zh.md

@@ -20,7 +20,7 @@ Session 格式 v2 将每次模型尝试的紧凑流(`AssistantStreamRecord[]`
 - Run 读取器:`runFirstTokenTime` 与 `runFirstVisibleTime` 从 `time0` 与 `dt` 间隔重建首个合格成员的时间并停止扫描;带名称的 Tool-call run 直接产出 `time0`,不读片段。
 - 流读取器:`assistantStreamFirstTokenTime`、`assistantStreamHasVisibleContent`、`assistantStreamHasVisibleText`、`lastAssistantStreamChunk(stream, type)`(逆向扫描)、`assistantStreamChunks(stream, type)`、`joinAssistantStreamText` 与 `assembleAssistantStream`(每个 run 向 `BlockAssembler` 喂入一个拼接后的 delta;组装只做拼接,因此 blocks、usage、finish 与 replay state 与逐成员结果一致)。`RawStreamChunkType` 排除 delta 类型,因此原始 chunk 查找不可能静默跳过打包成员。
 
-Session Stats、Chat 与 Trajectory 从 `assistant/attempt` 和 `assistant/message` 读取 `assistantStreamFirstTokenTime`,跨重试保留步骤的首个 token。Chat 与 Trajectory 从组装后的消息结算内容,并独立读取计时,因此重新打开历史时无需展开流便能保留 TTFT 与解码指标。token 计量读取 `lastAssistantStreamChunk(stream, 'usage')` 并通过 `assembleAssistantStream` 组装提供商输出;子代理输出折叠追加 `joinAssistantStreamText`;Session Controller 用 `assistantStreamChunks(stream, 'block-end')` 扫描镜像。
+Session Stats 与 Trajectory 从 `assistant/attempt` 和 `assistant/message` 读取 `assistantStreamFirstTokenTime`,跨重试保留步骤的首个 token。Trajectory 从组装后的消息结算内容,并独立读取计时,因此重新打开历史时无需展开流便能保留 TTFT 与解码指标。Chat 遵循[已结算回复的计时策略](../bug-fix/2026-09-14-chat-presentation-defaults.zh.md)。token 计量读取 `lastAssistantStreamChunk(stream, 'usage')` 并通过 `assembleAssistantStream` 组装提供商输出;子代理输出折叠追加 `joinAssistantStreamText`;Session Controller 用 `assistantStreamChunks(stream, 'block-end')` 扫描镜像。
 
 `expandAssistantStream` 保留其严格校验与其余调用方(需要每个成员或在持久边界校验流):Session 恢复校验、v1-to-v2 迁移校验器与发布 Worker 重放、重连基线、测试支撑。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-07-sidebar-responsive-tab-info.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-07-sidebar-responsive-tab-info.md
-2026-09-07-sidebar-responsive-tab-info.md: ade20148cccd2233224417f762e65492828943bb
-2026-09-07-sidebar-responsive-tab-info.zh.md: 0903e4ff06ca187fcc1fd0e9d8de63f70d5cc427
+2026-09-07-sidebar-responsive-tab-info.md: 96f7f047d00a33d6cd48b026ce7762929720d9d6
+2026-09-07-sidebar-responsive-tab-info.zh.md: f020068e71777272fd15a967de547ce153884402

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-07-sidebar-responsive-tab-info.md

@@ -12,7 +12,7 @@ Tab extensions need consistent live information about their containing pane and
 
 The slot framework injects one `useTabInfo()` returning nested `sidebar`, `panel`, and `tab` fields. It composes the framework-bound layout and navigation hooks; extensions neither subscribe themselves nor receive a service object. Body visibility requires an active tab in an expanded Sidebar; title visibility does not require an active tab. Hiding or switching Sessions leaves the tab lifetime intact. Closing the record aborts its signal. Tab actions stay bound to their owning Session. Store adoption is a private capability of the plugin assembly, not a public controller operation.
 
-The frame protects 400px for the conversation by shrinking the right column, then closing it before shrinking the conversation. Its first-open preference is 45% of the viewport, retained thereafter in pixels, with a 300px floor and 70% viewport ceiling. The left column keeps its preference at widths of at least 1024px. Closing is recorded state: widening never opens it, while a user action or explicit Session API may. Refresh restores defaults rather than persisting layout.
+The frame protects 400px for the conversation by shrinking the right column, then closing it before shrinking the conversation. Its first-open preference is 45% of the viewport, retained thereafter in pixels, with a 300px floor and 70% viewport ceiling. The left column keeps its preference at widths of at least 1024px. Closing is recorded state: widening never opens it, while a user action or explicit Session API may. [Layout persistence and provider recovery](2026-09-14-sidebar-layout-provider-recovery.md) owns layout restoration after reload; frame width preferences still initialize from the viewport.
 
 Fullscreen uses the same mounted content tree and covers the viewport while retaining the underlying column reservation. Opening below 768px selects automatic fullscreen; exiting it there closes the Sidebar. Widening can end automatic fullscreen but leaves manually selected fullscreen intact. The product permits two horizontal panes, a 50/50 initial split, and a 20–80% divider; narrow panes refuse new splits. The generic docking engine retains its independent capabilities. A fullscreen entry completes its slide before reporting the underlying track; that covered width change is instantaneous, so neither entry nor returning to normal reveals a background reflow. At the two-pane budget the split control is hidden; a one-pane width refusal remains disabled. On exit, the frame prepares the destination before the overlay retreats: no right track for close, a normal track for restore. Transition suppression survives clearing the fullscreen report and ends on the next geometry action.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-07-sidebar-responsive-tab-info.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 Slot 框架注入一个 `useTabInfo()`,返回嵌套的 `sidebar`、`panel` 与 `tab` 字段。它组合框架绑定的布局与导航 hook;扩展既不自行订阅,也不接收服务对象。正文可见要求 Sidebar 展开且标签活跃;标题可见不要求标签活跃。隐藏或切换 Session 保留标签生命周期。关闭记录会中止其 signal。标签动作始终绑定到所属 Session。Store 收编是插件组装的私有能力,不是公共控制器操作。
 
-框架先缩小右列,再关闭右列,最后才缩小会话区,以保护会话区的 400px 宽度。右列首次打开偏好为视口的 45%,此后按像素保留,下限为 300px,上限为视口的 70%。在宽度至少为 1024px 时,左列保持自身偏好。关闭是被记录的状态:变宽不会打开右栏,用户动作或显式 Session API 可以打开。刷新恢复默认值,不持久化布局。
+框架先缩小右列,再关闭右列,最后才缩小会话区,以保护会话区的 400px 宽度。右列首次打开偏好为视口的 45%,此后按像素保留,下限为 300px,上限为视口的 70%。在宽度至少为 1024px 时,左列保持自身偏好。关闭是被记录的状态:变宽不会打开右栏,用户动作或显式 Session API 可以打开。[布局持久化与 provider 恢复](2026-09-14-sidebar-layout-provider-recovery.zh.md)负责刷新后的布局还原;框架宽度偏好仍根据视口初始化。
 
 全屏使用同一棵已挂载内容树,覆盖视口并保留底层列的占位。在 768px 以下打开会选择自动全屏;在此宽度下退出全屏会关闭 Sidebar。变宽可以结束自动全屏,但保留手动选择的全屏。产品允许两个水平窗格,初始按 50/50 分割,分割线范围为 20–80%;窄窗格拒绝新分栏。通用停靠引擎保留其独立能力。 全屏入场先完成滑入,再报告底层轨道;被覆盖的宽度变化瞬间完成,因此入场及返回普通模式都不暴露底层重排。达到两格预算时隐藏分栏控件;单格宽度不足时仍显示禁用控件。 退场时,框架先准备目标布局,再让覆盖层退出:关闭不留右轨道,恢复保留普通轨道。清除全屏报告时仍保留过渡抑制,直到下一次几何操作才结束。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.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-08-document-preview-operations.md
-2026-09-08-document-preview-operations.md: 3703933273e743c8df32bf0352fc276fe21dcb93
-2026-09-08-document-preview-operations.zh.md: b4896e95959d0f276ee69dfeaee9528319981714
+2026-09-08-document-preview-operations.md: 43cc8d935763512a53379466bb796b5cac469793
+2026-09-08-document-preview-operations.zh.md: 44eccba894b3748a1d8640561a9eb760de481919

+ 3 - 3
.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md

@@ -16,9 +16,9 @@ Document Preview separates resource observation from content reads. The [resourc
 
 Readable files use `dsh-resource://file/session/<sessionId>/<path>`. The path may be workspace-relative or absolute; an encoded absolute path retains its leading slash. `fileAddressFor` always emits this Session-address form. The provider and Preview RPC take the Session only from that address, never from the current selection, first holder, or owning tab. A Session-less `absolute` URI cannot be read; the provider reports `workspace-file/unknown-workspace`. Session authorization is a file-protocol rule, not an additional Resource identity.
 
-[Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns format selection and loading policy. Metadata registers with `ctx.documentPreviews`; components register separately into the keyed `sidebar.right.tab.document` Slot. Extension registrations precede builtins, then longer suffixes and registration order decide. The toolbar lists matching alternatives and remembers a manual choice per tab; plain text is the fallback. The child receives accumulated text or complete native bytes, the original resource address, and the standard `useResource` and `useTabInfo` hooks. Preview calls existing `read`, `readAll`, and `readRelated` through ordinary injection and decodes bytes in its own `rpc.ts`. Refresh remains per tab, with no resource reload, shared `changed` acknowledgement, extra resource wrapper, or content Session.
+[Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns format selection and loading policy. Metadata registers with `ctx.documentPreviews`; components register separately into the keyed `sidebar.right.tab.document` Slot. Extension registrations precede builtins, then longer suffixes and registration order decide. The toolbar lists matching alternatives only when at least two exist and remembers a manual choice per tab; plain text is the fallback except for suffixes a registration declares binary or the owner's unviewable list names ([sidebar preview polish](../feature/2026-09-11-sidebar-document-preview-polish.md)). The child receives accumulated text or complete native bytes, the original resource address, and the standard `useResource` and `useTabInfo` hooks. Preview calls existing `read`, `readAll`, and `readRelated` through ordinary injection and decodes bytes in its own `rpc.ts`. Refresh remains per tab, with no resource reload, shared `changed` acknowledgement, extra resource wrapper, or content Session.
 
-Markdown and code reuse the incremental primitives with cumulative paged text. HTML, PDF, and images read complete `Uint8Array<ArrayBuffer>` data; Host transport remains base64. Published buffers are borrowed read-only and never persist into layout or Session JSON. PDF.js runs in an owned Worker with version-matched bundled font and decoder data, and copies input before transfer to preserve Preview's retained buffer. HTML runs in a Blob iframe with `sandbox="allow-scripts"`, without same-origin, popup, form, download, or top-navigation privileges. The browser retains its normal external-network rules. Bounded static local JS/CSS reads stay in the parent; the opaque frame creates its own asset Blobs, because it cannot load parent-origin Blobs. PNG, JPEG, GIF, WebP, BMP, ICO, and SVG use image-specific Blob URLs in an `<img>` static-image context. They retain intrinsic CSS-pixel dimensions; auto margins centre images smaller than the shared scroller, while larger dimensions extend its horizontal or vertical scroll range. The renderer provides no zoom or drag-to-pan. SVG markup never enters the application DOM or an iframe, so scripts remain inert and cannot reach the parent page. Replacing HTML or an image revokes its root Blob URL.
+Markdown and code reuse the incremental primitives with cumulative paged text. HTML, PDF, and images read complete `Uint8Array<ArrayBuffer>` data; Host transport remains base64. Published buffers are borrowed read-only and never persist into layout or Session JSON. PDF.js runs in an owned Worker with version-matched bundled font and decoder data, and copies input before transfer to preserve Preview's retained buffer. HTML runs in a Blob iframe with `sandbox="allow-scripts"`, without same-origin, popup, form, download, or top-navigation privileges. The browser retains its normal external-network rules. Bounded static local JS/CSS reads stay in the parent; the opaque frame creates its own asset Blobs, because it cannot load parent-origin Blobs. PNG, JPEG, GIF, WebP, BMP, ICO, and SVG use image-specific Blob URLs in an `<img>` static-image context. An image wider than the pane scales down to its width at its aspect ratio; a smaller image keeps its intrinsic CSS-pixel dimensions centred by auto margins, and a taller image extends the shared scroller's vertical range ([sidebar preview polish](../feature/2026-09-11-sidebar-document-preview-polish.md)). The renderer provides no zoom or drag-to-pan. SVG markup never enters the application DOM or an iframe, so scripts remain inert and cannot reach the parent page. Replacing HTML or an image revokes its root Blob URL.
 
 ## Alternatives considered
 
@@ -38,4 +38,4 @@ Markdown and code reuse the incremental primitives with cumulative paged text. H
 
 ## Consequences
 
-Renderers can be replaced without changing the tab or file protocol. Full-file formats pay bounded whole-file memory and PDF adds bundled Worker/font/decoder bytes. Format selection and view state are page-local, not durable Session data. Preview owns RPC cancellation and native buffers independently of metadata observation. A tab retains its read version and the observation version captured at read start; refreshing it neither discards another tab's content nor clears its change notice. File reads remain non-transactional, and opaque versions are compared for equality, not ordering. The [recorded browser scenario](../../../../apps/web/tests/document-preview.e2e.ts) exercises the shared toolbar, incremental text, isolated HTML dependencies, intrinsic raster and SVG rendering with two-axis scrolling, inert SVG scripts, and lazy continuous PDF Worker rendering.
+Renderers can be replaced without changing the tab or file protocol. Full-file formats pay bounded whole-file memory and PDF adds bundled Worker/font/decoder bytes. Format selection and view state are page-local, not durable Session data. Preview owns RPC cancellation and native buffers independently of metadata observation. A tab retains its read version and the observation version captured at read start; refreshing it neither discards another tab's content nor clears its change notice. File reads remain non-transactional, and opaque versions are compared for equality, not ordering. The [recorded browser scenario](../../../../apps/web/tests/document-preview.e2e.ts) exercises the shared toolbar, incremental text, isolated HTML dependencies, width-fitted raster and SVG rendering, inert SVG scripts, and lazy continuous PDF Worker rendering.

+ 3 - 3
.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md

@@ -16,9 +16,9 @@ Document Preview 将资源观察与内容读取分开。[资源模型](2026-09-0
 
 可读取的文件使用 `dsh-resource://file/session/<sessionId>/<path>`。路径可以相对工作区,也可以是绝对路径;编码后的绝对路径保留前导斜杠。`fileAddressFor` 始终生成这种 Session 地址。提供方与 Preview RPC 只从该地址取 Session,不取当前选择、首个持有者或 tab 所属 Session。不带 Session 的 `absolute` URI 无法读取;提供方报告 `workspace-file/unknown-workspace`。Session 授权是文件协议规则,不是额外的 Resource 身份。
 
-[Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md) 负责格式选择和加载策略。元数据通过 `ctx.documentPreviews` 注册;组件单独注册到 keyed `sidebar.right.tab.document` Slot。扩展注册优先于内置注册,其次比较后缀长度和注册顺序。工具栏列出匹配候选,按 tab 记住手动选择;纯文本是兜底。子组件收到累积文本或完整原生字节、原始资源地址,以及标准 `useResource` 和 `useTabInfo` 钩子。Preview 经普通注入调用既有 `read`、`readAll` 与 `readRelated`,在自己的 `rpc.ts` 解码字节。刷新仍按 tab 独立进行,不引入资源 reload、共享 `changed` 确认、额外资源包装层或内容 Session。
+[Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md) 负责格式选择和加载策略。元数据通过 `ctx.documentPreviews` 注册;组件单独注册到 keyed `sidebar.right.tab.document` Slot。扩展注册优先于内置注册,其次比较后缀长度和注册顺序。工具栏仅在候选不少于两个时列出匹配候选,按 tab 记住手动选择;纯文本是兜底,但注册声明为二进制的后缀和 owner 的 unviewable 清单所列后缀除外([侧边栏预览打磨](../feature/2026-09-11-sidebar-document-preview-polish.zh.md))。子组件收到累积文本或完整原生字节、原始资源地址,以及标准 `useResource` 和 `useTabInfo` 钩子。Preview 经普通注入调用既有 `read`、`readAll` 与 `readRelated`,在自己的 `rpc.ts` 解码字节。刷新仍按 tab 独立进行,不引入资源 reload、共享 `changed` 确认、额外资源包装层或内容 Session。
 
-Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML、PDF 和图片读取完整 `Uint8Array<ArrayBuffer>` 数据;Host 传输保持 base64。发布后的缓冲区只读借用,绝不持久化进布局或 Session JSON。PDF.js 在自有 Worker 中运行,字体和解码数据以相同版本随包发布,转移输入前先复制,以保留 Preview 的缓冲区。HTML 在 Blob iframe 中运行,设置 `sandbox="allow-scripts"`,不授予同源、弹窗、表单、下载或顶层导航权限。浏览器保持正常的外部网络规则。有上限的静态本地 JS/CSS 读取由父页面负责;不透明源 iframe 创建自己的资源 Blob,因为它不能加载父源创建的 Blob。PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 使用图片专用 Blob URL,在 `<img>` 静态图片上下文中渲染。它们保留固有 CSS 像素尺寸;auto margin 让小于共享滚动区的图片居中,较大的尺寸则扩展横向或纵向滚动范围。渲染器不提供缩放或拖拽平移。SVG 标记绝不进入应用 DOM 或 iframe,因此脚本保持不可执行,也无法访问父页面。替换 HTML 或图片时会撤销其根 Blob URL。
+Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML、PDF 和图片读取完整 `Uint8Array<ArrayBuffer>` 数据;Host 传输保持 base64。发布后的缓冲区只读借用,绝不持久化进布局或 Session JSON。PDF.js 在自有 Worker 中运行,字体和解码数据以相同版本随包发布,转移输入前先复制,以保留 Preview 的缓冲区。HTML 在 Blob iframe 中运行,设置 `sandbox="allow-scripts"`,不授予同源、弹窗、表单、下载或顶层导航权限。浏览器保持正常的外部网络规则。有上限的静态本地 JS/CSS 读取由父页面负责;不透明源 iframe 创建自己的资源 Blob,因为它不能加载父源创建的 Blob。PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 使用图片专用 Blob URL,在 `<img>` 静态图片上下文中渲染。比面板宽的图片按纵横比缩小到面板宽度;较小的图片保留固有 CSS 像素尺寸并由 auto margin 居中,较高的图片扩展共享滚动区的纵向范围([侧边栏预览打磨](../feature/2026-09-11-sidebar-document-preview-polish.zh.md))。渲染器不提供缩放或拖拽平移。SVG 标记绝不进入应用 DOM 或 iframe,因此脚本保持不可执行,也无法访问父页面。替换 HTML 或图片时会撤销其根 Blob URL。
 
 ## 考虑过的替代方案
 
@@ -38,4 +38,4 @@ Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML、P
 
 ## 影响
 
-替换渲染器不需要改变 Tab 或文件协议。全文格式承担有上限的整文件内存成本,PDF 增加随包发布的 Worker、字体和解码器字节。格式选择和查看状态仅属于当前页面,不是持久 Session 数据。Preview 独立于元数据观察,拥有 RPC 取消和原生缓冲区。tab 保留读取版本及读取开始时捕获的观察版本;刷新它既不丢弃其他 tab 的内容,也不清除其变更提示。文件读取仍非事务,不透明版本只比较相等性、不排序。[录制的浏览器场景](../../../../apps/web/tests/document-preview.e2e.ts) 覆盖共用工具栏、增量文本、隔离的 HTML 依赖、可双轴滚动的固有尺寸位图与 SVG 渲染、不可执行的 SVG 脚本,以及惰性连续 PDF Worker 渲染。
+替换渲染器不需要改变 Tab 或文件协议。全文格式承担有上限的整文件内存成本,PDF 增加随包发布的 Worker、字体和解码器字节。格式选择和查看状态仅属于当前页面,不是持久 Session 数据。Preview 独立于元数据观察,拥有 RPC 取消和原生缓冲区。tab 保留读取版本及读取开始时捕获的观察版本;刷新它既不丢弃其他 tab 的内容,也不清除其变更提示。文件读取仍非事务,不透明版本只比较相等性、不排序。[录制的浏览器场景](../../../../apps/web/tests/document-preview.e2e.ts) 覆盖共用工具栏、增量文本、隔离的 HTML 依赖、按宽度适配的位图与 SVG 渲染、不可执行的 SVG 脚本,以及惰性连续 PDF Worker 渲染。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.i18n.yaml

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

+ 118 - 0
.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.md

@@ -0,0 +1,118 @@
+# Agent Note: Add immutable profile resolution generations
+
+Status: implemented
+
+English | [中文](2026-09-09-profile-resolution-generations.zh.md)
+
+## Problem
+
+A profile loads plugin rows from its own package project, while Harness packages and packages carried by selected bundles can live outside that project's ordinary dependency tree. The current launcher bridges the trees by calculating package precedence at startup and materializing that result as shared symlinks, profile-owned links, or packaged-executable proxy packages. The files persist across processes and installations, require reconciliation and locking, expose generated proxy manifests to metadata readers, and cannot represent a process-local change atomically.
+
+The runtime design preserves the existing selection rules rather than introducing a second package policy. It covers imports performed by plugin modules as well as Loader row imports and works in the main thread and Harness-owned Workers. Generation replacement accepts only additive package sets and never mutates a live table entry by entry.
+
+## Decision
+
+Profile startup computes one immutable `ResolutionGeneration` from the same dependency traversal that supplies the disk module fallback. The launcher defaults to link mode and preserves the existing materialized lookup behavior. Internal callers and tests can select runtime mode, which installs the generation into Node's ESM and CommonJS resolvers, or dual mode, which materializes and verifies the same generation. `PluginPackages.replace()` publishes a complete additive successor with one reference replacement.
+
+### One selection algorithm
+
+The package traversal remains in `@deepseek-ai/dsh-app-boot` beside profile loading. The disk materializer and the runtime resolver consume one pure plan; neither owns a copy of the precedence algorithm. Ordinary Node callers can select link, dual, or runtime mode, while an omitted mode selects link. Packaged executables and the Electron Host select runtime mode because their dependency trees may live in a virtual filesystem; dual remains an internal comparison path.
+
+The installation manifest is the first root. Its graph traverses `dependencies` followed by `peerDependencies` breadth-first, resolving each edge from the manifest that declares it. The first installed package reached under a name owns that name. Selected bundle roots then run in profile order, with each earlier root's complete graph taking precedence over every later root. Names supplied by the installation are reserved, and bundle package roots themselves do not become plugin fallbacks. Missing declared packages are skipped as before.
+
+Profile-local and plugin-private `node_modules` entries stay outside the fallback entries, and Node checks them before the virtual fallback position. The generation records only installed direct profile package names for a no-I/O native fast path. Each fallback entry records the package name, version, selected lookup directory, declaring manifest anchor, and scope needed to rerun Node's native resolution from the selected package and validate that a successor preserves existing mappings.
+
+The existing `healProfilesModuleFallback()` remains as the disk materializer for the same computed result, which permits direct comparison without rewriting the selection rules. The launcher uses it by default. Runtime mode computes the generation without materializing it, while dual mode materializes and installs that generation for comparison.
+
+### Immutable generations
+
+A resolver registration holds one `current` generation. Each synchronous resolution captures that reference once. Generation construction reads every required manifest before publication; an error leaves the current generation unchanged. Successful publication replaces one reference, and in-flight calls may finish against the generation they captured.
+
+Selection and package-metadata caches belong to a generation. Publishing a successor invalidates them by making the old generation unreachable after its callers finish; update code does not mutate or clear individual entries. A generation hit and a successful native selection can be cached, but a generation miss is rescanned so a profile-local package installed after the miss becomes visible as it does in link mode. Calls with explicit CommonJS paths or non-default conditions never reuse a default-resolution cache entry.
+
+The launcher constructs one startup generation. The service accepts an additive successor, but no package-manager transaction invokes replacement in this implementation.
+
+### Shared ESM and CommonJS rule
+
+The resolver uses `node-addon-require-builtin` to read `internal/modules/esm/loader` and `internal/modules/cjs/loader`. The ESM adapter wraps the per-thread singleton `CascadedLoader` resolve methods. The CommonJS adapter wraps the internal builtin's `Module._resolveFilename`; that `Module` is the same object exported by `node:module`.
+
+Both adapters call one routing function. It ignores builtins, relative or absolute paths, URLs, parents outside the profile scope, and explicit calls outside the supported lookup. A `#imports` request uses Node's mapping from its owning manifest; an external bare target follows the same local, generation, and after-fallback package order with the request's conditions, while Node retains exact target resolution. For a scoped bare request, a package self-reference keeps the original parent even when an npm alias gives its installed directory another name. A profile-local or plugin-private package also keeps the original parent when Node resolves the requested entry before the virtual shared-fallback position; a CommonJS package directory without `exports` does not suppress the fallback when only its requested subpath is absent. Otherwise the router uses a generation hit through that entry's declaring anchor or continues native lookup after the virtual fallback. Explicit CommonJS path lists apply the same insertion rule independently to each path in caller order.
+
+The adapters call the captured native resolver after routing. Node remains responsible for exports, import and require conditions, main files, subpaths, extensions, native caches, and error codes. Routed ESM failures replace the internal lookup anchor in Node's diagnostic with the original importer. A selected package's invalid export or missing target does not trigger another same-name candidate. CommonJS does not replace `_findPath` or reproduce `_resolveFilename`.
+
+The guarantee covers Node's default `import`, `import()`, `import.meta.resolve`, `require`, and `require.resolve` after installation in that thread. It does not cover already linked modules, custom `vm` linkers, opaque non-Node importers, or third-party Workers.
+
+### Active plugin list and package metadata
+
+The resolution generation lists available fallback packages; Loader entries form the active plugin list. Consumers keep using Loader's existing entry lifecycle and filter the entries relevant to their own scope. Consumers that need package metadata pass a specifier and owning tree base URL to a lightweight `app-boot` service without requiring a `./package.json` export. An installed generation is authoritative, including a miss; a service created without a generation retains native lookup for low-level embedders.
+
+The resolver does not expose `imported(entry)` and does not observe ModuleJobs, wrap Entry methods, associate fibers with import calls, replace registry or tree methods, or adapt HMR transactions. A repeated query uses the same generation and therefore cannot drift from the route used for the import. Non-Node importers that need package metadata must explicitly implement the same deterministic resolver interface.
+
+The implementation lives under `app-boot/src/profile-resolution/`. `service.ts` provides the long-lived `ctx.pluginPackages` and owns the main-thread resolver and Worker-generation lifetimes; `resolver.ts` implements generation lookup and the Node Internal adapters; `worker-bootstrap.ts` installs an inherited generation in one thread. Existing profile selection and disk materialization remain in `profile.ts`. Workers reference the bootstrap only through the public `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap` export.
+
+The service definition and provider remain together in `app-boot` because profile boot owns the resolver lifetime. Extracting a separate capability seam becomes warranted when a launcher-independent provider or independently evolving consumers require it.
+
+### Workers and generation updates
+
+The main thread publishes a structured-clone representation of the current generation through Worker environment data. Each built Harness-owned Worker uses its build banner to obtain its own ESM and CommonJS internal objects and install the same adapters without traversing manifests. The bootstrap bundle has no static package imports. Source Worker entries retain their existing self-contained dependencies. Third-party Workers remain unchanged.
+
+New Workers inherit the latest published generation. Existing Workers keep the generation they inherited, so a caller that publishes a successor must restart them. The ESM bootstrap cannot affect static dependencies linked before its execution, so Worker bundles keep pre-bootstrap static imports natively resolvable and start code needing the profile resolver through a later dynamic import.
+
+### Additive package changes
+
+A caller adding a package completes its pnpm transaction before constructing a successor generation. Replacement rejects any generation that changes the directory or version of an existing package. The caller publishes an additive successor before mounting the new Loader row; this implementation does not provide that package transaction. A mount failure may leave the package installed but inactive.
+
+Replacing, upgrading, or removing an already loaded package requires process restart because Node's ESM Module Map, CommonJS cache, existing object references, and running Workers can retain the old module identity. Generation replacement does not claim to unload modules.
+
+### Disk migration
+
+Runtime-only launch paths do not create, update, or retire symlinks and proxy packages. The resolver treats the legacy shared fallback and `.dsh-module-fallback` projections as virtual insertion positions: a generation hit uses the table target, while a miss skips those old positions before continuing native ancestor lookup. During a dual phase, the launcher materializes and installs the same generation; tests disable each backend in turn and compare their targets.
+
+Legacy disk state remains available to link-only launches, old processes, and rollback without participating in runtime-only selection. Removing that state is a separate maintenance operation outside this change.
+
+### Mode behavior
+
+Link, dual, and runtime modes use the same generation schema and dependency-selection policy. Link mode persists the computed result, runtime mode installs it only in the process, and dual mode requires Node's materialized result to equal the generation route.
+
+The `dsh` launcher selects link mode when an ordinary Node caller omits `resolutionMode`, so existing npm-installed profile startup keeps its filesystem behavior. A pkg executable always selects runtime mode, and the Electron Host installs its runtime generation before any profile row mounts. Tests and low-level embedders can still select runtime or dual explicitly.
+
+Runtime mode requires a supported Node Internal loader interface and does not create, update, or retire fallback links. Dual mode retains link writes and fails when Node's disk result differs from the generation. Writable profile state and package-manager transactions remain outside the resolver.
+
+Pkg and packaged Electron carriers force runtime resolution. The Electron Host runs through the Electron executable with `ELECTRON_RUN_AS_NODE=1`, reads its dsh tree from ASAR, and maps executable ASAR entries to electron-builder's unpacked tree. Neither carrier creates, updates, or removes legacy resolution links.
+
+### Performance and verification
+
+Generation construction is startup or update work, not resolve work, and its absolute latency is reported separately. Ordinary hot paths consist of scope classification, bare-name extraction, local-before-fallback selection, a Map lookup, and one native resolution; a cache hit returns the generation-owned result directly. A CommonJS request may perform one native probe followed by one routed resolution when a local package directory exists without `exports` but lacks the requested subpath. Out-of-scope calls do not read manifests and cache only whether each parent belongs to the profile scope.
+
+One-off local measurements taken during implementation ran built JavaScript under plain Node in fresh processes and compared it with a process that installed no hook. The measurement script and results are not committed, and these figures are not a benchmark or CI budget. Seven alternating rounds covered outside, profile-local, and fallback imports through dynamic import, `import.meta.resolve`, require, and `require.resolve`. Across Node 22.19, 24.18, and 26.8, the largest positive hot-path median was 4.5%. On Node 24.18, a 256-package cold workload regressed by at most 11.2% and generation construction took 16.027 ms median; the 32-package local `require.resolve` case added 1.033 ms across the batch (+34.7%) from fixed startup cost.
+
+Behavior tests compare the runtime generation with the disk materializer over the same package trees, then exercise root order, transitive and peer dependencies, local and external precedence, exports and subpath errors, conditions, and explicit CommonJS options. The Node compatibility matrix runs the resolver, service, and bootstrap specifications across the supported internal-loader variants. Worker tests verify environment-data publication and bootstrap installation with mocked thread and native-loader interfaces; they do not launch a built Worker. Generation tests prove failed construction does not publish partial state and successful replacement is atomic.
+
+## Alternatives considered
+
+**Keep disk projections permanently.** This preserves native lookup without process hooks, but retains cross-process mutation, stale generations, proxy manifests, writer locks, and packaged-runtime divergence. A bounded dual migration remains useful because both backends consume the same generation.
+
+**Expand the dependency graph lazily during resolve.** This spreads manifest reads and errors across first-use calls, changes timing from the disk implementation, complicates Worker startup, and makes the hot path depend on graph size. Complete generation construction is easier to compare and replace atomically.
+
+**Use `module.registerHooks`.** The public API puts every relevant resolution through Node's global hook dispatch before profile scope can reject it. Direct access to the existing internal ESM and CommonJS resolver objects permits a smaller fast path while retaining Node as the final resolver.
+
+**Record each Entry's actual import through Loader and HMR adapters.** Actual import records support stateful resolvers that return different targets for identical inputs. This design instead makes the generation authoritative and deterministic, so those records duplicate the resolver's answer while adding Entry, fiber, registry, ModuleJob, and HMR lifecycle state.
+
+**Mutate one long-lived table after each package operation.** Incremental mutation exposes partial graphs and requires targeted cache invalidation. Building a complete successor makes failure atomic and keeps all caches generation-owned.
+
+**Hot-replace already loaded package versions.** A resolution-table swap cannot invalidate every live module instance or object reference. Restart preserves one package identity per process.
+
+## Verification
+
+- One eager computation supplies the retained disk materializer and runtime generation.
+- Link-only, dual, and runtime-only tests consume the same generation; runtime startup neither writes nor retires module-resolution data.
+- Pkg and Electron carriers select runtime resolution; Electron executes its Host in Node mode from the ASAR-backed dsh tree while native executable entries remain unpacked.
+- ESM and CommonJS adapters share one router and delegate final resolution to Node without `module.registerHooks` or `_findPath` replacement.
+- Production metadata lookup does not record Loader import results or wrap Entry, registry, tree, or HMR methods.
+- The Node compatibility matrix runs main-thread resolver specifications across supported loader interfaces; service and bootstrap specifications cover Worker environment-data and installation interfaces without launching a built Worker.
+- One-off built plain-Node measurements produced the hot and cold observations above against no-hook Node; the script and results are not committed evidence.
+- Package READMEs, architecture references, generated catalogs, and the bilingual pair describe the shipped implementation.
+
+## Consequences
+
+Runtime startup avoids disk mutation and proxy manifests while preserving the existing package-selection algorithm. It accepts the maintenance cost of Node Internal compatibility tests and an early, self-contained bootstrap in each owned Worker. Link remains the ordinary Node launcher default, dual keeps a migration comparison path, and pkg plus Electron carriers force runtime resolution without retiring old links. Generation replacement remains additive until the product owns module-cache invalidation and Worker restart.

+ 118 - 0
.agents/notes/implemented/architecture/2026-09-09-profile-resolution-generations.zh.md

@@ -0,0 +1,118 @@
+# Agent Note: 增加不可变 profile 解析代际
+
+Status: implemented
+
+[English](2026-09-09-profile-resolution-generations.md) | 中文
+
+## Problem
+
+profile 从自己的包项目加载插件配置项,而 Harness 包和所选 bundle 携带的包可能位于该项目普通依赖树之外。当前启动器在启动时计算包优先级,再将结果物化为共享 symlink、profile 自有链接或打包可执行文件的代理包。文件跨进程和安装版本持续存在,需要协调和锁来维护,并向元数据读取方暴露生成的代理 manifest,也无法原子表示进程内变更。
+
+运行时设计保留现有选包规则,不另建一套包策略。它覆盖插件模块内部的 import 以及 Loader 配置项的 import,并在主线程和 Harness 自有 Worker 中工作。generation 替换只接受新增包的集合,不会逐项修改正在使用的表。
+
+## Decision
+
+profile 启动从磁盘 module fallback 使用的同一套依赖遍历生成一个不可变 `ResolutionGeneration`。launcher 默认使用 link 模式,保留现有的物化查找行为。内部调用方和测试可以选择 runtime 模式,把 generation 安装到 Node 的 ESM 与 CommonJS resolver;也可以选择 dual 模式,同时物化并校验同一份 generation。`PluginPackages.replace()` 通过一次引用替换发布完整的新增型后继 generation。
+
+### 唯一选包算法
+
+包遍历继续放在 `@deepseek-ai/dsh-app-boot` 的 profile 加载代码旁。磁盘 materializer 和运行时解析器消费同一个纯计划;两者都不持有另一份优先级算法。普通 Node 调用方可以选择 link、dual 或 runtime 模式,省略模式时使用 link。打包可执行文件与 Electron Host 会选择 runtime,因为其依赖树可能位于虚拟文件系统;dual 保留为内部对比路径。
+
+安装 manifest 是第一个根。它按 BFS 依次遍历 `dependencies` 和 `peerDependencies`,每条边从声明它的 manifest 解析,同名包由第一次找到的已安装包占有。所选 bundle 随后按 profile 顺序逐根遍历;每个较早根的完整依赖图优先于所有较晚根。安装闭包中的名称被保留,bundle 包根本身不成为插件 fallback。与旧行为相同,已声明但未安装的包会被跳过。
+
+profile 本地和插件私有 `node_modules` 不进入 fallback entries,由 Node 在虚拟 fallback 位置之前选择。generation 只记录已安装的 profile 直接包名用于 native 快速分流;每个 fallback 记录包名、版本、旧规则选中的查找目录、声明该边的 manifest 锚点和作用域,足以从选定包重新进入 Node 原生解析并验证换代保持既有映射。
+
+旧 `healProfilesModuleFallback()` 保留为相同纯计算结果的磁盘 materializer,便于直接比较并避免重写旧规则。launcher 默认调用它。runtime 模式只计算 generation 而不物化,dual 模式会物化并安装该 generation 进行比较。
+
+### 不可变 generation
+
+一个解析器 registration 持有一个 `current` generation。每个同步 resolve 在入口只捕获一次该引用,完整调用只读该引用。generation 构造在发布前读取所有必需 manifest;失败时当前 generation 不变。发布成功只替换一个引用,执行中的调用可以继续使用它已捕获的 generation。
+
+选包缓存和包元数据缓存归 generation 所有。发布下一代后,旧 generation 在调用方退出后自然不可达,不逐项清理缓存。generation 命中和原生解析成功结果可以缓存,但 generation 未命中会重新扫描,因此未命中后安装的 profile 本地包会像 link 模式一样变为可见。显式 CommonJS paths 或非默认 conditions 不得复用默认解析缓存。
+
+launcher 只构造启动 generation。服务接受新增型后继 generation,但本实现没有包管理器事务调用替换操作。
+
+### ESM 与 CommonJS 共用规则
+
+resolver 使用 `node-addon-require-builtin` 读取 `internal/modules/esm/loader` 和 `internal/modules/cjs/loader`。ESM 适配器包装每线程单例 `CascadedLoader` 的 resolve 方法。CommonJS 适配器包装内部 builtin 导出的 `Module._resolveFilename`;该 `Module` 与 `node:module` 导出的对象相同。
+
+两个适配器调用同一个路由函数。builtin、相对或绝对路径、URL、profile 作用域外 parent 和支持的查找以外的显式调用都直接委托原生实现。`#imports` 请求使用所属 manifest 中的 Node 映射;外部 bare target 按相同 conditions 遵循本地包、generation 和 after-fallback 的选包顺序,精确 target 解析仍由 Node 负责。对于作用域内的 bare request,package self-reference 保留原 parent,即使 npm alias 使安装目录使用另一个名称。Node 能在虚拟共享 fallback 之前从 profile 本地包或插件私有包解析到所请求入口时,也保留原 parent;没有 `exports` 的 CommonJS 包目录仅缺少所请求 subpath 时,不会压过 fallback。其他请求在 generation 命中时通过该条目的声明锚点解析,未命中时从虚拟 fallback 之后继续原生查找。显式 CommonJS path 列表按调用方顺序,对每个 path 独立应用相同的插入规则。
+
+适配器完成路由后调用捕获的原生 resolver。exports、import/require conditions、main、subpath、扩展名、原生缓存和错误码仍归 Node 处理。路由后的 ESM 失败会把 Node 诊断中的内部查找锚点替换为原始 importer。选中包的无效 export 或缺失目标不会触发另一个同名候选。CommonJS 不替换 `_findPath`,也不复制 `_resolveFilename`。
+
+保证范围是当前线程安装后发生的 Node 默认 `import`、`import()`、`import.meta.resolve`、`require` 和 `require.resolve`。已经链接的模块、自定义 `vm` linker、不透明的非 Node importer 和第三方 Worker 不在透明保证范围。
+
+### 活动插件列表与包元数据
+
+resolution generation 列出可用 fallback 包;Loader entries 组成活动插件列表,两者不能合并。消费方继续使用 Loader 原有 entry 生命周期,并按自身 scope 过滤相关 entries。需要 package metadata 的消费方将 specifier 和所属树的 base URL 交给 app-boot 中的轻量服务,无需 package 导出 `./package.json`。安装 generation 后,即使查询未命中也以 generation 为准;底层嵌入方只安装服务而不提供 generation 时,服务保留 Node 原生查找。
+
+解析器不提供 `imported(entry)`,不观察 ModuleJob,不包装 Entry 方法,不把 fiber 与 import 调用关联,也不替换 registry、tree 或 HMR 方法。重复查询读取同一个 generation,因此不会偏离 import 使用的路线。需要包元数据的非 Node importer 必须显式实现同一个确定性 resolver 接口,不能把调用来源推断重新引入 Node 主路径。
+
+实现集中在 `app-boot/src/profile-resolution/`。`service.ts` 提供长期存在的 `ctx.pluginPackages`,并拥有主线程 resolver 与 Worker generation 的生命周期;`resolver.ts` 实现 generation 查询和 Node Internal 适配器;`worker-bootstrap.ts` 在线程内安装继承的 generation。旧 profile 选包和磁盘 materialize 逻辑留在 `profile.ts`。Worker 只通过 `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap` 公开入口引用 bootstrap。
+
+服务定义与提供方继续放在 `app-boot`,因为 profile boot 拥有 resolver 生命周期。出现与 launcher 无关的提供方或需要独立演进的消费方时,再抽出单独的能力 seam。
+
+### Worker 与 generation 更新
+
+主线程通过 Worker environment data 发布当前 generation 的可结构化克隆表示和 profile scope。每个 Harness 自有 Worker 构建产物通过构建 banner 获取自己的 ESM/CJS Internal 并安装同一适配器,不重新遍历 manifest。bootstrap bundle 不静态导入任何包。源码 Worker 入口保持原有自包含依赖;第三方 Worker 保持不变。
+
+新 Worker 继承最新发布的 generation。已运行的 Worker 保留启动时继承的 generation,因此发布后继 generation 的调用方必须重启它们。ESM bootstrap 无法影响其执行前已链接的静态依赖,因此 Worker bundle 必须保证 bootstrap 之前的静态 import 可由原生 Node 解析,需要 profile resolver 的业务入口在 bootstrap 后通过 dynamic import 启动。
+
+### 只增加包的变更
+
+添加包的调用方先完成 pnpm 事务,再构造下一代。替换操作会拒绝改变任何既有 package name 的目录或版本。调用方先发布只增加映射的后继 generation,再挂载新的 Loader 配置项;本实现不提供该包事务。挂载失败可以留下已安装但未启用的包。
+
+替换、升级或删除已加载包需要重启,因为 Node 的 ESM Module Map、CommonJS cache、现存对象引用和运行中的 Worker 都可能保留旧模块 identity。generation 换代不声称卸载模块。
+
+### 磁盘迁移
+
+runtime-only 启动流程不创建、更新或退休 symlink 和代理包。resolver 把旧共享 fallback 和 `.dsh-module-fallback` 投影视为虚拟插入位置:generation 命中时使用表中目标,未命中时越过旧位置继续原生祖先查找。dual 阶段物化并安装同一个 generation;测试分别禁用一个后端并比较目标。
+
+旧磁盘状态继续供 link-only 启动、旧进程和回滚使用,但不参与 runtime-only 选择。清理旧链接是本次变更之外的独立维护操作。
+
+### 模式行为
+
+link、dual 与 runtime 模式使用同一种 generation schema 和依赖选择策略。link 模式持久化计算结果,runtime 模式只在进程内安装,dual 模式要求 Node 的磁盘结果与 generation 路由一致。
+
+普通 Node 调用方省略 `resolutionMode` 时,`dsh` launcher 选择 link 模式,因此既有 npm 安装的 profile 启动保留文件系统行为。pkg 可执行文件始终选择 runtime,Electron Host 则在挂载任何 profile 条目前安装 runtime generation。测试与底层嵌入方仍可显式选择 runtime 或 dual。
+
+runtime 模式要求受支持的 Node Internal loader 接口,并且不会创建、更新或退休 fallback 链接。dual 模式保留链接写入,并在 Node 的磁盘结果与 generation 不同时失败。可写 profile 状态和包管理器事务不属于 resolver。
+
+pkg 与打包 Electron 载体强制使用 runtime 解析。Electron Host 通过设置 `ELECTRON_RUN_AS_NODE=1` 的 Electron 可执行文件运行,从 ASAR 读取 dsh 依赖树,并把 ASAR 中的可执行条目映射到 electron-builder 的 unpacked 目录。两种载体都不会创建、更新或删除旧解析链接。
+
+### 性能与验证
+
+generation 构造发生在启动或显式更新阶段,不属于单次 resolve,但需要单独报告绝对延迟。普通热路径只包括 scope 分类、bare name 提取、本地优先判断、Map 查询和一次原生解析;缓存命中直接返回 generation 级结果。当本地 CommonJS 包目录没有 `exports` 且仅缺少所请求 subpath 时,一次请求可能先执行一次原生探测,再执行一次路由解析。作用域外调用不读取 manifest,只缓存 parent 是否位于 profile scope。
+
+实现期间的一次性本地测量用 plain Node 在全新进程中执行构建后的 JavaScript,并与完全没有安装 hook 的进程比较。测量脚本和结果未提交,这些数据不是 benchmark 或 CI 预算。七轮交替顺序覆盖 outside、profile-local 和 fallback 的 dynamic import、`import.meta.resolve`、require、`require.resolve`。Node 22.19、24.18 和 26.8 的热路径中位数最大正向回退为 4.5%。Node 24.18 的 256 包 cold workload 最大回退为 11.2%,generation 构造中位数为 16.027 ms;32 包本地 `require.resolve` 因固定启动成本在整批增加 1.033 ms(+34.7%)。
+
+行为测试在同一包树上比较运行时 generation 与磁盘 materializer,再覆盖根顺序、传递依赖和 peer、本地与外层优先级、exports 与 subpath 错误、conditions 和显式 CommonJS options。Node 兼容矩阵会在受支持的内部 loader 变体上运行 resolver、service 和 bootstrap 规格。Worker 测试通过 mock 线程与 native loader 接口验证 environment data 发布和 bootstrap 安装,但不会启动构建后的 Worker。generation 测试证明构造失败不发布部分状态,成功换代只做原子引用替换。
+
+## Alternatives considered
+
+**永久保留磁盘投影。** 这能在没有进程 hook 时沿用原生查找,但仍有跨进程写入、陈旧 generation、代理 manifest、写锁和打包运行时差异。迁移期 dual 模式仍有价值,因为两个后端消费同一个 generation。
+
+**在 resolve 时惰性扩展依赖图。** 这会把 manifest 读取和错误分散到首次使用,改变磁盘实现的时机,使 Worker 启动更复杂,并让热路径成本随依赖图变化。完整构造 generation 更容易比较和原子替换。
+
+**使用 `module.registerHooks`。** 公共 API 会在 profile scope 拒绝请求之前让相关解析进入 Node 的全局 hook 分发。直接访问已有 ESM/CJS 内部解析器可以保留更小的快速路径,并继续让 Node 完成最终解析。
+
+**通过 Loader 和 HMR 适配器记录每个 Entry 的实际 import。** 实际 import 记录能支持相同输入返回不同目标的有状态 resolver。本设计改为以 generation 作为确定性权威,因此这些记录只会复制 resolver 的答案,同时增加 Entry、fiber、registry、ModuleJob 和 HMR 生命周期状态。
+
+**每次包操作增量修改一张长期表。** 增量修改会暴露半成品依赖图,并要求定点失效缓存。完整构造下一代使失败保持原子,并让所有缓存随 generation 生命周期存在。
+
+**热替换已经加载的包版本。** 解析表换代无法使所有存活模块实例和对象引用失效。重启可以保证每个进程只使用一个 package identity。
+
+## Verification
+
+- 一次 eager 计算同时供应保留的磁盘 materializer 和运行时 generation。
+- link-only、dual 和 runtime-only 测试消费同一个 generation;runtime 启动既不写入也不退休模块解析数据。
+- pkg 与 Electron 载体选择 runtime 解析;Electron 以 Node 模式从 ASAR 承载的 dsh 依赖树执行 Host,原生可执行条目保持 unpacked。
+- ESM 与 CommonJS 适配器共享同一个路由器,并把最终解析委托给 Node,不使用 `module.registerHooks` 或替换 `_findPath`。
+- 生产 package metadata 查询不记录 Loader import 结果,也不包装 Entry、registry、tree 或 HMR 方法。
+- Node 兼容矩阵会在受支持的 loader 接口上运行主线程 resolver 规格;service 和 bootstrap 规格覆盖 Worker environment data 与安装接口,但不会启动构建后的 Worker。
+- 一次性 plain Node 构建产物测量得到上述相对无 hook Node 的热路径和 cold 观察结果;脚本与结果并未提交为证据。
+- package README、架构引用、生成目录和双语文档对描述已交付实现。
+
+## Consequences
+
+runtime 启动避免磁盘修改和代理 manifest,同时保留既有选包算法。代价是持续维护 Node Internal 兼容测试,并在每个自有 Worker 中最早执行自包含 bootstrap。link 保持普通 Node launcher 的默认值,dual 保留迁移比较路径,pkg 与 Electron 载体则强制使用 runtime 且不退休旧链接。在产品拥有模块缓存失效和 Worker 重启前,generation 替换只能新增映射。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.i18n.yaml

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

+ 28 - 0
.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.md

@@ -0,0 +1,28 @@
+# Agent Note: Default DeepSeek Session-log upload
+
+Status: implemented
+
+English | [中文](2026-09-14-session-log-upload-default.zh.md)
+## Problem
+
+Ordinary DeepSeek requests do not contain the complete canonical Session trajectory. Requiring each installation to enable log contribution prevents the default product configuration from supplying that trajectory. Recorded-session scenarios also need stable, explicit upload policies because acceptance events are part of their expected logs.
+
+## Decision
+
+`session-log-deepseek.Config.enabled` defaults to `true` in every process. An explicit `enabled: false` disables the contribution. The plugin does not inspect test-runner or snapshot environment variables.
+
+This supersedes only the opt-in default in the [request-extension decision](2026-08-21-deepseek-llm-api-request-extensions.md); that note still owns field serialization, destinations, acceptance, and retry semantics. The headless and ACP corpus base patches and Web scaffold explicitly disable upload. Later scenario patches can enable it. The SDK text-turn recording omits the setting and exercises the shipped default, including durable acceptance events.
+
+## Alternatives considered
+
+**Derive the production default from test environment markers.** This also changes downstream SDK behavior when callers inherit those variables and makes ordinary tests exercise a different product default.
+
+**Refresh every recorded Session to include upload acceptance.** Explicit test composition preserves the existing scenarios while a default-configured SDK recording and Loader regression cover the default-on path.
+
+**Retain opt-in upload.** This does not supply the complete trajectory from ordinary product requests without installation-specific configuration.
+
+## Consequences
+
+Eligible requests send the complete unaccepted canonical log suffix, including message text, tool arguments and results, workspace paths, and feedback, to the resolved DeepSeek endpoint or configured gateway. No prompt tokens or model-visible content are added. Request bodies can grow substantially, and provider rejection still fails the request. OTel remains independent; disabling OTel does not disable this contribution.
+
+Configuration tests cover default-on and explicit overrides with and without test environment markers. Both DeepSeek protocol Loader cases observe the default request field and recorded acceptance watermark, and explicit-off cases observe their absence. Existing headless, ACP, Web, and SDK recordings validate their declared upload policies without rewriting committed Session generations.

+ 28 - 0
.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.zh.md

@@ -0,0 +1,28 @@
+# Agent Note: DeepSeek 会话日志默认上传
+
+Status: implemented
+
+[English](2026-09-14-session-log-upload-default.md) | 中文
+## 问题
+
+普通 DeepSeek 请求不包含完整的规范会话轨迹。要求每个安装环境启用日志贡献,会使默认产品配置无法提供该轨迹。录制会话场景也需要稳定、明确的上传策略,因为接受事件属于其期望日志。
+
+## 决策
+
+`session-log-deepseek.Config.enabled` 在所有进程中均默认为 `true`。显式设置 `enabled: false` 可关闭贡献。插件不读取测试运行器或快照环境变量。
+
+本决策仅取代[请求扩展决策](2026-08-21-deepseek-llm-api-request-extensions.zh.md)中的主动启用默认策略;该记录仍负责字段序列化、目的地址、接受与重试语义。headless 和 ACP 语料的基础 patch 以及 Web scaffold 显式关闭上传。后续场景 patch 可以启用上传。SDK text-turn 录制省略该设置,验证产品默认值及持久接受事件。
+
+## 考虑过的替代方案
+
+**根据测试环境标记决定生产默认值。** 调用者继承这些变量时,下游 SDK 行为也会改变,普通测试还会采用不同的产品默认值。
+
+**刷新所有录制会话以包含上传接受事件。** 显式测试配置保留现有场景,同时使用默认配置的 SDK 录制和 Loader 回归验证默认开启路径。
+
+**保留主动启用策略。** 没有安装环境专用配置时,该策略无法从普通产品请求提供完整轨迹。
+
+## 后果
+
+符合条件的请求会向解析后的 DeepSeek 端点或已配置网关发送完整的未接受规范日志后缀,包括消息文本、工具参数与结果、工作区路径和反馈。不增加提示词 token 或模型可见内容。请求正文可能显著增大,提供方拒绝仍会使请求失败。OTel 保持独立,关闭 OTel 不会关闭此贡献。
+
+配置测试覆盖有无测试环境标记时的默认开启与显式覆盖。两种 DeepSeek 协议的 Loader 用例观察默认请求字段和已记录的接受水位,显式关闭用例观察两者均不存在。现有 headless、ACP、Web 和 SDK 录制验证各自声明的上传策略,无需重写已提交的会话代际。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-14-sidebar-layout-provider-recovery.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-14-sidebar-layout-provider-recovery.md
+2026-09-14-sidebar-layout-provider-recovery.md: f7ec0324f9205a556750e0af54eb856002a4ea74
+2026-09-14-sidebar-layout-provider-recovery.zh.md: 02440391e5601e3ba125c33c4c8971d4d46cbaa8

+ 35 - 0
.agents/notes/implemented/architecture/2026-09-14-sidebar-layout-provider-recovery.md

@@ -0,0 +1,35 @@
+# Agent Note: Sidebar layout persistence and provider recovery
+
+Status: implemented
+
+English | [中文](2026-09-14-sidebar-layout-provider-recovery.zh.md)
+
+## Problem
+
+Reloading the browser loses open files, pane placement and selection. Recovering retained terminals by opening new tabs cannot reproduce that arrangement and can reopen a collapsed sidebar.
+
+## Decision
+
+The sidebar persists a validated current-layout snapshot and identity counter per Session, while the Client store keeps undo history in memory. Invalid saved types or layout references clear only that Session snapshot. Undo history resets on reload so persisted data does not grow with past interactions. It restores layout and tab identities before rendering bodies, including resource pins. Providers restore their own content; the sidebar has no terminal-specific state or reconnection logic. Transient navigation parameters and resource contents remain outside the layout snapshot.
+
+The terminal provider reads the adopted Session's tab records and restores terminal models before querying Host terminals that have no view. Its Client controller saves globally unique content-to-terminal identities as independent records before allocation, reuses them after reload, and removes them after recording explicit close intent. Layout-local tab ids identify separate live views but cannot identify persisted processes across windows; per-record writes and deletes avoid overwriting unrelated bindings. A restored identity never authorizes creating a replacement process. The Host owns process liveness, titles and screen contents; the saved association is only a recovery target. A saved association without a restored tab does not suppress Host discovery.
+
+OpenCode's `packages/app/src/context/layout.tsx` saves Session tabs, while `context/terminal.tsx` separately saves terminal identities. DSH uses its existing Session-scoped store and provider services for the same separation, retaining Session ownership for terminals.
+
+This partially supersedes the persistence exclusion in the [Web terminal decision](../feature/2026-09-09-web-sidebar-terminal.md). That note remains active because process ownership, cleanup, streaming and input-control decisions still apply.
+
+## Alternatives considered
+
+**Recover every retained terminal as a new tab.** Host discovery cannot recover tab placement, selection or a collapsed layout and duplicates restored tabs.
+
+**Put terminal recovery into the sidebar.** That would make the layout owner depend on a specific content provider. A generic read of committed tab records lets each provider restore its own state.
+
+**Persist process metadata and terminal output.** These values become stale independently of browser layout. Reattaching to the Host supplies current metadata and a consistent screen.
+
+## Consequences
+
+Layout survives reload within the same browser origin, with independent Session storage keys. Windows share the last saved layout for each Session; active windows retain their own layout until reload. Storage failure leaves memory state usable. Browser reload does not restart missing or exited processes; Host restart still cannot restore a shell. Provider state requires provider-owned recovery support, and transient file navigation parameters are not restored.
+
+Store tests cover layout round trips, bounded persisted state, invalid records, Session isolation and resource adoption. Terminal tests cover identity reuse, missing targets, inactive closes and discovery without a restored tab. Assembled-browser tests exercise file preview, split panes, selection, fullscreen and collapsed reload with real terminal process identities.
+
+The [two-hour unattended-terminal reclamation](../feature/2026-09-14-unattended-browser-terminal-reclamation.md) extends provider recovery with window-held terminal lifetimes while retaining this layout/content ownership split.

+ 35 - 0
.agents/notes/implemented/architecture/2026-09-14-sidebar-layout-provider-recovery.zh.md

@@ -0,0 +1,35 @@
+# Agent Note: 侧栏布局持久化与 provider 恢复
+
+Status: implemented
+
+[English](2026-09-14-sidebar-layout-provider-recovery.md) | 中文
+
+## Problem
+
+刷新浏览器会丢失已打开文件、分栏位置和选中项。把保留的终端重新打开为新标签无法还原布局,还可能展开原本折叠的侧栏。
+
+## Decision
+
+侧栏按 Session 持久化经过校验的当前布局快照和身份计数,Client store 将撤销历史保留在内存中。保存的类型或布局引用无效时,只清理对应 Session 的快照。刷新后撤销历史清空,因此持久化数据不会随过去的操作持续增长。它在渲染正文前恢复布局和标签身份,并建立资源 pin。provider 恢复自身内容;侧栏不保存终端专属状态,也不负责重连。临时导航参数和资源内容不进入布局快照。
+
+terminal provider 读取已采用的 Session 标签记录,先恢复终端模型,再查询尚无视图的 Host 终端。其 Client controller 在分配前将全局唯一内容与终端身份的关联保存为独立记录,刷新后复用,并在记录显式关闭意图后删除。布局内的 tab id 标识独立活动视图,不能跨窗口标识持久化进程;逐条记录的写入和删除避免覆盖无关关联。恢复身份不允许创建替代进程。Host 负责进程存活状态、标题和屏幕内容;保存的关联只是恢复目标。没有对应恢复标签的关联不会阻止 Host 探测。
+
+OpenCode 的 `packages/app/src/context/layout.tsx` 保存 Session 标签,`context/terminal.tsx` 独立保存终端身份。DSH 使用现有 Session 作用域 store 和 provider 服务实现相同的职责划分,终端仍归 Session 所有。
+
+本决策部分替代 [Web 终端决策](../feature/2026-09-09-web-sidebar-terminal.zh.md)中不持久化的限定。原记录继续保留,因为进程归属、清理、流传输和输入控制决策仍适用。
+
+## Alternatives considered
+
+**把每个保留终端恢复为新标签。** Host 探测无法还原标签位置、选中项或折叠布局,还会重复创建已恢复的标签。
+
+**让侧栏负责终端恢复。** 布局所有者会因此依赖特定内容 provider。提供通用的已提交标签查询即可让各 provider 恢复自身状态。
+
+**持久化进程元数据和终端输出。** 这些值会独立于浏览器布局失效。重新连接 Host 能取得当前元数据和一致的屏幕。
+
+## Consequences
+
+同一浏览器站点内刷新会保留布局,各 Session 使用独立存储键。窗口共享每个 Session 最后保存的布局;活动窗口在刷新前保留各自布局。存储失败时内存状态仍可用。刷新不会重启缺失或已退出的进程;Host 重启仍无法恢复 shell。provider 状态需要自身支持恢复,临时文件导航参数不恢复。
+
+store 测试覆盖布局读写、有界持久化状态、无效记录、Session 隔离和资源采用。终端测试覆盖身份复用、目标缺失、未激活标签关闭,以及没有恢复标签时的探测。完整浏览器测试使用真实终端进程身份,验证文件预览、分栏、选中项、全屏和折叠状态下的刷新。
+
+[无人连接终端的两小时回收](../feature/2026-09-14-unattended-browser-terminal-reclamation.zh.md)为 provider 恢复增加窗口持有的终端生命周期,保留此处的布局/内容职责划分。

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-09-14-chat-presentation-defaults.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-14-chat-presentation-defaults.md
+2026-09-14-chat-presentation-defaults.md: 6703a14e2ad108c4b43dd50692a44c159cf3ea5a
+2026-09-14-chat-presentation-defaults.zh.md: 67ca433339ecf6e44d3f74b6c3ff9e465f802b0b

+ 25 - 0
.agents/notes/implemented/bug-fix/2026-09-14-chat-presentation-defaults.md

@@ -0,0 +1,25 @@
+# Agent Note: Keep Chat presentation independent of Trajectory inspection
+
+Status: implemented
+
+English | [中文](2026-09-14-chat-presentation-defaults.zh.md)
+
+## Problem
+
+Trajectory inspection benefits from exposing complete recorded reasoning. Applying that default to Chat expands the live transcript during reasoning and changes its height when an answer or Tool call arrives. The Trajectory inspection change also added historical first-token recovery to Chat without a separate Chat behavior decision.
+
+## Decision
+
+[Chat](../../../../packages/client/ui-chat/README.md#turn-process-folding) starts each reasoning row collapsed and retains the reader's manual disclosure choice through subsequent output and settlement. Settlement retires the observed live chunks and rebuilds Chat reply nodes from durable events without recovering first-token time from embedded streams. Consequently, completed-turn TTFT and decoding speed are absent after live settlement as well as after reopening history. Turn-level process folding remains independently owned.
+
+[Trajectory inspection](../feature/2026-09-09-ptc-trajectory-code-inspection.md) keeps its expanded reasoning default, recorded timing, JSON controls, and PTC code inspector. The [compact stream readers](../architecture/2026-09-06-embedded-stream-record-readers.md) remain available to Trajectory and other consumers. These decisions partially supersede the Chat presentation additions while preserving both notes' independent rationale.
+
+## Alternatives considered
+
+**Keep Chat's automatic expansion and historical timing recovery.** These change Chat behavior beyond the requested Trajectory inspection work. Reintroducing either requires a separate Chat product decision and its own verification.
+
+**Revert the entire inspection change.** That would remove the requested Trajectory behavior along with the unintended Chat changes.
+
+## Consequences
+
+Chat reasoning requires a click to inspect in full. Elapsed turn time and the independently projected Session Stats remain available. Assembler tests distinguish transient retirement at live settlement from reopening durable history; browser replay verifies the completed-turn timing dialog before and after reload. Component tests cover collapsed streaming and reasoning-only replies and manual disclosure across answer and Tool-call arrival. Trajectory tests retain its separate defaults and timing.

+ 25 - 0
.agents/notes/implemented/bug-fix/2026-09-14-chat-presentation-defaults.zh.md

@@ -0,0 +1,25 @@
+# Agent Note: 保持 Chat 展示与 Trajectory 查看独立
+
+Status: implemented
+
+[English](2026-09-14-chat-presentation-defaults.md) | 中文
+
+## 问题
+
+Trajectory 查看适合直接展示完整的已记录思考。将该默认行为应用到 Chat 会在思考期间展开实时对话,并在回答或工具调用到来时改变其高度。Trajectory 查看改动还为 Chat 增加了历史首 token 计时恢复,却没有独立的 Chat 行为决策。
+
+## 决策
+
+[Chat](../../../../packages/client/ui-chat/README.zh.md#turn-process-folding) 的每个思考行初始都折叠,后续输出和结算保留用户手动选择的展开状态。结算会移除观测到的实时 chunk,并从持久事件重建 Chat 回复节点,不从内嵌流恢复首 token 时间。因此,实时结算后和重新打开历史后,已完成轮次都不显示 TTFT 和解码速度。轮次级过程折叠仍独立维护。
+
+[Trajectory 查看](../feature/2026-09-09-ptc-trajectory-code-inspection.zh.md) 保留思考默认展开、已记录计时、JSON 控件和 PTC 代码查看器。[紧凑流读取器](../architecture/2026-09-06-embedded-stream-record-readers.zh.md) 仍供 Trajectory 和其他消费方使用。这些决策部分取代了 Chat 展示增量,同时保留两份记录各自独立的理由。
+
+## 考虑过的替代方案
+
+**保留 Chat 自动展开和历史计时恢复。** 这些改变了所请求的 Trajectory 查看工作之外的 Chat 行为。重新引入任一行为都需要独立的 Chat 产品决策及相应验证。
+
+**撤回整个查看改动。** 这会在撤回意外 Chat 改动的同时移除所请求的 Trajectory 行为。
+
+## 影响
+
+Chat 思考需要点击才能查看全文。轮次总耗时和独立投影的 Session Stats 仍可用。组装器测试区分实时结算时移除临时数据与重新打开持久历史;浏览器回放验证重载前后的已完成轮次计时对话框。组件测试覆盖流式与仅思考回复的默认折叠,以及回答和工具调用到来时的手动展开状态。Trajectory 测试保留其独立默认值和计时。

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.md
+2026-09-14-transcript-width-handle-layering.md: 01d70e646e874d8266633597f1b510ad73fc2287
+2026-09-14-transcript-width-handle-layering.zh.md: c6e03ec5775256f5aac87d2f7c1ed66a12cf0c2a

+ 33 - 0
.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.md

@@ -0,0 +1,33 @@
+# Agent Note: Transcript width handles stay behind chat content
+
+Status: implemented
+
+English | [中文](2026-09-14-transcript-width-handle-layering.zh.md)
+
+## Problem
+
+The Conversation shell renders each content-width handle as a full-height absolute strip beside the reading column. A high stacking level placed that strip above wide Markdown tables that legitimately overflow the column. The handle glow covered their content and its pointer target could replace their interaction target.
+
+## Decision
+
+The Conversation shell keeps width handles at level zero without creating a body-wide stacking context. Chat raises concrete table elements to level one, while the reading column, column-bounded tool cards, and table breakout wrapper create no new stacking level. A table therefore owns the gutter only where its painted box actually reaches it; a fitting four-column table leaves the adjacent gutter draggable. Fixed message tooltips remain outside the table stacking context and can still paint over the sticky composer.
+
+This rule relies on the supported Chromium engine not turning ChatView's inline-size query container into an intermediate stacking context. The same engine behavior already lets the back-to-bottom control outrank the sticky composer. The Chromium hit-test scenario pins that assumption; raising the whole Chat root would instead make its transparent full-width box reclaim the gutter.
+
+A width handle starts resizing only for the primary pointer button. Ordinary wheel input over the handle is normalized from pixel, computed line-height, or page units and forwarded to the direct sibling transcript scrollport, so hovering the gutter does not suspend reading. Ctrl+wheel is not forwarded because it represents browser zoom or a trackpad pinch gesture.
+
+Each side's hit strip is at most 10px wide. Its hover indicator uses the lower-contrast resting scrollbar tint and is a 2px line with a 16px solid center and 28px fades, for a 72px total visible length.
+
+Composer chrome retains its higher layer, so its full sticky footer band intentionally does not start a width resize. A handle whose pointer capture began above that band temporarily rises to level eight, keeping the indicator visible until release. A full-view composer takeover continues to hide the handles entirely.
+
+## Alternatives considered
+
+**Move the handles farther from the reading column.** Wide tables can use the available transcript width, so a fixed larger inset would only reduce the overlap for some window sizes and would make the handles harder to reach.
+
+**Disable the handles whenever a transcript contains wide content.** One wide row would remove resizing from the entire Session, including empty gutter beside unrelated rows.
+
+**Inspect the elements under the pointer in JavaScript.** Dynamic hit testing would duplicate browser stacking and pointer dispatch rules and could still disagree with new plugin renderers.
+
+## Consequences
+
+Visible Chat content owns pointer input only where its painted element extends into a width-handle strip, while bare gutter keeps a quieter resize affordance, primary-button drag, and transcript wheel scrolling. Unit tests pin the declared handle geometry, pointer-button behavior, direct scrollport lookup, zoom exclusion, and every DOM delta mode. Chromium `elementFromPoint` coverage proves an overflowing table wins the hit while a fitting `md-table-wide` wrapper does not, and a message-action test proves fixed tooltips still paint above the sticky composer.

+ 33 - 0
.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.zh.md

@@ -0,0 +1,33 @@
+# Agent Note: 正文宽度拖拽条位于 Chat 内容下方
+
+Status: implemented
+
+[English](2026-09-14-transcript-width-handle-layering.md) | 中文
+
+## 问题
+
+Conversation shell 将每个正文宽度拖拽条渲染为阅读列旁的全高绝对定位区域。较高的堆叠层级使该区域盖在会合理溢出阅读列的宽 Markdown 表格之上。拖拽条的高亮会遮住表格内容,其指针目标也可能取代表格自身的交互目标。
+
+## 决策
+
+Conversation shell 将宽度拖拽条保持在第零层,不创建覆盖整个 body 的堆叠上下文。Chat 只将具体的表格元素提到第一层,阅读列、限定在列内的工具卡片与表格 breakout 包装层都不创建新的堆叠层级。因此只有表格的可见盒子实际延伸进沟槽时才会占有指针;能容纳在正文中的四列表格不会阻断旁边的沟槽拖拽。消息的固定定位 Tooltip 保持在表格堆叠上下文之外,仍可绘制在粘滞 composer 上方。
+
+该规则依赖交付的 Chromium 引擎不会把 ChatView 的 inline-size query container 变成中间堆叠上下文。back-to-bottom 控件能高于粘滞 composer 也已依赖相同的引擎行为。Chromium 命中测试固定这一假设;如果改为提高整个 Chat root,其透明全宽盒子反而会重新占用沟槽。
+
+宽度拖拽条仅使用主指针按键开始调整。拖拽条上的普通滚轮输入会从像素、计算后的行高或页面单位规整为像素,再转发给直接相邻的 transcript scrollport,因此指针悬停在沟槽时不会中断阅读滚动。Ctrl+滚轮表示浏览器缩放或触控板捏合手势,不会被转发。
+
+每侧的命中区域最宽为 10px。悬停指示线使用对比度更低的静止滚动条颜色,线宽 2px,中心实色段长 16px,两端各渐隐 28px,可见总长度为 72px。
+
+Composer chrome 保留更高层级,因此完整的粘滞底部区带刻意不会开始宽度调整。如果拖拽条在进入该区带之前已获得指针捕获,它会临时提到第八层,使指示线在松开前保持可见。占据完整 View 的 composer takeover 仍会完全隐藏拖拽条。
+
+## 考虑过的替代方案
+
+**将拖拽条移到离阅读列更远的位置。** 宽表格可以使用 transcript 的可用宽度,因此固定增加间距只能减少部分窗口尺寸下的重叠,还会让拖拽条更难触达。
+
+**只要 transcript 包含宽内容就禁用拖拽条。** 一行宽内容会让整个 Session 都无法调整宽度,包括与其他行相邻的空白沟槽。
+
+**在 JavaScript 中检查指针下方的元素。** 动态命中测试会重复浏览器的堆叠与指针分发规则,并且仍可能与新的插件 renderer 不一致。
+
+## 后果
+
+可见 Chat 内容只在其可见元素实际延伸进宽度拖拽区域时拥有指针输入,裸露沟槽则保留更轻的宽度调整提示、主按键拖拽和 transcript 滚轮滚动。单元测试固定声明的拖拽条尺寸、指针按键行为、直接 scrollport 查找、缩放排除和每种 DOM delta mode。Chromium `elementFromPoint` 覆盖证明溢出表格获得指针命中,而可容纳的 `md-table-wide` 包装层不会;消息操作测试证明固定定位 Tooltip 仍绘制在粘滞 composer 上方。

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-09-14-web-diff-context.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-14-web-diff-context.md
+2026-09-14-web-diff-context.md: 072af0966689d475d7fdd1df83861fa847f3745f
+2026-09-14-web-diff-context.zh.md: cd165a443451fc5ba580078b1b431a2788671dbe

+ 33 - 0
.agents/notes/implemented/bug-fix/2026-09-14-web-diff-context.md

@@ -0,0 +1,33 @@
+# Agent Note: Web diff cards compare contextual content
+
+Status: implemented
+
+English | [中文](2026-09-14-web-diff-context.zh.md)
+
+## Problem
+
+Filesystem result metadata carries before/after fragments that include unchanged context. Treating each complete fragment as removed or added mislabels shared lines and inflates both card and collapsed-row totals.
+
+## Decision
+
+The Web primitive derives line patches with the maintained `diff` library, using `maxEditLength: 256`. Each exact change includes up to three neutral context lines on either side; distant changes use separate hunks and shared context contributes to neither total. Beyond 256 additions/deletions per fragment, search stops and the complete old/new fragments render as a coarse replacement, including shared lines in the display, copy, and counts. The card and `diffTotals` use this same deterministic derivation. This remains Client presentation under the [tool presentation ownership decision](../architecture/2026-08-23-client-derived-tool-presentation.md), without changing persisted metadata or public props.
+
+## Alternatives considered
+
+Unbounded comparison stalls collapsed summaries on heavily changed fragments. A deterministic edit-distance limit preserves exact sparse edits regardless of file length; a wall-clock timeout could make the summary and body choose different results under load. A coarse replacement sacrifices alignment above the limit while retaining every input line. Caching or asynchronous rendering adds ownership and invalidation work that the bounded comparison does not require. Extending durable metadata or maintaining a custom diff algorithm is unnecessary for this presentation behavior.
+
+## Measurement
+
+A local CPU diagnostic bundled the production `DiffBlock.tsx` entry with esbuild (`--bundle --platform=node --format=esm`) and timed `diffTotals` under Node 26.5.0 on macOS ARM64. Each fragment has 10,000 lines: unique indexed lines replaced completely, 100 evenly spaced replacements, or alternating repeated `old`/`shared` versus `new`/`shared` lines. Input construction and module loading are excluded; returned totals remain reachable. These are function timings, not browser paint or input latency, and carry no CI timing threshold.
+
+| Input | Unbounded milliseconds | Bounded search milliseconds |
+| --- | --- | --- |
+| Complete replacement | 6492.16, 6777.79, 6786.79 | 5.25, 4.66, 4.19 |
+| 100 sparse replacements | 8.19, 4.58, 4.01 | 7.58, 4.26, 4.13 |
+| Alternating repeated lines | 3383.13 | 10.26, 7.46, 5.52 |
+
+The bound admits all 100 sparse replacements unchanged. The 129-replacement regression fails without it because the unbounded implementation returns exact counts instead of the required complete-fragment fallback.
+
+## Consequences
+
+The browser build includes `diff`. The bound limits edit-graph search, not wall-clock duration: normalization, fallback rows, and copied output still scale with input length, and expanded cards also derive their rows separately. The content-line rule treats a final newline as a terminator. Regressions cover exact output at 256 edits, complete coarse output above the limit, a sparse edit in 10,000 lines, shared and distant context, repeated lines, copied prefixes, and summary/footer parity. Authored and borrowed Session snapshots cover coarse and exact browser cards respectively.

+ 33 - 0
.agents/notes/implemented/bug-fix/2026-09-14-web-diff-context.zh.md

@@ -0,0 +1,33 @@
+# Agent Note: Web diff 卡片比较含上下文的内容
+
+Status: implemented
+
+[English](2026-09-14-web-diff-context.md) | 中文
+
+## Problem
+
+文件系统结果元数据携带的前后文本片段包含未改动的上下文。把完整片段分别当作删除和新增会错误标记共享行,并夸大卡片和折叠工具行的统计。
+
+## Decision
+
+Web 原语通过维护中的 `diff` 库生成行补丁,使用 `maxEditLength: 256`。每处精确改动两侧最多保留三行中性上下文;远距离改动分成独立 hunk,共享上下文不计入增删统计。每个片段的新增与删除行数超过 256 时,搜索停止,完整新旧片段按粗粒度替换呈现,共享行也计入显示、复制和统计。卡片与 `diffTotals` 使用同一确定性推导。这遵循[工具呈现归属决策](../architecture/2026-08-23-client-derived-tool-presentation.zh.md),仍属于 Client 呈现,不改变持久化元数据或公开 props。
+
+## Alternatives considered
+
+无上限比较会让大量改动片段的折叠摘要停顿。确定性的编辑距离上限能让稀疏编辑保持精确,不受文件长度影响;墙钟超时可能使摘要和正文在负载下选择不同结果。粗粒度替换在超过上限后放弃对齐,但保留全部输入行。缓存或异步渲染会增加归属和失效处理,而有界比较不需要这些机制。该呈现行为无需扩展持久化元数据或维护自定义 diff 算法。
+
+## Measurement
+
+本地 CPU 诊断用 esbuild(`--bundle --platform=node --format=esm`)打包生产 `DiffBlock.tsx` 入口,并在 macOS ARM64 的 Node 26.5.0 下计时 `diffTotals`。每个片段有一万行:全部替换带唯一索引的行、均匀分布的 100 处替换,或交替重复的 `old`/`shared` 与 `new`/`shared` 行。不计输入构造和模块加载;返回的统计值保持可达。这些是函数耗时,不是浏览器绘制或输入延迟,也没有作为 CI 时间阈值。
+
+| 输入 | 无上限毫秒数 | 有界搜索毫秒数 |
+| --- | --- | --- |
+| 全部替换 | 6492.16, 6777.79, 6786.79 | 5.25, 4.66, 4.19 |
+| 100 处稀疏替换 | 8.19, 4.58, 4.01 | 7.58, 4.26, 4.13 |
+| 交替重复行 | 3383.13 | 10.26, 7.46, 5.52 |
+
+上限允许全部 100 处稀疏替换保持精确。移除上限时,129 处替换回归会失败,因为无上限实现返回精确统计,而非要求的完整片段回退。
+
+## Consequences
+
+浏览器构建包含 `diff`。上限约束编辑图搜索,不约束墙钟时长:规范化、回退行及复制内容仍随输入长度增长,展开卡片也会单独推导正文行。内容行规则把末尾换行视为终止符。回归覆盖 256 次编辑时的精确输出、超过上限后的完整粗粒度输出、一万行中的稀疏编辑、共享和远距离上下文、重复行、复制前缀及摘要与底部统计一致性。编写和借用的 Session 快照分别覆盖粗粒度和精确的浏览器卡片。

+ 6 - 0
.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.i18n.yaml

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

+ 41 - 0
.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.md

@@ -0,0 +1,41 @@
+# Agent Note: Web sticky collapsible headers — Think and compaction toggles pin while scrolling
+
+Status: implemented
+
+English | [中文](2026-08-03-web-sticky-collapsible-headers.zh.md)
+
+## Problem
+
+Two conversation blocks render their expanded body uncapped, flowing with the page instead of scrolling inside a bounded surface: the Web Think row (`.thinkBody`) and the compaction marker (`.compactionBody`). Every other tool row caps its body and scrolls it inside its own card, so its disclosure header stays visible. The two uncapped blocks do not. A long chain of thought or a long compaction summary carries its own disclosure header off the top of the viewport, so a reader who wants to collapse the block again must scroll the full body back up to reach the toggle.
+
+## Decision
+
+The disclosure header of each uncapped block sticks to the conversation scroll container's top while the block is open. The header remains in normal flow when collapsed, so a collapsed block scrolls away like any other row.
+
+Both blocks already scroll against the shared conversation scroll container (`[data-conversation-scroll]`), not an inner box, so `position: sticky; top: 0` on the header pins it against that container. A base-token background masks the prose that scrolls under the pinned header.
+
+The pinned header's stacking rank differs by block, because their bodies differ. The Think body is plain text with no sticky descendant, so `z-index: 1` suffices. The compaction body renders markdown, and a fenced code block in the summary pins its own banner at `z-index: 6` (`packages/client/ui-primitives/src/markdown/CodeBlock.module.css`) with a Copy control inside it; the compaction header therefore uses `z-index: 7` and holds that banner below its own band, so the header never covers the Copy control and the banner never covers the toggle. The band's height is one component-local measurement (`--dsh-compaction-header-height` on `.compactionRow`) shared by the toggle's `height` and the banner's `top`; the banner rule wins over CodeBlock's `top: 0` by specificity, because the two sheets load in their own packages. The pinned header also overrides its hover fill to the opaque `--dsw-alias-interactive-bg-hover-solid` token and squares its corners for as long as the row is open: the default translucent hover token would let the scrolling prose show through the moment the pointer lands on the toggle, and the base 6px radius would leave the same prose visible at the corners. `:hover` raises the rule's specificity above the later base hover rule, so declaration order does not decide the winner. Two other elements take `z-index: 7` to clear the same code banners: the composer seat and the turn-navigation rail slot (`TurnNavigator.module.css`, ui-chat). Their order among equal ranks is stated once, in the composer seat's comment (`packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css`).
+
+The rules are scoped so only the two uncapped blocks are affected; the capped tool rows keep their existing behavior, since stacking sticky headers across a run of tool rows would pile them at the top. The Think rule is `packages/client/ui-chat/src/client/chat/ReasoningRow.module.css` `.root[data-expanded] [data-open] [data-disclosure-row]` — gated on `DisclosureRow`'s `data-open` so a collapsed Think row never sticks, and scoped under the Think row's own root so no tool-call variant is touched. The compaction rule is `packages/client/ui-chat/src/client/chat/MessageItem.module.css` `.compactionRow:has(.compactionBody) .compactionButton` — the body sibling exists in the DOM only while open, so `:has()` gates the stick on the open state.
+
+No session, wire, durable event, or model-visible contract changes; this is a presentation-only CSS change owned by the existing components.
+
+## Alternatives considered
+
+**Cap the two bodies with `max-height` + internal scroll, matching the tool cards.** Rejected: the Think body is deliberately uncapped so reasoning reads as ordinary message prose ([web-thinking-tail-scroll](../../archived/feature/2026-08-02-web-thinking-tail-scroll.md) and the `.thinkBody` comment own that intent), and the compaction summary is a reading surface. An inner scrollport introduces nested scrolling — the wheel switches from page to box under the cursor — and compresses long technical prose into a small window that is worse to read. Sticky headers keep the flowing-prose reading model and still keep the toggle reachable.
+
+**Add sticky headers to every collapsible row for consistency.** Rejected: the capped tool rows already keep their header visible because their body scrolls internally, so they have no problem to solve. Making their headers sticky against the page would stack one pinned header per open row at the top of the viewport during a scroll through a run of tool calls, which is visual noise, not consistency.
+
+**Offset the summary's code banner from `CodeBlock` rather than from the compaction rule.** Rejected: `CodeBlock` could read a property such as `--dsl-code-block-banner-top` that this consumer sets, which would leave the code block the owner of its own geometry and avoid a `:has()` selector reaching into another package's DOM. It also routes every consumer's layout through a property only this block sets, and the offset is this block's presentation concern rather than the code block's: the local rule states the offset beside the toggle that creates it, and `[data-code-block-banner]` is the hook `CodeBlock` already publishes for owner styling.
+
+**A shared sticky rule on the `DisclosureRow` primitive.** Rejected: `DisclosureRow` backs Think, every tool-call variant, and the context-injection row; a rule there would hit the capped rows too. The behavior belongs only to the uncapped consumers, so each scopes the rule to its own block.
+
+## Consequences
+
+The collapse toggle for a long Think block or compaction summary stays reachable without scrolling the body back to its start, while both bodies keep flowing as page prose. The change is CSS-only: no timer, subscription, durable state, DOM structure change, or transport traffic. The compaction rule uses the CSS `:has()` selector, supported across the browsers the Web UI targets.
+
+## Testing
+
+The unit specs pin the DOM anchors the selectors key on: `packages/client/ui-chat/tests/reasoning-row.client.spec.tsx` asserts an open Think row nests `[data-disclosure-row]` under `[data-variant='think'][data-expanded] [data-open]` and that a collapsed row has no `[data-open]`; `packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx` asserts the compaction body appears under `.compactionRow` only while open. `packages/client/ui-chat/tests/sticky-header-styles.client.spec.ts` reads the rules as CSS text and pins the declarations the pinning depends on: `position: sticky`, `top: 0`, the square corners, the `z-index` rank of each side, the single measurement shared by the toggle's height and the code banner's offset, and the opaque hover token, because jsdom computes no sticky layout and the render specs cannot fail on a changed declaration.
+
+The real-browser evidence is two keyless Chromium e2e paths. `apps/web/tests/lifecycle-chrome.e2e.ts` expands the settled turn's process row, opens the Think row, and asserts its header computes `position: sticky`, `top: 0px` (and is not sticky while collapsed); that fixture's recorded reasoning is a single line, too short to overflow, so it proves only that the CSS resolves onto the Think header. `apps/web/tests/seeded-history.e2e.ts` carries the pinned-while-scrolling evidence: it seeds a compaction whose summary length this suite controls (a fenced code block plus 40 list items), shrinks the viewport to force overflow, scrolls the marker into its pinned state, and asserts the header computes `position: sticky`, `top: 0px`, a `z-index` greater than the code block banner's, that it holds at the scrollport top after the scroll, that its own center is the topmost hit-tested element (the toggle stays clickable), and that its hover fill stays fully opaque (alpha 1). The same case scrolls the summary's code banner into its own stuck position and asserts the banner stops below the toggle's band and that the banner's Copy control owns its center, so the offset cannot regress into a covered control. The case also writes a keyless geometry golden (`snapshots/web/seeded-history/sticky-geometry.expected.md`) fixing these platform-independent semantic facts, since this is a user-visible CSS behavior that changes no DOM and no accessible name, so the aria goldens cannot capture it. The PR demo GIF, recorded against a real server and a real model round, carries the visual evidence that the pinned header stays at the top while the body scrolls.

+ 41 - 0
.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.zh.md

@@ -0,0 +1,41 @@
+# Agent Note:Web 可折叠块的钉住标题 —— Think 与压缩标记的折叠按钮在滚动时钉住
+
+Status: implemented
+
+[English](2026-08-03-web-sticky-collapsible-headers.md) | 中文
+
+## 问题
+
+会话里有两个块的展开正文不封顶,随整页滚动,而不是在有界的表面内部滚动:Web Think 行(`.thinkBody`)和压缩标记(`.compactionBody`)。其他每个工具行都给正文封顶并在自己的卡片内部滚动,所以折叠标题始终可见。这两个不封顶的块做不到。一段很长的思维链或很长的压缩摘要会把自己的折叠标题顶出视口上方,想再次折叠该块的读者必须把整段正文滚回顶部才能够到折叠按钮。
+
+## 决策
+
+每个不封顶块的折叠标题在块展开时钉在会话滚动容器的顶部。折叠时标题保持在正常文档流中,所以折叠的块会像其他行一样滚走。
+
+这两个块本来就是相对共享的会话滚动容器(`[data-conversation-scroll]`)滚动,而非某个内层框,所以在标题上加 `position: sticky; top: 0` 就把它钉在该容器上。一个 base token 背景遮住在钉住的标题下方滚过的正文。
+
+钉住的标题的层叠级别按块而异,因为两者正文不同。Think 正文是纯文本、无 sticky 后代,`z-index: 1` 就够。压缩正文渲染 markdown,摘要里的围栏代码块会把自己的 banner 钉在 `z-index: 6`(`packages/client/ui-primitives/src/markdown/CodeBlock.module.css`),banner 里带一个 Copy 控件;因此压缩标题用 `z-index: 7`,并让那个 banner 停在自己的标题带下方,这样标题永远不会盖住 Copy 控件,banner 也不会盖住折叠按钮。标题带高度是一个组件局部量(`.compactionRow` 上的 `--dsh-compaction-header-height`),由折叠按钮的 `height` 与 banner 的 `top` 共用;banner 规则靠特异性压过 CodeBlock 的 `top: 0`,因为两张样式表分属不同的包。钉住的标题还把 hover 底覆盖为不透明的 `--dsw-alias-interactive-bg-hover-solid` token,并在整行展开期间都保持直角:默认的半透明 hover token 会在指针落到折叠按钮准备折叠的瞬间让滚动的正文透出,而基础的 6px 圆角会让同样的正文在四角露出来。`:hover` 把该规则的特异性抬到文件更靠后的 hover 基础规则之上,所以胜负不由声明顺序决定。另外还有两个元素为了避开同一批代码 banner 取 `z-index: 7`:输入框座与轮次导航轨道槽(`TurnNavigator.module.css`,ui-chat)。同级之间的先后顺序只写在一处:`packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css` 中输入框座的注释。
+
+规则被限定作用域,只影响这两个不封顶的块;封顶的工具行保持原有行为,因为让一连串工具行的 sticky 标题层层堆叠会把它们全挤在顶部。Think 规则是 `packages/client/ui-chat/src/client/chat/ReasoningRow.module.css` 的 `.root[data-expanded] [data-open] [data-disclosure-row]`,用 `DisclosureRow` 的 `data-open` 门控,折叠的 Think 行绝不钉住,并限定在 Think 行自己的 root 之下,不触及任何工具调用 variant。压缩规则是 `packages/client/ui-chat/src/client/chat/MessageItem.module.css` 的 `.compactionRow:has(.compactionBody) .compactionButton`,正文兄弟节点只在展开时存在于 DOM,所以 `:has()` 就以展开状态门控钉住。
+
+不改动任何 session、wire、durable event 或 model-visible 契约;这是一处纯展示层的 CSS 改动,由既有组件拥有。
+
+## 曾考虑的替代方案
+
+**用 `max-height` 加内部滚动给这两个正文封顶,与工具卡片一致。** 否决:Think 正文是刻意不封顶的,好让推理读起来像普通消息正文([web-thinking-tail-scroll](../../archived/feature/2026-08-02-web-thinking-tail-scroll.md) 和 `.thinkBody` 注释拥有这一意图),压缩摘要是一个阅读表面。内层滚动框会引入嵌套滚动,滚轮在光标下从整页切换到框内,并把很长的技术性正文压进一个更难读的小窗口。钉住标题保留了流式正文的阅读模型,同时让折叠按钮依然够得着。
+
+**为一致性给每个可折叠行都加钉住标题。** 否决:封顶的工具行因为正文在内部滚动,标题本来就一直可见,没有需要解决的问题。让它们的标题相对整页钉住,会在滚过一连串工具调用时把每个展开行各自钉住的标题堆叠在视口顶部,这是视觉噪音,不是一致性。
+
+**把摘要内代码栏的偏移交给 `CodeBlock` 承担。** 否决:可以让 `CodeBlock` 读取使用方设置的 `--dsl-code-block-banner-top` 之类的属性,这样代码块继续拥有自己的几何,也避免用 `:has()` 选择器伸进另一个包的 DOM。代价是把所有使用方的布局都接到一个只有这个块会设置的属性上,而这段偏移是这个块的呈现问题、不是代码块的:局部规则把偏移写在造成它的折叠按钮旁边,而 `[data-code-block-banner]` 本来就是 `CodeBlock` 为使用者样式发布的钩子。
+
+**在 `DisclosureRow` 基元上加一条共享的 sticky 规则。** 否决:`DisclosureRow` 支撑 Think、每个工具调用 variant 以及 context-injection 行;在那里加规则会一并命中封顶行。该行为只属于不封顶的消费者,所以各自把规则限定在自己的块上。
+
+## 后果
+
+很长的 Think 块或压缩摘要的折叠按钮无需把正文滚回起点就能够到,同时两个正文都保持作为整页正文流动。改动是纯 CSS:没有计时器、订阅、durable state、DOM 结构改动或传输流量。压缩规则使用 CSS `:has()` 选择器,Web UI 所面向的各浏览器均支持。
+
+## 测试
+
+单元测试钉住选择器所依赖的 DOM 锚点:`packages/client/ui-chat/tests/reasoning-row.client.spec.tsx` 断言展开的 Think 行在 `[data-variant='think'][data-expanded] [data-open]` 之下嵌套了 `[data-disclosure-row]`,且折叠行没有 `[data-open]`;`packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx` 断言压缩正文只在展开时出现在 `.compactionRow` 之下。`packages/client/ui-chat/tests/sticky-header-styles.client.spec.ts` 把这两条规则当作 CSS 文本读取,逐条固定钉住所依赖的声明:`position: sticky`、`top: 0`、直角圆角、两侧各自的 `z-index` 级别、折叠按钮高度与代码 banner 偏移共用的那一个量,以及不透明的 hover token——因为 jsdom 不计算 sticky 布局,渲染类测试也无法在某条声明被改动时变红。
+
+真实浏览器证据由两条 keyless Chromium e2e 路径承载。`apps/web/tests/lifecycle-chrome.e2e.ts` 展开已结束轮次的 process 行、展开 Think 行,并断言其标题计算出 `position: sticky`、`top: 0px`(折叠时非 sticky);该 fixture 录制的 reasoning 只有一行,太短不足以溢出,所以它只证明 CSS 解析到了 Think 标题。`apps/web/tests/seeded-history.e2e.ts` 承载「钉住态随滚动」的证据:它种入一个摘要长度由本套件控制的压缩(一个围栏代码块加 40 个列表项),把视口压小以强制溢出,滚动到 marker 的钉住态,断言标题计算出 `position: sticky`、`top: 0px`、`z-index` 大于代码块 banner、滚动后仍停在滚动口顶边、其自身中心是命中测试命中的最上层元素(折叠按钮保持可点击),以及其 hover 底保持完全不透明(alpha 1)。同一用例还把摘要里的代码 banner 滚到它自己的钉住位置,断言 banner 停在标题带下方、且 banner 的 Copy 控件在它的中心点命中自身,使这条偏移不会退化成控件被盖住。同一用例还写出一份 keyless 几何 golden(`snapshots/web/seeded-history/sticky-geometry.expected.md`),把这些与平台无关的语义事实固定下来,因为这是一处用户可见、但不改动 DOM 与无障碍名称的 CSS 行为,无障碍 golden 捕获不到它。PR demo GIF 用真实服务器加真实模型轮次录制,承载视觉证据:钉住的标题在正文滚动时停留在顶部。

+ 2 - 2
.agents/notes/implemented/feature/2026-09-04-right-sidebar-docking-infrastructure.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-04-right-sidebar-docking-infrastructure.md
-2026-09-04-right-sidebar-docking-infrastructure.md: 936f73c9210baf4297eace2deb270bb9594d7e8d
-2026-09-04-right-sidebar-docking-infrastructure.zh.md: d234dbcbb2f60ba1dd3b3d9fca2936a16521751d
+2026-09-04-right-sidebar-docking-infrastructure.md: 3ebfe4b3a95ab5820682098be2a4c43f172d1a02
+2026-09-04-right-sidebar-docking-infrastructure.zh.md: dc6516324a368dd86b3e01afb4051a4b20697363

+ 2 - 2
.agents/notes/implemented/feature/2026-09-04-right-sidebar-docking-infrastructure.md

@@ -39,7 +39,7 @@ The right Sidebar uses one mounted content tree in normal and fullscreen modes;
 
 [Default pages](2026-09-08-sidebar-default-pages.md) supersede default-guide reseeding here; [last-tab close rules](2026-09-08-sidebar-last-tab-close-rules.md) own explicit closing, while moving tabs still settles emptied panes.
 
-`ui-sidebar-right` keeps one `SurfaceState` per session id — the layout, its history, and the mint counter — in a store declared at the seat registration. Every action mints the ids its intent needs, asks a kit planner for the operations, runs the settle planner over the result, and records the whole intent as one history entry before assigning the session's surface back; no action edits a layout in place. The settle step is the product's rule: a docked pane whose last tab is closed, moved out, or floated is merged away, and an expanded empty root pane receives the current default page. A collapsed surface may remain empty until its next expansion; no separate pane-closing gesture exists. State is memory-only: a reload returns every session to the collapsed default, and switching sessions keeps each surface where it was. Layout is presentation state and never enters the session log.
+`ui-sidebar-right` keeps one `SurfaceState` per session id — the layout, its history, and the mint counter — in a store declared at the seat registration. Every action mints the ids its intent needs, asks a kit planner for the operations, runs the settle planner over the result, and records the whole intent as one history entry before assigning the session's surface back; no action edits a layout in place. The settle step is the product's rule: a docked pane whose last tab is closed, moved out, or floated is merged away, and an expanded empty root pane receives the current default page. A collapsed surface may remain empty until its next expansion; no separate pane-closing gesture exists. [Layout persistence and provider recovery](../architecture/2026-09-14-sidebar-layout-provider-recovery.md) owns Session-scoped browser storage and reload. Layout is presentation state and never enters the session log.
 
 ### Beyond the surface
 
@@ -76,7 +76,7 @@ The surface renders tabs whose bodies it does not know: each tab carries a `kind
 ## Consequences
 
 - The docking surface itself no longer overflows its panel: `.surface` and `.pane` clamp to the column (`min-width: 0`, `overflow: hidden`), so a long unwrapped line scrolls inside the body and the strip's controls stay in view in every split.
-- Layout is undoable and per session, and it is memory-only; a reload starts every session collapsed. Undo is reachable only through `@internal` service methods; the product shows no history controls.
+- Layout is undoable, persisted and per Session. Undo is reachable only through `@internal` service methods; the product shows no history controls.
 - An expanded surface has no empty panes. Empty side panes merge away; an empty root receives the current default page only while expanded. New sessions and a collapsed surface whose last tab closed remain empty until expansion.
 - A pane holds at most one guide tab: a second one cannot be added, opened, duplicated, or moved in; the guide's uniqueness is per pane, so a split still seeds its new pane with a guide.
 - A pane may split only when each equal half can still hold what cannot shrink: the strip's fixed controls (its width minus the chip box and the fill, so the top-right pane's chrome counts on the half that hosts it) plus one chip at its minimum, measured in the component layer after every commit and on resize. Otherwise the split control stays, disabled with its own copy, the matching edge drop zones are withheld, and panes the user narrows keep their size; the product permits at most two horizontal panes, regardless of widening or divider movement.

+ 2 - 2
.agents/notes/implemented/feature/2026-09-04-right-sidebar-docking-infrastructure.zh.md

@@ -39,7 +39,7 @@ Agent 产出的文件是最尖锐的案例。产出文件 chip 或 `read` 行的
 
 [默认页](2026-09-08-sidebar-default-pages.zh.md)取代此处的默认补入引导页;[最后一个 tab 的关闭规则](2026-09-08-sidebar-last-tab-close-rules.zh.md)负责显式关闭,移动 tab 仍会处理被清空的格。
 
-`ui-sidebar-right` 为每个会话 id 保存一份 `SurfaceState`——布局、历史与铸造计数——住在坑位注册时声明的 store 里。每个 action 先铸造意图所需的 id,向库的 planner 索取操作,对结果跑一遍 settle planner,把整个意图记为一条历史账,再把该会话的 surface 整体赋回;没有 action 就地改布局。settle 是产品规则:最后一个 tab 被关闭、拖走或悬浮出去的停靠 pane 会被合并掉;展开且为空的根 pane 会填入当前默认页。折叠的布局可以保持为空,直到下次展开;没有单独的关闭 pane 手势。状态仅在内存:刷新使所有会话回到折叠默认态,切换会话时各 surface 保持原样。布局是呈现状态,永不进入会话日志。
+`ui-sidebar-right` 为每个会话 id 保存一份 `SurfaceState`——布局、历史与铸造计数——住在坑位注册时声明的 store 里。每个 action 先铸造意图所需的 id,向库的 planner 索取操作,对结果跑一遍 settle planner,把整个意图记为一条历史账,再把该会话的 surface 整体赋回;没有 action 就地改布局。settle 是产品规则:最后一个 tab 被关闭、拖走或悬浮出去的停靠 pane 会被合并掉;展开且为空的根 pane 会填入当前默认页。折叠的布局可以保持为空,直到下次展开;没有单独的关闭 pane 手势。[布局持久化与 provider 恢复](../architecture/2026-09-14-sidebar-layout-provider-recovery.zh.md)负责 Session 作用域的浏览器存储和刷新。布局是呈现状态,永不进入会话日志。
 
 ### 面之外
 
@@ -76,7 +76,7 @@ Agent 产出的文件是最尖锐的案例。产出文件 chip 或 `read` 行的
 ## Consequences
 
 - 停靠面自身不再溢出面板:`.surface` 与 `.pane` 收在列内(`min-width: 0`、`overflow: hidden`),长的不换行行在正文内滚动,tab 条控件在任何分栏下都可见。
-- 布局可撤销且按会话隔离,同时仅在内存;刷新使所有会话回到折叠态。undo 只能经 `@internal` 服务方法触达;产品不显示历史控件。
+- 布局可撤销、可持久化且按 Session 隔离。undo 只能经 `@internal` 服务方法触达;产品不显示历史控件。
 - 展开的布局不保留空 pane。空侧 pane 被合并,空根 pane 只在展开时填入当前默认页。新会话以及关闭最后一个 tab 后收起的布局保持为空,直到下次展开。
 - 一个 pane 最多持有一个引导 tab:第二个不能被添加、打开、复制或搬入;唯一性按 pane 算,所以分栏仍给新 pane 种引导。
 - pane 只有在等分后的两半都仍能容下不可收缩部分时才可分栏:tab 条的固定控件(条宽减去 chip 盒与填充,因此右上 pane 的面板控件只计在承载它的那一半)加一个最小宽度的 chip,由组件层在每次提交与尺寸变化后测量。否则分栏控件保留但禁用并带自己的文案,对应的边缘落区不再提供,用户拖窄的 pane 保持原尺寸;产品最多两个水平窗格,不因拉宽或拖分隔条而提高上限。

+ 2 - 2
.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.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-ptc-trajectory-code-inspection.md
-2026-09-09-ptc-trajectory-code-inspection.md: 477d199f519b5d515e5d58430bd902d9d209e46b
-2026-09-09-ptc-trajectory-code-inspection.zh.md: f305569176214c63ac549b0ec5103289e3e57a88
+2026-09-09-ptc-trajectory-code-inspection.md: 19779d20e8d08ce0ca9678ab6626765faa13cf65
+2026-09-09-ptc-trajectory-code-inspection.zh.md: 7cdb2321d61893e43426798d311847662450d289

+ 3 - 1
.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md

@@ -16,6 +16,8 @@ The result view preserves recorded text and uses a tree only for complete JSON o
 
 The [PTC runtime decision](2026-06-15-ptc.md) still owns execution and settlement; the [client presentation decision](../architecture/2026-08-23-client-derived-tool-presentation.md) still owns deriving UI from recorded facts. This inspector adds no Session events or host presentation fields.
 
+Trajectory thinking opens by default and supports manual disclosure. [Chat disclosure defaults](../bug-fix/2026-09-14-chat-presentation-defaults.md) are a separate presentation decision.
+
 ## Alternatives considered
 
 **Keep source inside the argument tree.** JSON escaping obscures program structure and makes copying executable source cumbersome.
@@ -26,4 +28,4 @@ The [PTC runtime decision](2026-06-15-ptc.md) still owns execution and settlemen
 
 ## Consequences
 
-Readers can inspect and copy recorded programs without changing replay data. Schemas with no recognizable language hint receive no syntax highlighting. Component tests cover recorded-name recognition, schema fallback, exact source and argument copying, output states, and independent wrapping. JSON-tree tests cover clipping geometry, missing `ResizeObserver`, clipboard settlement after hover changes or unmount, and value-read counts during hover. Thinking tests cover body arrival, manual disclosure, and switching records; the [PTC browser scenario](../../../../apps/web/tests/ptc-round.e2e.ts) pins the assembled inspector and verifies overflow and the original-JSON round trip.
+Readers can inspect and copy recorded programs without changing replay data. Schemas with no recognizable language hint receive no syntax highlighting. Component tests cover recorded-name recognition, schema fallback, exact source and argument copying, output states, and independent wrapping. JSON-tree tests cover clipping geometry, missing `ResizeObserver`, clipboard settlement after hover changes or unmount, and value-read counts during hover. Thinking tests cover manual disclosure and switching Trajectory records; the [PTC browser scenario](../../../../apps/web/tests/ptc-round.e2e.ts) pins the assembled inspector and verifies overflow and the original-JSON round trip.

+ 3 - 1
.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.zh.md

@@ -16,6 +16,8 @@ PTC 程序以 JSON 字符串参数传入。转义使长程序在通用参数树
 
 [PTC 运行时决策](2026-06-15-ptc.zh.md) 仍负责执行与结算;[客户端展示决策](../architecture/2026-08-23-client-derived-tool-presentation.zh.md) 仍负责从已记录事实派生 UI。此检查器不增加 Session 事件或宿主展示字段。
 
+Trajectory 思考默认展开,并支持手动展开折叠。[Chat 展开默认值](../bug-fix/2026-09-14-chat-presentation-defaults.zh.md)是独立的展示决策。
+
 ## 考虑过的替代方案
 
 **把源码保留在参数树中。** JSON 转义遮蔽程序结构,也使复制可执行源码变得繁琐。
@@ -26,4 +28,4 @@ PTC 程序以 JSON 字符串参数传入。转义使长程序在通用参数树
 
 ## 后果
 
-读者可以检查和复制已记录的程序,无需修改回放数据。Schema 没有可识别的语言提示时不提供语法高亮。组件测试覆盖记录工具名识别、Schema 回退、源码与参数原样复制、输出状态及独立换行。JSON 树测试覆盖裁剪几何、缺少 `ResizeObserver`、悬停切换或卸载后剪贴板写入落定,以及悬停期间读取值的次数。思考测试覆盖正文到达、手动展开折叠及记录切换;[PTC 浏览器场景](../../../../apps/web/tests/ptc-round.e2e.ts) 固定组装后的检查器展示,并验证溢出和原始 JSON 的往返切换。
+读者可以检查和复制已记录的程序,无需修改回放数据。Schema 没有可识别的语言提示时不提供语法高亮。组件测试覆盖记录工具名识别、Schema 回退、源码与参数原样复制、输出状态及独立换行。JSON 树测试覆盖裁剪几何、缺少 `ResizeObserver`、悬停切换或卸载后剪贴板写入落定,以及悬停期间读取值的次数。思考测试覆盖手动展开折叠及 Trajectory 记录切换;[PTC 浏览器场景](../../../../apps/web/tests/ptc-round.e2e.ts) 固定组装后的检查器展示,并验证溢出和原始 JSON 的往返切换。

+ 2 - 2
.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md
-2026-09-09-web-sidebar-terminal.md: db7f42e9f53940262d806ff1133c1059e3f74829
-2026-09-09-web-sidebar-terminal.zh.md: cbb6bc6bab5e3ed4927399b90363332f1624825b
+2026-09-09-web-sidebar-terminal.md: 2649610e776029b10b11fc4ea87d75a24a58d32b
+2026-09-09-web-sidebar-terminal.zh.md: 9a3fd828ff52142cb945ff14d84d13c9e8522ecf

+ 10 - 6
.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md

@@ -10,15 +10,17 @@ Web users need an interactive shell beside a Session to inspect the workspace an
 
 ## Decision
 
-The application theme owns terminal background, default text, cursor and selection colors. The terminal body reads the rendered DSH palette after framework-delivered theme changes, so CSS token overrides remain effective. Updating the existing xterm instance preserves its output and PTY attachment; shell-provided ANSI colors remain independent.
+Guide entries declare stable ids within their provider. A keyed `sidebar.right.tab.guide.entry` slot dispatches by the active provider id, so an extension replacing a builtin also controls its guide rendering. The sidebar owns card placement and the default fallback; provider components own their controls and read the enclosing tab through framework hooks. An entry slot allows a shell menu without replacing the entire guide or nesting an interactive button inside another button.
 
-`api-terminal-controller` owns user terminals by Session and exposes the `terminal` Remote namespace. `ui-sidebar-terminal` registers native right-sidebar tabs, xterm.js rendering and FitAddon sizing. A new terminal offers installed shells and waits for Start. Host discovery verifies the configured candidates, with the execution default first; creation accepts only a currently discovered path. The browser remembers the last selected shell path in origin-scoped localStorage and falls back to the current default if that path is unavailable. The terminal type declares independent instances, so ordinary page deduplication cannot collapse separate processes when opening or docking tabs. The existing sidebar controls open additional tabs; double-clicking a tab title renames its terminal. Terminal processes use the composed subprocess provider and Session sandbox policy. Shell resolution occurs during discovery and creation; reading limits and reconnecting an existing process do not depend on the default executable remaining available. Interactive shell configuration supplies Tab completion and optional inline suggestions.
+The application theme supplies terminal default colors. The body reads resolved CSS tokens, and updates xterm only when those colors change. Public OSC parser observers retain indexed and default-color overrides separately from the DSH defaults; resets remove the corresponding override before restoring the current theme. Observers delegate queries and color handling to xterm. xterm's minimum contrast adjustment improves text legibility without remapping ANSI backgrounds. The DOM cursor reads the rendered cell background after each render and uses a contrasting fill through scoped CSS variables, so cursor movement never resets the palette. Browser checks cover indexed, true-color and inverse cells, light/dark switching, OSC retention and reset, and blinking cursor styles.
+
+`api-terminal-controller` owns user terminals by Session and exposes the `terminal` Remote namespace. `ui-sidebar-terminal` registers native right-sidebar tabs, xterm.js rendering and FitAddon sizing. The terminal guide card has a primary action for the remembered available shell and a separate installed-shell menu. Selecting a menu item records its path and opens a new terminal immediately; discovery alone allocates no process. Each tab owns its startup and close lifecycle. Host discovery verifies the configured candidates, with the execution default first; creation accepts only a currently discovered path. The browser remembers the last selected shell path in origin-scoped localStorage and falls back to the current default if that path is unavailable. The terminal type declares independent instances, so ordinary page deduplication cannot collapse separate processes when opening or docking tabs. The existing sidebar controls open additional tabs; double-clicking a tab title renames its terminal. Terminal processes use the composed subprocess provider and Session sandbox policy. Shell resolution occurs during discovery and creation; reading limits and reconnecting an existing process do not depend on the default executable remaining available. Interactive shell configuration supplies Tab completion and optional inline suggestions.
 
 Close and replacement remove the tab synchronously and run process cleanup in the background. The Client first records the unfinished close request under a terminal-specific localStorage key; success removes it, and startup retries requests that remain. A cleanup failure produces a lightweight notification with a retry action without reopening the tab. Independent keys prevent another window from overwriting unrelated cleanup requests. Collapse, tab/Session switching, floating, fullscreen and browser disconnection preserve the process. Component cleanup and `TabDomain.signal` only detach browser work because the same lifetime can end during plugin reload. Failed process cleanup retains ownership, including failures after allocation but before create publication. Session owner disposal and Host plugin disposal also clean up terminals. A definitive missing-Session response retires its saved close request because the Session owns process cleanup; transport failures remain retryable. Client plugin disposal awaits every detached stream so a replacement plugin does not inherit unfinished Client cleanup.
 
-Sidebar layout, open-tab mappings, selection and process PIDs are not persisted. When the Session header mounts, the Client queries `terminal.list` and opens retained Host terminals as new tabs. Listing takes the Session ID directly because history can outlive its Agent and terminal owner; an offline Session has no retained terminals to restore. Their `params.terminalId` association exists only in the current page. New and recovered views use different `createWhenMissing` values: only a new view may allocate a process; a recovered target that disappears reports an error. Host state supplies recovery identities and titles, so the browser does not maintain a second active-terminal registry.
+[Sidebar layout persistence and provider recovery](../architecture/2026-09-14-sidebar-layout-provider-recovery.md) owns browser reload: layout and tab identities are restored before the terminal provider reconnects its views. Listing still takes the Session ID directly because history can outlive its Agent and terminal owner; an offline Session has no retained terminals to restore. Only a new view may allocate a process; a recovered target that disappears reports an error. The Host remains authoritative for process state, titles and screen contents.
 
-The caller retains a terminal ID in memory before creating it. Repeating create with the same Session and open ID does not allocate a second process. Closing an uncertain create uses that ID even when no creation response arrived. The Host records closed IDs before awaiting allocation, preventing a delayed create from reviving a closed terminal. Each attachment begins with a consistent, bounded serialized xterm screen; ordered output follows through the Gateway's existing multiplexed Remote stream. Output and screen snapshots share one operation queue. Followers retain final output on normal closure and fail explicitly on buffer overflow. Browser render acknowledgement prevents React batching from dropping increments.
+The caller retains a terminal ID before creating it. Repeating create with the same Session and open ID does not allocate a second process. Closing an uncertain create uses that ID even when no creation response arrived. The Host records closed IDs before awaiting allocation, preventing a delayed create from reviving a closed terminal. Each attachment begins with a consistent, bounded serialized xterm screen; ordered output follows through the Gateway's existing multiplexed Remote stream. Output and screen snapshots share one operation queue. Followers retain final output on normal closure and fail explicitly on buffer overflow. Browser render acknowledgement prevents React batching from dropping increments.
 
 The latest attachment controls input and dimensions; other attachments remain read-only. Every physical stream opening gets a fresh input attachment identity, including automatic transport recovery. Input RPCs are serialized, and results from a superseded attachment cannot downgrade a replacement connection. Terminal output creates no model input, Agent tool result or Session event. The existing [persistent Agent terminal decision](2026-07-16-persistent-pty-sessions.md) continues to govern model-owned sessions; this feature extends the [portable subprocess provider](../architecture/2026-07-28-portable-execution-world-consumers.md) with terminal environment facts and resize. Control-transfer and process-exit refusals only disable input, preserving the healthy output view. Client-owned error identifiers are translated by the terminal UI, including guidance to close retained exited terminals when the quota is full.
 
@@ -28,7 +30,7 @@ The latest attachment controls input and dimensions; other attachments remain re
 
 **Keep the tab visible until process cleanup finishes.** A slow or failed termination would delay the user's close action. Saving the cleanup intent allows immediate removal while preserving failure reporting and retry.
 
-**Persist sidebar layout or an active tab-to-process registry.** Layout persistence is outside this feature. An additional active registry duplicates Host state and can restore stale associations. Only an unfinished close is an independent user request that must survive page reload.
+**Persist an authoritative active-process registry.** Browser process state can become stale independently of Host state. The [layout persistence decision](../architecture/2026-09-14-sidebar-layout-provider-recovery.md) supersedes the exclusion of saved tab associations while retaining Host authority for process liveness.
 
 **Share the Agent terminal registry.** Its controlled prompts and semantic send/wait behavior would change human shell configuration and blur process ownership. User terminals share the subprocess capability instead.
 
@@ -42,6 +44,8 @@ The latest attachment controls input and dimensions; other attachments remain re
 
 ## Consequences
 
-A kept-open terminal retains a process and bounded screen memory. Reload restores Host-retained terminals rather than the previous sidebar layout; Host restart does not restore processes. An exited shell remains visible without automatic respawn. Background cleanup may outlive its tab, and unavailable browser storage limits retry recovery to the current page. Native PTY support and descendant cleanup guarantees remain provider-specific. One writable attachment avoids competing resize and input streams, while explicit takeover permits recovery from another page. Changing sandbox mode requires closing retained terminals first.
+A kept-open terminal retains a process and bounded screen memory. Reload restores the sidebar layout and reconnects Host-retained terminals; Host restart does not restore processes. An exited shell remains visible without automatic respawn. Background cleanup may outlive its tab, and unavailable browser storage limits retry recovery to the current page. Native PTY support and descendant cleanup guarantees remain provider-specific. One writable attachment avoids competing resize and input streams, while explicit takeover permits recovery from another page. Changing sandbox mode requires closing retained terminals first.
 
 The implementation retains the Agent-terminal and portable-execution notes because their ownership and provider decisions remain independently useful; neither is superseded by browser terminals.
+
+The [two-hour unattended-terminal reclamation](2026-09-14-unattended-browser-terminal-reclamation.md) adds an idle grace period after all frontend holders disconnect, preserving busy or uncertain work.

+ 10 - 6
.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.zh.md

@@ -10,15 +10,17 @@ Web 用户需要在 Session 旁使用交互式 shell 检查工作区和运行命
 
 ## 决定
 
-应用主题负责终端背景、默认文字、光标和选区颜色。终端正文在收到框架传递的主题变化后读取 DSH 渲染配色,使 CSS token 覆盖继续生效。更新现有 xterm 实例会保留输出和 PTY 连接;shell 输出的 ANSI 颜色独立生效。
+开始页入口声明 provider 内稳定的 id。keyed `sidebar.right.tab.guide.entry` slot 按当前生效的 provider id 分发,因此替代 builtin 的 extension 也控制对应入口的渲染。侧栏负责卡片排列和默认回退,provider 组件负责自己的控件,并通过框架 hook 读取所在标签页。入口 slot 可以承载 shell 菜单,无需替换整个开始页或把交互按钮嵌套在另一个按钮内。
 
-`api-terminal-controller` 按 Session 管理用户终端并提供 `terminal` Remote namespace。`ui-sidebar-terminal` 注册原生右侧栏标签页,使用 xterm.js 渲染和 FitAddon 测量尺寸。新终端提供已安装 shell 的选择并等待用户启动。Host 探测会验证配置的候选,并把执行环境默认项放在首位;创建只接受当前探测返回的路径。浏览器在当前站点 localStorage 中记住上次选择的 shell 路径,该路径不可用时回到当前默认项。终端类型声明独立实例,因此打开或停靠标签页时,普通页面的去重规则不会合并不同进程。已有侧栏控件负责打开更多标签页,双击标签页标题可重命名终端。终端进程使用组合的 subprocess provider 和 Session sandbox policy。shell 在探测和创建时解析;读取限制和重新连接已有进程不依赖默认可执行文件仍然可用。交互式 shell 配置提供 Tab 补全和可选的内联建议。
+应用主题提供终端的默认颜色。终端正文读取解析后的 CSS 令牌,仅在颜色变化时更新 xterm。公开的 OSC 解析观察器将索引色和默认颜色覆盖与 DSH 默认值分开保存;重置命令先删除对应覆盖,再恢复当前主题。观察器将查询和颜色处理交给 xterm。xterm 的最小对比度调整改善文字可读性,同时不重新映射 ANSI 背景色。DOM 光标在每次渲染后读取单元格实际背景,通过局部 CSS 变量使用有足够对比度的填充色,因此光标移动不会重置调色板。浏览器检查覆盖索引色、真彩色、反色单元格、明暗主题切换、OSC 保留和重置,以及闪烁光标样式。
+
+`api-terminal-controller` 按 Session 管理用户终端并提供 `terminal` Remote namespace。`ui-sidebar-terminal` 注册原生右侧栏标签页,使用 xterm.js 渲染和 FitAddon 测量尺寸。终端开始页卡片的主操作打开上次选择且仍可用的 shell,独立菜单提供已安装 shell。选择菜单项会记录路径并立即打开新终端;仅探测 shell 不分配进程。每个标签页拥有自己的启动和关闭生命周期。Host 探测会验证配置的候选,并把执行环境默认项放在首位;创建只接受当前探测返回的路径。浏览器在当前站点 localStorage 中记住上次选择的 shell 路径,该路径不可用时回到当前默认项。终端类型声明独立实例,因此打开或停靠标签页时,普通页面的去重规则不会合并不同进程。已有侧栏控件负责打开更多标签页,双击标签页标题可重命名终端。终端进程使用组合的 subprocess provider 和 Session sandbox policy。shell 在探测和创建时解析;读取限制和重新连接已有进程不依赖默认可执行文件仍然可用。交互式 shell 配置提供 Tab 补全和可选的内联建议。
 
 关闭和替换会同步移除标签页,并在后台清理进程。Client 先以终端独立的 localStorage key 保存未完成的关闭请求;成功后删除,启动时重试剩余请求。清理失败时显示带重试操作的轻量通知,不重新打开标签页。独立 key 避免其他窗口覆盖无关的清理请求。折叠、切换标签页或 Session、浮动、全屏和浏览器断线均保留进程。组件清理和 `TabDomain.signal` 只停止浏览器工作,因为插件重新加载也会结束这些生命周期。进程清理失败时保留所有权,包括分配完成但 create 尚未发布时的失败。Session owner 和 Host 插件卸载也会清理终端。 明确的 Session 不存在响应会清除已保存的关闭请求,因为进程清理由 Session 负责;传输失败仍可重试。Client 插件卸载等待所有断开的流结束,避免替换插件继承未完成的 Client 清理。
 
-侧栏布局、打开标签页映射、选中项和进程 PID 不持久化。Session header 挂载时,Client 查询 `terminal.list`,把 Host 保留的终端打开为新标签页。列表直接使用 Session ID,因为历史记录可以比 Agent 和终端 owner 存活更久;离线 Session 没有需要恢复的保留终端。`params.terminalId` 关联只在当前页面中保留。新视图与恢复视图使用不同的 `createWhenMissing`:只有新视图可以分配进程,恢复目标消失时显示错误。恢复标识和标题来自 Host 状态,浏览器不维护第二份活跃终端注册表。
+[侧栏布局持久化与 provider 恢复](../architecture/2026-09-14-sidebar-layout-provider-recovery.zh.md)负责浏览器刷新:先恢复布局和标签身份,再由 terminal provider 重连视图。列表仍直接使用 Session ID,因为历史记录可以比 Agent 和终端 owner 存活更久;离线 Session 没有需要恢复的保留终端。只有新视图可以分配进程,恢复目标消失时显示错误。进程状态、标题和屏幕内容仍以 Host 为准。
 
-调用者在创建之前把终端 ID 保留在内存中。相同 Session 和未关闭 ID 的重复 create 不再分配进程。创建结果不确定时,关闭仍使用该 ID,即使没有收到创建响应。Host 在等待分配完成前记录已关闭 ID,防止迟到的 create 复活已关闭终端。每次连接先接收一致、有界的 xterm 序列化屏幕,后续有序输出使用 Gateway 已有的复用 Remote stream。输出和屏幕快照共享操作队列。订阅者正常关闭时保留末尾输出,缓存超限时明确失败。浏览器在完成渲染后确认帧,避免 React 批处理丢失增量。
+调用者在创建之前保留终端 ID。相同 Session 和未关闭 ID 的重复 create 不再分配进程。创建结果不确定时,关闭仍使用该 ID,即使没有收到创建响应。Host 在等待分配完成前记录已关闭 ID,防止迟到的 create 复活已关闭终端。每次连接先接收一致、有界的 xterm 序列化屏幕,后续有序输出使用 Gateway 已有的复用 Remote stream。输出和屏幕快照共享操作队列。订阅者正常关闭时保留末尾输出,缓存超限时明确失败。浏览器在完成渲染后确认帧,避免 React 批处理丢失增量。
 
 最新连接控制输入和尺寸,其他连接保持只读。每次物理流建立都创建新的输入连接标识,包括传输自动恢复。输入 RPC 按序发送,旧连接的结果不能把新连接降级为失败。终端输出不产生模型输入、Agent 工具结果或 Session 事件。[Agent 持久终端决策](2026-07-16-persistent-pty-sessions.zh.md)仍约束模型拥有的终端;此功能为[可移植 subprocess provider](../architecture/2026-07-28-portable-execution-world-consumers.zh.md)增加执行环境事实和 resize。 控制权转移和进程退出导致的拒绝只禁用输入,保留健康的输出视图。Client 自产错误标识由终端 UI 翻译,包括名额用满时关闭已保留的退出终端的提示。
 
@@ -28,7 +30,7 @@ Web 用户需要在 Session 旁使用交互式 shell 检查工作区和运行命
 
 **进程清理完成前保留标签页。** 缓慢或失败的终止会拖延用户关闭操作。保存清理意图后,可以立即移除标签页,同时保留错误反馈与重试。
 
-**持久化侧栏布局或活跃标签页到进程的注册表。** 布局持久化不属于此功能。额外的活跃注册表重复 Host 状态,可能恢复过期关联。只有未完成的关闭操作是需要跨页面刷新保留的独立用户请求。
+**持久化作为权威的活跃进程注册表。** 浏览器进程状态可能独立于 Host 状态失效。[布局持久化决策](../architecture/2026-09-14-sidebar-layout-provider-recovery.zh.md)取代了不保存标签关联的限定,同时保留 Host 对进程存活状态的决定权。
 
 **共用 Agent 终端注册表。** 受控提示符和语义化 send/wait 会改变人工 shell 配置并混淆进程所有权。用户终端只共享 subprocess 能力。
 
@@ -42,6 +44,8 @@ Web 用户需要在 Session 旁使用交互式 shell 检查工作区和运行命
 
 ## 影响
 
-保留终端会保留进程和有界屏幕内存。刷新恢复的是 Host 保留的终端,不是此前的侧栏布局;Host 重启不恢复进程。shell 退出后保持可见,不自动重启。后台清理可能比标签页存活更久,浏览器存储不可用时只能在当前页面保留重试能力。原生 PTY 支持和后代进程清理保证仍由 provider 决定。单一可写连接避免竞争的输入和尺寸流,显式接管允许从另一页面恢复操作。改变 sandbox mode 前需要关闭保留的终端。
+保留终端会保留进程和有界屏幕内存。刷新恢复侧栏布局并重连 Host 保留的终端;Host 重启不恢复进程。shell 退出后保持可见,不自动重启。后台清理可能比标签页存活更久,浏览器存储不可用时只能在当前页面保留重试能力。原生 PTY 支持和后代进程清理保证仍由 provider 决定。单一可写连接避免竞争的输入和尺寸流,显式接管允许从另一页面恢复操作。改变 sandbox mode 前需要关闭保留的终端。
 
 Agent 终端和可移植执行环境两篇记录仍保留,其所有权与 provider 决策继续独立有效,不被浏览器终端取代。
+
+[无人连接终端的两小时回收](2026-09-14-unattended-browser-terminal-reclamation.zh.md)在全部前端持有者断开后设置空闲宽限期,保留忙碌或状态不确定的工作。

+ 6 - 0
.agents/notes/implemented/feature/2026-09-11-sidebar-document-preview-polish.i18n.yaml

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

+ 35 - 0
.agents/notes/implemented/feature/2026-09-11-sidebar-document-preview-polish.md

@@ -0,0 +1,35 @@
+# Agent Note: Sidebar document preview polish
+
+Status: implemented
+
+English | [中文](2026-09-11-sidebar-document-preview-polish.zh.md)
+
+## Problem
+
+The Sidebar document preview accumulated several experience defects (issue #3974). Images rendered at their intrinsic CSS-pixel size, so a wide image overflowed the pane and forced horizontal scrolling. The viewer dropdown always appended the plain-text fallback, so bitmap and PDF files offered a "Plain text" choice whose result is unreadable bytes, and files with one real renderer still showed a control with nothing meaningful to switch to. Binary containers with no renderer at all (video, archives, office documents) fell into the plain-text reader and surfaced a read error instead of a designed empty state. Each renderer carried its own loading copy and position, so opening a file flashed through several differently worded, differently placed indicators. PDF pages sat inside a double inset that shrank every page below the pane's width. Switching sidebar tabs remounted the file tree at scroll top, losing the reader's place.
+
+## Decision
+
+**Image width fit.** The image frame follows the scroller's width with a 12px inset; the image itself carries `max-width: 100%` and an 8px corner radius, so a wider image scales down to the pane's width at its aspect ratio, a smaller image keeps its intrinsic size centred by auto margins, and a taller image scrolls vertically in the shared body. Height fitting was considered and dropped: it needs a fixed-height frame, and mixed portrait cases produced surprising layouts for no user request.
+
+**Binary viewer choices.** `DocumentPreviewDefinition` gains optional `binaryExtensions`, the suffixes among a renderer's `extensions` whose bytes are not readable text; `register` rejects an entry absent from `extensions`. The renderer owns this knowledge: the image body declares `png, jpg, jpeg, gif, webp, bmp, ico` and leaves `svg` out because SVG source is readable XML; the PDF body declares `pdf`. `binaryDocumentPath` in the registry module answers whether any registered definition declares a filename's suffix binary, and the preview owner skips the plain-text fallback for such files. The header renders the viewer menu only when at least two candidates exist; a single candidate shows no viewer control at all.
+
+**Unsupported empty state.** The owner-side list in `document/unviewable.ts` names binary container suffixes (video, audio, archives, office documents, executables, fonts, disk images, design formats) that no renderer claims. The owner consults it only when no implementation matches, so any renderer registration always wins; a matching file shows the path header, the file-type icon, and one `unsupportedFile` line, and never issues a read. An uncertain suffix stays out of the list and keeps the plain-text fallback; a text-claimed file whose bytes fail the reader's checks reports the same copy through `error.notText`.
+
+**Unified loading.** `LoadingIndicator` renders icon-only, carrying its label as `aria-label` with no visible text; every renderer's loading copy is the shared "Reading…". Every wait before content exists — the owner's first read, PDF parsing, HTML packaging, image decoding — centres the spinner in the pane, so opening a file shows one spinner in one position until the body appears. An unrendered PDF page holds its place as a static 3:4 placeholder block on the theme's skeleton token `--dsw-alias-bg-skeleton` with no spinner; the document-open wait stays the owner's centred read spinner rather than the first page's placeholder, because the read spinner precedes the placeholder and a handoff between them visibly jumps positions. Placeholders carry no shimmer animation, whose per-page cost outweighs its value.
+
+**PDF full-bleed and copy.** The PDF body and page insets are removed so pages fill the pane's width edge to edge; the image renderer keeps its own 12px inset. Chinese error and status lines across the preview dictionaries drop trailing full stops.
+
+**Files tree scroll restore.** `ui-sidebar-files` follows the document preview's own pattern: the tree store gains `scrollTop` with a `scrolled` action, the body tracks its scroll offset locally and commits it once, on unmount and only while the owner's signal is live, and a remount restores it in a layout effect. Loaded levels already outlive the body in the store, so the remounted tree lays out at full height before the offset re-lands.
+
+## Alternatives considered
+
+**A boolean `binary` flag per definition.** The image renderer covers both bitmaps and SVG under one registration, so binariness is a property of the suffix, not the renderer.
+
+**Filtering in the preview owner by a hardcoded suffix list for registered renderers.** The owner would duplicate knowledge each renderer already holds, and external renderers could not extend the set; the owner-side list exists only for suffixes no renderer claims.
+
+**A one-item static viewer label.** Rendering the single remaining candidate as a static name puts a control that offers no action in the header; it is noise, so the header renders nothing.
+
+## Consequences
+
+Images never scroll horizontally; the pane's width is the only layout input, so no zoom control was added. Bitmap and PDF tabs show no viewer control; SVG keeps the menu with the plain-text choice; unclaimed binary containers show the empty state without reading. From open to first content every preview shows one centred icon-only spinner, and PDF pages appear as quiet placeholder blocks. The file tree comes back where the reader left it after any tab switch. Registry unit tests pin `binaryDocumentPath` matching and `register`'s rejection of a binary suffix outside `extensions`, toolbar tests pin the fallback/menu/empty-state branches, files-body tests pin scroll capture on unmount and restore across a remount, and the keyless Web document-preview scenario measures the width-fitted SVG against the pane and asserts the viewer control's absence per suffix.

+ 35 - 0
.agents/notes/implemented/feature/2026-09-11-sidebar-document-preview-polish.zh.md

@@ -0,0 +1,35 @@
+# Agent Note:侧边栏文档预览打磨
+
+Status: implemented
+
+[English](2026-09-11-sidebar-document-preview-polish.md) | 中文
+
+## 问题
+
+侧边栏文档预览积累了多处体验缺陷(issue #3974)。图片按固有 CSS 像素尺寸渲染,宽图撑出面板、只能横向滚动。查看器下拉总是追加纯文本兜底,位图和 PDF 文件因此提供一个结果是不可读字节的「纯文本」选项,而只有一个真实渲染器的文件仍显示一个没有可切换项的控件。完全没有渲染器的二进制容器(视频、压缩包、office 文档)落入纯文本读取器,呈现的是读取报错而非设计过的空态。各渲染器自带不同的 loading 文案和位置,打开文件时会闪过多个措辞、位置都不同的指示器。PDF 页面被双层内边距包裹,每一页都窄于面板宽度。切换侧栏 tab 会让文件树重挂载回到顶部,丢掉读者原来的位置。
+
+## 决定
+
+**图片宽度适配。** 图片外框跟随滚动容器的宽度并带 12px 内边距;图片本身使用 `max-width: 100%` 和 8px 圆角,宽图按纵横比缩小到面板宽度,小图保持固有尺寸并由 auto margin 居中,超高图在共享正文中纵向滚动。高度适配曾被考虑后放弃:它需要外框定高,且混合纵向场景会在没有用户诉求的情况下产生意外布局。
+
+**二进制查看器选项。** `DocumentPreviewDefinition` 新增可选的 `binaryExtensions`,即渲染器 `extensions` 中字节不可按文本阅读的后缀;`register` 拒绝不在 `extensions` 内的条目。这份知识由渲染器持有:图片正文声明 `png, jpg, jpeg, gif, webp, bmp, ico`,不含 `svg`,因为 SVG 源码是可读的 XML;PDF 正文声明 `pdf`。注册表模块的 `binaryDocumentPath` 判断是否有已注册定义把文件名后缀声明为二进制,预览 owner 对这类文件跳过纯文本兜底。头部仅在候选不少于两个时渲染查看器菜单;只剩一个候选时完全不渲染查看器控件。
+
+**不支持预览的空态。** owner 侧的 `document/unviewable.ts` 列出没有渲染器认领的二进制容器后缀(视频、音频、压缩包、office 文档、可执行文件、字体、磁盘镜像、设计格式)。owner 仅在没有实现匹配时查询该列表,因此任何渲染器注册始终优先;匹配的文件显示路径头部、文件类型图标和一行 `unsupportedFile` 说明,并且不会发起读取。不确定的后缀不进入列表、保留纯文本兜底;被文本认领但字节未通过读取器检查的文件通过 `error.notText` 报告同一句文案。
+
+**统一 loading。** `LoadingIndicator` 只渲染图标,标签作为 `aria-label` 携带、没有可见文字;所有渲染器的 loading 文案统一为共享的「正在读取…」。内容出现前的每个等待——owner 首次读取、PDF 解析、HTML 打包、图片解码——都把 spinner 居中在面板中,打开文件到正文出现始终是同一位置的一个 spinner。未渲染的 PDF 页以主题骨架色 `--dsw-alias-bg-skeleton` 上的静态 3:4 占位块保持位置、不带 spinner;文档打开的等待保持为 owner 的居中读取 spinner,而不是特化进第一页的占位块,因为读取 spinner 在占位块之前出现,两者交接会明显跳位。占位块不带扫光动画,其逐页动画成本高于价值。
+
+**PDF 铺满与文案。** 移除 PDF 正文和页面的内边距,页面贴边占满面板宽度;图片渲染器保留自己的 12px 内边距。预览各字典的中文报错与状态文案去掉句尾句号。
+
+**文件树滚动恢复。** `ui-sidebar-files` 沿用文档预览自己的模式:树存储新增 `scrollTop` 与 `scrolled` action,正文在本地记录自己的滚动偏移、只在卸载时且 owner 的 signal 仍存活的情况下提交一次,重挂载时在 layout effect 中恢复。已加载的层本就在存储中比正文活得久,因此重挂载的树在偏移落回之前已按完整高度布局。
+
+## 曾考虑的替代方案
+
+**按定义级布尔 `binary` 标志。** 图片渲染器在一个注册中同时覆盖位图和 SVG,二进制与否是后缀的属性,不是渲染器的属性。
+
+**在预览 owner 中按硬编码后缀列表过滤已注册渲染器。** owner 会重复各渲染器已持有的知识,外部渲染器也无法扩展该集合;owner 侧列表只服务于没有渲染器认领的后缀。
+
+**单项静态查看器标签。** 把唯一候选渲染为静态名称,会在头部放一个不提供任何操作的控件;它只是噪音,因此头部什么都不渲染。
+
+## 后果
+
+图片不再产生横向滚动;面板宽度是唯一布局输入,因此未增加缩放控件。位图和 PDF tab 不显示查看器控件;SVG 保留含纯文本选项的菜单;未被认领的二进制容器显示空态且不读取。从打开到首个内容,每种预览都只显示一个居中的纯图标 spinner,PDF 页面以安静的占位块出现。任何 tab 切换之后,文件树都回到读者离开的位置。注册表单元测试固定 `binaryDocumentPath` 的匹配行为与 `register` 对 `extensions` 之外二进制后缀的拒绝,工具栏测试固定兜底/菜单/空态分支,files-body 测试固定卸载时的滚动捕获与跨重挂载的恢复,keyless Web document-preview 场景按面板测量适配宽度后的 SVG 并按后缀断言查看器控件的有无。

+ 6 - 0
.agents/notes/implemented/feature/2026-09-14-docs-mermaid-viewer.i18n.yaml

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

+ 33 - 0
.agents/notes/implemented/feature/2026-09-14-docs-mermaid-viewer.md

@@ -0,0 +1,33 @@
+# Agent Note: Documentation Mermaid viewer
+
+Status: implemented
+
+English | [中文](2026-09-14-docs-mermaid-viewer.zh.md)
+
+## Problem
+
+Complex Mermaid diagrams lose readable detail when scaled to the documentation column. Long sequence diagrams need both magnification and movement to inspect interactions while retaining an overview.
+
+## Decision
+
+The [VitePress theme](../../../../website/.vitepress/theme/index.ts) adds a corner fullscreen icon to each rendered Mermaid SVG. A native modal dialog provides an inert background and Escape dismissal. Its visible title also names it for assistive technology; a missing or blank page heading uses the localized viewer title. A floating toolbar groups zoom controls, the current scale, and fit; close stays in the top corner, and help opens on demand. Keyboard focus cycles through all five buttons. Panzoom supplies pointer, wheel, and pinch interaction. Arrow keys pan in fixed screen distances. Closing restores the entry's focus without scrolling and restores the page's previous overflow setting.
+
+The viewer copies the SVG into a shadow root. Mermaid's embedded selectors and fragment IDs stay local to the copy, so its markers and styles cannot resolve against the original diagram. Panzoom transforms a viewport-sized canvas containing the SVG at its natural viewBox dimensions, so pointer coordinates and the transform origin share the same center; the initial scale and every resize fit the entire diagram without enlarging it beyond its natural size. This preserves vector detail while keeping fit independent of the narrow document column. The canvas has no visible frame; reserved space keeps controls clear of the fitted diagram.
+
+Viewer resources belong to the mounted theme. Route, language, theme, and source-SVG replacement close the active view; asynchronous Mermaid renders receive a fresh entry. The body overflow lock relies on the default theme retaining visible overflow on the HTML element. It avoids mutating HTML attributes, which the Mermaid plugin observes and rerenders in response. The [implementation](../../../../website/.vitepress/theme/mermaid-viewer.ts) leaves Markdown, raw page copies, and `llms.txt` generation with their existing owners.
+
+## Alternatives considered
+
+**Widening the document column.** A wider column cannot provide readable detail for arbitrarily large diagrams, and long diagrams still exceed the viewport.
+
+**A custom overlay with document-wide listeners.** A native dialog already makes the background inert and handles modal dismissal. Theme-owned resources make navigation and teardown explicit; persistent document listeners would require a separate lifetime mechanism.
+
+**A bitmap preview or a same-document SVG clone.** A bitmap loses vector detail at high zoom. A same-document clone duplicates Mermaid's IDs and embedded styles; rewriting all SVG and CSS references would add a parser obligation that a shadow root avoids.
+
+## Consequences
+
+The website gains Panzoom as a direct dependency and uses native dialog, shadow-root, and resize-observer support. Viewer zoom and position are transient: resizing refits the diagram, and navigating or changing the theme closes it. Existing page diagrams remain the reading and link-navigation source.
+
+The [focused tests](../../../../website/tests/mermaid-viewer.spec.ts) run in the root unit-test suite; `docs:check` and `doc-sync` also select them so documentation-only validation exercises the viewer. They cover late rendering, accessible titles, initial and resized fit, control wiring, keyboard cycling, replacement, and resource release.
+
+**CI coverage gap.** The DOM tests mock Panzoom and do not execute browser layout. Native modal behavior, SVG markers, pointer-anchored wheel zoom, canvas dragging, theme colors, and narrow-screen geometry require real-browser verification. The recorded browser demonstration supplies evidence for the current implementation, but it is not an automated regression check.

+ 33 - 0
.agents/notes/implemented/feature/2026-09-14-docs-mermaid-viewer.zh.md

@@ -0,0 +1,33 @@
+# Agent Note: 文档站 Mermaid 查看器
+
+Status: implemented
+
+[English](2026-09-14-docs-mermaid-viewer.md) | 中文
+
+## 问题
+
+复杂 Mermaid 图表缩放到文档正文列宽后,细节难以辨认。阅读长时序图需要在保留全图概览的同时放大和平移,以检查交互细节。
+
+## 决策
+
+[VitePress 主题](../../../../website/.vitepress/theme/index.ts) 在每张已渲染的 Mermaid SVG 角落增加全屏图标。原生模态对话框使背景不可交互,并支持 Escape 退出。可见标题也用于辅助技术识别对话框;页面标题缺失或为空白时,使用本地化的查看器标题。浮动工具栏集中显示缩放控件、当前比例和适应窗口操作;关闭按钮位于顶部角落,帮助按需展开。键盘焦点在五个按钮之间循环。Panzoom 提供指针、滚轮和双指交互。方向键按固定的屏幕距离平移。关闭时恢复入口焦点且不滚动页面,并恢复页面原有的 overflow 设置。
+
+查看器将 SVG 复制到 shadow root 中。Mermaid 内嵌的选择器和片段 ID 限定在副本内部,因此副本的标记和样式不会解析到原始图表上。Panzoom 变换与视口等大的画布,其中的 SVG 使用 viewBox 的自然尺寸,使指针坐标与变换原点共享同一个中心;初始缩放以及每次窗口尺寸变化都会适配整张图表,且不会放大到超出自然尺寸。这样既保留矢量细节,也使适配不受文档窄列宽度的影响。画布没有可见边框;预留空间使控件不会遮挡适配后的图表。
+
+查看器资源由已挂载的主题持有。路由、语言、主题和源 SVG 替换都会关闭当前视图;异步渲染的 Mermaid 图表会获得新的入口。body 的 overflow 滚动锁依赖默认主题让 HTML 元素保持 visible overflow。它避免修改 HTML 属性,因为 Mermaid 插件会观察这些属性并重新渲染。[实现](../../../../website/.vitepress/theme/mermaid-viewer.ts) 将 Markdown、原始页面副本和 `llms.txt` 的生成保留在原有归属处。
+
+## 考虑过的替代方案
+
+**加宽文档正文列。** 更宽的正文列无法让任意大小图表的细节都清晰可读,长图仍然会超出视口。
+
+**使用自定义遮罩和文档级监听器。** 原生对话框已经能使背景不可交互,并处理模态退出。由主题持有资源让导航和资源清理的职责明确;持久的文档级监听器则需要单独的生命周期机制。
+
+**位图预览或同文档内的 SVG 副本。** 位图在高倍率缩放下会丢失矢量细节。同文档内的副本会重复 Mermaid 的 ID 和内嵌样式;重写全部 SVG 和 CSS 引用会带来额外的解析职责,而 shadow root 可以避免这项职责。
+
+## 影响
+
+文档站增加了 Panzoom 直接依赖,并使用原生 dialog、shadow root 和 ResizeObserver 支持。查看器的缩放和平移状态是临时的:窗口尺寸变化会重新适配图表,导航或主题变化会关闭视图。原有页面图表仍然是阅读和链接导航的来源。
+
+[定向测试](../../../../website/tests/mermaid-viewer.spec.ts) 属于根单元测试集;`docs:check` 和 `doc-sync`(文档同步门禁)也会选中这些测试,使仅运行文档验证时同样覆盖查看器。它们验证延迟渲染、无障碍标题、初始及窗口变化后的适配、控件连接、键盘焦点循环、图表替换和资源释放。
+
+**CI 覆盖缺口。** DOM 测试模拟了 Panzoom,不执行浏览器布局。原生模态行为、SVG 标记、以指针为锚点的滚轮缩放、画布拖动、主题颜色和窄屏几何布局需要真实浏览器验证。浏览器演示记录提供了当前实现的证据,但不属于自动回归检查。

+ 6 - 0
.agents/notes/implemented/feature/2026-09-14-unattended-browser-terminal-reclamation.i18n.yaml

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

+ 56 - 0
.agents/notes/implemented/feature/2026-09-14-unattended-browser-terminal-reclamation.md

@@ -0,0 +1,56 @@
+# Agent Note: Two-hour reclamation of unattended browser terminals
+
+Status: implemented
+
+English | [中文](2026-09-14-unattended-browser-terminal-reclamation.zh.md)
+
+## Problem
+
+A browser can disappear without closing its terminal tabs. Keeping every abandoned terminal retains shells, descendant processes and screen buffers indefinitely. Output subscriptions do not identify abandonment: hidden tabs and inactive Sessions legitimately stop following the screen.
+
+## Decision
+
+A browser terminal is reclaimed only after no window holds it and the Host continuously confirms idle activity for two hours. Running work and unknown activity cancel the idle deadline. When work finishes, a fresh full grace period starts; command runtime and output silence never impose a deadline. Explicit tab close and Session or Host disposal retain their authority to terminate work.
+
+| Situation | Behavior |
+|---|---|
+| A connected window holds the tab, including a collapsed sidebar or inactive Session | Retain the terminal. |
+| One of several holding windows disappears | Retain while another window holds it. |
+| The final window disappears while a command is running, stopped, waiting for input, or running in the background | Preserve the work, regardless of runtime. |
+| No window holds a positively confirmed idle terminal | Start the full grace period and recheck before cleanup. |
+| A window returns before cleanup | Reattach the same process and cancel reclamation. |
+| The saved process is gone | Preserve the tab and show localized unavailability with a New terminal action. Only an explicit click replaces it in place with a fresh identity. |
+
+For example, a window disconnecting at 14:00 while a command runs until 20:00 cannot cause reclamation before 22:00. The deadline starts at the first subsequent confirmed idle observation.
+
+The [sidebar](../../../../packages/client/ui-sidebar-right/README.md#state) persists layout per Session and publishes `openTabs` metadata for saved and adopted Sessions. Startup discovery does not mount dormant content, pin files or activate Agents. Adopted stores are authoritative within their window; another window's storage writes cannot revoke those live holds. Permanent scope removal drops its metadata.
+
+The terminal provider intersects that inventory with its own saved terminal associations and unfinished close requests. Each window uses one `retain(sessionId, id, signal)` Remote stream per distinct terminal. Its acknowledgement grants a hold without screen output, Agent activation, input control or creation. Restored output waits for an acknowledged current hold. Transport generations cancel independently, and the existing Gateway reconnect and heartbeat mechanisms own transport liveness.
+
+The subprocess seam supplies `inspectActivity()` with a state and revision. Ordinary non-login Bash 4.4+ and Zsh launches support opt-in lifecycle records, combined with complete process-table observations and original process identities. Zsh distinguishes an empty top-level editor prompt from `vared`, selection and continuation input. Input invalidates prompt evidence; background and stopped descendants prevent idle. Native Linux also checks the systemd task count so escaped descendants still inside its owned scope remain protected. Custom traps, asynchronous Zsh descriptor handlers, unsupported launches and incomplete observations remain unknown. Lifecycle files are private and disappear after successful cleanup.
+
+The Host [terminal controller](../../../../packages/api/terminal-controller/README.md#use-this-package) owns configurable `unattendedTimeoutMs` (7200000), `activityPollIntervalMs` (30000) and `cleanupRetryMs` (60000). Zero disables automatic reclamation only. It uses a monotonic clock; stale observations after gaps longer than twice the polling interval reset the grace period. Input and hold changes invalidate in-flight observations. A successful final check marks the identity closed before asynchronous termination, preventing late creation and admission. Closed identities cannot be reused, and each cleanup remains attached to its original Session owner.
+
+Cleanup awaits provider quiescence and final screen output. Failure retains ownership, rejects new holders and schedules one retry without a new idle grace. Failed allocation cleanup follows the same retry policy. Owner disposal stops timers and streams and joins cleanup and observations before reporting failures, including when cleanup rejects before an observation settles. No Agent terminal tool, model input or Session event changes.
+
+Peer research on Codex thread unloading, OpenCode Location scopes and [VS Code PTY grace periods](https://github.com/microsoft/vscode/blob/main/src/vs/platform/terminal/node/ptyService.ts) informed separate references and delayed cleanup. The two-hour value is DSH product policy, not an industry default. The [browser-terminal decision](2026-09-09-web-sidebar-terminal.md) and [layout/provider recovery decision](../architecture/2026-09-14-sidebar-layout-provider-recovery.md) remain active because their resource ownership and persistence separation still apply.
+
+## Alternatives considered
+
+**Count output followers.** Switching tabs or Sessions stops output subscriptions without abandoning the corresponding terminal. Window holds track open layout membership instead.
+
+**Kill every disconnected terminal after two hours.** A hard deadline would kill legitimate long-running commands. Busy and unknown work therefore has no automatic maximum runtime.
+
+**Infer idle from silence, low CPU or foreground identity.** Silent builds, sleeping jobs, shell builtins and commands waiting for input can satisfy these signals without completing. Positive lifecycle evidence and owned-job observations are both required.
+
+**Restore only the visible Session or retain every saved association.** The first loses dormant holds after refresh; the second lets obsolete associations keep abandoned processes alive. The layout/provider intersection represents the window's actual tabs.
+
+## Consequences
+
+Idle abandoned terminals have bounded retention while connected windows and long-running work survive refreshes and presentation changes. Busy, hung or permanently serving commands may remain indefinitely; so may unsupported shells and uncertain process observations. Explicit close remains available. PowerShell, fish, Windows, custom shell arguments and sandbox-wrapped launches currently report unknown activity.
+
+Root-shell exit does not authorize killing descendants. Failure to enumerate Linux processes is an unavailable observation, not an empty process range; activity stays unknown and cleanup retains ownership. Empty native Linux ranges and complete empty Linux sessions can permit cleanup of exited records. macOS cannot confirm an unobserved range after root exit and may retain its record until explicit close or owner disposal. Existing provider limits on escaped, unobserved descendants remain; the lifecycle record is not a security barrier against hostile same-user processes.
+
+Browser storage failure leaves current memory state usable but cannot guarantee recovery of dormant Sessions after reload. Host restart cannot restore PTYs. Screen history and per-Session terminal quotas retain their existing bounds; no separate unbounded expiry-reason cache is added.
+
+Fake-clock tests cover deadlines, reconnect races, observation invalidation and cleanup retries. Real PTY tests cover silent commands, builtins, background and stopped jobs, Zsh editing modes, startup files and traps. Browser tests cover persistent layouts, multiple windows, collapsed reload, transport loss, retained process identities, explicit replacement of unavailable tabs and reconnection to the original process after transport loss. The keyless recorded Session scenario verifies file-preview layout recovery.

+ 56 - 0
.agents/notes/implemented/feature/2026-09-14-unattended-browser-terminal-reclamation.zh.md

@@ -0,0 +1,56 @@
+# Agent Note: Two-hour reclamation of unattended browser terminals
+
+Status: implemented
+
+[English](2026-09-14-unattended-browser-terminal-reclamation.md) | 中文
+
+## 问题
+
+浏览器可能在没有关闭终端标签页的情况下消失。如果始终保留无人使用的终端,shell、后代进程和屏幕缓冲区会无限期占用资源。输出订阅不能代表是否遗弃:隐藏标签和非当前 Session 合理地停止跟随屏幕。
+
+## 决策
+
+只有没有任何窗口持有终端,并且 Host 持续确认空闲满两小时,才回收浏览器终端。运行中的工作和未知活动都会取消空闲截止时间。工作完成后重新给予完整宽限期;命令执行时长和无输出时间都不是截止条件。明确关闭标签,以及 Session 或 Host 卸载,仍有权终止工作。
+
+| 情况 | 行为 |
+|---|---|
+| 已连接窗口持有标签,包括侧栏折叠或非当前 Session | 保留终端。 |
+| 多个持有窗口中的一个消失 | 只要还有其他窗口持有,就继续保留。 |
+| 最后一个窗口消失时,命令仍在运行、停止、等待输入或后台执行 | 保留工作,不限制执行时长。 |
+| 无窗口持有,且明确确认终端空闲 | 开始完整宽限期,清理前再次检查。 |
+| 窗口在清理前返回 | 重连原进程,取消回收。 |
+| 保存的进程已经不存在 | 保留标签,显示本地化的不可用提示和“新建终端”。只有明确点击后,才在原位置用全新身份替换。 |
+
+例如,窗口在 14:00 断开,而命令持续到 20:00,则最早也不能在 22:00 前回收。期限从之后第一次确认空闲的观察开始计算。
+
+[侧栏](../../../../packages/client/ui-sidebar-right/README.zh.md#state) 按 Session 持久化布局,并通过 `openTabs` 发布已保存和已采用 Session 的标签元数据。启动发现不会挂载非当前内容、pin 文件或激活 Agent。窗口内以已采用的 store 为准;其他窗口对 storage 的写入不能撤销当前窗口的活动持有关系。永久删除 scope 时移除相应元数据。
+
+终端 provider 将该清单与自己保存的终端关联及未完成的关闭请求取交集。每个窗口为每个不同终端使用一条 `retain(sessionId, id, signal)` Remote 流。确认帧授予持有关系,不发送屏幕输出、不激活 Agent、不接管输入,也不创建进程。恢复输出前等待当前持有关系确认。传输代次独立取消,传输存活由现有 Gateway 重连和心跳机制负责。
+
+subprocess 能力接口提供带状态和 revision 的 `inspectActivity()`。普通非登录的 Bash 4.4+ 和 Zsh 支持显式启用生命周期记录,并结合完整进程表观察和原始进程身份。Zsh 区分空的顶层编辑提示符与 `vared`、选择及续行输入。输入使提示符证据失效;后台和停止的后代阻止空闲判断。原生 Linux 还检查 systemd task 数,保护脱离进程树但仍在自有 scope 中的后代。自定义 trap、Zsh 异步文件描述符 handler、不支持的启动方式和不完整观察保持 unknown。生命周期文件为私有文件,在清理成功后删除。
+
+Host [终端控制器](../../../../packages/api/terminal-controller/README.zh.md#use-this-package) 管理可配置的 `unattendedTimeoutMs`(7200000)、`activityPollIntervalMs`(30000)和 `cleanupRetryMs`(60000)。零只禁用自动回收。控制器使用单调时钟;观察间隔超过轮询周期两倍时,旧证据失效并重置宽限期。输入和持有关系变化会使正在进行的观察失效。最终检查通过后,在异步终止前将身份标记为关闭,阻止迟到的创建和连接。已关闭身份不能复用,每个清理任务始终属于原来的 Session owner。
+
+清理等待 provider 范围完全停稳和最终屏幕输出排空。失败时保留所有权、拒绝新持有关系,并安排一次重试,不重新给予空闲宽限期。分配失败后的清理采用相同重试策略。owner 卸载会停止计时器和流,在清理与观察都结束后才报告失败,包括清理早于观察结束而拒绝的情况。Agent 终端工具、模型输入和 Session 事件均不改变。
+
+Codex 线程卸载、OpenCode Location scope 和 [VS Code PTY 宽限期](https://github.com/microsoft/vscode/blob/main/src/vs/platform/terminal/node/ptyService.ts) 的调研支持了独立引用和延迟清理的设计。两小时是 DSH 的产品策略,不是行业默认值。[浏览器终端决策](2026-09-09-web-sidebar-terminal.zh.md) 与[布局/provider 恢复决策](../architecture/2026-09-14-sidebar-layout-provider-recovery.zh.md) 继续有效,因为其资源所有权和持久化职责划分仍然适用。
+
+## 考虑过的替代方案
+
+**统计输出订阅者。** 切换标签或 Session 会停止输出订阅,但并未放弃对应终端。窗口持有关系改为追踪布局中打开的标签。
+
+**断开满两小时就杀掉所有终端。** 固定截止时间会杀掉合理的长命令。因此运行中和未知状态的工作没有自动最长执行时间。
+
+**根据静默、低 CPU 或前台身份推断空闲。** 静默构建、休眠任务、shell 内建命令及等待输入的命令都可能满足这些条件却尚未完成。必须同时具有正面的生命周期证据和自有任务观察。
+
+**只恢复可见 Session,或保留每个保存的关联。** 前者在刷新后丢失非当前 Session 的持有关系;后者让过时关联保活无人使用的进程。布局与 provider 的交集才代表该窗口实际打开的标签。
+
+## 结果与代价
+
+无人使用的空闲终端具有保留期限,而已连接窗口和长任务能跨刷新及呈现变化继续存在。运行中、挂起或长期提供服务的命令可能无限期保留;不支持的 shell 和不确定的进程观察也是如此。仍可明确关闭。PowerShell、fish、Windows、自定义 shell 参数以及经过 sandbox 包装的启动当前返回 unknown。
+
+根 shell 退出不授予终止后代的权限。Linux 进程枚举失败表示观察不可用,而不是进程范围为空;活动保持 unknown,清理保留所有权。明确为空的原生 Linux 范围和完整且为空的 Linux 会话允许清理已退出记录。macOS 无法在根进程退出后确认未观察到的范围,可能保留记录直到明确关闭或 owner 卸载。provider 对已逃逸且未观察到的后代的限制仍然存在;生命周期记录不是用于防御同一用户恶意进程的安全屏障。
+
+浏览器 storage 失败时,当前内存状态仍可使用,但不能保证刷新后恢复非当前 Session。Host 重启不能恢复 PTY。屏幕历史和每个 Session 的终端配额保持原有上限,不新增无限增长的到期原因缓存。
+
+虚拟时钟测试覆盖截止时间、重连竞争、观察失效和清理重试。真实 PTY 测试覆盖静默命令、内建命令、后台和停止任务、Zsh 编辑模式、启动文件及 trap。浏览器测试覆盖持久化布局、多窗口、折叠后刷新、传输中断、保留的进程身份、明确操作后替换失效标签,以及传输中断后重连原进程。无需密钥的已记录 Session 场景验证文件预览布局恢复。

+ 2 - 2
.agents/notes/implemented/process/2026-08-10-npm-release-sequences.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/process/2026-08-10-npm-release-sequences.md
-2026-08-10-npm-release-sequences.md: 77b73b8c461238ad80afd7abd609a68df0bcf058
-2026-08-10-npm-release-sequences.zh.md: f56234fc50d776658d2d9191056b0b04e1609f1e
+2026-08-10-npm-release-sequences.md: 6f52c00a31870939981994965b70504e8f8143ca
+2026-08-10-npm-release-sequences.zh.md: effa5538cefbc8f25b2e047ee40e0dc7306bb2fb

+ 2 - 0
.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md

@@ -96,6 +96,8 @@ A dependency in `optionalDependencies`, or a peer carrying `peerDependenciesMeta
 
 A violation names the package, the declaration that made it optional, and the way out in order — import it as a type, which is all that declaration merging needs, or restructure so module scope does not need the package. A dynamic `import()` only moves the failure to first use, so it belongs to a caller that genuinely requires the package and handles its absence; reaching for it is a sign the dependency is not optional, and the gate does not offer it as the remedy.
 
+A required CommonJS-compatible Host dependency whose initialization is unrelated to startup may use `createLazyRequire(specifier, import.meta.url)`. The caller keeps a type-only import, supplies a literal dependency specifier, and invokes the returned loader at the owning operation. `verify-package-dependencies` recognizes that literal as a Host runtime edge, so Client/Host packages retain it in `dependencies` even though no static value import remains. The utility caches only a successful load and preserves caller-relative resolution; it does not make an optional dependency required or hide first-use failure.
+
 ### Release family objects
 
 The entity in this domain is a **release family**: a set of packages sharing one version baseline and tag naming that publishes as a unit. Adding a family means adding a subclass and a workflow lane, not changing the core.

+ 2 - 0
.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md

@@ -96,6 +96,8 @@ registry 的两个行为决定了「怎么尝试一次发布」。写入之间
 
 报错会点名这个包、点名是哪条声明把它标成 optional 的,并按顺序给出出路——把它作为类型引入(声明合并需要的仅此而已),或者调整写法让模块作用域不再需要这个包。动态 `import()` 只是把失败推迟到首次使用,它属于那种确实需要这个包、并且自己处理缺失的调用方;会想到它,往往说明这个依赖并不 optional,所以门禁不把它作为解法给出。
 
+初始化与启动无关、且兼容 CommonJS 的必需 Host 依赖可以使用 `createLazyRequire(specifier, import.meta.url)`。调用方保留 type-only import,传入字面量依赖 specifier,并在所属操作中调用返回的 loader。`verify-package-dependencies` 会把该字面量识别为 Host runtime edge,因此 Client/Host 包即使没有静态值 import,仍会把它保留在 `dependencies`。该工具只缓存成功加载,并保留调用方相对解析;它不会把 optional 依赖变成必需依赖,也不会隐藏首次使用失败。
+
 ### 发布族对象
 
 这个领域里的实体是**发布族**:一组共享版本基线与 tag 命名、可整体发布的包。新增一族等于加一个子类和一条 workflow lane,不改核心。

+ 6 - 0
.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.md
+2026-09-14-composer-model-and-draft-editor.md: 3f66ab2c73da0a8f66bf43a821cbe5f85363867a
+2026-09-14-composer-model-and-draft-editor.zh.md: aa040b0fa68961d4d5884d73791c4986b8bfab50

+ 116 - 0
.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.md

@@ -0,0 +1,116 @@
+# Agent Note: Two-stage Composer and DraftEditor isolation
+
+Status: proposed
+
+English | [中文](2026-09-14-composer-model-and-draft-editor.zh.md)
+
+## Problem
+
+One Client needs to edit the same Session's draft and pending attachments in multiple views. A Lexical editor binds only one DOM root; multiple presentation locations need multiple editor instances, but must not own unrelated drafts or upload tasks, or make the Session Controller understand carets, composition, or DOM state.
+
+The current [SessionInputShell](../../../../packages/client/ui-conversation/src/client/input/facade.ts) combines Lexical operations, draft projection, the submission state machine, and failure recovery. [InputBar](../../../../packages/client/ui-conversation/src/client/skeleton/InputBar.tsx) combines editor presentation, DOM bindings, attachment intake, and submission controls. Implementing multiple instances directly in these files would mix code extraction with behavior changes.
+
+[ConversationController](../../../../packages/client/ui-conversation/src/client/service.ts) already owns attachment entities and upload tasks centrally; the shell retains only ordered attachment IDs. Selecting a skill inserts ordinary `/name` text whose highlighting derives from a lexicon; atomic file and Session references use chips carrying source identity. A shared draft must not lose these references by synchronizing text alone, and does not require copying attachment entities.
+
+This proposal details editor isolation for [#3951](https://github.com/deepseek-ai/deepseek-harness/pull/3951), following [Client Session and UI ownership](../../implemented/architecture/2026-08-20-client-session-conversation-ownership.md). The Session activity view, residency states, and eviction policy are designed independently; [#4138](https://github.com/deepseek-ai/deepseek-harness/pull/4138) is only a Host lifecycle reference. This proposal implements none of those features and does not repeat the Conversation component decomposition in #3984.
+
+## Proposal
+
+Use two independent PRs. Stage one concentrates existing editor implementation into explicit locations for behavior changes; stage two changes behavior only. `DraftEditor` names the draft-editing area, while Composer names the complete writing area including attachments and submission controls. Keep `input/`, `skeleton/`, `InputBar`, `InputHub`, and `SessionInputShell`; directory moves and renames are not refactoring deliverables.
+
+### Stage one: five mechanical responsibility extractions
+
+The ui-conversation paths below are relative to `packages/client/ui-conversation/src/client/`. Every new file must contain logic already executed today, not placeholder interfaces or future features.
+
+| Original location | Extraction destination | Location for later behavior changes |
+|---|---|---|
+| Lexical creation, registration, projection, node operations, and cleanup in `input/facade.ts` | `input/editor/runtime.ts` | One editor's implementation and its creation, binding, and disposal |
+| Text-area JSX in `skeleton/InputBar.tsx` | `input/editor/DraftEditor.tsx` | One editor's presentation, excluding the attachment rail and submission orchestration |
+| Focus, selection reveal, wheel, keymap, and picker binding functions in `InputBar.tsx` | `input/editor/view-binding.ts` | DOM interaction and editor bindings for one mounted view |
+| Range, reference, and keyboard interface types in `contract/input.ts` | `contract/draft-editor.ts` | Editor-facing data and operation types; submission and shared state stay in the original file |
+| Document drop effect implementation in `ui-attachment/src/client/ComposerAttachments.tsx` | `ui-attachment/src/client/drop-events.ts` | Document drag-and-drop registration, routing, and cleanup |
+
+The existing shell creates and delegates to the internal object in `runtime.ts`. That object retains the original editor, NodeKey map, projection, and Lexical registrations; it neither copies those states nor independently decides whether editing or submission is permitted. The shell retains SubmitMachine, draft revision, attachment IDs, notices, attempts, serialization, and success/failure recovery decisions.
+
+Methods combining guards and node operations retain their guards at the original location. For example, beginCommand keeps its span/phase checks, node replacement, and machine dispatch in the same order; failure recovery preserves batch ordering, revision guards, restoration flags, and history cleanup timing. Editor updates still call the shell synchronously at the original publication point, without another Promise, effect, or notification turn.
+
+`DraftEditor.tsx` extracts presentation without adding a DOM wrapper. All existing React hooks, refs, dependency arrays, and relative effect order remain in InputBar; effects delegate to ordinary functions at their original call sites. CSS files, class keys, React keys, placeholder order, and decorator order remain unchanged. The new component does not take over editor creation or hold another draft.
+
+`contract/draft-editor.ts` receives `TokenSpan`, `ReferenceInsert`, `ArbitrateKey`, `ArbitrateOutcome`, `ComposerKeyboard`, `EditSelection`, and `Occurrence`. Names and members remain unchanged, consumers import from the actual declaration owner, and existing public exports retain their names and visibility. `ComposerKeyboard` temporarily still depends on shared `InputState`; this is not an independent controlled-editor protocol.
+
+#### Stage-one invariants
+
+- InputHub still creates one shell and one editor per Session, with unchanged creation, reuse, and disposal timing and counts.
+- Lexical remains the draft authority; Undo/Redo, NodeKey identity, span checks, and revision rules remain unchanged, without a second document or store.
+- `useInput`, `inputActions`, Slots, events, inject declarations, and public APIs retain their names, payloads, and behavior; Host protocols and persistence formats do not change.
+- Attachment selection, upload timing, image previews, submission batches, success clearing, failure restoration, and notice rules remain unchanged.
+- Each original component still registers document drop listeners in the same effect; the single picker, duplicate drop, and single editor/root limitations remain.
+- Tests change only type imports that actually need updating; test filenames, assertions, recorded Sessions, and expected outputs remain unchanged, without snapshot refreshes.
+- No existing file moves, existing private-name changes, CSS changes, new packages, dependencies, renderer scopes, or general state framework.
+
+#### Isolation actually achieved in stage one
+
+| Subject | Result |
+|---|---|
+| Editor implementation | Node and projection operations belong to runtime, presentation to DraftEditor, and DOM bindings to view-binding |
+| Submission and attachment orchestration | Still owned by the original shell and ConversationController, not DraftEditor |
+| State across different Sessions | Remains isolated under existing rules |
+| Shared state within one Session | Still reuses the original shell, without duplicate attachments or uploads |
+| Independent editors within one Session | Not implemented; views still share one Lexical editor |
+| Independent selection, IME, Undo, and menu origins | Not implemented; separating code does not change runtime ownership |
+| Multi-view picker, focus, and document drop routing | Not implemented; binding code has a separate location for modification |
+
+### Stage two: behavior changes only
+
+Stage two implements a shared draft and multiple editors directly in the locations above. It must not move existing files or directories, perform pure renames or helper/class/component extractions, reorder existing tests, or clean up formatting or comments. New types, implementations, and tests required by new behavior may be added, but copying old code into a new file and deleting its original does not evade this restriction.
+
+If behavior implementation still needs structural preparation, complete stage one first: amend its PR before merge, or add a separate mechanical prerequisite PR after merge. The behavior PR uses that mechanical result as its base and cannot include the preparation.
+
+#### Final state ownership
+
+The shared Composer model evolves the responsibilities of the existing SessionInputShell without requiring another rename. The Session Controller continues to own only Session business state and does not import DraftEditor, Lexical, or the shared draft document.
+
+| State | Final owner | Multi-view requirement |
+|---|---|---|
+| Draft text, semantic references, and content revision | Session-associated shared Composer model | Publish edits from either view to every view through one reactive source |
+| Ordered attachment IDs, claims, submission attempts, and failure recovery | Shared Composer model | Settle each submission once; operations in either view affect the same pending input |
+| File, Blob URL, upload tasks, progress, and receipts | Existing attachment owner | Do not copy per view; unmounting one view does not cancel resources used by another |
+| Lexical, DOM, NodeKey mappings, selection, and IME preedit | Each DraftEditor instance | Two independent editors/roots; unmounting one does not detach the other |
+| Menu anchor, file dialog, and focus | Initiating view | Route by operation origin, not a single Session picker |
+| Session history, running, and queue | Session Controller | Keep reading existing sources instead of copying them into the draft model |
+
+The renderer still binds React hooks from bare observables, and business components read and write through existing standard props. The shared model accepts neither DOM, Lexical NodeKeys, nor composition intermediate state; DraftEditor receives neither Session/Context nor upload services, only draft data, presentation data, and editing/intent callbacks.
+
+#### Shared content and synchronization requirements
+
+Draft content must represent ordinary text, newlines, and atomic references with complete `ReferenceInsert` information independently of Lexical. Shared reference identity must not depend on one editor's NodeKey; each instance privately maps it to its own nodes. Skills remain ordinary `/name` text, with both views deriving highlights from the same text and lexicon, without an extra selected-skill list or changes to Host recognition.
+
+Draft text is small, so synchronization may use complete semantic documents without requiring a collaborative-editing algorithm. The shared model accepts edits, assigns revisions, and publishes; editors distinguish local changes from external rendering to avoid feedback loops. Callbacks from stale revisions, prior model lifetimes, or unmounted views must not overwrite current content. Submission freezing, success clearing, failure restoration, and attachment changes must reach all views through the same shared source.
+
+IME preedit belongs to the local instance, and updates from another view must not directly disrupt text under composition. Stage two must define and verify how another view's edits, submission clearing, and model release interact with composition. Undo/Redo must also operate on one logical draft, rather than letting two Lexical histories restore stale whole documents over each other; synchronization and history implementation are outside the mechanical stage.
+
+Programmatic insertion, menu selection, file selection, and focus restoration need the initiating view's temporary identity. Closing that view must not redirect late UI actions into another view of the same Session. Document drop must select one explicit target and process the drop exactly once; origin routing and deduplication are stage-two behavior.
+
+Existing text-draft restoration after refresh must remain, without implicitly promising persistence for structured references, File objects, or cross-browser collaboration. The Session activity view and LRU/timeout policy remain independent of this editing protocol.
+
+## Alternatives considered
+
+**Only rename input or relocate it to composer.** This does not separate Lexical operations, view bindings, and submission decisions; behavior implementation would still need to extract old code from large files, so it is not a stage-one deliverable.
+
+**Bind one Lexical editor to two DOM roots.** This conflicts with Lexical's single-root model; copying React presentation does not create two independently interactive editors.
+
+**Give each Composer an independent draft and attachments.** This fails the same-Session shared-editing requirement and introduces conflicting attachment and submission ownership.
+
+**Implement a shared DraftDocument, Undo, or drop deduplication in the mechanical stage.** This changes authority, lifecycle, or event-processing counts and cannot be reviewed as behavior-preserving preparation.
+
+## Acceptance criteria
+
+Stage one completes the five extractions and required imports, JSDoc, and README updates; review compares original method bodies, branches, callback order, hooks, DOM, and cleanup. Existing editing, reference, claim, attachment, submission, failure-restoration, and unmount tests continue to pass; focused browser regressions run against built artifacts with unchanged expected output. Type and documentation checks cover relocated declarations and bilingual pairs. New dual-instance functionality is not a stage-one acceptance condition.
+
+Stage two uses two genuinely mounted Composers for one Session to verify bidirectional text and chip synchronization, skill highlights, shared attachments and progress, submission clearing/failure restoration, IME/Undo, origin routing, and continued operation after either view unmounts. Its diff contains behavior implementation and corresponding tests only, without mechanical cleanup.
+
+## Risks
+
+Even stateless JSX extraction can alter ref or effect timing; therefore hooks and refs retain their host, and DOM gains no wrapper. Lexical extraction can alter nested updates, projection caching, or history cleanup order; therefore retain original operation bodies and compare execution order instead of rewriting algorithms.
+
+Stage one still cannot mount two editors for one Session and retains the existing picker/drop limitations. Confusing directory isolation with state isolation could cause roots to detach each other, duplicate attachment intake, or misroute focus; stage-two dual-instance behavior tests must close these gaps.

+ 116 - 0
.agents/notes/proposed/architecture/2026-09-14-composer-model-and-draft-editor.zh.md

@@ -0,0 +1,116 @@
+# Agent Note: Composer 与 DraftEditor 隔离的两阶段重构
+
+Status: proposed
+
+[English](2026-09-14-composer-model-and-draft-editor.md) | 中文
+
+## 问题
+
+同一个 Client 需要在不同视图中编辑同一个 Session 的草稿和待发送附件。Lexical 的一个 editor 只能绑定一个 DOM root;多个呈现位置需要多个编辑实例,但不能各自拥有互不相干的草稿和上传任务,也不能让 Session Controller 理解光标、输入法或 DOM。
+
+当前 [SessionInputShell](../../../../packages/client/ui-conversation/src/client/input/facade.ts) 同时包含 Lexical 操作、草稿投影、提交状态机和失败恢复。[InputBar](../../../../packages/client/ui-conversation/src/client/skeleton/InputBar.tsx) 同时包含编辑区呈现、DOM 绑定、附件入口和发送控件。直接在这两个文件中实现多实例会让代码提取与行为差异混在一起。
+
+附件实体和上传任务已经由 [ConversationController](../../../../packages/client/ui-conversation/src/client/service.ts) 集中管理,shell 只保留有序附件 IDs。skill(技能)选择插入普通 `/name` 文本,高亮由词表派生;文件和 Session 的原子引用则使用带来源身份的 chip。共享草稿不能只同步文字而丢失这些引用,也不需要复制附件实体。
+
+本提案细化 [#3951](https://github.com/deepseek-ai/deepseek-harness/pull/3951) 的编辑器隔离,遵循 [Client Session 与 UI 所有权](../../implemented/architecture/2026-08-20-client-session-conversation-ownership.zh.md)。Session 活跃视图、驻留状态与回收策略独立设计;[#4138](https://github.com/deepseek-ai/deepseek-harness/pull/4138) 仅作为 Host 生命周期参考。本提案不实现这些功能,也不重复 #3984 的 Conversation 组件拆分。
+
+## 提案
+
+采用两个独立 PR(Pull Request)。第一阶段集中既有编辑实现,为行为改动准备明确位置;第二阶段只修改行为。`DraftEditor` 专指草稿编辑区,Composer 指包含附件和发送控件的完整编写区域。保留 `input/`、`skeleton/`、`InputBar`、`InputHub` 和 `SessionInputShell`,不以改目录或改名作为重构成果。
+
+### 第一阶段:五处机械职责提取
+
+以下 ui-conversation 路径相对 `packages/client/ui-conversation/src/client/`。每个新文件必须承载当前已经执行的逻辑,不建立占位接口或未来功能。
+
+| 原位置 | 提取位置 | 后续行为修改的落点 |
+|---|---|---|
+| `input/facade.ts` 的 Lexical 创建、注册、投影、节点操作和清理 | `input/editor/runtime.ts` | 单个 editor 的实现及其创建、绑定和释放 |
+| `skeleton/InputBar.tsx` 的文字区域 JSX | `input/editor/DraftEditor.tsx` | 单份编辑区的呈现,不包含附件栏和提交编排 |
+| `InputBar.tsx` 的 focus、selection reveal、wheel、keymap、picker 绑定函数 | `input/editor/view-binding.ts` | 单个挂载视图的 DOM 交互和编辑器绑定 |
+| `contract/input.ts` 的范围、引用、键盘接口类型 | `contract/draft-editor.ts` | 编辑器对外数据与操作类型;提交和共享状态仍留原文件 |
+| `ui-attachment/src/client/ComposerAttachments.tsx` 的 document drop effect 实现 | `ui-attachment/src/client/drop-events.ts` | document 拖放监听的注册、路由和清理 |
+
+`runtime.ts` 内部对象由现有 shell 创建并委托调用。它保管原 editor、NodeKey 映射、投影和 Lexical 注册;不复制这些状态,也不独立决定能否编辑或提交。shell 继续持有 SubmitMachine、draft revision、附件 IDs、通知、attempt、序列化以及成功/失败恢复决策。
+
+同时涉及判断和节点操作的方法在原位置保留判断。例如 beginCommand 的 span/phase 检查、节点替换、machine dispatch 的顺序不变;失败恢复的批次排序、revision 保护、恢复标志和 history 清理时点不变。editor 更新仍在原同步位置回调 shell 发布状态,不增加 Promise、effect 或通知轮次。
+
+`DraftEditor.tsx` 是无额外 DOM 包装的呈现提取。所有既有 React 钩子、refs、依赖数组和 effect 相对顺序仍留在 InputBar;effect 只在原调用位置委托普通函数。CSS 文件、class keys、React keys、placeholder 与 decorator 顺序均不变。新组件不接管 editor 创建或持有另一份草稿。
+
+`contract/draft-editor.ts` 移入 `TokenSpan`、`ReferenceInsert`、`ArbitrateKey`、`ArbitrateOutcome`、`ComposerKeyboard`、`EditSelection` 和 `Occurrence`。名称和成员不变,所有消费方从实际声明处导入;既有公开出口保持原名称和可见集合。`ComposerKeyboard` 仍暂时依赖共享 `InputState`,这不是独立受控编辑协议。
+
+#### 第一阶段不变项
+
+- InputHub 仍按 Session 创建一个 shell 和一个 editor,创建、复用、dispose(资源释放)的时点及次数不变。
+- 草稿真值仍在 Lexical,Undo/Redo、NodeKey 身份、span 检查和 revision 规则不变;不增加第二份文档或存储。
+- `useInput`、`inputActions`、Slot、事件、inject 及公开 API 的名称、载荷和行为不变;不改 Host 协议和持久化格式。
+- 附件选择、上传时机、图片预览、提交批次、成功清空、失败恢复及通知规则不变。
+- document drop 仍在每个原组件的同一 effect 中注册;单 picker、重复 drop、单 editor/root 的限制原样保留。
+- 测试只修改实际需要的类型导入;不改测试文件名、断言、录制 Session 或预期输出,不刷新快照。
+- 不搬已有文件,不改已有私有名字,不改 CSS,不增包、依赖、renderer scope 或通用状态框架。
+
+#### 第一阶段实际隔离程度
+
+| 内容 | 完成后的状态 |
+|---|---|
+| 编辑器实现 | 节点和投影操作归 runtime,呈现归 DraftEditor,DOM 绑定归 view-binding |
+| 提交与附件编排 | 仍由原 shell 和 ConversationController 管理,不落入 DraftEditor |
+| 不同 Session 的状态 | 继续按原规则隔离 |
+| 同 Session 的共享状态 | 继续复用原 shell;没有双份附件或上传任务 |
+| 同 Session 的独立编辑实例 | 未实现,仍共享一个 Lexical editor |
+| 独立 selection、IME、Undo 和菜单来源 | 未实现;代码位置分开不代表运行时归属已改变 |
+| picker、focus、document drop 的多视图路由 | 未实现;绑定代码已有单独修改位置 |
+
+### 第二阶段:只调整行为
+
+第二阶段直接在上述位置实现共享草稿和多编辑实例。禁止移动已有文件或目录、纯改名、纯提取 helper/class/component、重排已有测试,以及格式或注释清理。新增行为需要的新类型、实现和测试可以增加,但不得复制旧代码到新文件后删除原文来规避约束。
+
+如果行为实现仍需要结构准备,必须先补第一阶段:未合入时修改第一阶段 PR;已合入时单独增加机械前置 PR。行为 PR 以该机械结果为 base,不能夹带机械准备。
+
+#### 最终状态归属
+
+共享 Composer 模型是现有 SessionInputShell 的职责演进,不要求再次改名。Session Controller 继续只管理会话业务,不 import DraftEditor、Lexical 或共享草稿文档。
+
+| 状态 | 最终 owner | 多视图要求 |
+|---|---|---|
+| 草稿正文、语义引用、内容 revision | Session 关联的共享 Composer 模型 | 任一处编辑后,通过同一个响应式来源发布给所有视图 |
+| 有序附件 IDs、认领、提交 attempt 和失败恢复 | 共享 Composer 模型 | 每个提交只结算一次,任一视图的操作作用于同一批输入 |
+| File、Blob URL、上传任务、进度和凭证 | 既有附件管理 owner | 不随视图复制;卸载一个视图不取消其他视图所用资源 |
+| Lexical、DOM、NodeKey 映射、selection、IME preedit | 各 DraftEditor 实例 | 两个独立 editor/root;卸载一处不解绑另一处 |
+| 菜单锚点、文件对话框和焦点 | 发起操作的视图 | 按操作来源路由,不以 Session 唯一 picker 代替来源 |
+| Session 历史、running、queue | Session Controller | 继续读取现有来源,不复制进草稿模型 |
+
+React 钩子仍由 renderer 从裸 observable 绑定,业务组件通过现有标准 props 读写。共享模型不接收 DOM、Lexical NodeKey 或输入法中间态;DraftEditor 不接收 Session/Context 或上传服务,只接收草稿数据、显示数据与编辑/意图回调。
+
+#### 共享内容和同步要求
+
+草稿内容必须能独立于 Lexical 表示普通文本、换行和带完整 `ReferenceInsert` 信息的原子引用。引用的共享身份不能依赖某个 editor 的 NodeKey;各实例私有映射到自己的节点。skill 保持普通 `/name` 文本,两处从同一文本和词表派生高亮,不引入额外的已选 skill 列表或改变 Host 识别规则。
+
+草稿文本量小,可以使用完整语义文档同步,不要求协同编辑算法。共享模型负责接受编辑、分配 revision 和发布,编辑器区分本地产生的变化与外部呈现,避免回声循环。旧 revision、旧模型生命周期和已卸载视图的回调不能覆盖新内容。提交冻结、成功清空、失败恢复和附件变化必须通过同一共享来源到达所有视图。
+
+IME preedit 属于本地实例,远端视图更新不能直接破坏正在组合的文本。来自另一视图的修改、发送清空和模型释放如何与组合态相遇,必须在第二阶段定义并验证。Undo/Redo 也必须作用于同一份逻辑草稿,不能让两份 Lexical history 互相恢复陈旧整篇文档;具体同步与历史实现不属于机械阶段。
+
+程序化插入、菜单选择、文件选择器和焦点恢复要携带发起视图的临时身份。视图关闭后不能把迟到的 UI 操作转发给另一个同 Session 视图。document drop 必须明确一次拖放选哪个目标并保证仅处理一次;来源路由和去重均是第二阶段行为。
+
+刷新后的既有文字草稿恢复应保留,但不默认新增结构化引用、File 或跨浏览器协作的持久化承诺。Session 活跃视图及 LRU/时限策略不与这份编辑协议绑定。
+
+## 考虑过的替代方案
+
+**仅把 input 改名或平移到 composer。** 不能分离 Lexical 操作、视图绑定和提交决策,后续行为实现仍需从大文件中提取旧代码,因此不作为第一阶段成果。
+
+**让一个 Lexical editor 同时挂两个 DOM root。** 与 Lexical 的单 root 模型冲突,不能用 React 复制呈现来获得两个可独立交互的编辑器。
+
+**每个 Composer 独立草稿和附件。** 不满足同一 Session 共享编辑的要求,还会引入附件和提交所有权分歧。
+
+**机械阶段直接实现共享 DraftDocument、Undo 或 drop 去重。** 改变真值、生命周期或事件处理次数,无法作为行为不变的前置改动审查。
+
+## 验收标准
+
+第一阶段必须完成五处提取及必要导入、JSDoc 和 README 更新;逐项核对原方法体、分支、回调次序、Hooks、DOM 和清理。现有编辑、引用、认领、附件、提交、失败恢复和卸载测试继续通过;用构建产物运行针对性浏览器回归,预期输出不变。类型和文档检查覆盖移动后的声明与双语配对。不以新双实例功能作为第一阶段验收条件。
+
+第二阶段必须用同一 Session 的两个真实挂载 Composer 验证双向文字和 chip 同步、skill 高亮、共享附件和进度、发送清空/失败恢复、IME/Undo、来源路由,以及任一视图卸载后另一处继续工作。其差异必须仅包含行为实现及相应测试,不包含机械整理。
+
+## 风险
+
+无状态 JSX 提取仍可能改变 ref 或 effect 时序;因此 Hook 和 ref 的宿主保持不变,DOM 不增加包装。Lexical 提取可能改变嵌套 update、projection 缓存或 history 清理顺序;因此保留原操作体并对照执行次序,而非重写算法。
+
+第一阶段仍不能同时挂载同 Session 的两个编辑器,且保留原 picker/drop 限制。后续若误把目录隔离当成状态隔离,会造成 root 相互解绑、重复附件接收或错误焦点路由;这些限制必须由第二阶段的双实例行为测试关闭。

+ 2 - 0
THIRD_PARTY_NOTICES.md

@@ -158,6 +158,7 @@ External packages **directly declared** for development, tests, types, or toolin
 | [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk) | MIT |
 | [`@modelcontextprotocol/server-everything`](https://github.com/modelcontextprotocol/servers) | MIT / Apache-2.0 |
 | [`@modelcontextprotocol/server-filesystem`](https://github.com/modelcontextprotocol/servers) | MIT / Apache-2.0 |
+| [`@panzoom/panzoom`](https://github.com/timmywil/panzoom) | MIT |
 | [`@stylistic/eslint-plugin`](https://github.com/eslint-stylistic/eslint-stylistic) | MIT |
 | [`@testing-library/dom`](https://github.com/testing-library/dom-testing-library) | MIT |
 | [`@testing-library/react`](https://github.com/testing-library/react-testing-library) | MIT |
@@ -218,6 +219,7 @@ External packages **directly declared** for development, tests, types, or toolin
 | [`vitepress`](https://github.com/vuejs/vitepress) | MIT |
 | [`vitepress-plugin-mermaid`](https://github.com/emersonbottero/vitepress-plugin-mermaid) | MIT |
 | [`vitest`](https://github.com/vitest-dev/vitest) | MIT |
+| [`vue`](https://github.com/vuejs/core) | MIT |
 
 `eslint-plugin-sonarjs` (LGPL-3.0-only) and `lightningcss` (MPL-2.0) run only as development tooling; their code is not linked into or distributed with any DeepSeek Harness artifact.
 

+ 2 - 2
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-rc.2",
+  "version": "0.1.6-alpha.1",
   "publishConfig": {
     "access": "public"
   },
@@ -98,7 +98,7 @@
     "@deepseek-ai/schemastery": "workspace:^",
     "commander": "^15.0.0",
     "js-yaml": "^4.2.0",
-    "node-addon-require-builtin": "^0.1.4",
+    "node-addon-require-builtin": "^0.1.6",
     "@deepseek-ai/dsh-http-proxy": "workspace:^",
     "@deepseek-ai/dsh-mcp-resources": "workspace:^"
   },

+ 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: 5a92052eb24c8af432c2ede371a2ff2bc84f57c0
-README.zh.md: a9268ad4920a7d5a72a4b0ead9b2672d22e10805
+README.md: b2931e05c1c10c6de13427b2cdaf38a0e78db904
+README.zh.md: 2a9014a5d338c3d81d9976d8cb47474a95c45cb5

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

@@ -8,7 +8,7 @@ This reference defines the profile, web-alias, plugin-management, and config-dum
 
 `dsh --profile <name>` boots the profile at `$DSH_HOME/profiles/<name>`. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's `dsh.profile.bundles` list, the profile's own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml` (machine-local preferences shared by every profile, so it outranks the per-profile layer), and each `--patch <path>` overlay in argv order. Later layers win per row; a patch replaces the targeted row's complete `config` value rather than deep-merging keys, and may insert new rows. `dsh.profile.patchReload` selects `live` patch-file watching or `startup` one-time loading; omission defaults a custom profile to `live`. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit.
 
-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.
+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`. Before mounting rows, the launcher traverses the installation and selected bundles in that order and materializes the resulting fallback links. The internal runtime and dual modes consume the same immutable generation in tests without changing the CLI's link-mode behavior. Profile-installed packages keep native priority in every mode.
 
 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>`.
 
@@ -98,9 +98,9 @@ New sessions in base-backed profiles default to the `workspace-write` permission
 
 ## Shared deployment behavior
 
-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.
+The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search` and `web_fetch`, the public-only HTTP fetch provider, default-on 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 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.
+Feedback is recorded in the Session log without starting model work. The [DeepSeek session-log contributor](../../../packages/session/session-log-deepseek/README.md) sends complete unaccepted log suffixes with subsequent DeepSeek requests by default, including requests sent through configured gateways; set its `enabled` configuration to `false` to opt out. [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.
 

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

@@ -10,7 +10,7 @@
 
 `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` 文件系统。每次启动还会把仅由所选外部组合包携带的包经 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`。挂载配置行前,launcher 会按此顺序遍历安装与所选 bundle,并物化计算出的 fallback 链接。内部 runtime 与 dual 模式会在测试中消费同一份不可变 generation,但不改变 CLI 的 link 模式行为。所有模式都保留 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>`。
 
@@ -100,9 +100,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 适配器、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 请求发送尚未确认接收的完整日志后缀,包括经已配置网关发送的请求;将其 `enabled` 配置设为 `false` 可关闭上传。[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 沙箱之外的受信任可执行代码。
 

+ 24 - 4
apps/cli/src/profile-boot.ts

@@ -20,17 +20,21 @@ import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
 import {
   boot,
   composeEntries,
+  createProfileResolutionGeneration,
   healProfilesModuleFallback,
   initProfile,
   installFailLoud,
   loadOptionalPatches,
   loadOverlayPatches,
   loadProfile,
+  PluginPackages,
   PROFILE_PATCH_FILENAME,
   PROFILE_TEMPLATES,
   resolveProfileDir,
   watchUserPatches,
   type Profile,
+  type ProfileResolutionGeneration,
+  type ProfileResolutionMode,
 } from '@deepseek-ai/dsh-app-boot'
 import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
 import { installProxyFromEnvironment } from '@deepseek-ai/dsh-http-proxy'
@@ -194,6 +198,8 @@ export function prepareProfile(name: string, userLayer = true, fromDefaultProfil
 /** One profile's patch layers, in application order. */
 interface ComposedProfile {
   profile: Profile
+  /** Immutable package fallback selected before any plugin imports. */
+  resolution: ProfileResolutionGeneration
   /** Bundle layers concatenated — the part below the user layers on a live reload. */
   bundlePatches: PatchOptions[]
   /** The home-level user layer (`$DSH_HOME/cordis.patch.yml`), applied after the profile's own. */
@@ -226,10 +232,14 @@ function allPatches(composed: ComposedProfile): PatchOptions[] {
 async function composeProfile(
   name: string,
   patchFiles: readonly string[],
+  resolutionMode: ProfileResolutionMode,
   fromDefaultProfile?: string,
 ): Promise<ComposedProfile> {
   const profile = prepareProfile(name, true, fromDefaultProfile)
-  await healProfilesModuleFallback({ installAnchor: INSTALL_ANCHOR, profile })
+  const resolutionOptions = { installAnchor: INSTALL_ANCHOR, profile }
+  const resolution = resolutionMode === 'runtime'
+    ? await createProfileResolutionGeneration(resolutionOptions)
+    : await healProfilesModuleFallback(resolutionOptions)
   const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
   const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
   const bundlePatches = profile.layers.flatMap(layer => layer.patches)
@@ -240,7 +250,7 @@ async function composeProfile(
   const composedOverlays = [...overlays]
   const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
   if (telemetryPatch !== undefined) composedOverlays.push(telemetryPatch)
-  return { profile, bundlePatches, homePatches, overlays: composedOverlays }
+  return { profile, resolution, bundlePatches, homePatches, overlays: composedOverlays }
 }
 
 /** Options for {@link runProfile}. */
@@ -255,6 +265,8 @@ export interface RunProfileOptions {
   patchFiles: readonly string[]
   /** The invocation's inner arguments, handed to the tree through `ctx.cmdlineArgs`. */
   args: readonly string[]
+  /** Module fallback backend; pkg executables always use runtime resolution. */
+  resolutionMode?: ProfileResolutionMode
 }
 
 /**
@@ -289,7 +301,11 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
     (message) => { process.stderr.write(`${NAME}: ${message}\n`) },
   )
 
-  const composed = await composeProfile(options.profile, options.patchFiles, options.fromDefaultProfile)
+  const packaged = (process as NodeJS.Process & { pkg?: unknown }).pkg !== undefined
+  const resolutionMode = packaged ? 'runtime' : options.resolutionMode ?? 'link'
+  const composed = await composeProfile(
+    options.profile, options.patchFiles, resolutionMode, options.fromDefaultProfile,
+  )
   const app: { current?: Context } = {}
   const appReady = createAppReady()
   const shutdown = createProcessShutdown(async () => {
@@ -333,11 +349,15 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
   ])
   // Cloned for the same insert-aliasing reason as composeLive: the boot
   // application must not mutate the objects later reloads recompose from.
-  const ctx = await boot(NAME, rootConfig, structuredClone(allPatches(composed)), (hostCtx) => {
+  const ctx = await boot(NAME, rootConfig, structuredClone(allPatches(composed)), async (hostCtx) => {
     app.current = hostCtx
     // Before any config-tree entry mounts, so plugins resolve all launch-time
     // environment values from the same immutable launch snapshot.
     hostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, options.environment)
+    await hostCtx.plugin(PluginPackages, resolutionMode === 'link' ? {} : {
+      generation: composed.resolution,
+      behavior: resolutionMode === 'dual' ? 'verify' : 'enforce',
+    })
     // The command line and bounded exit request are launcher facts available
     // to every app plugin that injects the argument snapshot.
     provideCmdline(hostCtx, {

+ 1 - 0
apps/cli/tests/built-bin.e2e.ts

@@ -868,6 +868,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
     try {
       await waitForFile(fixture.ready)
       expect(readFileSync(fixture.echo, 'utf8')).toBe('bundle-default')
+      expect(existsSync(join(fixture.home, 'profiles', 'node_modules'))).toBe(true)
       requestProfileShutdown(child, fixture)
       expect((await child).exitCode).toBe(0)
     } finally {

+ 4 - 0
apps/cli/tests/profiles/headless/goal-snapshot.patch.yml

@@ -24,3 +24,7 @@
 - insert:
     - id: llm-replay
       name: '@deepseek-ai/dsh-llm-replay'
+
+- id: session-log-deepseek
+  config:
+    enabled: false

+ 4 - 0
apps/cli/tests/profiles/headless/semantic-checkpoint-snapshot.patch.yml

@@ -23,3 +23,7 @@
     # Publish the persisted agent before the driver selects the single root.
     - id: resumed-agent
       name: './tests/fixtures/semantic-checkpoint-agent.ts'
+
+- id: session-log-deepseek
+  config:
+    enabled: false

+ 4 - 0
apps/cli/tests/profiles/headless/subagent-diagnostic-snapshot.patch.yml

@@ -26,3 +26,7 @@
     # Publish the persisted agent before the driver selects the single root.
     - id: resumed-agent
       name: './tests/fixtures/subagent-diagnostic-agent.ts'
+
+- id: session-log-deepseek
+  config:
+    enabled: false

+ 4 - 0
apps/cli/tests/profiles/headless/subagent-inheritance-snapshot.patch.yml

@@ -32,3 +32,7 @@
     # Publish the persisted agent before the driver selects the single root.
     - id: resumed-agent
       name: './tests/fixtures/subagent-inheritance-agent.ts'
+
+- id: session-log-deepseek
+  config:
+    enabled: false

+ 4 - 0
apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml

@@ -28,3 +28,7 @@
     # Hold the parent's second step until the manager notice enters its inbox.
     - id: settlement-fence
       name: './tests/fixtures/subagent-settlement-fence.ts'
+
+- id: session-log-deepseek
+  config:
+    enabled: false

+ 4 - 0
apps/cli/tests/profiles/headless/workspace-context-resume-snapshot.patch.yml

@@ -28,3 +28,7 @@
     # Publish the persisted agent before the driver selects the single root.
     - id: resumed-agent
       name: './tests/fixtures/workspace-context-resume-agent.ts'
+
+- id: session-log-deepseek
+  config:
+    enabled: false

+ 20 - 8
apps/cli/tests/web-agent-presets.e2e.ts

@@ -4,7 +4,14 @@ import { tmpdir } from 'node:os'
 import { fileURLToPath } from 'node:url'
 import { dirname, join } from 'node:path'
 import { Context } from '@deepseek-ai/cordis'
-import { boot, healProfilesModuleFallback, loadOverlayPatches, loadProfile } from '@deepseek-ai/dsh-app-boot'
+import {
+  boot,
+  createProfileResolutionGeneration,
+  loadOverlayPatches,
+  loadProfile,
+  PluginPackages,
+  type Profile,
+} from '@deepseek-ai/dsh-app-boot'
 import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
 import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
 import type { Agent } from '@deepseek-ai/dsh-agent'
@@ -112,12 +119,7 @@ async function bootWeb(
     { id: 'agent-presets', config: { default: 'standard', includeUserRoot: false } },
     ...extra,
   ]
-  // The surface is patch layers over an empty preset root, so the root sits
-  // outside this workspace and bare plugin names cannot resolve by Node's
-  // upward walk. The flat fallback the preset boot maintains is what makes
-  // them resolvable — the same mechanism, not a test-only shim.
   const home = dirname(settingsFile)
-  await healProfilesModuleFallback({ installAnchor: INSTALL_ANCHOR, home })
   const profileDir = join(home, 'profiles', 'spec')
   await mkdir(profileDir, { recursive: true })
   // Product Bundles are installed into the Profile, not the dsh app. Model
@@ -130,6 +132,14 @@ async function bootWeb(
     await mkdir(dirname(link), { recursive: true })
     await symlink(packageDir, link, 'junction')
   }
+  let profile: Profile = {
+    name: 'spec',
+    dir: profileDir,
+    layers: [],
+    patchPath: join(profileDir, 'cordis.patch.yml'),
+    patches: [],
+    patchReload: 'startup',
+  }
   let bundlePatches: PatchOptions[] = [
     ...loadOverlayPatches('dsh-test', BASE_PATCH),
     ...loadOverlayPatches('dsh-test', WEB_PATCH),
@@ -140,12 +150,14 @@ async function bootWeb(
       dependencies: Object.fromEntries(profileBundles.map(name => [name, 'workspace:*'])),
       dsh: { profile: { bundles: profileBundles } },
     }, null, 2) + '\n')
-    const profile = loadProfile('dsh-test', 'spec', INSTALL_ANCHOR, home, { userLayer: false })
+    profile = loadProfile('dsh-test', 'spec', INSTALL_ANCHOR, home, { userLayer: false })
     bundlePatches = profile.layers.flatMap(layer => layer.patches)
   }
+  const resolution = await createProfileResolutionGeneration({ installAnchor: INSTALL_ANCHOR, home, profile })
   const rootConfig = join(profileDir, 'cordis.yml')
   await writeFile(rootConfig, '[]\n')
-  return await boot('dsh-test', rootConfig, [...bundlePatches, ...overrides], (bootCtx) => {
+  return await boot('dsh-test', rootConfig, [...bundlePatches, ...overrides], async (bootCtx) => {
+    await bootCtx.plugin(PluginPackages, { generation: resolution })
     bootCtx.provide('connection', {
       fetch: { register: () => () => {} },
       rpc: { intercept: () => () => {} },

+ 2 - 2
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-rc.2",
+  "description": "Private Node-mode host process for the Electron desktop application",
+  "version": "0.1.6-alpha.1",
   "private": true,
   "license": "MIT",
   "type": "module",

+ 28 - 11
apps/desktop-host/src/index.ts

@@ -15,9 +15,12 @@ import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
 import {
   boot,
   composeEntries,
+  createProfileResolutionGeneration,
   loadLayeredEnv,
   loadProfileDirectory,
   loadOverlayPatches,
+  PluginPackages,
+  type Profile,
 } from '@deepseek-ai/dsh-app-boot'
 import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
 import { DSH_LAUNCH_ENVIRONMENT_KEY } from '@deepseek-ai/dsh-launch-environment'
@@ -149,9 +152,20 @@ function isProjectPath(projectDir: string, target: string): boolean {
   return path === root || path.startsWith(root + sep)
 }
 
-function desktopPatches(runtimeDir: string, projectDir: string, allowLinkedPackages: boolean): PatchOptions[] {
-  const dshRoot = dirname(packageManifestPath(runtimeDir, '@deepseek-ai/dsh'))
-  const profile = loadProfileDirectory('dsh desktop', projectDir, join(dshRoot, 'package.json'))
+interface DesktopComposition {
+  readonly installAnchor: string
+  readonly profile: Profile
+  readonly patches: PatchOptions[]
+}
+
+function desktopComposition(
+  runtimeDir: string,
+  projectDir: string,
+  allowLinkedPackages: boolean,
+): DesktopComposition {
+  const installAnchor = packageManifestPath(runtimeDir, '@deepseek-ai/dsh')
+  const dshRoot = dirname(installAnchor)
+  const profile = loadProfileDirectory('dsh desktop', projectDir, installAnchor)
   for (const layer of profile.layers) {
     if (!allowLinkedPackages && !isProjectPath(projectDir, layer.packageDir) && !isProjectPath(runtimeDir, layer.packageDir)) {
       throw new Error(`dsh desktop: profile bundle ${JSON.stringify(layer.packageName)} resolved outside the Desktop runtime and profile`)
@@ -173,7 +187,7 @@ function desktopPatches(runtimeDir: string, projectDir: string, allowLinkedPacka
       },
     }])
   }
-  return layers.flat()
+  return { installAnchor, profile, patches: layers.flat() }
 }
 
 function dshVersion(runtimeDir: string): string {
@@ -282,19 +296,22 @@ export async function runDesktopHost(
   writeResponse: (frame: Buffer) => Promise<void>,
   options: { allowLinkedPackages?: boolean } = {},
 ): Promise<DesktopHostController> {
+  const absoluteRuntime = resolve(runtimeDir)
   const absoluteProject = resolve(projectDir)
   mkdirSync(absoluteProject, { recursive: true })
   const rootConfig = join(absoluteProject, ROOT_CONFIG_FILENAME)
   writeFileSync(rootConfig, ROOT_CONFIG)
   const environment = loadLayeredEnv('dsh desktop')
+  const composition = desktopComposition(absoluteRuntime, absoluteProject, options.allowLinkedPackages === true)
+  const resolution = await createProfileResolutionGeneration({
+    installAnchor: composition.installAnchor,
+    profile: composition.profile,
+  })
   let current: Context | undefined
-  const ctx = await boot('dsh desktop', rootConfig, structuredClone(desktopPatches(
-    resolve(runtimeDir),
-    absoluteProject,
-    options.allowLinkedPackages === true,
-  )), (hostCtx) => {
+  const ctx = await boot('dsh desktop', rootConfig, structuredClone(composition.patches), async (hostCtx) => {
     current = hostCtx
     hostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, environment)
+    await hostCtx.plugin(PluginPackages, { generation: resolution })
     provideCmdline(hostCtx, { args: [], exit: () => {} })
   })
   current = ctx
@@ -306,7 +323,7 @@ export async function runDesktopHost(
     throw new Error('dsh desktop: composition did not provide connection, typertGateway, and clientModules')
   }
   const api = connection.createSharedFetchHandler('/api')
-  const assets = assetHandler(ctx, resolve(runtimeDir))
+  const assets = assetHandler(ctx, absoluteRuntime)
   const streams = remoteStreamHandler(ctx)
   const requests = new Map<number, AbortController>()
   let disposing: Promise<void> | undefined
@@ -322,7 +339,7 @@ export async function runDesktopHost(
   }
 
   return {
-    dshVersion: dshVersion(resolve(runtimeDir)),
+    dshVersion: dshVersion(absoluteRuntime),
     cancel(streamId) {
       requests.get(streamId)?.abort()
     },

+ 9 - 4
apps/desktop/electron-builder.config.d.mts

@@ -4,11 +4,16 @@ export interface DesktopElectronBuilderConfig {
   readonly directories: {
     readonly output: string
   }
-  readonly extraResources: readonly [
-    { readonly from: string, readonly to: 'runtime' },
-    { readonly from: string, readonly to: 'dsh' },
-    { readonly from: string, readonly to: 'dsh/node_modules' },
+  readonly files: readonly [
+    string,
+    string,
+    string,
+    string,
+    { readonly from: string, readonly to: 'dsh', readonly filter: readonly ['**/*'] },
+    { readonly from: string, readonly to: 'dsh/node_modules', readonly filter: readonly ['**/*'] },
   ]
+  readonly asarUnpack: readonly string[]
+  readonly extraResources: readonly [{ readonly from: string, readonly to: 'runtime' }]
   readonly mac: {
     readonly identity: string | undefined
     readonly forceCodeSigning: boolean

+ 11 - 13
apps/desktop/electron-builder.config.mjs

@@ -63,20 +63,26 @@ export function createElectronBuilderConfig(
       'lib/*.cjs',
       'renderer/**/*',
       'package.json',
+      { from: buildPaths.dsh, to: 'dsh', filter: ['**/*'] },
+      // electron-builder excludes a source directory's root node_modules.
+      { from: join(buildPaths.dsh, 'node_modules'), to: 'dsh/node_modules', filter: ['**/*'] },
+    ],
+    asarUnpack: [
+      '**/*.{node,dylib,dll,so,exe}',
+      '**/*.so.*',
+      '**/spawn-helper',
+      '**/@vscode/ripgrep/bin/rg',
     ],
     extraResources: [
       { from: buildPaths.runtime, to: 'runtime' },
-      { from: buildPaths.dsh, to: 'dsh' },
-      // electron-builder excludes a source directory's root node_modules.
-      { from: join(buildPaths.dsh, 'node_modules'), to: 'dsh/node_modules' },
     ],
     mac: {
       category: 'public.app-category.developer-tools',
       identity: macOSSigning?.signingIdentity,
       forceCodeSigning: true,
       hardenedRuntime: true,
-      // Native runtime files are pre-signed; PAK resources are sealed by their enclosing bundle.
-      signIgnore: ['/Contents/Resources/dsh(?:/|$)', '\\.pak$'],
+      // ASAR-unpacked native runtime files are pre-signed; PAK resources are sealed by their enclosing bundle.
+      signIgnore: ['/Contents/Resources/app\\.asar\\.unpacked/dsh(?:/|$)', '\\.pak$'],
       notarize: true,
       target: ['dmg', 'zip'],
     },
@@ -84,16 +90,8 @@ export function createElectronBuilderConfig(
       sign: true,
       writeUpdateInfo: false,
     },
-    afterPack: async context => {
-      const { verifyDesktopRuntime } = await import('./lib/types/runtime-tree.js')
-      await verifyDesktopRuntime(join(context.packager.getResourcesDir(context.appOutDir), 'dsh'),
-        context.packager.appInfo.version, { platform: resolvedPlatform, arch: resolvedArch })
-    },
     afterSign: async context => {
       if (context.electronPlatformName !== 'darwin') return
-      const { verifyDesktopRuntime } = await import('./lib/types/runtime-tree.js')
-      await verifyDesktopRuntime(join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`, 'Contents', 'Resources', 'dsh'),
-        context.packager.appInfo.version, { platform: 'darwin', arch: resolvedArch })
       verifyMacOSSignatureAfterSign(context, macOSSigning ?? resolveMacOSSigningEnvironment(env))
     },
     artifactBuildCompleted: artifact => {

+ 1 - 1
apps/desktop/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-desktop",
   "description": "Electron desktop shell for a bundled dsh runtime and external plugins",
-  "version": "0.1.5-rc.2",
+  "version": "0.1.6-alpha.1",
   "private": true,
   "license": "MIT",
   "type": "module",

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio