Просмотр исходного кода

Merge master into feat/docs-platform-command-tabs

gengruilin 3 дней назад
Родитель
Сommit
4ab1fc3bfe
100 измененных файлов с 1606 добавлено и 368 удалено
  1. 2 2
      .agents/notes/README.i18n.yaml
  2. 2 2
      .agents/notes/README.md
  3. 2 2
      .agents/notes/README.zh.md
  4. 2 2
      .agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.i18n.yaml
  5. 30 0
      .agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.md
  6. 30 0
      .agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.zh.md
  7. 6 0
      .agents/notes/archived/architecture/2026-09-10-local-office-preview.i18n.yaml
  8. 52 0
      .agents/notes/archived/architecture/2026-09-10-local-office-preview.md
  9. 52 0
      .agents/notes/archived/architecture/2026-09-10-local-office-preview.zh.md
  10. 6 0
      .agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.i18n.yaml
  11. 36 0
      .agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.md
  12. 36 0
      .agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.zh.md
  13. 6 0
      .agents/notes/archived/bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml
  14. 26 0
      .agents/notes/archived/bug-fix/2026-09-09-content-sized-diagram-previews.md
  15. 26 0
      .agents/notes/archived/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md
  16. 15 0
      .agents/notes/archived/manifest.json
  17. 6 0
      .agents/notes/archived/process/2026-09-11-independent-libreoffice-package.i18n.yaml
  18. 32 0
      .agents/notes/archived/process/2026-09-11-independent-libreoffice-package.md
  19. 32 0
      .agents/notes/archived/process/2026-09-11-independent-libreoffice-package.zh.md
  20. 2 2
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
  21. 2 0
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
  22. 2 0
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
  23. 2 2
      .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml
  24. 1 1
      .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
  25. 1 1
      .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md
  26. 2 2
      .agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.i18n.yaml
  27. 13 5
      .agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md
  28. 19 11
      .agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.zh.md
  29. 2 2
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml
  30. 19 15
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md
  31. 19 15
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md
  32. 2 2
      .agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.i18n.yaml
  33. 4 4
      .agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md
  34. 4 4
      .agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md
  35. 2 2
      .agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.i18n.yaml
  36. 10 12
      .agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md
  37. 10 12
      .agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.zh.md
  38. 2 2
      .agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.i18n.yaml
  39. 7 6
      .agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md
  40. 7 6
      .agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.zh.md
  41. 2 2
      .agents/notes/implemented/architecture/2026-08-20-client-session-conversation-ownership.i18n.yaml
  42. 28 27
      .agents/notes/implemented/architecture/2026-08-20-client-session-conversation-ownership.md
  43. 28 27
      .agents/notes/implemented/architecture/2026-08-20-client-session-conversation-ownership.zh.md
  44. 2 2
      .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml
  45. 4 2
      .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md
  46. 4 2
      .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md
  47. 2 2
      .agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml
  48. 12 19
      .agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md
  49. 37 45
      .agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md
  50. 2 2
      .agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml
  51. 0 1
      .agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md
  52. 0 1
      .agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md
  53. 2 2
      .agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.i18n.yaml
  54. 18 16
      .agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.md
  55. 18 16
      .agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md
  56. 2 2
      .agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml
  57. 5 1
      .agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md
  58. 5 1
      .agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md
  59. 2 2
      .agents/notes/implemented/architecture/2026-09-08-global-main-panels.i18n.yaml
  60. 3 3
      .agents/notes/implemented/architecture/2026-09-08-global-main-panels.md
  61. 3 3
      .agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md
  62. 2 2
      .agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.i18n.yaml
  63. 4 2
      .agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.md
  64. 4 2
      .agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.zh.md
  65. 2 2
      .agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.i18n.yaml
  66. 6 6
      .agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.md
  67. 6 6
      .agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.zh.md
  68. 0 27
      .agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.md
  69. 0 27
      .agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.zh.md
  70. 6 0
      .agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.i18n.yaml
  71. 31 0
      .agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.md
  72. 31 0
      .agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.zh.md
  73. 6 0
      .agents/notes/implemented/architecture/2026-09-10-component-factories-and-local-slots.i18n.yaml
  74. 105 0
      .agents/notes/implemented/architecture/2026-09-10-component-factories-and-local-slots.md
  75. 105 0
      .agents/notes/implemented/architecture/2026-09-10-component-factories-and-local-slots.zh.md
  76. 6 0
      .agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.i18n.yaml
  77. 51 0
      .agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md
  78. 51 0
      .agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.zh.md
  79. 6 0
      .agents/notes/implemented/architecture/2026-09-10-windows-native-installer-pages.i18n.yaml
  80. 31 0
      .agents/notes/implemented/architecture/2026-09-10-windows-native-installer-pages.md
  81. 31 0
      .agents/notes/implemented/architecture/2026-09-10-windows-native-installer-pages.zh.md
  82. 6 0
      .agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.i18n.yaml
  83. 27 0
      .agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.md
  84. 27 0
      .agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.zh.md
  85. 6 0
      .agents/notes/implemented/architecture/2026-09-11-node-office-kit.i18n.yaml
  86. 65 0
      .agents/notes/implemented/architecture/2026-09-11-node-office-kit.md
  87. 65 0
      .agents/notes/implemented/architecture/2026-09-11-node-office-kit.zh.md
  88. 2 2
      .agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.i18n.yaml
  89. 2 0
      .agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.md
  90. 2 0
      .agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md
  91. 6 0
      .agents/notes/implemented/architecture/2026-09-11-windows-directory-installation.i18n.yaml
  92. 29 0
      .agents/notes/implemented/architecture/2026-09-11-windows-directory-installation.md
  93. 29 0
      .agents/notes/implemented/architecture/2026-09-11-windows-directory-installation.zh.md
  94. 6 0
      .agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.i18n.yaml
  95. 35 0
      .agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.md
  96. 35 0
      .agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.zh.md
  97. 6 0
      .agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.i18n.yaml
  98. 29 0
      .agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md
  99. 29 0
      .agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md
  100. 6 0
      .agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.i18n.yaml

+ 2 - 2
.agents/notes/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 .agents/notes/README.md
-README.md: 8bf1adf875b54eb4580b4d189dfeed07c9f671c0
-README.zh.md: ff9bdb358d584d4f4ad0aea295c31d8d8fff9376
+README.md: 9fdb4039b525bbc4051ab3c8058b117e4de5f2f4
+README.zh.md: 2d7e07b963d3c4d77da9148b011e1478fa2ae5da

+ 2 - 2
.agents/notes/README.md

@@ -43,9 +43,9 @@ Once sealed, every archived triplet is permanently frozen. Do not edit, translat
 
 ## When to write one
 
-Every non-trivial change MUST add or update at least one Agent Note in the same PR. A change is non-trivial when it alters behavior, architecture, a contract shared across files or packages, process or tooling, testing strategy, an on-disk, wire, or configuration format, or another decision a maintainer may reasonably revisit. A proposal for substantial future work starts in `proposed/`; a decision already made starts in `implemented/`. Pick the class folder that matches the decision (see [Classification](#classification)).
+Add or update an Agent Note in the same PR only for lasting decision rationale that code, tests, and existing documentation do not explain. A proposal for substantial future work starts in `proposed/`; a decision already made starts in `implemented/`. Pick the class folder that matches the decision (see [Classification](#classification)).
 
-Updating the Agent Note that already owns the decision satisfies the rule; do not create a duplicate. Only a purely mechanical or local edit with no change to behavior, contracts, structure, process, or rationale is exempt. An Agent Note is never edited into a *different decision*: supersede it with a new one, and keep both notes cross-linked unless the old note is later fully consolidated under the rule below. Editing an `implemented/` Agent Note to track where its existing decision lives is required, not forbidden; see [implemented/AGENTS.md](implemented/AGENTS.md).
+Updating the Agent Note that already owns the decision satisfies the rule; do not create a duplicate. Mechanical or local edits, including local UI presentation and interaction changes, are exempt. An Agent Note is never edited into a *different decision*: supersede it with a new one, and keep both notes cross-linked unless the old note is later fully consolidated under the rule below. Editing an `implemented/` Agent Note to track where its existing decision lives is required, not forbidden; see [implemented/AGENTS.md](implemented/AGENTS.md).
 
 An implemented Agent Note that is fully superseded may be consolidated into the current owning note and deleted. Before deletion, the owner must preserve every unique rationale, alternative, consequence, required verification, and named coverage gap; repair every inbound link; and delete the Chinese counterpart and consistency record in the same change. Partial supersession does not qualify: keep both notes cross-linked and update every fact that remains current. Consolidation must not rewrite the old file into its opposite or rely on git history as the only copy of rationale.
 

+ 2 - 2
.agents/notes/README.zh.md

@@ -47,9 +47,9 @@
 
 ## 何时需要写一份
 
-每个非平凡变更都必须在同一 PR(Pull Request)中新增或更新至少一份 Agent Note。如果变更修改了行为、架构、跨文件或跨包约定、流程或工具、测试策略、磁盘存储格式、协议格式(wire format)或配置格式,或者维护者可能合理重新审视的其他决策,就属于非平凡变更。对未来重大工作的提案从 `proposed/` 开始;已经做出的决策从 `implemented/` 开始。选择与决策匹配的类别文件夹(见[分类](#classification))。
+只有代码、测试和现有文档未能解释的决策理由具有长期价值时,才在同一 PR(Pull Request)中新增或更新 Agent Note。对未来重大工作的提案从 `proposed/` 开始;已经做出的决策从 `implemented/` 开始。选择与决策匹配的类别文件夹(见[分类](#classification))。
 
-更新已经拥有该决策的 Agent Note 即可满足规则;不要创建重复记录。只有不涉及行为、约定、结构、流程或理由变化的纯机械性或局部编辑才可豁免。Agent Note 永远不会被编辑为一个*不同的决策*:用新 Agent Note 取代旧记录,并让两个记录保持互相链接,除非后续依据下方规则完全合并旧记录。编辑 `implemented/` Agent Note 以跟踪其现有决策的所在位置是必需的,而非禁止的;见 [implemented/AGENTS.md](implemented/AGENTS.md)。
+更新已经拥有该决策的 Agent Note 即可满足规则;不要创建重复记录。机械性或局部编辑均可豁免,包括局部 UI 展示和交互调整。Agent Note 永远不会被编辑为一个*不同的决策*:用新 Agent Note 取代旧记录,并让两个记录保持互相链接,除非后续依据下方规则完全合并旧记录。编辑 `implemented/` Agent Note 以跟踪其现有决策的所在位置是必需的,而非禁止的;见 [implemented/AGENTS.md](implemented/AGENTS.md)。
 
 被完全取代的 implemented Agent Note 可以合并到当前持有该决策的记录中,并删除原文件。删除前,当前记录必须保存所有独有的决策依据、备选方案、影响、必需的验证和明确指出的覆盖缺口;修复所有入站链接;并在同一变更中删除中文对侧文件和一致性记录。仅部分被取代的记录不符合此条件:保留两个记录并让它们互相链接,同时更新所有仍然适用的事实。合并不得将旧文件改写成与其相反的决策,也不得让 git 历史成为决策依据的唯一副本。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.i18n.yaml → .agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.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-09-desktop-in-place-profile.md
-2026-09-09-desktop-in-place-profile.md: 9a221bb334983a18366baeec00a179646dc76385
-2026-09-09-desktop-in-place-profile.zh.md: 53a744ae9ea284c32c3807f67ad365d270217287
+2026-09-09-desktop-in-place-profile.md: 57dabc0d03c999b90dc18baa9d833aa90320863a
+2026-09-09-desktop-in-place-profile.zh.md: 2b57c49ce45b1937552596cf4f59e0251b2d5774

+ 30 - 0
.agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.md

@@ -0,0 +1,30 @@
+# Agent Note: Modify the Desktop profile in place
+
+Status: implemented
+Archived: 2026-09-17
+
+English | [中文](2026-09-09-desktop-in-place-profile.zh.md)
+
+## Problem
+
+Staging preserves an old plugin installation but adds profile copying, directory moves, a recovery journal, and rollback state. Local plugin changes accept explicit repair after failure instead of this complexity.
+
+## Decision
+
+Application-owned package retention is qualified by the [production cleanup decision](../bug-fix/2026-09-15-desktop-profile-core-cleanup.md).
+
+Desktop stops the Host and modifies the current profile directly. Shared app-boot cleanup detaches its own fallback links before package changes; the Host’s shared profile runner supplies required links on startup. Package locking and configured lifecycle scripts remain. Upgrades refresh module links without copying plugin files.
+
+Package or Host failures retain partial changes for repair and retry. There is no staging profile, activation journal, directory-swap recovery, or automatic rollback. Existing scratch directories are not interpreted or deleted.
+
+This supersedes staging and rollback in [the packaging decision](2026-08-25-electron-desktop-packaging-and-updates.md), [the bundled-runtime decision](2026-09-08-desktop-bundled-runtime-and-external-plugins.md), and [the immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md). Host boot follows the [thin-wrapper decision](2026-09-10-desktop-web-wrapper.md); release, module ownership, and window lifecycle remain separate decisions.
+
+Desktop delegates installation and lifecycle scripts to pnpm, without a pending-operation startup gate, frozen-lockfile reinstall, or automatic rebuild. Failed package operations preserve partial changes and leave disable, remove, and startup retry available. The Host inherits the user environment, and profiles may use directory links. An unchanged legacy Desktop-generated pnpm configuration is replaced with the Web defaults; customized configuration remains user-owned.
+
+## Alternatives considered
+
+Staging protects the previous installation at the cost of copying and crash recovery. Versioned directories still need preparation, selection, and cleanup. Direct writes give up automatic recovery; reintroduction requires an unattended-recovery product requirement that justifies these costs.
+
+## Consequences
+
+Tests cover offline initialization, in-place upgrades, failure before writes, retained changes after Host failure, partial pnpm failure, restored host links, and exclusive package ownership. Signed application and GUI acceptance remain release-environment checks.

+ 30 - 0
.agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.zh.md

@@ -0,0 +1,30 @@
+# Agent Note: 直接修改 Desktop profile
+
+Status: implemented
+Archived: 2026-09-17
+
+[English](2026-09-09-desktop-in-place-profile.md) | 中文
+
+## 问题
+
+staging 能保留旧插件安装,但增加 profile 复制、目录移动、恢复日志和回滚状态。本地插件变更接受失败后显式修复,以避免这些复杂度。
+
+## 决策
+
+应用管理包的保留范围受[生产清理决策](../bug-fix/2026-09-15-desktop-profile-core-cleanup.zh.md)限定。
+
+Desktop 停止 Host 后直接修改当前 profile。共享 app-boot 清理在包变更前分离其拥有的模块补全链接;Host 的共享 profile runner 在启动时补全所需链接。包锁及配置允许的生命周期脚本保留。升级刷新模块链接,不复制插件文件。
+
+包操作或 Host 失败会保留部分修改,供修复和重试。不使用 staging profile、激活日志、目录切换恢复或自动回滚。已有临时目录不会被解释或删除。
+
+本记录取代[打包决策](2026-08-25-electron-desktop-packaging-and-updates.zh.md)、[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)及[立即显示窗口决策](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)中的暂存与回滚。Host 启动遵循[薄壳决策](2026-09-10-desktop-web-wrapper.zh.md);发布、模块归属与窗口生命周期仍由各自决策负责。
+
+Desktop 将安装和生命周期脚本交给 pnpm,不设置待完成操作启动门禁、不强制按锁文件重装,也不自动重建。包操作失败会保留部分变更,仍可禁用、删除和重试启动。Host 继承用户环境,profile 可以使用目录链接。未经修改的旧版 Desktop 生成 pnpm 配置替换为 Web 默认值;自定义配置仍由用户管理。
+
+## 考虑过的替代方案
+
+staging 以复制和崩溃恢复为代价保护旧安装。版本化目录仍需要准备、选择和清理。直接写入放弃自动恢复;只有无人值守恢复的产品要求能证明这些成本合理时,才重新引入。
+
+## 后果
+
+测试覆盖离线初始化、原地升级、写入前失败、Host 失败后保留修改、pnpm 部分失败、宿主链接恢复及独占包操作。签名应用和 GUI 验收仍由发布环境负责。

+ 6 - 0
.agents/notes/archived/architecture/2026-09-10-local-office-preview.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-10-local-office-preview.md
+2026-09-10-local-office-preview.md: ea32e37f0b2dcba866b78947c785dcee8e475b29
+2026-09-10-local-office-preview.zh.md: a1ac8337a1cf22f5842485c0790c6554635dc125

+ 52 - 0
.agents/notes/archived/architecture/2026-09-10-local-office-preview.md

@@ -0,0 +1,52 @@
+# Agent Note: Local Office preview through WASM PDF bytes
+
+Status: implemented
+Archived: 2026-09-11
+
+English | [中文](2026-09-10-local-office-preview.zh.md)
+
+## Problem
+
+Office Open XML files are ZIP archives, so text fallback cannot provide a useful preview. Document conversion must stay local without taking focus or adding document content to a model conversation. Engine success alone cannot prove that the input has the claimed format or that a PDF was produced.
+
+## Decision
+
+A [document-render Service Definition](../../../../packages/document/document-render/README.md), a LibreOffice WASM provider, and Client/API consumers form the conversion capability. [Boot composition](../../../../packages/document/document-render-auto/README.md) mounts the provider and Remote controller only when `wasm.artifactDirectory` is configured. Invalid explicit configuration fails initialization. The Office Client registration always exists so an unconfigured Host receives guidance. Availability follows the generated Remote namespace without executable discovery or a duplicate Client flag.
+
+The [WASM provider](../../../../packages/document/document-render-libreoffice-wasm/README.md) uses pinned official LibreOffice source, Emscripten, and a LibreOfficeKit adapter. Each Node Worker owns one engine instance, its pthreads, and its memory filesystem. Terminating the Worker cancels synchronous engine calls; every conversion waits for its Worker to stop before returning. OOXML inspection verifies archive membership, content type, and resource bounds before startup.
+
+A VCL callback requests a font family before substitution or missing Unicode characters during glyph fallback. Host code indexes installed font metadata and copies selected files into engine memory, including complete font collections. Exact installed families precede configured alternatives; original-first fontconfig aliases preserve that ordering inside LibreOffice. Optional initial families use the same resolver. The engine has no Host filesystem mount, and rendering neither installs nor downloads fonts. The [build recipe](../../../../native/libreoffice-wasm/README.md) records source and toolchain revisions, patches, and asset hashes. Engine bundles remain immutable during provider use; a different build uses a new directory.
+
+The [independent engine release](../process/2026-09-11-independent-libreoffice-package.md) owns precompiled npm packaging and keeps engine compilation outside ordinary DSH builds.
+
+The provider returns caller-owned PDF bytes and independent `succeeded`, `timedOut`, and `cancelled` facts. PDF bytes are copied out of engine memory before teardown. The API consumer authorizes the source through [Workspace Files](2026-09-09-workspace-file-read-authority.md), requires successful uninterrupted conversion, and encodes the PDF directly. Source path and freshness version survive the PDF transport. Workspace Files limits source reads; the provider's `maxOutputBytes` alone limits generated PDFs. No temporary PDF or file lease is needed.
+
+The Office Client watches the selected Conversation's existing Deliverables projection and warms recent Office files without opening tabs. Its bounded memory cache checks authorized source metadata on every read and shares pending conversions between background and foreground callers. Only the last departing reader cancels a shared conversion; failures are not cached and connection reset discards cached bytes. [Document Preview](2026-09-08-document-preview-operations.md) owns format selection, loading, cancellation, and the PDF.js Worker.
+
+PDF.js's official TextLayerBuilder owns selection boundaries and copy normalization over the width-fitted canvas, with shared page cleanup and a component-owned resize observer. Its end-of-content marker and stacking rules constrain selection in blank regions; line-break highlighting is suppressed. Per-page cancellation uses the builder's cleanup rather than aborting the first page's signal, because the official selection listeners are shared across pages.
+
+Missing-family observations accompany the PDF through the worker and Remote response. The Office plugin renders a keyed notice above the shared PDF scrollport, so dismissing it removes its layout height. The font resolver intersects explicit source and theme references with actual document requests, excluding engine defaults and font-table inventories; installed aliases and glyph fallback do not by themselves mean a family is absent.
+
+## Alternatives considered
+
+**Keep an installed native LibreOffice provider.** A second provider adds executable discovery, version queries, subprocess supervision, scratch files, and platform-specific confinement. This design uses WASM for all supported Office previews. A native provider needs a demonstrated rendering or deployment requirement that WASM cannot satisfy.
+
+**Return a temporary PDF path.** The Worker already supplies independent PDF bytes. Writing them to disk, authorizing another file read, and maintaining leases adds file ownership without a current consumer. Source authorization and output limits apply directly to the memory result.
+
+**Automate installed Microsoft Office.** Word for Mac requires GUI automation rather than headless conversion, introducing focus changes and per-file operating-system authorization. Native Office automation remains outside this preview capability.
+
+**Use an online converter or download the engine automatically.** Remote conversion uploads document content. Automatic installation adds runtime distribution and update responsibilities; explicit artifact configuration leaves deployment with the operator.
+
+**Convert PDF pages to PNG.** PDF.js already owns PDF presentation. Rasterizing adds another image pipeline and loses the existing PDF controls and selectable text.
+
+**Add a model-facing rendering tool or durable preview events.** Client preview does not supply model input. A model-facing tool requires logged facts and both SDK projections, so it is a separate consumer.
+
+**Scan every font-table entry for warnings.** Font tables include unused styles and templates. Reporting them would make the notice unrelated to rendered content; collection instead starts after engine initialization and ends after PDF save.
+
+## Consequences
+
+The single-provider design assumes WASM covers the required Office rendering behavior; it is not evidence of complete fidelity equivalence with native LibreOffice. Fidelity depends on the engine build and installed fonts. The [real-engine tests](../../../../packages/document/document-render-libreoffice-wasm/tests/libreoffice-wasm.e2e.ts) cover DOCX, XLSX, PPTX, selectable Chinese text, and embedded system fonts; ABI fixtures alone cannot establish rendering fidelity or assembled browser presentation.
+
+WASM instances have substantial memory and startup costs, so the provider bounds concurrency. The [font reuse and image resolution decision](2026-09-11-wasm-preview-font-and-image-budgets.md) owns shared Host metadata, Worker request memoization, and raster export limits; only selected fonts enter engine memory. Large collections require bounded memory growth, and CFF export requires checked stack space. A fatal abort prevents further calls into C++. A device without a font covering a character cannot render it correctly without additional fonts. Owning the WASM build adds compiler compatibility, patch maintenance, and redistribution obligations.
+
+Prewarming consumes conversion resources to reduce foreground waits; entry and byte limits bound retained Client results. Source and PDF bytes remain outside Session storage and persistent caches. The [provider tests](../../../../packages/document/document-render-libreoffice-wasm/tests/provider.spec.ts) cover independent output facts, input/output rejection, queued cancellation, and active Worker termination. Existing preview registry, file read authority, and Session ownership decisions remain active.

+ 52 - 0
.agents/notes/archived/architecture/2026-09-10-local-office-preview.zh.md

@@ -0,0 +1,52 @@
+# Agent Note: 通过 WASM PDF 字节进行本地 Office 预览
+
+Status: implemented
+Archived: 2026-09-11
+
+[English](2026-09-10-local-office-preview.md) | 中文
+
+## 问题
+
+Office Open XML 文件是 ZIP 归档,因此文本回退无法提供有用的预览。文档转换必须留在本地,不抢占焦点,也不将文档内容放入模型对话。仅凭引擎成功状态无法证明输入具有声明的格式或生成了 PDF。
+
+## 决定
+
+[document-render Service Definition](../../../../packages/document/document-render/README.zh.md)、LibreOffice WASM 提供方及 Client/API 消费方构成转换能力。[启动组合](../../../../packages/document/document-render-auto/README.zh.md)仅在配置 `wasm.artifactDirectory` 时挂载提供方和 Remote 控制器。无效的显式配置会使初始化失败。Office Client 注册始终存在,为未配置的 Host 提供指引。可用性由生成的 Remote 命名空间决定,不发现可执行文件,也不维护重复的 Client 标记。
+
+[WASM 提供方](../../../../packages/document/document-render-libreoffice-wasm/README.zh.md)使用固定的官方 LibreOffice 源码、Emscripten 和 LibreOfficeKit 适配器。每个 Node Worker 拥有一个引擎实例、其 pthread 和内存文件系统。终止 Worker 可取消同步引擎调用;每次转换均在返回前等待 Worker 停止。OOXML 检查在启动前验证归档成员、内容类型和资源限制。
+
+VCL 回调在替换前请求字体族,或在字形回退时请求缺失的 Unicode 字符。Host 代码索引已安装字体元数据,将选中的文件复制到引擎内存,包括完整字体集合。精确匹配的已安装字体优先于配置的替代项;原字体优先的 fontconfig 别名在 LibreOffice 内保留此顺序。可选的初始字体族使用同一解析器。引擎不挂载 Host 文件系统,渲染不安装或下载字体。[构建配方](../../../../native/libreoffice-wasm/README.zh.md)记录源码与工具链版本、补丁及产物哈希。提供方使用期间,引擎包保持不可变;不同构建使用新目录。
+
+[独立引擎发布](../process/2026-09-11-independent-libreoffice-package.zh.md)负责预编译 npm 打包,并将引擎编译与普通 DSH 构建隔离。
+
+提供方返回调用方拥有的 PDF 字节,以及独立的 `succeeded`、`timedOut` 和 `cancelled` 事实。PDF 字节在清理前从引擎内存复制出来。API 消费方通过 [Workspace Files](2026-09-09-workspace-file-read-authority.zh.md)授权源文件,要求转换成功且未被中断,并直接编码 PDF。源路径和新鲜度版本保留在 PDF 传输中。Workspace Files 限制源文件读取;生成 PDF 仅受提供方的 `maxOutputBytes` 限制。无需临时 PDF 或文件租约。
+
+Office Client 观察所选 Conversation 现有的 Deliverables 投影,在不打开 tab 的情况下预转换最近的 Office 文件。有上限的内存缓存每次读取都检查已授权的源文件元数据,并在后台和前台调用方之间共享进行中的转换。仅最后一个退出的读取方取消共享转换;失败不缓存,连接重置丢弃缓存字节。[Document Preview](2026-09-08-document-preview-operations.zh.md)负责格式选择、加载、取消和 PDF.js Worker。
+
+PDF.js 官方 TextLayerBuilder 在适配宽度的 canvas 上负责选择边界和复制规范化,共享页面清理,并使用由组件拥有的 resize observer。其内容结束标记和堆叠规则限制空白区域中的选择;换行高亮被抑制。逐页取消使用 builder 的清理操作,而不 abort 第一页的信号,因为官方选择监听器跨页面共享。
+
+缺失字体族观察值随 PDF 经过 Worker 和 Remote 响应。Office 插件在共享 PDF 滚动区域上方呈现 keyed 提示,关闭时移除其布局高度。字体解析器取源文档及主题显式引用与实际文档请求的交集,排除引擎默认字体和字体表清单;已安装别名和字形回退本身不代表字体族缺失。
+
+## 考虑过的替代方案
+
+**保留已安装的原生 LibreOffice 提供方。** 第二个提供方增加可执行文件发现、版本查询、子进程监督、临时文件和平台特定隔离。本设计将 WASM 用于全部受支持的 Office 预览。引入原生提供方需要证明存在 WASM 无法满足的渲染或部署需求。
+
+**返回临时 PDF 路径。** Worker 已提供独立的 PDF 字节。将其写入磁盘、授权另一处文件读取并维护租约,会增加文件所有权,而没有当前消费方需要这些操作。源文件授权和输出限制直接应用于内存结果。
+
+**自动操作已安装的 Microsoft Office。** Word for Mac 需要 GUI 自动化而非 headless 转换,会引入焦点变化和逐文件操作系统授权。原生 Office 自动化不属于本预览能力。
+
+**使用在线转换器或自动下载引擎。** 远程转换会上传文档内容。自动安装增加运行时分发和更新责任;显式产物配置将部署留给运维者。
+
+**将 PDF 页面转换为 PNG。** PDF.js 已负责 PDF 展示。栅格化增加另一条图像流水线,并失去现有 PDF 控件和可选择文本。
+
+**添加面向模型的渲染工具或持久化预览事件。** Client 预览不提供模型输入。面向模型的工具需要记录事实并更新两种 SDK 投影,因此属于独立消费方。
+
+**扫描全部字体表条目生成提示。** 字体表包含未使用的样式和模板。报告这些条目会使提示与实际渲染内容无关,因此收集从引擎初始化完成后开始,在 PDF 保存后结束。
+
+## 影响
+
+单一提供方设计假设 WASM 覆盖所需的 Office 渲染行为;这不是与原生 LibreOffice 完全等价的保真度证据。保真度取决于引擎构建和已安装字体。[真实引擎测试](../../../../packages/document/document-render-libreoffice-wasm/tests/libreoffice-wasm.e2e.ts)覆盖 DOCX、XLSX、PPTX、可选择中文文本和嵌入系统字体;仅有 ABI fixture 无法证明渲染保真度或完整浏览器展示。
+
+WASM 实例有较高的内存与启动成本,因此提供方限制并发。[字体复用与图像分辨率决策](2026-09-11-wasm-preview-font-and-image-budgets.zh.md)负责共享 Host 元数据、Worker 请求记忆化和栅格导出限制;仅选中字体进入引擎内存。大型集合需要有界内存增长,CFF 导出需要受检查的栈空间。致命 abort 后不再调用 C++。设备没有覆盖某字符的字体时,无法在不添加字体的前提下正确渲染该字符。自行构建 WASM 会增加编译器兼容、补丁维护和再分发义务。
+
+预转换消耗转换资源以减少前台等待;条目数和字节限制约束 Client 保留的结果。源文件和 PDF 字节不进入 Session 存储或持久缓存。[提供方测试](../../../../packages/document/document-render-libreoffice-wasm/tests/provider.spec.ts)覆盖独立输出事实、输入/输出拒绝、排队取消和活跃 Worker 终止。现有预览注册表、文件读权限和 Session 所有权决策保持有效。

+ 6 - 0
.agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.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-11-wasm-preview-font-and-image-budgets.md
+2026-09-11-wasm-preview-font-and-image-budgets.md: ab33b0bb6e28e5749ac38f36c08bc7539a368515
+2026-09-11-wasm-preview-font-and-image-budgets.zh.md: e6ff458e8b68ce08ff5d81ce6767a08a82974a74

+ 36 - 0
.agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.md

@@ -0,0 +1,36 @@
+# Agent Note: Font reuse and image resolution in WASM previews
+
+Status: implemented
+Archived: 2026-09-11
+
+English | [中文](2026-09-11-wasm-preview-font-and-image-budgets.zh.md)
+
+## Problem
+
+Office conversion repeats font metadata parsing across documents and font matching within one document. Image-heavy documents also spend substantial PDF export time resampling images above preview resolution. These costs need separate controls because font reuse does not reduce image decoding or resampling.
+
+## Decision
+
+The [WASM provider](../../../../packages/document/document-render-libreoffice-wasm/README.md) builds a font metadata snapshot once during initialization. Each conversion Worker receives a structured clone. The Host retains names, face attributes, paths, sizes, and modification times; glyph coverage, request caches, and imported bytes belong to the Worker. Reloading the provider refreshes the snapshot. Workers validate indexed files when reading them, and an already imported font remains usable within that conversion.
+
+Each Worker memoizes complete requests: family, style, weight, italic, width, pitch, language, and ordered code points. The cache stores MEMFS paths and missing-family observations. Hits replay those observations because an initialization request can recur after document font collection starts. Worker termination releases both the cache and engine memory.
+
+Runtime PDF filter options reduce raster images to a configurable `maxImageResolution`, defaulting to 192 DPI. The [shared PDF canvas](../../../../packages/client/ui-sidebar-documentpreview/src/client/pdf/document.ts) renders at 96 CSS DPI times the device pixel ratio; the default covers ratio 2. Text and vector graphics remain scalable. Explicit bookmark export preserves LibreOfficeKit's default when JSON filter options replace its implicit filter data.
+
+The [local Office preview decision](2026-09-10-local-office-preview.md) continues to own conversion lifetime, authorization, missing-font presentation, and PDF transport.
+
+## Alternatives considered
+
+**Index fonts inside every conversion Worker.** This keeps discovery off the Host event loop and sees newly installed fonts immediately, but repeats full-file metadata parsing across previews. A provider snapshot removes that repeated work at the cost of synchronous initialization and explicit reload after font changes.
+
+**Cache only the family name.** Style, language, pitch, and missing characters can select different files. A complete request key retains those distinctions and still captures repeated layout requests.
+
+**Keep 300 DPI for every preview.** Higher raster resolution retains detail for high pixel ratios and magnification but increases image export work. The configurable 192-DPI default follows the common display target without rasterizing text or rebuilding the engine.
+
+## Consequences
+
+Cold provider startup includes indexing and remains separate from conversion deadlines. Shared metadata retains no original font buffers, while each Worker still reads selected fonts and lazily decodes coverage. Installing or replacing fonts requires provider reload; a file changed since indexing can fail a later conversion.
+
+Reducing image resolution trades raster detail at high magnification for less export work. It is not an engine memory cap: LibreOffice can still decode full-resolution source images, load large font collections, or exhaust its build-time WASM memory maximum. No persistent font cache, filesystem watcher, or cross-document engine instance is introduced.
+
+[Font tests](../../../../packages/document/document-render-libreoffice-wasm/tests/fonts.spec.ts) cover complete request keys, repeated missing-family observations, snapshot isolation, and failed imports. [Provider tests](../../../../packages/document/document-render-libreoffice-wasm/tests/provider.spec.ts) cover snapshot reuse and reload. The [real-engine tests](../../../../packages/document/document-render-libreoffice-wasm/tests/libreoffice-wasm.e2e.ts) inspect exported image dimensions with default and overridden DPI settings alongside selectable text and page count; ABI forwarding alone cannot establish filter behavior.

+ 36 - 0
.agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.zh.md

@@ -0,0 +1,36 @@
+# Agent Note: WASM 预览的字体复用与图像分辨率
+
+Status: implemented
+Archived: 2026-09-11
+
+[English](2026-09-11-wasm-preview-font-and-image-budgets.md) | 中文
+
+## 问题
+
+Office 转换在不同文档间重复解析字体元数据,并在同一文档内重复匹配字体。图片较多的文档还会在 PDF 导出时花费大量时间,重采样高于预览显示分辨率的图像。这些成本需要分别控制,因为字体复用不会减少图像解码或重采样。
+
+## 决策
+
+[WASM 提供方](../../../../packages/document/document-render-libreoffice-wasm/README.zh.md)在初始化时建立一次字体元数据快照。每个转换 Worker 接收结构化克隆。Host 保留名称、字体面属性、路径、大小和修改时间;字形覆盖、请求缓存及导入字节属于 Worker。重新加载提供方会刷新快照。Worker 读取已索引文件时校验文件,已经导入的字体在该次转换内仍可使用。
+
+每个 Worker 记忆化完整请求:字体族、样式、字重、斜体、字宽、字距类型、语言及有序码点。缓存保存 MEMFS 路径和缺失字体族观察值。命中时重放这些观察值,因为初始化请求可能在文档字体收集开始后再次出现。Worker 终止时释放缓存及引擎内存。
+
+运行时 PDF 过滤器选项将栅格图像降低到可配置的 `maxImageResolution`,默认 192 DPI。[共享 PDF 画布](../../../../packages/client/ui-sidebar-documentpreview/src/client/pdf/document.ts)按 96 CSS DPI 乘以设备像素比渲染;默认值覆盖像素比 2。文本和矢量图形仍可缩放。JSON 过滤器选项替换隐含过滤器数据时,显式导出书签保留 LibreOfficeKit 的默认行为。
+
+[本地 Office 预览决策](2026-09-10-local-office-preview.zh.md)继续负责转换生命周期、授权、缺失字体展示和 PDF 传输。
+
+## 考虑过的替代方案
+
+**在每个转换 Worker 内索引字体。** 这会将发现工作移出 Host 事件循环,并立即识别新安装的字体,但会在不同预览间重复读取完整字体文件并解析元数据。提供方快照消除重复工作,代价是同步初始化,以及字体变化后显式重新加载。
+
+**只缓存字体族名称。** 样式、语言、字距类型和缺失字符可能选择不同文件。完整请求键保留这些区别,同时仍能命中重复排版请求。
+
+**所有预览保持 300 DPI。** 更高栅格分辨率保留高像素比及放大时的细节,但增加图像导出工作。可配置的 192 DPI 默认值符合常见显示目标,无需栅格化文本或重新构建引擎。
+
+## 影响
+
+提供方冷启动包含索引成本,不计入转换时限。共享元数据不保留原始字体缓冲区,各 Worker 仍会读取选中字体并按需解码字形覆盖。安装或替换字体后需要重新加载提供方;索引后发生变化的文件可能使后续转换失败。
+
+降低图像分辨率以高倍缩放下的栅格细节换取更少的导出工作。它不是引擎内存上限:LibreOffice 仍可能解码全分辨率原图、加载大型字体集合,或耗尽构建时设定的 WASM 内存上限。此实现不引入持久化字体缓存、文件系统监听器或跨文档引擎实例。
+
+[字体测试](../../../../packages/document/document-render-libreoffice-wasm/tests/fonts.spec.ts)覆盖完整请求键、重复缺失字体族观察值、快照隔离和失败导入。[提供方测试](../../../../packages/document/document-render-libreoffice-wasm/tests/provider.spec.ts)覆盖快照复用和重新加载。[真实引擎测试](../../../../packages/document/document-render-libreoffice-wasm/tests/libreoffice-wasm.e2e.ts)检查默认及覆盖 DPI 设置时的导出图像尺寸、可选择文本和页数;仅验证 ABI 参数转发无法证明过滤器行为。

+ 6 - 0
.agents/notes/archived/bug-fix/2026-09-09-content-sized-diagram-previews.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-09-content-sized-diagram-previews.md
+2026-09-09-content-sized-diagram-previews.md: fec86111a68ec0fbc12ed6514a60c6a243924963
+2026-09-09-content-sized-diagram-previews.zh.md: 8880f12853f9e7d2d566fca3a482fd5e17514526

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

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

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

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

+ 15 - 0
.agents/notes/archived/manifest.json

@@ -364,6 +364,15 @@
     "architecture/2026-09-02-protocol-specific-model-listing-discovery.i18n.yaml": "sha256:ba8a62a9fa3263de4162709cb1aaf81f6ad4feee5011d7a5d8b24eb92ad81d30",
     "architecture/2026-09-02-protocol-specific-model-listing-discovery.md": "sha256:9db9c47559c17f0e2931b9e646f1d2d17b590df4d5699d4e779ed6d7f65fc371",
     "architecture/2026-09-02-protocol-specific-model-listing-discovery.zh.md": "sha256:9a266590d49093a097bcdb5d09865757cdd07625cfaaddce1303046d580551f1",
+    "architecture/2026-09-09-desktop-in-place-profile.i18n.yaml": "sha256:08543070a32503373d38ca98b7ce96ee60c972302534030c5ba380f19e04fa00",
+    "architecture/2026-09-09-desktop-in-place-profile.md": "sha256:82bdcad48207c5aecbc9695717aaaa4c8cca90c82ca58884bfa653b60f192290",
+    "architecture/2026-09-09-desktop-in-place-profile.zh.md": "sha256:f105636c9160c64b12bd1822c8ec81ee744ba27c0b0ab6eeed870561ccd81ce7",
+    "architecture/2026-09-10-local-office-preview.i18n.yaml": "sha256:a43a2370c7434293ef98d1901a29b7b4b03f400a108c119f3ec5f2f7ca8ca3f8",
+    "architecture/2026-09-10-local-office-preview.md": "sha256:754cfb4dde9f1ee70e495095f12fc9e3c43af4a959250bb3a59ae44fd32fd763",
+    "architecture/2026-09-10-local-office-preview.zh.md": "sha256:d8fb9a65f437b585f2dfa2b4aa091634eeabf571e98e881cf7344b7574704e53",
+    "architecture/2026-09-11-wasm-preview-font-and-image-budgets.i18n.yaml": "sha256:af027c7dae53a181e4d73fd2309f69e9ae3c48df0d3b1dfc53cae274aa60d69e",
+    "architecture/2026-09-11-wasm-preview-font-and-image-budgets.md": "sha256:44dd52839cdb67644c0dbcbc4beb6f36802628c4a8d17a0aad11db1f9be649ed",
+    "architecture/2026-09-11-wasm-preview-font-and-image-budgets.zh.md": "sha256:8285432ee1676be8b79a05953c4b6ac4a7ca5d8e701173b3a2b533608b584d0a",
     "bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.i18n.yaml": "sha256:8be5b0afd8820c593e2ec254d35fb8c384267814104f6234bb9af824c5f974ba",
     "bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.md": "sha256:861e6130e893489271478def5f54f212a106a1580b744e6e40f56356f73c7e10",
     "bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.zh.md": "sha256:3549257e06f074591de580ebfaa71284c7c384fb0ffb1963c30258d7a7c26db7",
@@ -739,6 +748,9 @@
     "bug-fix/2026-09-03-session-search-result-reveal.i18n.yaml": "sha256:ad9dcedeb25ab3eddbc51660935abaf6e6e92eefb66fd14f8852ba5f30e725b5",
     "bug-fix/2026-09-03-session-search-result-reveal.md": "sha256:ce8983a9ffa3d1b59946aeecf0e10177c8316c3621d5d0718a03e8eedcd05fd9",
     "bug-fix/2026-09-03-session-search-result-reveal.zh.md": "sha256:cd3ca0290f259a252e0b19fd0f15ac62232847be6f186e16f72652044a034bbe",
+    "bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml": "sha256:a4404fe8e35ca5b6371f9cdb58fea6724cf3e7737bb2db8a3a321d534454b17c",
+    "bug-fix/2026-09-09-content-sized-diagram-previews.md": "sha256:5239e98e5c057d6f7e10c3f10a0f67273778d99b0c4f10b31a1921c3c6474b80",
+    "bug-fix/2026-09-09-content-sized-diagram-previews.zh.md": "sha256:c1ee3790a9d5c3561e1d93d5e649afa904c7d19b9d77248c85ef16daccd953e2",
     "feature/2026-06-14-acp-agent-client-protocol.i18n.yaml": "sha256:006795baa43ae962a8d125cc0f1e9f134bc2ee9fb758b6e7669e3fa0126e1918",
     "feature/2026-06-14-acp-agent-client-protocol.md": "sha256:6828c0af74bb3fb96206ca6b21c0e56a000b50e4744aad4bc2c05092f3a5a31b",
     "feature/2026-06-14-acp-agent-client-protocol.zh.md": "sha256:ba104e841a1fb84edbd3b6c8119d50445b7785255a7a8d13bb9ac8a2cb4d2e69",
@@ -1609,6 +1621,9 @@
     "process/2026-09-03-workspace-version-coherence-gate.i18n.yaml": "sha256:ac442dee172d396395b8516d6d38c4fcedbc855d735c86c378aec5c1098ecd8d",
     "process/2026-09-03-workspace-version-coherence-gate.md": "sha256:37b63a78506a8741eb04826dc1eea7e3a64c19fd4494c2d7754054a5602d308b",
     "process/2026-09-03-workspace-version-coherence-gate.zh.md": "sha256:cfa2c8fe6cf1d4665121a987861b70a91b5b41eb99e7ed192c85760a916fddf6",
+    "process/2026-09-11-independent-libreoffice-package.i18n.yaml": "sha256:e8d2fe75e05ac53438513a7c51dab6510bcecf644019391ee6650e73961c0f8d",
+    "process/2026-09-11-independent-libreoffice-package.md": "sha256:1292b409b7b4cbc4420868de5e3b7417b3c855889023ccfd9a61d824bf3019d8",
+    "process/2026-09-11-independent-libreoffice-package.zh.md": "sha256:384783da1614c7e2ea84ea513013af6b20e9d4545a03cf7fa068415078126932",
     "simplification/2026-06-19-drop-mutable-session-summary.i18n.yaml": "sha256:43fe5daadc1491f94a3e34595182cad1591f76b64e1f13b48811b80fd7e53b94",
     "simplification/2026-06-19-drop-mutable-session-summary.md": "sha256:01647a5a14aa4e195328d4d39c6d80eb0723739e5a543115314716b280a93560",
     "simplification/2026-06-19-drop-mutable-session-summary.zh.md": "sha256:22389e0c29158b1f7a5a43ed6073f8795bb87cf51c04cf83be055359cd65c9f5",

+ 6 - 0
.agents/notes/archived/process/2026-09-11-independent-libreoffice-package.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/process/2026-09-11-independent-libreoffice-package.md
+2026-09-11-independent-libreoffice-package.md: 05fd8eb86ffb75b564008eaffda5c98669afee27
+2026-09-11-independent-libreoffice-package.zh.md: b2abf61f8a5cf9b247fa2b94697eb90a252d4cff

+ 32 - 0
.agents/notes/archived/process/2026-09-11-independent-libreoffice-package.md

@@ -0,0 +1,32 @@
+# Agent Note: Independent precompiled LibreOffice package
+
+Status: implemented
+Archived: 2026-09-11
+
+English | [中文](2026-09-11-independent-libreoffice-package.zh.md)
+
+## Problem
+
+Compiling LibreOffice inside ordinary DSH builds would require every contributor and CI job to acquire its toolchain and repeat a large native build. Desktop also needs immutable engine resources that can travel with its offline installation material.
+
+## Decision
+
+The [engine package](../../../../native/libreoffice-wasm/package.json) has its own version and an asset-only manifest export. Its source directory stays outside the main pnpm workspace. Installation has no lifecycle hook, and packaging verifies an existing successful build without compiling. The [release workflow](../../../../.github/workflows/libreoffice-wasm-release.yml) compiles only on explicit dispatch; publication requires a matching engine version tag and the protected npm environment.
+
+Each tarball contains the engine manifest and assets, corresponding source pins, patches, full source diff, and the built distribution's license and notices. Packaging rejects mismatched build receipts, modified assets, unlisted engine files, missing notices, and bundled fonts. A failure never triggers compilation automatically.
+
+The [local preview decision](../architecture/2026-09-10-local-office-preview.md) continues to own conversion and explicit artifact configuration. This change establishes release machinery; it does not add an unpublished engine dependency to DSH. The existing [Desktop package set](../../../../apps/desktop/scripts/prepare-package-set.ts) remains the intended carrier for a future fixed-version dependency and its offline seed.
+
+## Alternatives considered
+
+**Compile during ordinary builds or package installation.** This puts engine toolchain availability and compilation cost on every consumer, even when the engine version has not changed.
+
+**Extract a general Node conversion library.** Font handling, Worker ownership, and the conversion API can remain with the provider. Moving them is unnecessary to distribute precompiled assets or isolate compilation from DSH CI.
+
+**Link the engine source as a workspace dependency.** Workspace linking would replace a published precompiled package with a source directory that lacks its engine files.
+
+## Consequences
+
+The engine can be compiled and versioned independently of DSH. The package exposes files rather than a conversion API; the provider retains runtime ownership. Ordinary builds remain free of LibreOffice compilation, as checked by the [packaging tests](../../../../scripts/libreoffice-package.spec.ts).
+
+Initial publication and fixed-version consumer integration remain incomplete. A source tree that differs from its recorded recipe cannot produce a release tarball. Real-engine conversion, installation from the packed package, and Desktop offline seed qualification must accompany the first consumed engine release.

+ 32 - 0
.agents/notes/archived/process/2026-09-11-independent-libreoffice-package.zh.md

@@ -0,0 +1,32 @@
+# Agent Note: 独立的预编译 LibreOffice 包
+
+Status: implemented
+Archived: 2026-09-11
+
+[English](2026-09-11-independent-libreoffice-package.md) | 中文
+
+## 问题
+
+在普通 DSH 构建中编译 LibreOffice,会要求每个贡献者和 CI job 获取工具链并重复执行大型本机构建。Desktop 也需要能随离线安装材料分发的不可变引擎资源。
+
+## 决策
+
+[引擎包](../../../../native/libreoffice-wasm/package.json)采用独立版本,只导出产物清单。源码目录不属于主 pnpm workspace。安装没有生命周期 hook,打包只校验已有的成功构建,不执行编译。[发布工作流](../../../../.github/workflows/libreoffice-wasm-release.yml)仅在明确调度时编译;发布要求匹配的引擎版本 tag 和受保护的 npm 环境。
+
+每个 tarball 包含引擎清单和产物、对应的源码版本、补丁、完整源码差异,以及构建发行物的许可证和声明。打包拒绝不匹配的构建记录、被修改的产物、未列入清单的引擎文件、缺失的声明和内置字体。失败不会自动触发编译。
+
+[本地预览决策](../architecture/2026-09-10-local-office-preview.zh.md)继续负责转换与显式产物配置。此改动建立发布机制,不向 DSH 添加尚未发布的引擎依赖。现有 [Desktop 包集合](../../../../apps/desktop/scripts/prepare-package-set.ts)仍是未来固定版本依赖及其离线 seed 的预期载体。
+
+## 考虑过的替代方案
+
+**在普通构建或包安装期间编译。** 即使引擎版本没有变化,每个消费方也要承担工具链可用性要求和编译开销。
+
+**抽出通用 Node 转换库。** 字体处理、Worker 所有权和转换 API 可以继续由提供方负责。分发预编译产物及将编译与 DSH CI 隔离不需要迁移这些逻辑。
+
+**将引擎源码链接为 workspace 依赖。** 工作区链接会把已发布的预编译包替换为缺少引擎文件的源码目录。
+
+## 影响
+
+引擎可以独立于 DSH 编译和发布版本。包暴露文件而非转换 API;提供方保留运行时所有权。[打包测试](../../../../scripts/libreoffice-package.spec.ts)检查普通构建不编译 LibreOffice。
+
+首次发布和固定版本消费方接入尚未完成。与记录的配方不一致的源码树不能产生发布 tarball。首个被消费的引擎发行版必须同时完成真实引擎转换、打包安装,以及 Desktop 离线 seed 验证。

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.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-10-single-file-executable-sdk-runtime-distribution.md
-2026-07-10-single-file-executable-sdk-runtime-distribution.md: 665364e6d39a78f7ac497198f18ed9aa160a4cbd
-2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 8b39df71c615dc59f3ef1bf622a5736a70b085b3
+2026-07-10-single-file-executable-sdk-runtime-distribution.md: 56c9770966647e97763e0eaf339e4589fba673fb
+2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 04351105a56304020f5696cf1ce82d7a83ed2ced

Разница между файлами не показана из-за своего большого размера
+ 2 - 0
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md


Разница между файлами не показана из-за своего большого размера
+ 2 - 0
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md


+ 2 - 2
.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
-2026-07-19-gui-web-client-architecture.md: 55421d1ad6df192d08c431af3633675036a4a857
-2026-07-19-gui-web-client-architecture.zh.md: 6fb3f9a512389710f6708b7f36f42e90eef11b28
+2026-07-19-gui-web-client-architecture.md: 4d62d7ddbeddd2d5c02e42194b035ee08b5cf041
+2026-07-19-gui-web-client-architecture.zh.md: aeb6c89e677d7b65daef03d5d31372f484ae3117

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md

@@ -69,7 +69,7 @@ Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES──
 ```
 
 - **Session** (session.ts): lazily built, resident — once created it keeps eating frames in the background, so switching away and back renders instantly. Operations: `prompt`/`cancel` (RPC passthrough; failures land in the snapshot's `promptError`), `open` (pull the tail history page, idempotent), `loadOlder` (upward paging, reentry-guarded), `resync` (reconnect = clear the window and rerun open). Subscription: `subscribe`/`getSnapshot` (always the cached reference) — `implements ObservableSnapshot<ConversationSnapshot>`, with `useSelector = bindSnapshotSelector(this)` attached at construction, so a Session is directly a uSES source. Frame dispatch is one switch: `session/event` frames dedup by seq (the only dedup key), buffer while open is in flight, otherwise append + incremental projection; open/stitch merges the live buffer by seq and backfills once if `subscribed.lastSeq` outruns the window tail.
-- **ConversationSnapshot** (conversation.ts): the top-level immutable snapshot contract. `chat` contains structural `order`, an identity-stable keyed Node reader, Turn/Step indexes, and the timeline; `nodes`, `partial`, `runningCalls`, `turnTimings`, and `turnEnds` are the compatibility slice for unmigrated Trajectory consumers. Pending interactions, queue, running, removal, open state, paging, and prompt errors remain Session facts. **Reference discipline** (the premise of memo and uSES): unchanged substructures and Node values keep their references; one business update replaces only the corresponding key's value unless its order or Location changes. React still subscribes to the Session as the sole observable source, while the framework-provided `useSession(selector)` isolates Node and Location aggregate updates.
+- **ConversationSnapshot** (conversation.ts): the top-level immutable snapshot contract. `chat` contains structural `order`, an identity-stable keyed Node reader, Turn/Step indexes, and the timeline; `nodes`, `partial`, `runningCalls`, `turnTimings`, and `turnEnds` are the compatibility slice for unmigrated Trajectory consumers. Pending interactions, running, removal, open state, paging, and prompt errors remain Session facts; pending Inbox values live in the generic Session projection store. **Reference discipline** (the premise of memo and uSES): unchanged substructures and Node values keep their references; one business update replaces only the corresponding key's value unless its order or Location changes. React reads Session lifecycle through `useSession(selector)` and domain projections through `useProjection(key, selector)`, so each hook isolates unrelated updates.
 - **SessionManager** (manager.ts): instance cluster + frame entry + the session list. sessionId-bearing frames go only to existing instances (a mux broadcast must not instantiate every session); approval/question `requested` frames are the exception — they never land in history, so they buffer in `pendingBuffers` and replay on instantiation.
 - **Notifier** (notifier.ts): two channels chosen by change source. `markDirty()` (default; frame-driven changes always) batches per microtask — N changes, one notification, one re-render; the flush rebuilds the snapshot cache before notifying. `notifyNow()` (only direct echoes of user gestures) rebuilds and notifies in the same tick — controlled inputs roll the DOM back and jump the caret if their echo defers to a microtask. Frame-driven code using notifyNow collapses batching back to per-frame renders; banned.
 - **ConversationNodeAssembler** (`runtime/src/client/conversation/`): the Session-owned incremental engine runs independently registered Definitions over raw events. `match(event)` selects `(kind, id)` without Context scans; start/update build Definition state; engine-computed Locations carry Turn/Step closure; backward Context reads record dependencies repaired by later prepends; `buildViewNode(target)` materializes only dirty Contexts. The Chat builder preserves structural order and per-key value identity, `useSession` selectors isolate consumption, and Assistant token publication coalesces to one animation frame. The [Conversation Node decision](2026-08-09-client-conversation-node-assembly.md) owns assembly, while [Tool presentation ownership](../../archived/architecture/2026-08-08-client-tool-presentation-ownership.md) owns recursive Tool rendering.

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md

@@ -69,7 +69,7 @@ Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES──
 ```
 
 - **Session**(session.ts):懒建、常驻——建成后在后台持续吃帧,切走切回秒显。操作面:`prompt`/`cancel`(RPC 透传;失败落进快照的 `promptError`)、`open`(拉尾页 history,幂等)、`loadOlder`(向上翻页,防重入)、`resync`(重连 = 清窗口重跑 open)。订阅面:`subscribe`/`getSnapshot`(恒返缓存引用)——`implements ObservableSnapshot<ConversationSnapshot>`,构造时挂 `useSelector = bindSnapshotSelector(this)`,Session 本身就是 uSES 源。帧分发是一个 switch:`session/event` 帧按 seq 去重(唯一去重键),open 在途时缓冲,否则追加 + 增量投影;open/缝合按 seq 合并 live 缓冲并去重,`subscribed.lastSeq` 超出窗口尾则回补一次。
-- **ConversationSnapshot**(conversation.ts):顶层不可变快照约定。`chat` 包含结构化 `order`、identity 稳定的 keyed Node reader、Turn/Step index 和 timeline;`nodes`、`partial`、`runningCalls`、`turnTimings`、`turnEnds` 是未迁移 Trajectory 消费方使用的兼容 slice。pending interaction、queue、running、removed、open state、paging 和 prompt error 仍是 Session 信息。**引用纪律**(memo 与 uSES 的前提):未变化的子结构和 Node value 保持引用;单个业务更新只替换对应 key 的 value,除非它的顺序或 Location 发生变化。React 仍只订阅 Session 这一处 observable source,并由框架提供的 `useSession(selector)` 隔离 Node 与 Location 聚合更新。
+- **ConversationSnapshot**(conversation.ts):顶层不可变快照约定。`chat` 包含结构化 `order`、identity 稳定的 keyed Node reader、Turn/Step index 和 timeline;`nodes`、`partial`、`runningCalls`、`turnTimings`、`turnEnds` 是未迁移 Trajectory 消费方使用的兼容 slice。pending interaction、running、removed、open state、paging 和 prompt error 仍是 Session 信息;待处理 Inbox 值则位于通用 Session projection store。**引用纪律**(memo 与 uSES 的前提):未变化的子结构和 Node value 保持引用;单个业务更新只替换对应 key 的 value,除非它的顺序或 Location 发生变化。React 通过 `useSession(selector)` 读取 Session lifecycle,通过 `useProjection(key, selector)` 读取领域投影,使每个 hook 都隔离无关更新。
 - **SessionManager**(manager.ts):实例簇 + 帧总入口 + 会话列表。带 sessionId 的帧只投已存在实例(mux 广播不得把每个会话都实例化);例外是审批/问答 `requested` 帧——它们不落 history、open 无法回补,故缓冲进 `pendingBuffers`,实例化时回放。
 - **Notifier**(notifier.ts):两条通知通道,按变更来源取用。`markDirty()`(默认;帧驱动一律用它)按微任务合批——N 次变更、一次通知、一次重渲染;flush 先重建快照缓存再通知。`notifyNow()`(仅用户手势的直接回响)同 tick 重建并通知——受控输入的回响若延到微任务,DOM 会回滚、光标跳尾。帧驱动代码用 notifyNow 会让合批塌回逐帧渲染;禁。
 - **ConversationNodeAssembler**(`runtime/src/client/conversation/`):Session 拥有的增量引擎在原始事件上运行各自独立注册的 Definition。`match(event)` 无须扫描 Context 即可选出 `(kind, id)`;start/update 构造 Definition state;引擎计算的 Location 携带 Turn/Step 关闭信息;向前查询 Context 时记录依赖,并由后续 prepend 修复;`buildViewNode(target)` 只物化 dirty Context。Chat builder 保留结构顺序和 per-key value identity,`useSession` selector 负责消费隔离,Assistant token 发布则合并到每个 animation frame 一次。[Conversation Node 决策](2026-08-09-client-conversation-node-assembly.zh.md)拥有组装边界,[Tool 展示所有权](../../archived/architecture/2026-08-08-client-tool-presentation-ownership.md)拥有 Tool 递归渲染。

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-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-23-client-plugin-loading-model.md
-2026-07-23-client-plugin-loading-model.md: d0f9b20f0adabda6cc7132e5411bcadd60c9e108
-2026-07-23-client-plugin-loading-model.zh.md: 27d5026ed03509d6408a16389246cb1321a37959
+2026-07-23-client-plugin-loading-model.md: a8d016a70792ad4ac6a71083677ce5d82a30af0a
+2026-07-23-client-plugin-loading-model.zh.md: 1b94909c9611f01e5a6d7fcb50ec3f91ee88fab1

Разница между файлами не показана из-за своего большого размера
+ 13 - 5
.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md


+ 19 - 11
.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.zh.md

@@ -26,7 +26,7 @@ host 侧,cordis 插件装载站在 Node 的模块机制之上——require cac
 
 ### 包成员与模块请求
 
-[Client 外壳分层 Note](2026-08-15-client-shells-and-dynamic-packages.zh.md)定义当前的静态、动态包集合及其 import 规则。装载机件把每个 `dsh.client` 包视为一个 host graph row,且每个包只有一个普通 `lib/client.js` factory bundle。包声明携带 Cordis `inject` 边、同步模块表 `external` 请求,以及可选的 `immediately` 预取标记;负责组合的 app 只拥有挂载名册。
+[Client 外壳分层 Note](2026-08-15-client-shells-and-dynamic-packages.zh.md)定义当前的静态、动态包集合及其 import 规则。装载机件把每个 `dsh.client` 包视为一个 host graph row;每个包都有一个普通 `lib/client.js` factory bundle,还可有编译器生成的 `lib/client.<name>.js` chunk。包声明携带 Cordis `inject` 边、同步模块表 `external` 请求,以及可选的 `immediately` 预取标记;负责组合的 app 只拥有挂载名册。
 
 Web 内核保持不依赖框架,也不 import 任何动态包实体。Modules 本身是动态图 row,但 host parser 会在 Vite 主模块前送达其 factory。内核调用 `create()` 时,由 HTML 安装的 `__ModuleLoader__` facade 使用该 factory 构造模块系统。其他每个动态图 row 都归属一个 application combo 脚本;React、Cordis 与静态 UI 库的身份由外壳 seed 提供。
 
@@ -34,17 +34,19 @@ Web 内核保持不依赖框架,也不 import 任何动态包实体。Modules
 
 浏览器复刻 host 侧的分工。`dsh-client-modules`(`ClientModuleSystem`)坐上 host 侧由 Node 内部 ESM loader 占据的模块系统席位;同一份 vendored `@cordisjs/plugin-loader` 在两侧都坐治理席。二者的分界线一句话说尽:**模块系统拥有模块身份与字节——代码怎么到达、怎么登记、怎么变成导出内容;Loader 拥有插件生命周期——插件何时挂载、等待什么、如何拆除。**
 
-`ClientModuleSystem` 是一张 lazy CJS 表。执行 bundle 只**登记**其 factory——bundle 调用 `window.__ModuleLoader__.load({ id, factory })`,此外什么都不发生。模块体的一切副作用(包括 CSS 注入)都住在 factory 闭包里,在物化时运行:物化即该 id 的首次 `require`/import,此后记忆化。Import 和 prefetch 会先递归登记已声明的动态请求,再登记消费者;随后 factory 会同步物化任何已登记但尚未物化的请求。模块表按固定分支顺序解析:seed word → 记忆化记录 → graph row classic-script 登记 → 已登记 factory 物化 → 大声抛错。Modules factory 是自举例外:HTML facade 先物化它,构造过程再把同一 exports 直接写入记忆化表。最后这一抛是构建期纯度门禁在运行时的镜像。系统还保管逐模块簿记——名下 `<style data-plugin>` 标签 id、观测到的 require 边——并暴露 HMR(热模块替换)需要的两个动词:`prefetch(id)`(登记所请求的动态 factory 和本 row 自身的 factory;并发到达共享一个任务)与 `invalidate(id)`(丢弃非 bootstrap factory 与记录,下次到达即重新加载)
+`ClientModuleSystem` 是一张 lazy CJS 表。执行 bundle 只**登记**其 factory——入口调用 `window.__ModuleLoader__.load({ id, factory })`,chunk 还会提供其生成文件名——此外什么都不发生。模块体的一切副作用(包括 CSS 注入)都住在 factory 闭包里,在物化时运行:物化即该 id 的首次 `require`/import,此后记忆化。Import 和 prefetch 会先递归登记已声明的动态请求,再登记消费者;随后 factory 会同步物化任何已登记但尚未物化的请求。可调用的 `require` 解析同步模块表请求;其 `require.async` 操作返回 Promise,负责加载、登记并物化一个包内 chunk。共享 tsdown 预设把源码中针对包内 chunk 的 `import()` 表达式编译到这个独立操作。受支持的产物必须自包含:入口或 chunk 不能同步 require 另一个相对 `client*.js` 产物。模块表按固定分支顺序解析:seed word → 记忆化记录 → graph row classic-script 登记 → 已登记 factory 物化 → 大声抛错。Modules factory 是自举例外:HTML facade 先物化它,构造过程再把同一 exports 直接写入记忆化表。最后这一抛是构建期纯度门禁在运行时的镜像。系统还保管逐模块簿记——名下 `<style data-plugin>` 标签 id、观测到的 require 边——并暴露 HMR(热模块替换)需要的两个动词:`prefetch(id)`(登记所请求的动态 factory 和本 row 自身的 factory;并发到达共享一个任务)与 `invalidate(id)`(推进 owner 代次并丢弃非 bootstrap 包的入口和 chunk factory 及记录,让下次到达重新加载它们)。在旧代次捕获的 chunk 请求不能填充新代次
 
 vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点是 `tree.import`——并拥有一切 entry 形状的事务:entry 创建、fiber 经 cordis 服务等待的激活(注入的服务未就位即保持 PENDING,服务 provide 时级联激活)、update/refresh、拆除。治理代码按 vendor 政策与 host 侧逐字节相同。浏览器化是壳 vite 配置里的编译期映射:一个 `node:module` stub 别名加若干 `process.*` define,使 `ModuleLoader.fromInternal()` 返回 undefined——这正是留给壳来填的空槽。模块系统挂载为 `ctx.modules`。
 
 ### Combo 外部脚本到达与源码映射
 
-Host 会快照每个已构建插件产物,并把每个调度阶段的有序 row 划入一个或多个同源 classic script。它在更长的 map 形式请求 URL 保持在 3 KiB 以内时贪心填充每组,既保留 graph 顺序,也以增加请求代替超长 URL。每个脚本都由其中的 package 资源寻址,例如 `/plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>`。`bootstrap` 与 `application` 是图中的调度阶段,不是 URL 组成部分:HTML 先预加载所有 application URL,再执行所有阻塞 parser 的 bootstrap URL。模块系统按 combo URL 复用进行中的传输,因此同组 row 的并发到达只执行一个脚本。成功结算仍要求模块表中已经存在被请求 row 的 factory id;登记不会运行 factory,所以副作用边界依然是首次物化。
+Host 会快照每个已构建插件入口 bundle,并把每个调度阶段的有序 row 划入一个或多个同源 classic script。它在更长的 map 形式请求 URL 保持在 3 KiB 以内时贪心填充每组,既保留 graph 顺序,也以增加请求代替超长 URL。每个脚本只包含 `client.js` 资源,并由这些 package 资源寻址,例如 `/plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>`。Host 不扫描同级文件,也不把 chunk 加入启动 combo。`bootstrap` 与 `application` 是图中的调度阶段,不是 URL 组成部分:HTML 先预加载所有 application URL,再执行所有阻塞 parser 的 bootstrap URL。模块系统按 combo URL 复用进行中的传输,因此同组 row 的并发到达只执行一个脚本。成功结算仍要求模块表中已经存在被请求 row 的 factory id;登记不会运行 factory,所以副作用边界依然是首次物化。
 
-共享 tsdown 预设为每个插件产出 `client.js.map`,并把第一方源码路径重写成浏览器可识别的仓库形式 `/packages/<group>/<package>/src/...`。生产 Client 构建会消费 `lib/types`;预设把每份 tsc map 交给 Rolldown,并从原文件补齐 `sourcesContent`,使最终 map 回到 TypeScript/TSX,而不是停在编译后的 JavaScript。内联进 bundle 的其他 workspace 源码同样回到其 `packages/` 归属,依赖包路径保持原样。Combo 生成会移除每个局部调试指令、记录其生成行偏移、以原插件 map URL 解析每个自带 source,再产出 Indexed Source Map v3。插件有自带 map 时直接用于对应 section;没有时则生成 identity section,内嵌构建后 bundle,并在存在时把 packer 写入的 `sourceURL` 用作 source 名。绝对 map URL 会平行改写脚本资源列表中的每个 `client.js` 后缀,因此 `/plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` 指向 `/plugins/??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`。单资源也采用相同规则,仍产出只有一个 section 的 indexed map。Vite 壳同样产出 sourcemap,使壳代码与经 combo 加载的插件都能从 stack 和性能 profile 回到 TypeScript/TSX
+一次 `require.async("./client.<name>.js")` 调用会请求精确的带 revision URL:`/plugins/<package>/client.<name>.js?rev=<rev>`。Host 只在收到请求时读取该文件,把文件已有的 chunk 登记包装为一个脚本响应,并按 URL 缓存响应。并发调用共享一个进行中的脚本任务;成功结算要求 chunk 已登记,随后 loader 才会物化并记忆化其 exports。未知名称、缺失文件以及不同于所属 graph row 的 revision 都返回 404
 
-图为 HMR 保留每个 row 带 revision 的单资源 combo URL,并为每个启动 combo 请求增加按内容寻址的描述;多条描述可以使用同一调度阶段。初始 row revision 是进程级不透明 nonce,而不是内容哈希;它无需在启动时哈希每个插件,也能保证已快照的单资源响应不可变。watcher 观察到某个产物变化后,`rebuilt(id)` 只哈希该 bundle 与 map,并发布所得 revision。启动 combo revision 覆盖合并脚本输入与 indexed map。版本化脚本与 map 使用 immutable 缓存。Host 只提供精确生成的 URL;陈旧 revision 与未发布资源列表返回 404,不会别名到其他字节。外部脚本的 `error` 事件不给响应状态与正文,因此失败诊断只报告 URL;同源 Host 与构建期写入的 registration id 是身份边界,`load` 后的 factory 存在性检查负责拒绝未登记预期 id 的产物。
+共享 tsdown 预设为每个插件入口与 chunk 产出 map,并把第一方源码路径重写成浏览器可识别的仓库形式 `/packages/<group>/<package>/src/...`。生产 Client 构建会消费 `lib/types`;预设把每份 tsc map 交给 Rolldown,并从原文件补齐 `sourcesContent`,使最终 map 回到 TypeScript/TSX,而不是停在编译后的 JavaScript。内联进 bundle 的其他 workspace 源码同样回到其 `packages/` 归属,依赖包路径保持原样。Combo 生成会移除每个局部调试指令、记录其生成行偏移、以原插件 map URL 解析每个自带 source,再产出 Indexed Source Map v3。插件有自带 map 时直接用于对应 section;没有时则生成 identity section,内嵌构建后 bundle,并在存在时把 packer 写入的 `sourceURL` 用作 source 名。绝对 combo map URL 会平行改写脚本资源列表中的每个 `client.js` 后缀,因此 `/plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` 指向 `/plugins/??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`。chunk 脚本同样指向自己的 map URL。只有在这些 map URL 收到 `GET` 后,source map 文件才会被读取和组合;`HEAD` 不会物化脚本或 map body。Vite 壳同样产出 sourcemap,使壳代码与经外部加载的插件都能从 stack 和性能 profile 回到 TypeScript/TSX。
+
+图为 HMR 保留每个 row 带 revision 的单资源 combo URL,并为每个启动 combo 请求增加带 revision 的描述;多条描述可以使用同一调度阶段。初始 row revision 是进程级不透明 nonce,而不是内容哈希;它无需在启动时哈希每个插件,也能保证已快照的单资源响应不可变。共享预设会在每个包输出写完后标记入口。watcher 观察到该构建完成标记后,`rebuilt(id)` 会把入口字节与其时间戳一起哈希并发布所得 revision;因此只重建 chunk 也会推进 owner revision,无需 Host 扫描 sibling。启动 combo revision 从有序 row revision 派生。脚本 body 在首次 `GET` 时组合;source map 文件在首次 map `GET` 时单独读取并组合。`HEAD` 不会物化任一 body。版本化脚本与 map 使用 immutable 缓存,无关图重组会保留相同 revision 下已经物化的 chunk 响应。Host 只提供精确生成的 URL;陈旧 revision 与未发布资源列表返回 404,不会别名到其他字节。外部脚本的 `error` 事件不给响应状态与正文,因此失败诊断只报告 URL;同源 Host 与构建期写入的 registration id 是身份边界,`load` 后的 factory 存在性检查负责拒绝未登记预期 id 的产物。
 
 ### 装载流程,端到端
 
@@ -68,25 +70,31 @@ Host 会快照每个已构建插件产物,并把每个调度阶段的有序 ro
 4. `settled` = 每个 entry 已创建 + `loader.await()` 完全停稳 + 一次全 ACTIVE 扫描。扫描列出每个 import 失败、FAILED 或 PENDING 的 fiber 及其缺失的服务。它存在的理由:cordis 的 inject 等待没有超时——这次扫描就是大声失败的兜底线。
 5. 不依赖框架的 loading 页经 `internal/status` 投影真实 fiber 状态。检查完成后,内核调用 `ctx.uiRenderer.mount(container)`,一次切换到真实 UI。
 
+### 动态图对账
+
+modules 控制器只持有从启动清单创建的 Loader 条目。Host 的完整快照更新模块描述并对账这些条目;其他 Loader 贡献方保留自身所有权。新增模块通过单资源 URL 到达,因为重放启动 batch 可能重复注册现有 factory。移除使用 Loader 删除语义,随后等待已捕获 fiber 清理完毕,再移除未使用的模块与样式。已声明及已观察到的传递依赖使共享模块保持存活。Factory revision 的跟踪独立于条目激活,因为下载或物化完成后仍可能没有创建条目。图更新会在任何消费者导入依赖前,使未归属受管条目的陈旧 factory 和失败的到达目标失效,并清除其样式;重建帧也会对账因导入失败而缺失的条目。
+
+Host SSE 适配器转发现有图变化通知,并在连接时发送当前完整图。图描述浏览器的目标条目,不代表 Host 清理完成:Host 生命周期顺序属于其 Loader,每个浏览器则等待自身被移除 fiber 的清理。图对账与代码重建共用一个页面队列。本地代际阻止过期下载挂载;不透明 revision 只比较相等。失败页面报告本地错误,并可重试同一张图而不改变 Host 启用状态。这保留了无关页面状态,也无需重启应用或引入第二套插件执行器。Electron 的独立安装流程不属于此机制。
+
 ### 热重载:一个驱动插件,自行监视的 bundle
 
 热重载是一项组合决策:web 组合包无条件挂载 `client-hmr` 行(一个常规的插件包),其 node 半带来 bundle 监视与 SSE(Server-Sent Events)通道;没有重建 watcher 改写客户端 bundle 时链路保持空闲。不应暴露它的组合可以禁用该行。
 
-重建好的 bundle 怎么变成重载信号?hmr 的 node 半自己观察——没有构建器来通知它。模块 host 在读取每份启动快照前捕获 bundle 的 stat 基线,并通过 `ctx.clientModules.artifactBaseline(id)` 暴露它。HMR 自持的单个定时器把当前图的每个 row 与这份基线比较:未变化的 row 直接开始监视,不读取内容也不求哈希;基线捕获后的写入已经形成 stat 差异,只有该 row 会进入 `rebuilt(id)`。这同时消除了启动期的全量重哈希,并避开 `fs.watchFile` 以异步首次 stat 建立基线、可能静默吸收构造期重建的问题。监视集合的成员随 `onGraphChanged` 更新;消失的 row 撤下监视,轮询时缺失的 bundle 则让对应 row 保持标脏状态,文件重现时即使元数据相同也强制重哈希。Bundle 的 mtime 或 size 变化,或 row 处于标脏状态时,`rebuilt(id)` 是重哈希的唯一入口;它会在新产物快照中一并读取当前 source map,而仅写入 map 不会重新挂载未变化的可执行代码。`rev` 真正变化时,node 半才在 `GET /plugins/events` 上广播 `rebuilt` 帧——这是一条系统级 SSE 通道,连接即发全量图,变更时发 `rebuilt` 帧,仅供呈现的 wire,永不进会话日志。轮询是刻意选择:inotify 在 weka 网络挂载上不触发,构建侧监视器需要 `--poll` 也是同一原因;每个 row 每个间隔只需一次 bundle stat,轮询间隔是一个经校验的配置字段(默认 500ms),dispose(资源释放)会清掉那一个定时器。重建产物是任意一个 tsdown watch 进程的事——`scripts/dev-web.ts` 仍作为 watch 构建入口保留,其包清单在启动时扫描 `packages/*/*/package.json` 按 dsh.client 发现——构建器与 host 共享零协议。写一半的 bundle 被撕裂读取会自愈:写入完成期间 stat 持续变化,下一个轮询节拍会再次重哈希并广播最终的 rev。
+重建好的 bundle 怎么变成重载信号?hmr 的 node 半自己观察——没有构建器来通知它。模块 host 在读取每份启动快照前捕获入口 stat 基线,并通过 `ctx.clientModules.artifactBaseline(id)` 暴露它。HMR 自持的单个定时器把当前图的每个 row 与这份基线比较:未变化的 row 直接开始监视,不读取内容也不求哈希;基线捕获后的写入已经形成 stat 差异,只有该 row 会进入 `rebuilt(id)`。这同时消除了启动期的全量重哈希,并避开 `fs.watchFile` 以异步首次 stat 建立基线、可能静默吸收构造期重建的问题。监视集合的成员随 `onGraphChanged` 更新;消失的 row 撤下监视,轮询时缺失的入口则让对应 row 保持标脏状态,文件重现时即使元数据相同也强制生成新 revision。共享 tsdown 预设会在每个 sibling 输出写完后标记 `client.js`;入口 mtime 或 size 变化、或 row 处于标脏状态时,`rebuilt(id)` 从入口字节与构建完成时间戳派生 revision。只修改 chunk 因此也会更换 revision;部分写入之后的完成标记还会提供一次更晚的 stat 变化以完成自愈。`rev` 变化时,node 半在 `GET /plugins/events` 上广播 `rebuilt` 帧——这是一条系统级 SSE 通道,连接即发全量图,变更时发 `rebuilt` 帧,仅供呈现的 wire,永不进会话日志。轮询是刻意选择:inotify 在 weka 网络挂载上不触发,构建侧监视器需要 `--poll` 也是同一原因;每个 row 每个间隔只需一次入口 stat,轮询间隔是一个经校验的配置字段(默认 500ms),dispose(资源释放)会清掉那一个定时器。重建产物的进程必须使用共享 Client tsdown 预设;`scripts/dev-web.ts` 仍作为 watch 构建入口保留,其包清单在启动时扫描 `packages/*/*/package.json` 按 dsh.client 发现,构建器与 host 之间没有通知协议
 
-浏览器侧,驱动插件每帧重载一个插件,串行执行
+浏览器侧的传输把代码替换交给负责图对账的同一个 modules 控制器
 
 1. `invalidate`——丢弃陈旧的 factory 与记录,并把 rebuilt 帧的 revision 绑定到该 row 的单资源 combo URL。Factory 还活着会让下一步变成 no-op。
 2. `prefetch`——加载该单资源外部脚本并登记新 factory,旧 fiber 此刻仍在服役。初始多资源脚本不会再次执行。
 3. `registry.delete`——先于任何 fiber 操作。裸做 fiber dispose 会触发 vendored Loader 的自 dispose 分支,把 entry 永久停用。
 4. 排空旧 fiber 的各 disposer。
 5. 移除名下的 `<style data-plugin>` 标签。
-6. `entry.refresh()`——重新 import,物化新工厂。CSS 在这里重新注入,沿用同一批稳定标签 id
+6. 通过模块系统物化新导出,再调用 `entry.refresh()` 由 Loader 挂载。CSS 在旧 disposer 清理完成后重新注入;显式物化使导入错误能够被捕获,而不只留下 Loader 的控制台日志
 7. `fiber.await()`——让失败大声重抛。
 
-每个插件都共享同一套语义;`immediately` 行的重载与 lazy 行分毫不差。依赖级联不花一行 client 代码:fiber 的激活纪元串接着它各服务提供方的 uid,因此替换 connection 等基础 provider 的 fiber 时,每个依赖方都会经 cordis 本身重新装载——行为正确,但代价较高。
+Bootstrap 替换会在失效或卸载前被拒绝:模块系统保留其初始导出,重新挂载旧代码会重置消费者,却无法应用请求的 revision。页面会报告需要刷新,并保留 bootstrap fiber。所有非 bootstrap 插件都共享同一套替换语义;`immediately` 行的重载与 lazy 行分毫不差。依赖级联不花一行 client 代码:fiber 的激活纪元串接着它各服务提供方的 uid,因此替换 connection 等基础 provider 的 fiber 时,每个依赖方都会经 cordis 本身重新装载——行为正确,但代价较高。
 
-支持边界,如实陈述。重载粒度刻意做粗:全新 fiber、全新组件、React 状态丢失、数据层不动——react-refresh 级的状态保留与「重执行 bundle 即重跑 factory」相冲突,属刻意不做。静态装配包与外壳内核不是 entry:改动它们意味着外壳重建加整页刷新。重载不做回滚:import 失败让 entry 失去 fiber,下一个 rebuilt 帧从头重试;apply 失败留下 FAILED fiber 交给状态投影;两者都大声记录。自我重载可行——在途的重载在旧 bundle 的闭包里跑完,新的 apply 再开一条新 SSE 通道——但空窗期到达的帧会丢失,下次重建会再次通知。一处已知的仅限 dev 竞态:rebuilt 帧与仍在途的 boot 到达重叠时共享那次到达的任务,可能物化重建前的字节;下一帧自愈
+重载会创建新的 fiber 和组件状态,不保留被替换插件内部的 React 状态。静态组装库与应用壳需要重建后的页面。导入或激活失败仍可诊断和重试,不会回滚无关插件。自重载关闭旧 SSE 通道并打开新通道;其完整快照补齐遗漏的图变更。启动、图更新和重建帧共用同一队列,因此代码替换不会与初始模块到达重叠
 
 ## 包归属
 
@@ -96,7 +104,7 @@ Host 会快照每个已构建插件产物,并把每个调度阶段的有序 ro
 
 Wire 两侧运行同一份治理实现;浏览器特有层只包含一套模块系统和一个重载插件。动态包只有一种产物形态,因此纯度检查覆盖全部动态包。Cordis 依赖、模块请求与启动档位都与其所有者——manifest——同住,负责组合的 app 只握名册。Host graph 校验与递归请求到达使同步 factory 依赖保持显式。浏览器原生 script 装载保留插件网络资源、生成 bundle 与 TypeScript/TSX 源码之间的标准映射,模块系统也只保留一个可替换的 `loadBundle` 钩子。
 
-接受的代价:vendored Loader 在浏览器里背着闲置机件(EntryTree 持久化是 no-op,分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;graph `inject` row 指导 factory 到达,但服务可用性仍是激活权威,因此不匹配会在 settled 扫描时浮出;静态 UI 库保留直接实体导出;每个 bundle 多出一份 sourcemap 产物,外部 script 失败也只能给出粗粒度 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。Host 会保留逐插件 bundle/map 快照、生成的单资源响应、当前启动 combo 响应及上一代启动响应,因此内存会随组合出的客户端产物增长为数份副本。这组保留状态使 URL 保持不可变,并让进行中的请求跨越一次 HMR 重组后仍能完成
+接受的代价:vendored Loader 在浏览器里背着闲置机件(EntryTree 持久化是 no-op,分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;graph `inject` row 指导 factory 到达,但服务可用性仍是激活权威,因此不匹配会在 settled 扫描时浮出;静态 UI 库保留直接实体导出;每个 bundle 多出一份 sourcemap 产物,外部 script 失败也只能给出粗粒度 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。Host 为当前图及上一代启动图保留 bundle 快照与惰性响应计划。脚本和 map body 在首次 `GET` 后缓存,因此内存随 bundle 快照和已请求的响应 body 增长。每份已物化响应在其 URL 下保持固定;若上一代 map 在重建后才首次被请求,则会读取当前 map 文件
 
 名册位于 web 组合包的配置树(`packages/bundle/web-app/cordis.patch.yml`);`mountWebPlugins` 与 `CLIENT_PACKAGES` 常量已消失,重组一次部署等于替换 yml/overlay。Graph 组合器位于 `dsh-client-modules` node 半,由 parser 预载的 Client face 则自举浏览器模块表。Webserver 继续作为朴素路由注册插件;`/api/*` 绑定、浏览器认证、RPC envelope 与精确 Fetch 路由属于 Connection node 半,Remote 分发属于 API Gateway,开发期 bundle 监视与 SSE 通道属于 HMR node 半。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.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-25-web-client-session-scope-and-provide-channel.md
-2026-07-25-web-client-session-scope-and-provide-channel.md: 4cdca4c1cb5b512c1696a397bbb6e4d070a6a487
-2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 0258609fd8df90dc2133fcb8bf6e7e50efaab941
+2026-07-25-web-client-session-scope-and-provide-channel.md: 6126d06e48111a551a0dc57247659a3086986c66
+2026-07-25-web-client-session-scope-and-provide-channel.zh.md: e7bf4aa6952b4fd7b187c91e2ac5d64aac32cabb

+ 19 - 15
.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md

@@ -19,6 +19,8 @@ Hard constraints: the host is the single source of truth; every registration goe
 
 ## Decision
 
+The reference-owned lifetime and Provider targeting now follow [Client Session references](2026-09-15-client-session-references.md). This note retains the blank-Session and adoption rationale and describes their current realization.
+
 ### The parity model: client and host share one root state axis
 
 Host-side `session.create(workspaceId)` produces Session + Agent + cwd in one piece (an atomic bundle, never split); the client side is the mirror of that birth — the instant a session row enters the list mirror, the client mints its Agent scope (actx + provide + the full input surface mounted):
@@ -48,13 +50,13 @@ id→ctx handoff is allowed in only three kinds of places (business providers ne
 - Root coordination services self-addressing: from a projection's sessionId back to the actx via `sessions.scope(id)`.
 - Root untagged listeners: looking up their own store by the payload's sessionId.
 
-### Scope lifecycle: anchored to the list mirror — birth is entering view, death is prune
+### Scope lifecycle: anchored to explicit references
 
-Session instances share the scope's lifecycle; liveness eligibility = host-listed (one criterion, shared by mint and prune):
+Session instances share the scope's lifecycle, while the catalog reports discoverability without retaining a generation:
 
-- Birth = a session row entering client view (the list baseline pull / the local `create()` echo / the `host/session-added` frame); a lazy first resolve mints the scope (resolution is a pure function, render-safe).
-- One prune tears down three things together: the Session instance, the scope fiber (cascading through every consumer hung on the actx), and the session-keyed slot store. The staged session (= `list.current`) is the exception: removed while still on stage, it keeps a frozen read-only view, torn down only once the stage moves away.
-- Reopening = lazily rebuilding the instance + `open()` pulling history (the host session log is the durable truth).
+- Birth = the first explicit `sessions.retain(target, options)`; it synchronously returns a reference and mints the Session binding and scope before history is ready.
+- Final release withdraws the exact generation before tearing down its Session instance, scope fiber (cascading through every consumer hung on the actx), and session-keyed slot store. Catalog removal does not end a generation while references remain.
+- Reopening = a later retain lazily rebuilding the generation and exposing history readiness through `reference.ready` (the Host Session log is the durable truth).
 - Remaining TODO: approval/question frames never enter history and cannot be recovered across a prune (the manager-level pendingBuffers cover only the never-instantiated window).
 
 ### The blank bit: the empty session's visible projection, conversion, and reuse
@@ -67,7 +69,7 @@ A session "materialized but with no first prompt" is governed by the summary-der
   - The sender's own tab: the **successful response** to the first `prompt()` flips false (acceptance proves the user/message is already in the host log — this flip is confirmation, not optimism; `onEngaged` synchronously updates the list mirror, converting the current `New Session` row in place to an ordinary title, adding no list row). A rejected first prompt keeps the session blank: aligned with host authority, still shown as `New Session`, keeping its connectWorkspace reuse eligibility while it remains a Workspace member.
   - Other tabs: the `host/session-status (running:true)` frame flips it — a blank session never runs, so the first running necessarily means no longer blank;
   - Reconnect alignment: `session.list`'s summary.blank is authoritative, so a tab that missed frames aligns naturally on its next pull; a stale blank:true can never mark a converted session back to blank.
-- List discipline: the store retains every row; the Workspace browser's grouping, flat view, search, and counts share one visible projection — every non-blank session shows, while blank sessions show only the one with `session.id === sessions.current`, its title forced to `New Session`. After a Workspace switch, the old blank entity stays in the mirror but is hidden from the list while the target Workspace's current blank shows; the user-visible surface therefore holds at most one blank row globally.
+- List discipline: the store retains every row; the Workspace browser's grouping, flat view, search, and counts share one visible projection — every non-blank session shows, while blank sessions show only the row retained by the `mainView` source, with its title forced to `New Session`. After a Workspace switch, the old blank entity stays in the mirror but is hidden from the list while the target Workspace's main blank shows; the user-visible surface therefore holds at most one blank row globally.
 - The residue ledger takes zero GC: after a refresh, blank sessions come back with the bit intact and are reused on the next same-workspace connect while they remain members, so the ordinary single-tab path keeps at most one per workspace; after a host restart, blanks leave no disk trace and simply evaporate; the extra empty shells from multi-tab races only become non-current hidden rows, digested by later reuse, with no coordination.
 
 ### connectWorkspace: the sole entry point of New Session
@@ -77,29 +79,31 @@ A session "materialized but with no first prompt" is governed by the summary-der
 - The reuse arm: the list mirror is searched for `blank && cwd == workspace.path && sessionIds.includes(id)` — the host's own membership rule, never cwd alone. A cwd match without the account slot (a CLI/TUI session birthed at the host cwd, or a deleted/recreated registration) would open a session no grouping surface can show under this Workspace, so it falls through to the create arm instead (see the [membership reuse fix](../../archived/bug-fix/2026-08-05-workspace-blank-session-reuse-membership.md)); a hit returns that id directly, creating nothing.
 - The create arm: on a miss, `session.create({workspaceId})` returns the new id.
 - An unknown workspaceId fails loud (never silently creating somewhere else).
-- The resolution guarantee (one contract for both arms): when the promise resolves, the returned id is already in the list store and `sessions.binding(id)` resolves synchronously — `SessionRuntime.create` projects the list synchronously after RPC success before resolving, so a draft mover can write text into the new scope's machine before open, without waiting for a notifier flush.
-- The caller takes the id and does its own `sessions.open`; sending the first prompt is an ordinary `session.prompt` — the session already exists, a failure is an ordinary prompt failure, the draft text is still in the machine, and a retry is simply sending again.
-- The global New Session button defaults to `recentWorkspaceId`: first comparing each Workspace's newest Session `updatedAt`, falling back to the Workspace `createdAt` when it has no Sessions, and keeping host order on ties; only with no Workspace at all does it `sessions.clear()` into the no-session view. Create actions inside a Workspace group still hit that Workspace explicitly.
+- The resolution guarantee (one contract for both arms): when the promise resolves, the returned id is already in the list store. The view owner then retains it synchronously, so a draft mover can write text through that binding before history readiness without waiting for a notifier flush.
+- The caller takes the id and installs a `mainView` reference; sending the first prompt is an ordinary `session.prompt` — the Session already exists, a failure is an ordinary prompt failure, the draft text is still in the machine, and a retry is simply sending again.
+- The global New Session button defaults to `recentWorkspaceId`: first comparing each Workspace's newest Session `updatedAt`, falling back to the Workspace `createdAt` when it has no Sessions, and keeping Host order on ties; only with no Workspace at all does it clear the main-view reference into the no-Session view. Create actions inside a Workspace group still hit that Workspace explicitly.
 - At startup the runtime subscribes to the first complete baseline: a successfully restored current session is kept in place; otherwise it automatically calls `connectWorkspace(recentWorkspaceId)` and opens the returned blank session. The policy settles only once; a later user-initiated clear is never overridden by auto-selection again, and a connect failure waits for the next baseline projection to retry.
-- Re-picking the Workspace in the blank Hero also goes through `connectWorkspace`; when the target id differs from the current one, the current input machine's non-empty draft moves to the target scope first, then `sessions.open(nextId)`. The old blank entity is not deleted — it merely leaves the list by no longer being current.
+- Re-picking the Workspace in the blank Hero also goes through `connectWorkspace`; when the target id differs from the main one, `ui-workspace` retains the target, moves the current input machine's non-empty draft through the preparation callback, and then publishes the new main reference. The old blank entity is not deleted — it merely leaves the list when its `mainView` reference is released.
 
-### Per-session provisioning: the `sessions.provide` standard-kit channel
+### Per-session provisioning: the `uiSession.provide` standard-kit channel
 
-The sole provisioning path by which session slot components fetch their own session data. Plugins declare a fixed key map through the static descriptor `sessions.provide({hooks, props, resolve})` (a duplicate key throws at registration); `resolve(binding)` materializes values for a specific session and tears them down with the scope. ui-renderer's `standardKit` single loop binds the hooks compartment into `use<Name>` selector hooks (`observableHook`→uSES, anti-tearing) and passes the props compartment through as-is.
+The sole provisioning path by which Session slot components fetch their own Session data. Plugins declare a fixed key map through the static descriptor `uiSession.provide({hooks, props, resolve})` (a duplicate key throws at registration); `resolve(binding)` materializes values for a specific binding and tears them down with its scope. ui-renderer's `standardKit` single loop binds the hooks compartment into `use<Name>` selector hooks (`observableHook`→uSES, anti-tearing) and passes the props compartment through as-is.
 
 Slot scope is the closed set `root | session-maybe | session`:
 
 - `root` receives only the global standard kit, with no session identity or provisioning.
-- `session-maybe` follows the current session with ADOPTION identity (the only behavior — there is no hold-identity-forever mode): an incarnation born session-less keeps its React instance across the arrival of the FIRST session (the blank shell adopts it — no remount, the DOM survives), and from then on behaves exactly like a strict session entry — switching to a different session remounts, and dropping back to no-session remounts into a fresh blank incarnation that will adopt again. Component-local per-session state therefore clears by construction; state that must survive a switch belongs in session-bound sources (machine, store, hooks). With no session, `sessionId`, the results of `useSession`/`useInput`, and `inputActions` may all be absent. The unkeyed root `SessionMaybeProvider` drives these updates by subscribing to the runtime's atomic `currentProvide` projection — selection moves and provider-roster changes publish through the same source, so a roster change under a stable current id republishes the mounted bundle instead of stranding entries on an obsolete hook/prop schema — while `SessionMaybeProvideInfo` uses the static key map to retain the complete hook/prop shape even with no session; the per-entry adoption bookkeeping (incarnation-counter key) lives in the renderer's `SessionMaybeEntry`.
+- `session-maybe` inherits the nearest `SessionProvider` binding with ADOPTION identity: an incarnation born Session-less keeps its React instance when that Provider receives its first binding, then remounts when the Provider switches generation or returns to absence. Component-local per-Session state clears when the Provider switches generation. Across a switch, only persisted Store values survive generation retirement; binding-owned sources survive only when another reference keeps that generation alive. With no binding, `sessionId`, the results of `useSession`/`useInput`, and `inputActions` may all be absent. Provider-roster changes rematerialize the mounted binding without changing its identity, while the per-entry adoption bookkeeping lives in the renderer's `SessionMaybeEntry`.
 - `session` guarantees that `sessionId`, every hook source, and every prop exist; each strict entry's error boundary is keyed by `sessionId`, so switching sessions recreates that entry and its session store.
 
-`conversation` is the resident `session-maybe` shell: `ConversationRoot`, HeroShell, the Workspace picker, the root-owned scrollport and composer stack, and the overlay chain's fallback frame retain their React instances across the no-session → blank-session switch. Two strict entries fill fixed regions without reparenting that tree: `conversation.session.header` carries breadcrumb/tabs/actions above the scrollport, while `conversation.session` carries the view ring and draft mirror inside it; both share the same session-scoped chat store. The composer bar (`conversation.composer.bar`) is itself `session-maybe`: with no session its machine faces and message actions are inert, while the whole dashed card opens the existing Workspace picker by pointer and its read-only textarea does the same through Enter or Space. The same instance — textarea included — goes live when a session appears; the remaining input slots stay strict `session` and dispatch nothing until then. The blank → engaging/active transition never rebuilds the InputBar on a phase flip.
+`conversation` is the resident `session-maybe` shell under its owning `SessionProvider`: `ConversationRoot`, HeroShell, the Workspace picker, the scrollport and composer stack, and the overlay chain's fallback frame retain their React instances across the no-Session → blank-Session switch. Two strict entries fill fixed regions without reparenting that tree: `conversation.session.header` carries breadcrumb/tabs/actions above the scrollport, while `conversation.session` carries the view ring and draft mirror inside it; both share the same Session-scoped chat store. The composer bar (`conversation.composer.bar`) is itself `session-maybe`: with no Session its machine faces and message actions are inert, while the whole dashed card opens the existing Workspace picker by pointer and its read-only textarea does the same through Enter or Space. The same instance — textarea included — goes live when a binding appears; the remaining input slots stay strict `session` and dispatch nothing until then. The blank → engaging/active transition never rebuilds the InputBar on a phase flip.
+
+Blank Sessions retain the header's leading and corner slots so navigation controls, including the right-sidebar opener, are available before the first message. Title, actions, utilities, and View tabs remain hidden in the blank phase. The header still requires a selected Session; the Files and Terminal entries use that Session's workspace and execution services without requiring a recorded Turn.
 
 - The runtime's first built-in entry: the `'session'` hook — `useSession` itself rides the same mechanism, no special-casing.
 - Concurrent discipline: the render plane reads only from the hooks compartment (uSES consistency guarantee); props-compartment callbacks are used only in event-handler space; descriptor resolution is render-safe (idempotent caching, with prune reaping residue from abandoned renders).
 - Third-party components take zero value dependencies; types are a one-line type-only import (declaration merging into `SessionStandardProps` / `SessionMaybeStandardProps`).
 
-### The read-only queue mirror
+### Input delivery
 
 - Queue semantics: running does not lock input; ordinary messages queue through `session.prompt {mode:'queue'}`, and commands never queue.
 

+ 19 - 15
.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md

@@ -19,6 +19,8 @@ web client 只有一张全局会话面:slot 全部从根上下文渲染,插
 
 ## 决策
 
+[Client Session 引用](2026-09-15-client-session-references.zh.md)现已定义引用所有的生命周期与 Provider 定位。本 Note 保留 blank Session 与收养语义的理由,并描述它们的当前实现。
+
 ### 对等模型:client 与 host 同一根状态轴
 
 host 侧 `session.create(workspaceId)` 一体产出 Session + Agent + cwd(作为不可拆分的原子整体);client 侧就是这次出生的镜像——会话行进入 list mirror 的瞬间,client 为它铸 Agent scope(actx + provide + 输入面全套挂上):
@@ -48,13 +50,13 @@ id→ctx 换乘只许三类位置(业务提供方永不换乘):
 - root 协调服务自寻址:从投影的 sessionId 经 `sessions.scope(id)` 找回 actx。
 - root untagged listener:按 payload 的 sessionId 查自有 store。
 
-### scope 生命周期:挂靠 list mirror,出生即视野、死亡即 prune
+### scope 生命周期:挂靠显式引用
 
-Session 实例与 scope 同生命周期,存活资格 = host listed(一个判据,mint 与 prune 共用)
+Session 实例与 scope 同生命周期;catalog 只报告可发现性,不持有 generation
 
-- 出生 = 会话行进入 client 视野(list 基线拉取 / `create()` 本地回声 / `host/session-added` 帧),lazy 首次 resolve 铸 scope(resolution 纯函数、渲染安全)
-- prune 一次同拆三样:Session 实例、scope fiber(级联挂在 actx 上的一切消费方)、会话键控 slot store。暂存会话(= `list.current`)例外:被移除仍在台上时保留冻结只读视图,stage 移走才拆
-- 重开 = lazy 重建实例 + `open()` 拉 history(host 会话日志是持久真相)。
+- 出生 = 第一次显式调用 `sessions.retain(target, options)`;它同步返回 reference,并在历史就绪前铸造 Session binding 与 scope
+- 最后一份 reference 释放时,Controller 先撤下确切 generation,再拆除其 Session 实例、scope fiber(级联挂在 actx 上的一切消费方)与会话键控 slot store。仍有 reference 时,catalog 移除不会结束 generation
+- 重开 = 后续 retain 惰性重建 generation,并通过 `reference.ready` 暴露历史就绪结果(Host Session 日志是持久真相)。
 - 遗留 TODO:approval/question 帧不进 history,跨 prune 不可恢复(manager 级 pendingBuffers 只覆盖「从未实例化」窗口)。
 
 ### blank 位:空会话的可见投影、转正与复用
@@ -67,7 +69,7 @@ Session 实例与 scope 同生命周期,存活资格 = host listed(一个判
   - 发送方本地:首次 `prompt()` 的**成功响应**翻 false(受理即证明用户消息已入 host 日志——此点翻转是确证而非乐观;`onEngaged` 同步更新列表镜像,当前 `New Session` 行原地转为普通标题,不新增列表行)。首条提示词被拒则会话保持 blank:与 host 权威对齐、继续显示为 `New Session`、在仍为该工作区成员时保持 connectWorkspace 复用资格。
   - 其他端:`host/session-status (running:true)` 帧翻转——blank 会话从不 running,首次 running 必然已非 blank;
   - 重连对齐:`session.list` 的 summary.blank 是权威,错过帧的端下次拉取自然对齐;陈旧的 blank:true 不能把已转正的会话重新标回 blank。
-- 列表纪律:store 保留全部行;Workspace browser 的分组、平铺、搜索和计数共用同一可见投影——所有非 blank 会话都显示,blank 会话只显示 `session.id === sessions.current` 的一条,并强制标题为 `New Session`。切换 Workspace 后,旧 blank 实体仍在镜像中但从列表隐藏,目标 Workspace 的 current blank 显示;因此用户可见面全局至多一条 blank 行。
+- 列表纪律:store 保留全部行;Workspace browser 的分组、平铺、搜索和计数共用同一可见投影——所有非 blank 会话都显示,blank 会话只显示由 `mainView` 来源持有的一行,并强制标题为 `New Session`。切换 Workspace 后,旧 blank 实体仍在镜像中但从列表隐藏,目标 Workspace 的 blank 显示;因此用户可见面全局至多一条 blank 行。
 - 残留账零 GC:刷新后 blank 会话带位回来,下次同 workspace 且仍为成员时复用,普通单端路径使每个 workspace 至多保留一个;host 重启后 blank 无盘痕自然蒸发;多 tab 竞态多出的空壳只会成为非 current 隐藏行,后续复用消化,不做协调。
 
 ### connectWorkspace:New Session 的唯一入口
@@ -77,29 +79,31 @@ Session 实例与 scope 同生命周期,存活资格 = host listed(一个判
 - 复用臂:list mirror 中找 `blank && cwd == workspace.path && sessionIds.includes(id)`——host 自己的成员规则,绝不只按 cwd。没有账户槽位的 cwd 匹配(CLI(命令行界面)/TUI 在 host cwd 创建的会话,或已删除/重建的注册)会打开一个任何分组表面都无法显示在该工作区下的会话,因此落到新建臂(见[成员复用修复](../../archived/bug-fix/2026-08-05-workspace-blank-session-reuse-membership.md));命中直接返回该 id,不新建。
 - 新建臂:未命中则 `session.create({workspaceId})`,返回新 id。
 - 未知 workspaceId fail loud(不静默创建到别处)。
-- 解析保证(两臂同约定):promise resolve 时返回的 id 已在 list store 且 `sessions.binding(id)` 同步可解析——`SessionRuntime.create` 在 RPC 成功后同步投影列表再 resolve,使 draft 搬运方可以在 open 之前往新 scope 的 machine 写文本,不等 notifier flush。
-- 调用方拿 id 自行 `sessions.open`;首条提示词发送就是普通 `session.prompt`——会话本来就在,失败即普通提示词失败,draft 文本还在 machine 里,重试即再次发送。
-- 全局 New Session 按钮默认取 `recentWorkspaceId`:先比较各 Workspace 内 Session 的最新 `updatedAt`,无 Session 时回退 Workspace `createdAt`,同值保持 Host 顺序;只有完全没有 Workspace 时才 `sessions.clear()` 进入无会话视图。Workspace 分组内的创建动作仍显式命中该 Workspace。
+- 解析保证(两臂同约定):promise resolve 时返回的 id 已在 list store。视图 owner 随后同步 retain,因此 draft 搬运方可以在历史就绪前通过该 binding 写入文本,无需等待 notifier flush。
+- 调用方拿 id 安装一份 `mainView` reference;首条提示词发送就是普通 `session.prompt`——Session 本来就在,失败即普通提示词失败,draft 文本还在 machine 里,重试即再次发送。
+- 全局 New Session 按钮默认取 `recentWorkspaceId`:先比较各 Workspace 内 Session 的最新 `updatedAt`,无 Session 时回退 Workspace `createdAt`,同值保持 Host 顺序;只有完全没有 Workspace 时才释放主视图 reference,进入无 Session 视图。Workspace 分组内的创建动作仍显式命中该 Workspace。
 - 运行时启动时订阅首次完整基线:若已有恢复成功的 current 会话则保持不动,否则自动 `connectWorkspace(recentWorkspaceId)` 并 open 返回的 blank 会话。该策略只结算一次;之后用户主动 clear 不会再次被自动选择覆盖,连接失败则等下一次基线投影重试。
-- blank Hero 中改选 Workspace 也走 `connectWorkspace`;若目标 id 与当前 id 不同,先把当前 input machine 的非空 draft 搬到目标 scope,再 `sessions.open(nextId)`。旧 blank 实体不删除,只因不再 current 而从列表隐藏。
+- blank Hero 中改选 Workspace 也走 `connectWorkspace`;若目标 id 与主视图 id 不同,`ui-workspace` 先 retain 目标,通过 preparation callback 搬运当前 input machine 的非空 draft,再发布新的主 reference。旧 blank 实体不删除,只因其 `mainView` reference 被释放而从列表隐藏。
 
-### 逐会话供数:`sessions.provide` 标准件通道
+### 逐会话供数:`uiSession.provide` 标准件通道
 
-会话 slot 组件「自己拿会话数据」的唯一供数路径。插件以静态描述符 `sessions.provide({hooks, props, resolve})` 声明固定键表(重名 key 注册时 throw),`resolve(binding)` 在确定会话下物化值并随 scope 拆;ui-renderer `standardKit` 统一循环把 hooks 格绑成 `use<Name>` 选择器钩子(`observableHook`→uSES,防 tearing)、props 格原样透传。
+Session slot 组件「自己拿 Session 数据」的唯一供数路径。插件以静态描述符 `uiSession.provide({hooks, props, resolve})` 声明固定键表(重名 key 注册时 throw),`resolve(binding)` 在确定 binding 下物化值并随其 scope 拆;ui-renderer `standardKit` 统一循环把 hooks 格绑成 `use<Name>` 选择器钩子(`observableHook`→uSES,防 tearing)、props 格原样透传。
 
 slot scope 是闭集 `root | session-maybe | session`:
 
 - `root` 只拿全局标准件,不接收会话身份或供数。
-- `session-maybe` 以**收养(adoption)身份语义**跟随 current 会话(唯一行为——不存在「永久保持实例」模式):空态出生的化身在**第一个**会话到来时保持 React 实例(空壳收养它——不重挂,DOM 存活);此后行为与严格会话 entry 完全一致——切到不同会话重挂,跌回无会话也重挂为崭新的空态化身(之后再次收养)。因此组件本地的逐会话状态**由构造保证**随切换清零;需要活过切换的状态必须住会话绑定的源(machine、store、hooks)。无会话时 `sessionId`、`useSession`/`useInput` 的选择结果及 `inputActions` 均可缺省。根部无 key 的 `SessionMaybeProvider` 通过订阅运行时的原子 `currentProvide` 投影驱动这条更新——选择移动和提供方名册变化经同一 source 发布,current id 不变时的名册变化也会重发已挂载 bundle,而不是把 entry 困在过期的钩子/prop 形状上——`SessionMaybeProvideInfo` 靠静态键表在无会话时仍保留完整钩子/prop 形状;逐 entry 的收养记账(化身计数 key)住在 renderer 的 `SessionMaybeEntry`。
+- `session-maybe` 以**收养(adoption)身份语义**继承最近 `SessionProvider` 的 binding:空态出生的化身在该 Provider 第一次收到 binding 时保持 React 实例,此后 Provider 切换 generation 或回到空态时重挂。Provider 切换 generation 时,组件本地的逐 Session 状态会清零。切换过程中,只有持久化 Store 值能活过 generation 退休;只有另一份 reference 保活该 generation 时,binding 自有 source 才能保留。无 binding 时,`sessionId`、`useSession`/`useInput` 的结果与 `inputActions` 均可缺省。Provider roster 变化会重新物化已挂载 binding,但不改变其 identity;逐 entry 的收养记账住在 renderer 的 `SessionMaybeEntry`。
 - `session` 保证 `sessionId`、所有钩子 source 与 props 均存在;每个严格 entry 的错误边界以 `sessionId` 为 key,切换会话会重建该 entry 及其会话 store。
 
-`conversation` 是 `session-maybe` 的常驻外壳:`ConversationRoot`、HeroShell、Workspace picker、root 持有的 scrollport 与 composer stack,以及 overlay chain 的 fallback 外框,在无会话 → blank 会话的切换中保持 React 实例。两个严格 session entry 只填入固定区域,不改变该树的父级:`conversation.session.header` 在 scrollport 上方承载 breadcrumb/tab/action,`conversation.session` 在其内部承载 view ring 与 draft mirror;二者共享同一个 session scope chat store。composer bar(`conversation.composer.bar`)本身即为 `session-maybe`:无 session 时,其 machine faces 和消息动作保持惰性,整张虚线卡片可经指针打开现有 Workspace picker,只读 textarea 也可通过 Enter 或 Space 打开。session 出现后同一实例(含 textarea)转为 live;其余输入 slot 保持严格 `session`,在此之前不派发任何内容。blank → engaging/active 的 InputBar 不因 phase 翻转而重建。
+`conversation` 是其 owner `SessionProvider` 下的 `session-maybe` 常驻外壳:`ConversationRoot`、HeroShell、Workspace picker、scrollport 与 composer stack,以及 overlay chain 的 fallback 外框,在无 Session → blank Session 的切换中保持 React 实例。两个严格 session entry 只填入固定区域,不改变该树的父级:`conversation.session.header` 在 scrollport 上方承载 breadcrumb/tab/action,`conversation.session` 在其内部承载 view ring 与 draft mirror;二者共享同一个 Session scope chat store。composer bar(`conversation.composer.bar`)本身即为 `session-maybe`:无 Session 时,其 machine faces 和消息动作保持惰性,整张虚线卡片可经指针打开现有 Workspace picker,只读 textarea 也可通过 Enter 或 Space 打开。binding 出现后同一实例(含 textarea)转为 live;其余输入 slot 保持严格 `session`,在此之前不派发任何内容。blank → engaging/active 的 InputBar 不因 phase 翻转而重建。
+
+blank Session 保留 header 的 leading 与 corner slot,让右侧栏展开入口等导航控件在首条消息之前即可使用。标题、actions、utilities 和 View tabs 在 blank phase 中继续隐藏。header 仍要求已选中的 Session;Files 与 Terminal 入口使用该 Session 的工作区和执行服务,无需已有 Turn 记录。
 
 - 运行时内建第一条:`'session'` 钩子——`useSession` 本身走同一机制,无特判。
 - Concurrent 纪律:渲染平面只从 hooks 格读(uSES 一致性保证);props 格回调只在事件 handler 空间用;描述符解析 render-safe(幂等缓存、废弃渲染残留由 prune 收尸)。
 - 第三方组件值零依赖,类型一行 type-only import(declaration merging 进 `SessionStandardProps` / `SessionMaybeStandardProps`)。
 
-### 队列只读镜像
+### 输入投递
 
 - 队列语义:running 不锁输入;普通消息经 `session.prompt {mode:'queue'}` 排队,命令永不排队。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md
-2026-07-31-claimed-pre-step-inbox-lifecycle.md: 737e3835263a3215a0fd2e52dad4ee05402bd888
-2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md: ecb731df663e0d48b374a3118d7db7f6a34bfc18
+2026-07-31-claimed-pre-step-inbox-lifecycle.md: 3ebb73279e49c2aaffa05bea647fa3f93ba6a36f
+2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md: 809470e5b52b29cdcbe7b4d81bb584b476f5cfce

+ 4 - 4
.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md

@@ -18,13 +18,13 @@ Before every proposed step, the loop's package-internal `ReactLoopInbox` atomica
 
 The durable inbox remains two `UserMessage[]` lists addressed by `MessageId`. `append`, `prepend`, and `splice` take a target, while `replace(messageId, newMessage)` and `remove(messageId)` locate the pending message across both lists before committing a normalized splice. Replacement may change identity and emits the old message as discarded followed by the new message as inserted. Every insertion emits `agent/inbox/inserted { message }`; an ordinary removal records `outcome: 'canceled'` and emits `agent/inbox/discarded { message }`. Claiming records pure deletions without an outcome and emits claimed events from `ReactLoopInbox`. These live events add no placement, outcome, or batch fields.
 
-`Agent.inbox` exposes only the structural `Inbox` interface for reading and mutating pending work; loop-only `hasPending` and claim operations are absent from that public face. dsh-agent-loop constructs one `ReactLoopInbox` and uses it for both structural commands and driver operations. The concrete constructor receives `SessionProjectionRegistry` directly instead of the wider Cordis `Context` and registers the standard definition on the agent scope before its first read. `AgentLoop` requires the registry service at activation, and the registry reference-counts the definition across live agent scopes.
+`Agent.inbox` exposes only the structural `Inbox` interface for reading and mutating pending work; loop-only `hasPending` and claim operations are absent from that public face. dsh-agent-loop constructs one `ReactLoopInbox` and uses it for both structural commands and driver operations. The concrete constructor receives `SessionProjectionRegistry` directly instead of the wider Cordis `Context`. `AgentLoop` owns the standard projection registration for its service lifetime, keeping cold Inbox reads available without any live Agent; `ReactLoopInbox` only reads and mutates that shared state.
 
-The two event surfaces have separate consumers. Observers following one message use `agent/inbox/inserted`, `claimed`, and `discarded`. Each `ReactLoopInbox` contributes the standard `inbox` projection over the durable `agent/inbox/spliced` stream from its agent scope; UI edits and removals route through an Inbox mutation method so the same projection records every change. When that projection reconstructs durable history, it rejects unsafe or out-of-range coordinates and duplicate `MessageId` values across both lists, and reports the offending event seq. Whole-queue control consumers use the projection change feed: the Session controller publishes the projection frame, then derives the queue replacement from the same post-fold inbox value.
+The two event surfaces have separate consumers. Observers following one message use `agent/inbox/inserted`, `claimed`, and `discarded`. `AgentLoop` contributes the standard `inbox` projection over the durable `agent/inbox/spliced` stream; UI edits and removals route through an Inbox mutation method so the same projection records every change. When that projection reconstructs durable history, it rejects unsafe or out-of-range coordinates and duplicate `MessageId` values across both lists, and reports the offending event seq. Whole-queue control consumers use the generic projection change feed and read the complete Inbox value directly.
 
 Plugins that need current-step atomic rewriting return messages from `agent/pre-step`. Plugins that only need later context may mutate `agent.inbox` directly. Workspace context uses both paths: asynchronous filesystem projections stage one replaceable `next-step` item, while the next entering pre-step folds that item or a newly composed baseline into its final batch and removes the pending copy. Rejection keeps the item queued.
 
-The archived [addressable queue occurrence decision](../../archived/feature/2026-07-29-addressable-queue-operations.md) describes the superseded occurrence-wrapper design. `MessageId` owns addressability, while `ReactLoopInbox` contributes `inbox` as the standard session projection over durable splices. The generic projection carrier serves that fold for live updates, history-tail reconnect baselines, and cold process-restart recovery without a live Agent mirror.
+The archived [addressable queue occurrence decision](../../archived/feature/2026-07-29-addressable-queue-operations.md) describes the superseded occurrence-wrapper design. `MessageId` owns addressability, while `AgentLoop` contributes `inbox` as the standard session projection over durable splices. The generic projection carrier serves that fold for live updates, history-tail reconnect baselines, and cold process-restart recovery without a live Agent mirror.
 
 ## Alternatives considered
 
@@ -36,7 +36,7 @@ The archived [addressable queue occurrence decision](../../archived/feature/2026
 
 ## Verification
 
-Agent-loop coverage pins turn-start-before-claim-before-pre-step ordering, exact live event payloads, balanced no-step rejection, final-batch rewriting, input inserted after a claim, listener failure, cancellation, and agent-scope projection removal after the last owner unloads. Inbox and consumer tests pin pure claim deletions, canceled ordinary removals, agent-instructions staging, replacement, and same-step entry, plan/goal/hook behavior, UI cleanup, compaction, checkpointing, resumed durable projection, rejection of invalid persisted coordinates or cross-list identities, and post-fold queue replacement when the controller registers before the projection registry. Consumer-domain tests use a process-local Inbox stub only when durability is outside the test subject; claiming, durable projection, recovery, validation, and live-notification tests create Agents through the production AgentLoop test harness, so test support never reimplements the projection. Generated event and type catalogs expose only the new waterfall and payloads.
+Agent-loop coverage pins turn-start-before-claim-before-pre-step ordering, exact live event payloads, balanced no-step rejection, final-batch rewriting, input inserted after a claim, listener failure, cancellation, and Inbox availability across Agent unloads and projection removal when AgentLoop unloads. Inbox and consumer tests pin pure claim deletions, canceled ordinary removals, agent-instructions staging, replacement, and same-step entry, plan/goal/hook behavior, UI cleanup, compaction, checkpointing, resumed durable projection, rejection of invalid persisted coordinates or cross-list identities, and post-fold queue replacement when the controller registers before the projection registry. Consumer-domain tests use a process-local Inbox stub only when durability is outside the test subject; claiming, durable projection, recovery, validation, and live-notification tests create Agents through the production AgentLoop test harness, so test support never reimplements the projection. Generated event and type catalogs expose only the new waterfall and payloads.
 
 ## Consequences
 

+ 4 - 4
.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md

@@ -18,13 +18,13 @@ Status: implemented
 
 持久 inbox 仍是两份通过 `MessageId` 寻址的 `UserMessage[]` 列表。`append`、`prepend` 与 `splice` 接受 target;`replace(messageId, newMessage)` 与 `remove(messageId)` 则在提交规范化 splice 前,通过 `MessageId` 跨两份列表定位待处理消息。替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。每次插入发出 `agent/inbox/inserted { message }`;普通删除记录 `outcome: 'canceled'` 并发出 `agent/inbox/discarded { message }`。领取记录不带 outcome 的纯删除,并由 `ReactLoopInbox` 发出 claimed 事件。这些实时事件不增加 placement、outcome 或批次字段。
 
-`Agent.inbox` 只暴露用于读取和变更待处理工作的结构化 `Inbox` 接口;仅供循环使用的 `hasPending` 与领取操作不在该公开接口上。dsh-agent-loop 只构造一个 `ReactLoopInbox`,同时用于结构化命令与驱动器操作。具体构造函数直接接收 `SessionProjectionRegistry`,而不是更宽泛的 Cordis `Context`,并在首次读取前从 agent 作用域注册标准定义。`AgentLoop` 激活时要求该注册表服务存在,注册表则对多个 live agent 作用域贡献的定义进行引用计数
+`Agent.inbox` 只暴露用于读取和变更待处理工作的结构化 `Inbox` 接口;仅供循环使用的 `hasPending` 与领取操作不在该公开接口上。dsh-agent-loop 只构造一个 `ReactLoopInbox`,同时用于结构化命令与驱动器操作。具体构造函数直接接收 `SessionProjectionRegistry`,而不是更宽泛的 Cordis `Context`。`AgentLoop` 在服务生命周期内持有标准投影注册,让没有 live Agent 时的冷 Inbox 读取仍然可用;`ReactLoopInbox` 只读取和变更该共享状态
 
-两类事件接口服务不同消费方。跟踪单条消息的观察方使用 `agent/inbox/inserted`、`claimed` 与 `discarded`。每个 `ReactLoopInbox` 都从其 agent 作用域在持久 `agent/inbox/spliced` 流上贡献标准 `inbox` 投影;UI 编辑与移除通过 Inbox 变更方法处理,从而让同一投影记录所有变化。该投影重建持久历史时,会拒绝不安全或越界的坐标,以及跨两份列表重复的 `MessageId`,并报告出错事件的 seq。整体队列的 control 消费方使用投影变更流:Session controller 先发布 projection frame,再从同一份折叠后的 inbox 值派生 queue replacement
+两类事件接口服务不同消费方。跟踪单条消息的观察方使用 `agent/inbox/inserted`、`claimed` 与 `discarded`。`AgentLoop` 在持久 `agent/inbox/spliced` 流上贡献标准 `inbox` 投影;UI 编辑与移除通过 Inbox 变更方法处理,从而让同一投影记录所有变化。该投影重建持久历史时,会拒绝不安全或越界的坐标,以及跨两份列表重复的 `MessageId`,并报告出错事件的 seq。整体队列的 control 消费方使用通用投影变更流,直接读取完整的 Inbox 值
 
 必须对当前步骤进行原子改写的插件从 `agent/pre-step` 返回消息。只需要稍后上下文的插件可以直接修改 `agent.inbox`。Workspace context 同时使用两条路径:异步文件系统投影会暂存一条可替换的 `next-step` 消息,而下一次进入步骤的 pre-step 会把该消息或新组合的基线折入最终批次,并移除仍待处理的副本。reject 会让该条目继续排队。
 
-已归档的[可寻址队列项决策](../../archived/feature/2026-07-29-addressable-queue-operations.md)描述了已被取代的单次出现包装层设计。`MessageId` 负责寻址,而 `ReactLoopInbox` 把 `inbox` 作为持久 splice 上的标准会话投影贡献给投影注册表。通用投影传输层会将该折叠结果用于实时更新、历史尾页的重连基线和冷进程重启恢复,无需 live Agent 镜像。
+已归档的[可寻址队列项决策](../../archived/feature/2026-07-29-addressable-queue-operations.md)描述了已被取代的单次出现包装层设计。`MessageId` 负责寻址,而 `AgentLoop` 把 `inbox` 作为持久 splice 上的标准会话投影贡献给投影注册表。通用投影传输层会将该折叠结果用于实时更新、历史尾页的重连基线和冷进程重启恢复,无需 live Agent 镜像。
 
 ## 曾考虑的替代方案
 
@@ -36,7 +36,7 @@ Status: implemented
 
 ## 验证
 
-agent loop(智能体循环)覆盖固定先 `turn/start`、再领取、后 pre-step 的顺序、实时事件的确切载荷、边界平衡的无步骤 reject、最终批次改写、领取后插入的输入、监听器失败、取消,以及最后一个所有者卸载后移除 agent 作用域投影。Inbox 和消费方测试固定纯领取删除、普通删除的 canceled 结果、agent-instructions 的暂存、替换与同一步骤进入、plan/goal/钩子行为、UI 清理、压缩(compaction)、检查点、恢复后的持久投影、对非法持久坐标或跨列表重复标识的拒绝,以及 controller 早于投影注册表注册时仍使用折叠后队列值。只有当持久性不属于测试对象时,消费方领域测试才使用进程内 Inbox 桩;领取、持久投影、恢复、校验与实时通知测试通过生产 AgentLoop 测试 harness 创建 Agent,因此测试支持代码不会重新实现该投影。生成的事件与类型目录只公开新的 waterfall 与载荷。
+agent loop(智能体循环)覆盖固定先 `turn/start`、再领取、后 pre-step 的顺序、实时事件的确切载荷、边界平衡的无步骤 reject、最终批次改写、领取后插入的输入、监听器失败、取消,以及Agent 卸载后 Inbox 仍可读取,以及 AgentLoop 卸载时移除投影。Inbox 和消费方测试固定纯领取删除、普通删除的 canceled 结果、agent-instructions 的暂存、替换与同一步骤进入、plan/goal/钩子行为、UI 清理、压缩(compaction)、检查点、恢复后的持久投影、对非法持久坐标或跨列表重复标识的拒绝,以及 controller 早于投影注册表注册时仍使用折叠后队列值。只有当持久性不属于测试对象时,消费方领域测试才使用进程内 Inbox 桩;领取、持久投影、恢复、校验与实时通知测试通过生产 AgentLoop 测试 harness 创建 Agent,因此测试支持代码不会重新实现该投影。生成的事件与类型目录只公开新的 waterfall 与载荷。
 
 ## 后果
 

+ 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: b6551e1c7f8c94fb02a788aa62cb4acef1addffe
-2026-08-02-typert-remote-method-calls.zh.md: 058ec47e6749ee7576fd84fdcacfda350eec3fb8
+2026-08-02-typert-remote-method-calls.md: b599b3f9a353738bbb44a4da9b5a72be8309e2fb
+2026-08-02-typert-remote-method-calls.zh.md: 64dc84f0ae07b10d50452ef7e36c30db102953cf

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

@@ -31,7 +31,7 @@ The Remote consumer projection contains `.d.ts`, `.d.ts.map`, and `.js` files. T
 | `@deepseek-ai/dsh-typert-protocol` | Declares only the minimal `ctx.typert` protocol | `TypertRemoteService`, decorators, binding fallback, descriptors, lookup/Context, and the Remote map; no dependency on the compiler, Zod, Connection, or Browser |
 | Typert registry | `ctx.typert` | Separately stores reflection for the current environment, imported Remote contributions, lookup providers, and Context providers |
 | Typert generator/loader | No new business service | Generates three kinds of `lib` artifacts from the Host/Client Programs and registers the current environment's artifacts with `ctx.typert` |
-| API Gateway's Host face | `ctx.typertGateway` | Associates Host definitions with live Services, decodes parameters, resolves receivers, invokes methods, and encodes results |
+| API Gateway's Host face | `ctx.typertGateway` | Associates Host definitions with live Services, decodes parameters, resolves receivers, and invokes methods |
 | Connection | `ctx.connection` | Exclusively owns the HTTP Server/future WebSocket, the shared `/api` route, RPC envelope, rpcId, serialization, trust, error transport, Typert interception, and owner-registered exact Fetch routes on the same channel |
 | API Gateway's Client face | `ctx.remote`, `ctx.remote.<namespace>` | Mounts Remote contributions, materializes each namespace as a traced `remote.<namespace>` child Service, and delegates canonical calls to `ctx.connection.rpc` |
 | API Remotes | No new service | Owns Host Agent/Session lookup policy and serves as the only Client business facade, selecting and mounting `/remote` contributions while exposing the selected API declarations |
@@ -147,9 +147,9 @@ 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 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.
+A LIB codec contains a success-cached Zod schema factory and a canonical `typeSymbol` consisting of "package + public subpath + export name." The Host Gateway invokes parameter and identity factories when it first decodes strict input. The Client contribution retains the same codec metadata for strict input checks at mount but does not materialize invocation schemas; [Host-only Remote input validation](../simplification/2026-09-15-host-only-remote-input-validation.md) owns this placement. An SRC codec is marked only as `src-json`.
 
-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.
+Descriptors exist only in the local registry on each side. The wire carries only the `/api` channel, endpoint, and `{ args }` payload. The Client uses its descriptor to map positional arguments and Context identity into named fields. The Host uses its descriptor to validate those fields, resolve the receiver, and invoke the method.
 
 ## Typert runtime registry
 
@@ -181,7 +181,7 @@ Consequently, `SessionId`, the Agent wire ID, the request, and the result all re
 
 Remote methods themselves use declaration-map navigation. Typert anchors `InvocationModel.location` to the decorated Host method-name token and emits a source-map segment on the corresponding property of the namespace interface. For an adapter-backed endpoint, after the TypeScript editor resolves `ctx.remote.models.list` to its generated declaration, `typert.remote-client.d.ts.map` takes it to the Host Service's `remoteExportList` entry point. That entry point explicitly calls the existing, unrenamed `list()` method; the map does not misidentify the decorator, class, or full signature as the method definition.
 
-Typert generates a wire Zod codec for the same symbol key. The Host Gateway uses it to validate input and encode results, while the Client Remote uses it to encode arguments and validate responses. If a complex type cannot produce a strict codec, the LIB build fails instead of degrading to `unknown` or unchecked JSON.
+Typert generates a wire Zod codec for the same symbol key. The Host Gateway uses parameter and identity codecs to validate input. Client Remote trusts its generated TypeScript arguments and successful Host results instead of executing invocation codecs. If a complex type cannot produce a strict codec, the LIB build fails instead of degrading to `unknown` or unchecked JSON.
 
 Named business types referenced by Remote methods must be exported from public, type-only subpaths. If the only reachable entry also imports Host Services, Cordis `Context` merges, or Host-only implementations, the build fails and requires the business package to provide a safe type entry. Primitives, literals, and simple compositions explicitly supported by Typert need no additional names.
 
@@ -315,7 +315,7 @@ Client business packages depend only on `@deepseek-ai/dsh-api-remotes/client`, n
 
 `ctx.remote.$mount()` registers a contribution with `Typert.remotes`, installs its namespace Services and concrete methods, and resolves only after they are ready. Its disposer is owned by the Cordis fiber that called the method. Duplicate endpoints, conflicting invocation modes for the same namespace and method, or conflicts between a descriptor and an existing type identity fail immediately.
 
-The Client Remote Service materializes each `@Remote` descriptor as a real function on a `remote.<namespace>` child Service. The function constructs named `args` in descriptor parameter order, applies the Client's strict codec, and then calls `ctx.connection.rpc.call('/api', endpoint, { args }, signal)`. For a cancellation-aware descriptor, the generated function accepts a final optional signal and combines it with the contribution mount lifetime; unmounting therefore cancels every in-flight carrier call, while a caller can cancel one call independently.
+The Client Remote Service materializes each `@Remote` descriptor as a real function on a `remote.<namespace>` child Service. The function checks positional arity, constructs named `args` in descriptor parameter order without runtime type parsing, and then calls `ctx.connection.rpc.call('/api', endpoint, { args }, signal)`. For a cancellation-aware descriptor, the generated function accepts a final optional signal and combines it with the contribution mount lifetime; unmounting therefore cancels every in-flight carrier call, while a caller can cancel one call independently.
 
 Neither a direct descriptor with `scope` nor a `@RemoteScope` descriptor copies functions into every Agent Scope. The Client Remote Service creates one Cordis child Service per namespace, registered as `remote.<namespace>`, and materializes direct and scoped variants on it. Accessing a method through `agentCtx.remote.goals` captures the current Agent Context before returning the callable handle. The method then asks the corresponding Context binder for identity from that Context. A direct scoped projection substitutes this identity at the lookup position named by `scope.wire`; a Remote Scope descriptor writes the identity into the receiver's separate wire field. Both issue the same kind of `/api` call.
 
@@ -380,10 +380,9 @@ ctx.typertGateway.invoke({ namespace, method, args, signal })
 → direct 使用原 Service;context 先解析 scoped Context 和 Service
 → cancellation descriptor 存在时把 signal 追加到业务参数末尾
 → Reflect.apply(receiver[implementation ?? method], receiver, orderedArgs)
-→ result codec 编码业务结果
 ```
 
-`ctx.typertGateway.invoke()` is the carrier-independent Host entry point. It neither creates an rpcId, RPC envelope, nor HTTP response. It returns only the encoded result or raises a Gateway error that the Connection RPC adapter maps for transport.
+`ctx.typertGateway.invoke()` is the carrier-independent Host entry point. It neither creates an rpcId, RPC envelope, nor HTTP response. It returns the business result without runtime output decoding or raises a Gateway error that the Connection RPC adapter maps for transport.
 
 ## The shared `/api` call chain
 
@@ -426,7 +425,7 @@ The complete path is:
 
 ```text
 ctx.remote.goals.create(sessionId, request, signal?)
-→ Client InvocationDescriptor 编码 { args: { agentId, request } }
+→ Client InvocationDescriptor 组装 { args: { agentId, request } }
 → Client 合并 caller signal 与 contribution mount lifetime
 → ctx.connection.rpc.call('/api', 'goals/create', { args }, signal)
 → Connection 创建 rpcId 和既有 client-request envelope
@@ -435,9 +434,8 @@ ctx.remote.goals.create(sessionId, request, signal?)
 → 复合 FetchHandler 判断 endpoint ownership 并选择目标 FetchHandler
 → Typert interceptor 调用 ctx.typertGateway.invoke(..., request.signal)
 → Host InvocationDescriptor 解码、lookup、receiver 解析并把 signal 注入 Reflect.apply
-→ result codec 编码
 → Connection 写入既有 RPC result 并回送相同 rpcId
-→ Client result codec 验证并返回 CreateGoalResult
+→ Client 直接返回 CreateGoalResult
 ```
 
 Remote does not define a second-layer `{ ok, value/error }` response on the wire. Successful values and failures use the existing RPC response's `result` directly, and the failure branch carries the shared `{ code, message, details }` data. Owners, resolvers, and the Gateway all raise one class, `RemoteError`, whose code comes from the merged `RemoteErrorDetailsMap`: the Host encodes a structurally identified `RemoteError` onto the wire unchanged — including the Gateway's own `gateway/*` assembly codes and a resolver's `session/not-found` or `session/agent-busy` — and folds only an unclassified throw into `gateway/internal`, keeping its diagnostic in the message. The Client face rebuilds an instance for the `RemoteResult` error branch, so `throw result.error` keeps throw semantics. [The failure-vocabulary Agent Note](2026-08-28-ctx-remote-failure-vocabulary.md) owns the code table, its ownership rules, and why discrimination reads `code` instead of `instanceof`.
@@ -455,7 +453,7 @@ The Gateway registers only its ownership matcher and RPC handler with Connection
 - `@deepseek-ai/dsh-typert-protocol`: lightweight protocols for decorators, bindings, lookup, Remote Scope, and descriptors.
 - Typert generator: analyzes Host/Client Programs, generates local faces and Remote consumer projections, and emits canonical symbol/Zod information.
 - Typert runtime: separately stores the current environment's local reflection and imported Remote contributions.
-- `@deepseek-ai/dsh-api-gateway`: its default entry associates Host definitions with Services, claims Remote endpoints, performs lookup, resolves Context receivers, invokes methods, encodes results, and registers an `/api` interceptor with Connection; its `/client` entry mounts Remote contributions, creates strict Remote namespace Services and methods, and delegates calls to `ctx.connection.rpc`. The entries share the Remote protocol but do not import each other's Cordis interface merges.
+- `@deepseek-ai/dsh-api-gateway`: its default entry associates Host definitions with Services, claims Remote endpoints, validates input, performs lookup, resolves Context receivers, invokes methods, and registers an `/api` interceptor with Connection; its `/client` entry mounts Remote contributions, creates strict Remote namespace Services and methods, and delegates calls to `ctx.connection.rpc`. The entries share the Remote protocol but do not import each other's Cordis interface merges.
 - `@deepseek-ai/dsh-api-remotes`: the BFF layer; registers the application's forwarded Cordis event source and the Host home carried by generation readiness, selects Client `/remote` contributions, and exposes the merged Remote types to business packages through the shared `TypertClientRemote` contract.
 - Connection: owns the single HTTP Server/future WebSocket carrier, the shared `/api` route and its composite FetchHandler, owner-registered exact Fetch routes, the RPC envelope, rpcId, serialization, trust, and error transport.
 - Business-object packages such as Agent/Session: own lookup, Context providers, canonical ID types, and public type-only entries.
@@ -516,7 +514,7 @@ Canonical public types require business DTOs to have type-only entries, which ma
 
 Type imports and runtime contributions have different effects. `import type {}` extends only the static Remote surface. If a real calling environment omits the value contribution, the Client Remote Service must fail with an explicit "Remote not mounted" error.
 
-Browser and Host each hold their own Zod instances and cannot compare object identities across realms. Consistency is guaranteed only by canonical symbol keys, the same generated model, and wire behavior.
+Generated Host and Client artifacts carry matching Zod factories, but Client Remote does not materialize invocation schemas. Canonical symbol keys, the same generated model, and Host wire validation keep the two sides aligned without comparing schema object identities across realms.
 
 A consumer may import a Remote contract that is not currently mounted on the Host. The types mean "this protocol capability was selected by the consumer," not that a corresponding Service currently exists in the target process; an unavailable endpoint must fail explicitly at runtime.
 

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

@@ -31,7 +31,7 @@ Remote 消费端投影同时包含 `.d.ts`、`.d.ts.map` 和 `.js`。`.d.ts` 只
 | `@deepseek-ai/dsh-typert-protocol` | 只声明 `ctx.typert` 的最小协议 | `TypertRemoteService`、decorator、binding 回退、descriptor、lookup/Context 和 Remote map;不依赖 compiler、Zod、Connection 或 Browser |
 | Typert registry | `ctx.typert` | 分开保存当前环境 reflection、导入的 Remote contribution、lookup provider 和 Context provider |
 | Typert generator/loader | 无新增业务服务 | 从 Host/Client Program 生成三类 `lib` 产物,并把当前环境产物注册到 `ctx.typert` |
-| API Gateway 的 Host face | `ctx.typertGateway` | 关联 Host definition 与活 Service,解码参数、解析 receiver、调用方法和编码结果 |
+| API Gateway 的 Host face | `ctx.typertGateway` | 关联 Host definition 与活 Service,解码参数、解析 receiver 并调用方法 |
 | Connection | `ctx.connection` | 独占 HTTP Server/未来 WebSocket、共享 `/api` route、RPC envelope、rpcId、序列化、trust、错误传输、Typert 拦截,以及各 owner 在同一 channel 上注册的精确 Fetch route |
 | API Gateway 的 Client face | `ctx.remote`、`ctx.remote.<namespace>` | mount Remote contribution,把每个 namespace 实体化为可追踪的 `remote.<namespace>` 子 Service,并把规范调用交给 `ctx.connection.rpc` |
 | API Remotes | 无新增服务 | 负责 Host Agent/Session lookup 策略,并作为 Client 业务的唯一 facade,选择并挂载 `/remote` contribution,同时暴露所选 API 声明 |
@@ -147,9 +147,9 @@ InvocationDescriptor {
 
 参数顺序来自方法签名,HTTP 字段来自参数名或 lookup 声明。取消 descriptor 只保留最后一个 `signal` 位置,并使其不进入具名 `args`;实际 signal 由 Connection 或直接调用 Gateway 的调用方提供。Gateway 不根据请求内容推断可选字段、Context 类型、lookup 类型或缺失参数,也不会合成业务默认值。
 
-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 生成
+LIB codec 带有只缓存成功结果的 Zod schema factory 和「package + 公共 subpath + export name」的规范 `typeSymbol`。Host Gateway 首次解码严格输入时调用参数与身份 factory。Client contribution 保留同一 codec 元数据,以在挂载时检查严格输入,但不实例化调用 schema;[仅在 Host 校验 Remote 输入](../simplification/2026-09-15-host-only-remote-input-validation.zh.md)规定了这个位置。SRC codec 只标记 `src-json`
 
-descriptor 只存在于两端本地 registry。wire 上只有 `/api` channel、endpoint 和 `{ args }` payload;Host 用自己的 descriptor 解码和调用,Client 用自己的对应 descriptor 编码参数和验证结果
+descriptor 只存在于两端本地 registry。wire 上只有 `/api` channel、endpoint 和 `{ args }` payload。Client 用自己的 descriptor 把位置参数和 Context identity 映射为具名字段;Host 用自己的 descriptor 校验这些字段、解析 receiver 并调用方法
 
 ## Typert 运行时 registry
 
@@ -181,7 +181,7 @@ import type { CreateGoalRequest, CreateGoalResult } from '@deepseek-ai/dsh-goal/
 
 Remote 方法本身使用 declaration map 导航。Typert 把 `InvocationModel.location` 固定在 Host 被装饰方法的方法名 token,并在 namespace interface 的对应属性上写入 source-map segment。对于由适配器支撑的 endpoint,TypeScript editor 从 `ctx.remote.models.list` 取得生成 declaration 后,再沿 `typert.remote-client.d.ts.map` 跳到 Host Service 的 `remoteExportList` 远程出口。该出口继续显式调用不改名的存量 `list()`,map 不把 decorator、class 或整个签名误当成方法定义位置。
 
-Typert 为同一 symbol key 生成 wire Zod codec。Host Gateway 用它校验输入和编码结果,Client Remote 用它编码参数并校验响应;复杂类型无法生成严格 codec 时,LIB 构建失败,不降级为 `unknown` 或无校验 JSON。
+Typert 为同一 symbol key 生成 wire Zod codec。Host Gateway 用参数与 identity codec 校验输入;Client Remote 信任生成的 TypeScript 参数与成功的 Host 结果,不执行调用 codec。复杂类型无法生成严格 codec 时,LIB 构建失败,不降级为 `unknown` 或无校验 JSON。
 
 Remote 方法引用的命名业务类型必须从纯类型公共 subpath 导出。如果唯一可达入口会带入 Host Service、Cordis `Context` merge 或 Host-only 实现,构建失败并要求业务包提供安全的类型出口。原始值、字面量和 Typert 明确支持的简单组合不需要额外命名。
 
@@ -315,7 +315,7 @@ Client 业务包只引用 `@deepseek-ai/dsh-api-remotes/client`,不直接依
 
 `ctx.remote.$mount()` 把 contribution 注册到 `Typert.remotes`,安装它的 namespace Service 和具体方法,并在它们就绪后才 resolve。调用该方法的 Cordis fiber 持有 disposer。endpoint 重复、同一 namespace/method 模式冲突或 descriptor 与现有类型身份冲突时直接失败。
 
-Client Remote Service 把 `@Remote` descriptor 实体化为 `remote.<namespace>` 子 Service 上的真实函数。函数按 descriptor 的位置参数顺序构造具名 `args`,执行 Client strict codec,然后调用 `ctx.connection.rpc.call('/api', endpoint, { args }, signal)`。对于支持取消的 descriptor,生成的函数接受最后一个可选 signal,并将其与 contribution 的挂载生命周期合并;因此卸载会取消所有正在进行的 carrier 调用,而调用方也可以单独取消一次调用。
+Client Remote Service 把 `@Remote` descriptor 实体化为 `remote.<namespace>` 子 Service 上的真实函数。函数检查位置参数数量,按 descriptor 的参数顺序构造具名 `args`,不做运行时类型解析,然后调用 `ctx.connection.rpc.call('/api', endpoint, { args }, signal)`。对于支持取消的 descriptor,生成的函数接受最后一个可选 signal,并将其与 contribution 的挂载生命周期合并;因此卸载会取消所有正在进行的 carrier 调用,而调用方也可以单独取消一次调用。
 
 带 `scope` 的 direct descriptor 和 `@RemoteScope` descriptor 都不为每个 Agent Scope 复制函数。Client Remote Service 为每个 namespace 创建一个注册为 `remote.<namespace>` 的 Cordis 子 Service,并在其上实体化 direct 与 scoped 变体。通过 `agentCtx.remote.goals` 取得方法时,accessor 会在返回可调用句柄前捕获当前 Agent Context。方法再通过对应 Context binder 从该 Context 取得 identity。direct scoped 投影用 identity 替代 `scope.wire` 指定的 lookup 位置,Remote Scope descriptor 则把 identity 写入 receiver 的独立 wire 字段;两者都发起同一种 `/api` 调用。
 
@@ -380,10 +380,9 @@ ctx.typertGateway.invoke({ namespace, method, args, signal })
 → direct 使用原 Service;context 先解析 scoped Context 和 Service
 → cancellation descriptor 存在时把 signal 追加到业务参数末尾
 → Reflect.apply(receiver[implementation ?? method], receiver, orderedArgs)
-→ result codec 编码业务结果
 ```
 
-`ctx.typertGateway.invoke()` 是 carrier-independent 的 Host 入口。它不创建 rpcId、RPC envelope 或 HTTP response;它只返回编码结果,或产生由 Connection RPC adapter 映射的 Gateway 错误。
+`ctx.typertGateway.invoke()` 是 carrier-independent 的 Host 入口。它不创建 rpcId、RPC envelope 或 HTTP response;它直接返回未经运行时输出解码的业务结果,或产生由 Connection RPC adapter 映射的 Gateway 错误。
 
 ## 共享 `/api` 调用链
 
@@ -426,7 +425,7 @@ Remote payload 使用具名 JSON 对象,不使用位置数组,也不发送 `
 
 ```text
 ctx.remote.goals.create(sessionId, request, signal?)
-→ Client InvocationDescriptor 编码 { args: { agentId, request } }
+→ Client InvocationDescriptor 组装 { args: { agentId, request } }
 → Client 合并 caller signal 与 contribution mount lifetime
 → ctx.connection.rpc.call('/api', 'goals/create', { args }, signal)
 → Connection 创建 rpcId 和既有 client-request envelope
@@ -435,9 +434,8 @@ ctx.remote.goals.create(sessionId, request, signal?)
 → 复合 FetchHandler 判断 endpoint ownership 并选择目标 FetchHandler
 → Typert interceptor 调用 ctx.typertGateway.invoke(..., request.signal)
 → Host InvocationDescriptor 解码、lookup、receiver 解析并把 signal 注入 Reflect.apply
-→ result codec 编码
 → Connection 写入既有 RPC result 并回送相同 rpcId
-→ Client result codec 验证并返回 CreateGoalResult
+→ Client 直接返回 CreateGoalResult
 ```
 
 Remote 不在 wire 上定义第二层 `{ ok, value/error }` response。成功值与失败都直接使用既有 RPC response 的 `result`,失败分支携带共享的 `{ code, message, details }` 数据。owner、resolver 与 Gateway 抛的都是同一个类 `RemoteError`,其码来自合并后的 `RemoteErrorDetailsMap`:Host 把结构识别出的 `RemoteError` 原样编码上 wire——包括 Gateway 自己的 `gateway/*` 装配码,以及 resolver 的 `session/not-found`、`session/agent-busy`——只把未归类的 throw 折成 `gateway/internal`,并把诊断串留在 message 里。Client face 为 `RemoteResult` 的错误分支重建实例,因此 `throw result.error` 的 throw 语义成立。[失败词汇 Agent Note](2026-08-28-ctx-remote-failure-vocabulary.zh.md) 持有码表、落点规则,以及为什么判别读 `code` 而不用 `instanceof`。
@@ -455,7 +453,7 @@ Gateway 只向 Connection 注册 ownership matcher 和 RPC handler,不注册 H
 - `@deepseek-ai/dsh-typert-protocol`:轻量 decorator、binding、lookup、Remote Scope 和 descriptor 协议。
 - Typert generator:分析 Host/Client Program,生成本地 face 和 Remote 消费端投影,并生成规范 symbol/Zod 信息。
 - Typert runtime:分别保存当前环境的 local reflection 与导入的 Remote contribution。
-- `@deepseek-ai/dsh-api-gateway`:默认入口关联 Host definition 与 Service,认领 Remote endpoint,执行 lookup、Context receiver 解析、调用和结果编码,并向 Connection 注册 `/api` interceptor;`/client` 入口挂载 Remote contribution,创建严格 Remote namespace Service 和方法,并把调用交给 `ctx.connection.rpc`。两个入口共享 Remote 协议,但不互相导入各自的 Cordis interface merge。
+- `@deepseek-ai/dsh-api-gateway`:默认入口关联 Host definition 与 Service,认领 Remote endpoint,校验输入,执行 lookup、解析 Context receiver、调用方法,并向 Connection 注册 `/api` interceptor;`/client` 入口挂载 Remote contribution,创建严格 Remote namespace Service 和方法,并把调用交给 `ctx.connection.rpc`。两个入口共享 Remote 协议,但不互相导入各自的 Cordis interface merge。
 - `@deepseek-ai/dsh-api-remotes`:BFF 层;注册本应用转发的 Cordis 事件源与随 generation readiness 携带的 Host home,选择 Client `/remote` contribution,并通过共享的 `TypertClientRemote` 约定向业务包暴露合并后的 Remote 类型。
 - Connection:拥有唯一 HTTP Server/未来 WebSocket carrier、共享 `/api` route 与其复合 FetchHandler、各 owner 注册的精确 Fetch route、RPC envelope、rpcId、序列化、trust 和错误传输。
 - Agent/Session 等业务对象包:拥有 lookup、Context provider、唯一 ID 类型和纯类型公共出口。
@@ -516,7 +514,7 @@ SRC 弱 descriptor 不验证普通 JSON 内部结构。Host Remote 签名变化
 
 类型 import 与运行时 contribution 是两种不同效果。`import type {}` 只扩展静态 Remote surface;真实调用环境遗漏 value contribution 时,Client Remote Service 必须以明确的「Remote 未挂载」错误失败。
 
-Browser 与 Host 各自持有 Zod 实例,不能依赖对象 identity 跨 realm 比较;一致性只由规范 symbol key、同一生成模型和 wire 行为保证
+生成的 Host 与 Client 产物携带匹配的 Zod factory,但 Client Remote 不实例化调用 schema。规范 symbol key、同一生成模型和 Host wire 校验让两侧保持一致,而无需跨 realm 比较 schema 对象 identity
 
 消费端可以导入 Host 当前未挂载的 Remote contract。类型表示「该协议能力已被消费端选择」,不保证目标进程当前存在对应 Service;运行时 endpoint 不可用必须明确失败。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.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-session-history-and-event-transport.md
-2026-08-18-session-history-and-event-transport.md: 8781ea265798ff200a6e0a58542b7d03693d8bd3
-2026-08-18-session-history-and-event-transport.zh.md: 1603f5aed8adb7a42fecab92511176d2147be5c3
+2026-08-18-session-history-and-event-transport.md: db5c403562101bca76c0d39a037a199b2ca164f3
+2026-08-18-session-history-and-event-transport.zh.md: 41ff3ea2dd538edb457e0db3108a900bab220e47

+ 7 - 6
.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md

@@ -8,7 +8,7 @@ English | [中文](2026-08-18-session-history-and-event-transport.zh.md)
 
 The browser consumes three kinds of data with different lifecycles: persistable, paginated Session logs; process-local state that needs an opening baseline to converge after reconnect; and immediate notifications that need no replay.
 
-These kinds of data cannot share one recovery rule. Session logs have stable sequence numbers and persistence, so a cursor can fill gaps; queue, jobs, and Workspace lists need a complete snapshot to replace an old mirror; ordinary notifications only promise delivery within the current Connection generation.
+These kinds of data cannot share one recovery rule. Session logs have stable sequence numbers and persistence, so a cursor can fill gaps; jobs, projection values, and Workspace lists need a complete snapshot to replace an old mirror; ordinary notifications only promise delivery within the current Connection generation.
 
 Observing Session history, lists, and projections must allow cold reads. If transport performs a general Typert lookup whenever an argument contains a Session or Agent, opening a page, switching tabs, or reconnecting the network implicitly resumes an Agent, so observation gains execution side effects.
 
@@ -158,7 +158,8 @@ Each method explicitly selects a cold inspection, live-only lookup, or resume-ca
 | `session.follow(address)` | one live or prepared observation carrying the opening page and projections | Publishes the snapshot first, then promotes an ordinary cold Session once in the background |
 | `session.control()` | current attached Agents, pending registry, and process-local registries | Baseline and reconnect do not resume an Agent |
 | `session.attachment`, fork source read | authorized durable Session data | A read does not resume an Agent |
-| `session.updateQueue`, `cancel` | only the current live Agent | Does not resume vanished state |
+| `session.updateQueue` | live Agent or ordinary persisted Session | Resumes an ordinary cold Session before mutating its Inbox |
+| `session.cancel` | only the current live Agent | Does not resume vanished state |
 | `models`, `selectModel`, `rename`, `prompt` | command resolves the target Session | Resumes only when the method explicitly permits it |
 | `create` and fork target | new Session/Agent | The user command supplies creation authority |
 
@@ -204,9 +205,9 @@ A terminal failure from the initial page, repair page, or follow enters the curr
 
 `session.control()` is a Host-wide snapshot stream. One browser can observe transient state for all current live Sessions without opening a journal for every transcript.
 
-Each generation emits a complete baseline first, followed by queue, jobs, and projection deltas. The baseline reads attached Agents and process-local registries without resuming cold Agents.
+Each generation emits a complete baseline first, followed by jobs and projection deltas. The baseline reads process-local registries and folded projection values without resuming cold Agents.
 
-Queue and jobs use complete replacement values and apply last-wins. Agent attach, detach, Session disposal, and owner disposal can all clear a stale mirror through an empty value or a new baseline.
+Jobs use complete replacement values and apply last-wins. Projection updates carry monotonically increasing revisions, while a new baseline replaces the complete projection map. Session and owner disposal clear stale mirrors.
 
 The original `approval/request` and `user-questions/request` events are forwardable waterfalls. If an Agent-scoped Client listener claims a request, it returns directly. If all delivered Clients call `next()`, the original Cordis waterfall continues to later Host listeners. Session control neither stores nor replays these requests.
 
@@ -313,7 +314,7 @@ API Proxy carries only independent business APIs it owns. Session, Workspace, Re
 
 **Split Session transport and Session commands into two public packages.** Both depend on Session address, Agent activation policy, subagent ownership, error mapping, and Client mount ordering. One public Controller preserves unified ownership while internal classes can evolve independently.
 
-**Move queue, jobs, projection, Workspace, and logs to ordinary `$on`.** Ordinary events have no reconnect baseline, cursor, or gap repair, so one missed delivery leaves permanently stale state. Only notifications that need no recovery, can be repaired by an independent query, or carry their own lifetime as a waterfall fit `$on`.
+**Move jobs, projections, Workspace, and logs to ordinary `$on`.** Ordinary events have no reconnect baseline, cursor, or gap repair, so one missed delivery leaves permanently stale state. Only notifications that need no recovery, can be repaired by an independent query, or carry their own lifetime as a waterfall fit `$on`.
 
 **Make every domain Controller inherit a page/follow/retry base class.** Session journals and Workspace snapshots have different opening, recovery, and ordering rules. Gateway's three compositional stream objects reuse transport lifecycle while domain adapters declare only their own frame semantics.
 
@@ -345,7 +346,7 @@ Connection tests pin missing, duplicate, and withdrawn generation sources, readi
 
 Session Host tests pin cold page/follow without increasing attached Agents, contiguous events reaching a cold follow after an explicit prompt, direct-subagent ownership, message-aligned pagination, and terminal-error projection.
 
-Session control tests pin baseline-first delivery, no cold-Session resume, attach/detach cleanup, queue and jobs replacement, and the projection watermark.
+Session control tests pin baseline-first delivery, no cold-Session resume, jobs replacement, and the projection watermark.
 
 Session Client tests pin one journal owner per Session, no writeback from stale open epochs, independent cancellation of control and journal, and retaining the published window during carrier retry.
 

+ 7 - 6
.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.zh.md

@@ -8,7 +8,7 @@ Status: implemented
 
 浏览器同时消费三类生命周期不同的数据:可持久化并分页的 Session 日志、需要 opening baseline 才能在重连后收敛的进程内状态,以及无需重放的即时通知。
 
-这三类数据不能共用一种恢复规则。Session 日志有稳定 seq 和 persistence,可以按 cursor 补齐缺口;queue、jobs、Workspace 列表等状态需要以完整 snapshot 替换旧镜像;普通通知只保证当前 Connection generation 内投递。
+这三类数据不能共用一种恢复规则。Session 日志有稳定 seq 和 persistence,可以按 cursor 补齐缺口;jobs、projection 值和 Workspace 列表等状态需要以完整 snapshot 替换旧镜像;普通通知只保证当前 Connection generation 内投递。
 
 观察 Session 历史、列表和投影必须允许冷读取。若 transport 因参数中出现 Session 或 Agent 就触发通用 Typert lookup,打开页面、切换标签或网络重连都会隐式恢复 Agent,观察操作因此产生执行副作用。
 
@@ -158,7 +158,8 @@ Session Remote 方法传递 `SessionId` 或 `SessionAddress`,不靠参数类
 | `session.follow(address)` | 一份携带 opening page 与 projection 的 live 或 prepared observation | 先发布 snapshot,再在后台把普通冷 Session 提升一次 |
 | `session.control()` | 当前 attached Agent、pending registry 与进程内 registry | baseline 与重连不恢复 Agent |
 | `session.attachment`、fork 源读取 | 已授权的持久 Session 数据 | 读取不恢复 Agent |
-| `session.updateQueue`、`cancel` | 仅命中当前 live Agent | 不为已消失状态恢复 Agent |
+| `session.updateQueue` | live Agent 或普通持久 Session | 修改 Inbox 前恢复普通冷 Session |
+| `session.cancel` | 仅命中当前 live Agent | 不为已消失状态恢复 Agent |
 | `models`、`selectModel`、`rename`、`prompt` | 命令解析目标 Session | 仅按方法约定显式恢复 |
 | `create` 与 fork 目标 | 新 Session/Agent | 用户命令提供创建授权 |
 
@@ -204,9 +205,9 @@ initial page、repair page 或 follow 的 terminal failure 进入当前 Session
 
 `session.control()` 是 Host 范围的 snapshot stream,一个浏览器可观察所有当前 live Session 的瞬态状态,而不必为每个 transcript 打开 journal。
 
-每个 generation 先发完整 baseline,再发 queue、jobs 与 projection 增量帧。baseline 读取 attached Agent 和进程内 registry,不恢复冷 Agent。
+每个 generation 先发完整 baseline,再发 jobs 与 projection 增量帧。baseline 读取进程内 registry 和已折叠的 projection 值,不恢复冷 Agent。
 
-queue 与 jobs 使用完整 replacement 值并按 last-wins 应用。Agent attach、detach、Session disposal 与 owner disposal 都能用空值或新 baseline 清除陈旧镜像。
+jobs 使用完整 replacement 值并按 last-wins 应用。Projection update 携带单调递增 revision,新 baseline 则替换完整 projection map。Session 与 owner disposal 会清理陈旧镜像。
 
 原始 `approval/request` 与 `user-questions/request` 是可转发 waterfall。若某个 Agent-scoped Client listener claim,请求直接返回;若所有已投递 Client 都调用 `next()`,原 Cordis waterfall 继续到后续 Host listener。Session control 不保存或重放这些请求。
 
@@ -313,7 +314,7 @@ API Proxy 只承接自身拥有的独立业务 API,不是 Session、Workspace
 
 **把 Session transport 与 Session commands 拆成两个公开包。** 两者共同依赖 Session address、Agent 激活策略、subagent ownership、错误映射和 Client 挂载顺序;一个公开 Controller 保持统一所有权,内部 class 仍可独立演化。
 
-**把 queue、jobs、projection、Workspace 与日志都改成普通 `$on`。** 普通事件没有 reconnect baseline、cursor 或 gap repair,漏掉一次推送就会留下永久陈旧状态;只有无需恢复、可由独立查询修复,或以 waterfall 本身持有请求生命周期的通知适合 `$on`。
+**把 jobs、projection、Workspace 与日志都改成普通 `$on`。** 普通事件没有 reconnect baseline、cursor 或 gap repair,漏掉一次推送就会留下永久陈旧状态;只有无需恢复、可由独立查询修复,或以 waterfall 本身持有请求生命周期的通知适合 `$on`。
 
 **让每个领域 Controller 继承一个 page/follow/retry 基类。** Session journal 与 Workspace snapshot 的 opening、恢复和排序规则不同;Gateway 的三个组合式 stream 对象复用 transport 生命周期,同时让领域 adapter 只声明自己的 frame 语义。
 
@@ -345,7 +346,7 @@ Connection 测试固定 generation source 缺失、重复注册、撤回、ready
 
 Session Host 测试固定 cold page/follow 不增加 attached Agent、显式 prompt 后 cold follow 收到连续事件、direct subagent ownership、message-aligned pagination 和终止错误投影。
 
-Session control 测试固定 baseline-first、冷 Session 不恢复、attach/detach 清理、queue 与 jobs replacement,以及 projection watermark。
+Session control 测试固定 baseline-first、冷 Session 不恢复、jobs replacement 与 projection watermark。
 
 Session Client 测试固定每 Session 单一 journal owner、旧 open epoch 不写回、control 与 journal 独立取消,以及 carrier retry 期间保留已发布窗口。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-20-client-session-conversation-ownership.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-20-client-session-conversation-ownership.md
-2026-08-20-client-session-conversation-ownership.md: 89f27f745c13e070b7f9794d98747bc73bda3b5a
-2026-08-20-client-session-conversation-ownership.zh.md: 79be1b898a1fb5a75e5438d55d739d84775fe6d9
+2026-08-20-client-session-conversation-ownership.md: 0d2f19cff1d4625d678a25c87bcc55fc3f2bfad4
+2026-08-20-client-session-conversation-ownership.zh.md: fdb2420efff88e0a5a33dba79bebf0d072280453

+ 28 - 27
.agents/notes/implemented/architecture/2026-08-20-client-session-conversation-ownership.md

@@ -38,7 +38,7 @@ Client Session and Workspace objects belong to `api/session-controller/client` a
 
 The React adapters for Session and Workspace belong to `client/ui-session` and `client/ui-workspace`. The Store engine belongs to `client/store`; the Slot registry, scope materialization, and observable-to-hook binding belong to `client/ui-renderer`.
 
-The system has no aggregate `client/runtime` package and no replacement central facade. [Session history and event transport](2026-08-18-session-history-and-event-transport.md) defines Session history, Remote streams, pagination cursors, and reconnect continuity; this note starts from the Client objects and sources published by Controllers.
+The system has no aggregate `client/runtime` package and no replacement central facade. [Session history and event transport](2026-08-18-session-history-and-event-transport.md) defines history continuity. [Client Session references](2026-09-15-client-session-references.md) owns reference acquisition, exact-generation lifetimes, main-area ownership, and unified UI status; this note owns the layering and source-registration rules.
 
 ## Layering principles
 
@@ -55,9 +55,10 @@ Each standard hook belongs to the `ui-*` package closest to its data semantics.
 | Hook | Owner | Source |
 | --- | --- | --- |
 | `useSessions` | `client/ui-session` | Session Controller global list |
-| `useSession` | `client/ui-session` | Current Session snapshot |
-| `useProjection` | `client/ui-session` | Current Session keyed projection |
-| `useSessionPendingInteraction` | `client/ui-session` | Aggregated pending domains |
+| `useSession` | `client/ui-session` | Bound Session snapshot |
+| `useProjection` | `client/ui-session` | Bound Session keyed projection |
+| `useSessionStatus` | `client/ui-session` | Running, effective pending request, and unread completion |
+| `useSessionRetainInfo` | `client/ui-session` | Read-only Controller reference-source counts |
 | `useWorkspaces` | `client/ui-workspace` | Workspace Controller list |
 | `useConversation` | `client/ui-conversation` | Conversation binding snapshot |
 | `useChat` | `client/ui-chat` | `chat` target source |
@@ -77,10 +78,10 @@ Adding a target does not add a branch to the renderer or Session Controller. The
 
 | Package | Owns | Explicitly does not own |
 | --- | --- | --- |
-| `api/session-controller/client` | Session objects, list, selection, commands, projections, queue, event windows, and Agent Contexts | Conversation targets, React, Slots, Workspace |
+| `api/session-controller/client` | Session objects, catalog, references, source counts, commands, projections, event windows, and Agent Contexts | Navigation, completion reminders, Conversation targets, React, Slots, Workspace |
 | `api/workspace-controller/client` | Workspace objects, ordering, archive state, commands, and snapshots | React, Session navigation policy, directory UI |
-| `client/ui-session` | Session scope, standard sources, `SessionProvider`, and pending-interaction aggregation | Session transport, Conversation assembly, Approval/Question results |
-| `client/ui-workspace` | Workspace hook, browser UI, and cross-Controller navigation policy | Workspace transport, copies of Session data |
+| `client/ui-session` | Explicit Session scope, standard sources, `SessionProvider`, and unified UI status | Session transport, reference ownership, Conversation assembly, Approval/Question results |
+| `client/ui-workspace` | Workspace hook, browser UI, main-area reference, and cross-Controller navigation policy | Workspace transport, copies of Session data |
 | `client/ui-conversation` | Conversation core, registries, bindings, shell, input, composer, queue, and View navigation | Session transport, Chat/Trajectory snapshots |
 | `client/ui-chat` | Chat target, Node definitions, renderers, selection, details, and locale | Session lifecycle, generic View navigation, Trajectory, historical-image cache |
 | `client/ui-trajectory` | Trajectory target, event-record projection, and inspection view | Session snapshots, Chat snapshots |
@@ -117,9 +118,9 @@ Session data reaches the UI through this path:
        useChat            useTrajectory
 ```
 
-Workspace data enters the Workspace Controller from `ctx.remote.workspace`, then `ui-workspace` exposes it as `useWorkspaces`. For cross-domain navigation, `ui-workspace` temporarily reads the Session Controller and issues a selection or command.
+Workspace data enters the Workspace Controller from `ctx.remote.workspace`, then `ui-workspace` exposes it as `useWorkspaces`. `ui-workspace` reads explicit targets for cross-domain navigation and owns the main-area reference without making it a default business Context.
 
-Approval and Question arrive from the Host waterfall through `ctx.remote.$on` at their respective UI owners. Each owner publishes a Pending object; `ui-session.pendingInteractions` then supplies that same object to Session navigation state and Conversation composer selection.
+Approval and Question arrive from the Host waterfall through `ctx.remote.$on` at their respective UI owners. Each owner publishes a Pending object; `ui-session.sessionStatus` supplies the same effective object to Workspace indicators and Conversation composer selection.
 
 ## Session Controller Client
 
@@ -142,7 +143,7 @@ Whether a field derives from an event, control frame, or local command does not
 
 The Session Controller exposes three distinct read faces:
 
-1. The global Session list and current-selection source, used by navigation and `useSessions`.
+1. The Session catalog and local ownership sources, used by `useSessions` and read-only reference metadata consumers.
 2. A logical binding for each Session containing `sessionId`, a `SessionSnapshot` source, commands, and projection sources.
 3. A Conversation-facing `SessionEventSource` used only by the Conversation assembly core.
 
@@ -160,9 +161,9 @@ Initial open, reconnect, gap repair, and updates whose continuity cannot be prov
 
 ### Session binding lifecycle
 
-Each Session binding owns a Cordis Context and Fiber. The Session Controller creates and releases the binding.
+Each live Session generation owns a Cordis Context and Fiber. The Controller creates its binding on acquisition and retires it on final reference release or root disposal.
 
-Objects that depend on a Session register cleanup through `binding.ctx.effect()`. Releasing a binding cleans up Conversation bindings, UI materializations, and scoped Slot stores without a dedicated `onBindingRelease` or `onRelease` callback protocol.
+Objects that depend on a Session register cleanup through `binding.ctx.effect()`. Generation retirement cleans up Conversation bindings, UI materializations, and scoped Slot stores without a dedicated `onBindingRelease` or `onRelease` callback protocol.
 
 This cleanup does not require the Session Controller to know the roster of upper-layer consumers.
 
@@ -172,12 +173,12 @@ This cleanup does not require the Session Controller to know the roster of upper
 
 `client/ui-session` is the sole Session adapter between the Session Controller and the React/Slot system. It provides `ctx.uiSession` and:
 
-- observes the Session list, current selection, and per-Session bindings;
+- observes the Session catalog, local reference metadata, and explicitly supplied bindings;
 - installs the session and session-maybe scope adapters;
 - supplies `SessionProvider` rendering semantics;
 - supplies built-in Session snapshot, projection, and sessionId sources;
 - accepts Session-scoped source contributions from other domain packages;
-- aggregates pending interactions registered by business packages.
+- aggregates domain-owned pending interactions with running and completion-reminder facts in `sessionStatus`.
 
 It does not own Session transport, event folding, Conversation targets, or concrete business results.
 
@@ -193,12 +194,12 @@ The runtime rejects undeclared, missing, or duplicate standard props. `ui-sessio
 
 session and session-maybe use the same adapter with different binding semantics:
 
-- a strict session scope refuses to render without a current binding;
+- a strict session scope refuses to render without an explicitly supplied binding;
 - session-maybe uses a stable absent binding to preserve hook call order;
-- changing the current Session rebuilds the strict Session subtree under the `sessionId` key;
-- root and session-maybe entries may remain mounted across Session changes.
+- changing the exact Context generation remounts a bound subtree, including same-id replacement;
+- an unbound session-maybe entry adopts its first binding without remounting; root entries have no Session binding.
 
-Each real materialized binding retains the Controller binding's Context. `ui-session` removes the cache entry and withdraws the current binding through `binding.ctx.effect()`.
+Each UI materialization borrows the Controller binding's Context. `ui-session` removes that generation's cache entry and publishes absence through `binding.ctx.effect()`; it does not retain the Session.
 
 Changing the contribution roster rematerializes existing bindings and publishes a new source set. Source identity remains stable within one binding lifetime, as required by `useSyncExternalStore` caching.
 
@@ -206,9 +207,9 @@ Changing the contribution roster rematerializes existing bindings and publishes
 
 `SessionProvider` is a standard seat derived by `PropsRenderSlots` from a session-scoped child declaration, not a React Context imported directly by business components.
 
-It accepts ordinary `ReactNode` children rather than a `(sessionId) => ReactNode` render function; callers wrap `renderSlot('details', {})` directly.
+It accepts ordinary `ReactNode` children and a required `session={reference | undefined}`. The Provider borrows the caller-owned reference without acquiring or releasing it; callers wrap `renderSlot('details', {})` directly.
 
-Session identity comes from the scope binding and standard `sessionId` prop. The Provider handles only the absent branch and subtree isolation by Session identity; components do not obtain Session data through a Provider callback.
+Session identity reaches components through the explicit scope binding and standard `sessionId` prop. An absent Provider stays unbound, and neither nested Providers nor root entries fall back to a main-area Session.
 
 ### Pending interactions
 
@@ -220,7 +221,7 @@ Concurrent objects with the same key are rejected; replacement requests use a ne
 
 `ui-session` selects each Session's effective object using domain precedence. Higher precedence wins; at equal precedence, the later valid object in traversal order wins.
 
-The aggregate is published as `pendingInteractions: ObservableSnapshot<ReadonlyMap<SessionId, SessionPendingInteraction>>`; `useSessionPendingInteraction` is its React read face.
+The pending aggregate is private to `ui-session`; its effective request appears unchanged as `sessionStatus.getSnapshot().get(id)?.pendingInteraction`. `useSessionStatus` is the public UI read face.
 
 Session navigation state and composer takeover read the same effective object. They do not maintain separate status maps or takeover rosters.
 
@@ -242,7 +243,7 @@ These combined facts do not enter `WorkspaceSnapshot`:
 
 `client/ui-workspace` registers the Workspace list source as the root standard source `workspaces`, from which the renderer provides `useWorkspaces`.
 
-Initial selection, blank-Session reuse, new-session navigation, concurrent-create coalescing, and navigation after archival are UI navigation policy. That policy may read both `ctx.workspaces` and `ctx.sessions` at decision time, but it issues only Controller commands and selection actions and does not publish a combined snapshot.
+Initial restoration, blank-Session reuse, new-session navigation, concurrent-create coalescing, and navigation after archival are UI policy. `ui-workspace` may read both Controllers, but it keeps the main target and reference in its own navigation owner instead of writing UI selection into a Controller snapshot.
 
 Directory pickers, directory browsing, and `openPath` are separate directory capabilities and do not enter the Workspace Controller.
 
@@ -286,7 +287,7 @@ The shell phase is a pure composition of Session lifecycle and Conversation targ
 
 ### Input and composer
 
-The composer chain belongs to `ui-conversation`; a concrete takeover belongs to its business package. `ConversationRoot` reads the current Session's effective object through `useSessionPendingInteraction` and supplies it to chain selectors as `ComposerChainProps.pendingInteraction`.
+The composer chain belongs to `ui-conversation`; a concrete takeover belongs to its business package. `ConversationRoot` reads its bound Session's effective request through `useSessionStatus` and supplies it to chain selectors as `ComposerChainProps.pendingInteraction`.
 
 A selector is a pure function of owner currency. Its non-null result reaches the selected component as `matched`. A stable composer entry and the default composer remain mounted together, while the chain selects one effective presentation.
 
@@ -334,7 +335,7 @@ The Gateway requires only that Remote Event arguments and results are valid JSON
 
 ### One pending projection
 
-The Sidebar and composer consume the same `pendingInteractions` snapshot. Navigation displays approval, plan-review, or question state from the effective object's `kind`; each composer entry selects its own panel by object identity.
+The Sidebar and composer consume the same effective `sessionStatus` pending request. Navigation displays approval, plan-review, or question state from its `kind`; each composer entry selects its own panel by object identity.
 
 The same request identity drives both UI surfaces. A request that replaces another request of the same type uses a new key, so selectors and subscribers observe the identity change.
 
@@ -366,7 +367,7 @@ Stores hold viewing and interaction state such as drafts, View selection, Chat s
 
 When one plugin provides both a source and a Slot entry, it registers the source first and the entry second. Reverse Cordis disposal then removes the entry before the source, so a mounted entry never briefly loses a required hook.
 
-Releasing a Session binding cleans up UI materialization and scoped Stores through `binding.ctx.effect()`. Releasing a plugin fiber cleans up sources, listeners, and Slot entries through registration disposers.
+Final Session-reference release cleans up UI materialization and scoped Stores through `binding.ctx.effect()`. Releasing a plugin fiber cleans up sources, listeners, and Slot entries through registration disposers.
 
 Every disposer is idempotent and depends on no implicit callback outside the Cordis lifecycle.
 
@@ -386,7 +387,7 @@ UI components do not receive `ctx`. Cross-package collaboration uses Cordis serv
 
 Before adding state, choose its sole owner from its consumption semantics: Host communication, commands, and entity lifecycle belong to an API Controller; data assembled from Session events but independent of a target belongs to the Conversation core; projections serving only one View belong to that target package; drafts, selections, and panel state belong to the UI package that owns the interaction.
 
-The same fact must not be retained simultaneously in a Controller snapshot, Conversation snapshot, and Store. A cross-domain decision reads multiple sources and immediately issues a command; it does not create a joined snapshot or cache another domain's object.
+The same fact must not be retained simultaneously in a Controller snapshot, Conversation snapshot, and Store. Cross-domain navigation reads sources at decision time. A UI-owned status source may compose independent running, pending-request, and completion-reminder facts, but must preserve domain ownership and object identity rather than duplicate those domains' state.
 
 These are signs of incorrect ownership: a Controller imports React; the renderer branches on business types; a component traverses Session events; a Store holds Session or Workspace entities; changing one target requires changing the Session Controller.
 
@@ -421,7 +422,7 @@ A target must not use another target's snapshot as its data source. Optional col
 5. When it can handle the request, create the Pending object, publish it through the publication function, await its result, and remove it in `finally`.
 6. Test concurrent keys, precedence, user cancellation, transport abort, plugin disposal, and delegation without a Session.
 
-A request does not register Slots, declare child Slots, mutate the Session snapshot, or create a separate state index. Sidebar and composer both read one effective object from `useSessionPendingInteraction`.
+A request does not register Slots, declare child Slots, mutate the Session snapshot, or create a separate state index. Sidebar and composer read the same effective object from `useSessionStatus`.
 
 ### Review checks
 

+ 28 - 27
.agents/notes/implemented/architecture/2026-08-20-client-session-conversation-ownership.zh.md

@@ -38,7 +38,7 @@ Session 与 Workspace 的 Client 对象分别归 `api/session-controller/client`
 
 Session 与 Workspace 的 React 适配分别归 `client/ui-session` 和 `client/ui-workspace`。Store engine 归 `client/store`,Slot registry、scope materialization 和 observable-to-hook 绑定归 `client/ui-renderer`。
 
-系统不提供聚合式 `client/runtime` package,也不设置替代它的总控 facade。Session history、Remote stream、分页 cursor 和重连连续性由 [Session 历史与事件传输](2026-08-18-session-history-and-event-transport.zh.md) 定义;本 Note 从 Controller 发布的 Client 对象与 source 开始
+系统没有聚合式 `client/runtime` 包,也没有替代的中央 facade。[Session 历史与事件传输](2026-08-18-session-history-and-event-transport.zh.md)定义历史连续性。[Client 会话引用](2026-09-15-client-session-references.zh.md)拥有引用获取、精确代际生命周期、主区域所有权和统一 UI 状态;本篇拥有分层与数据源注册规则
 
 ## 分层原则
 
@@ -55,9 +55,10 @@ UI 层可以同时读取多个 Controller 做一次导航决定,但不得把
 | Hook | Owner | Source |
 | --- | --- | --- |
 | `useSessions` | `client/ui-session` | Session Controller 全局列表 |
-| `useSession` | `client/ui-session` | 当前 Session snapshot |
-| `useProjection` | `client/ui-session` | 当前 Session keyed projection |
-| `useSessionPendingInteraction` | `client/ui-session` | pending domain 聚合结果 |
+| `useSession` | `client/ui-session` | 已绑定会话快照 |
+| `useProjection` | `client/ui-session` | 已绑定会话的键控投影 |
+| `useSessionStatus` | `client/ui-session` | 运行状态、有效待处理请求和未读完成提醒 |
+| `useSessionRetainInfo` | `client/ui-session` | 控制器的只读引用来源计数 |
 | `useWorkspaces` | `client/ui-workspace` | Workspace Controller 列表 |
 | `useConversation` | `client/ui-conversation` | Conversation binding snapshot |
 | `useChat` | `client/ui-chat` | `chat` target source |
@@ -77,10 +78,10 @@ UI 层可以同时读取多个 Controller 做一次导航决定,但不得把
 
 | Package | 拥有内容 | 明确不拥有 |
 | --- | --- | --- |
-| `api/session-controller/client` | Session 对象、列表、选择、命令、projection、queue、事件窗口和 Agent Context | Conversation target、React、Slot、Workspace |
+| `api/session-controller/client` | 会话对象、目录、引用、来源计数、命令、投影、事件窗口与 Agent Context | 导航、完成提醒、Conversation target、React、Slot、Workspace |
 | `api/workspace-controller/client` | Workspace 对象、顺序、归档、命令和 snapshot | React、Session 导航策略、目录 UI |
-| `client/ui-session` | Session scope、标准 source、`SessionProvider`、pending interaction 聚合 | Session transport、Conversation 组装、Approval/Question 结果 |
-| `client/ui-workspace` | Workspace hook、浏览器 UI 和跨 Controller 导航策略 | Workspace transport、Session 数据副本 |
+| `client/ui-session` | 显式会话作用域、标准数据源、`SessionProvider` 与统一 UI 状态 | 会话传输、引用所有权、Conversation 组装、Approval/Question 结果 |
+| `client/ui-workspace` | Workspace 钩子、浏览器 UI、主区域引用与跨控制器导航策略 | Workspace 传输、会话数据副本 |
 | `client/ui-conversation` | Conversation core、registry、binding、shell、input、composer、queue 和 View 导航 | Session transport、Chat/Trajectory snapshot |
 | `client/ui-chat` | Chat target、Node definitions、renderer、selection、details 和 locale | Session 生命周期、通用 View 导航、Trajectory、历史图片 cache |
 | `client/ui-trajectory` | Trajectory target、事件记录投影和检查视图 | Session snapshot、Chat snapshot |
@@ -117,9 +118,9 @@ Session 数据按以下路径进入 UI:
        useChat            useTrajectory
 ```
 
-Workspace 数据从 `ctx.remote.workspace` 进入 Workspace Controller,再由 `ui-workspace` 暴露为 `useWorkspaces`;需要跨域导航时,`ui-workspace` 临时读取 Session Controller 并发出选择或命令
+Workspace 数据由 `ctx.remote.workspace` 进入 Workspace 控制器,再由 `ui-workspace` 通过 `useWorkspaces` 提供。`ui-workspace` 为跨领域导航读取显式目标并持有主区域引用,不把它变为默认业务 Context
 
-Approval 与 Question 从 Host waterfall 经 `ctx.remote.$on` 到达各自 UI owner。Owner 发布 Pending 对象,`ui-session.pendingInteractions` 再把同一对象送往 Session 导航状态和 Conversation composer selection
+Approval 与 Question 通过 `ctx.remote.$on` 从 Host waterfall 到达各自的 UI owner。各 owner 发布 Pending 对象;`ui-session.sessionStatus` 向 Workspace 标识和 Conversation composer 选择提供同一个有效对象
 
 ## Session Controller Client
 
@@ -142,7 +143,7 @@ Approval 与 Question 从 Host waterfall 经 `ctx.remote.$on` 到达各自 UI ow
 
 Session Controller 对外提供三个互不替代的读取面:
 
-1. 全局 Session list 与 current selection source,供导航和 `useSessions` 使用。
+1. 会话目录与本地所有权数据源,由 `useSessions` 和只读引用元数据消费方使用。
 2. 每个 Session 的逻辑 binding,包含 `sessionId`、`SessionSnapshot` source、commands 与 projection sources。
 3. Conversation-facing `SessionEventSource`,只供 Conversation assemble core 使用。
 
@@ -160,9 +161,9 @@ Session Controller 对外提供三个互不替代的读取面:
 
 ### Session binding 生命周期
 
-每个 Session binding 持有自己的 Cordis Context 与 Fiber。Session Controller 创建 binding,也负责释放它
+每个活跃会话 generation 持有 Cordis Context 与 Fiber。控制器在获取引用时创建绑定,并在最后一份引用释放或根销毁时结束该绑定
 
-依赖 Session 的对象把清理注册到 `binding.ctx.effect()`。Binding 释放会触发 Conversation binding、UI materialization 和 scoped Slot store 的清理,不存在额外的 `onBindingRelease` 或 `onRelease` 回调协议。
+依赖会话的对象通过 `binding.ctx.effect()` 注册清理。generation 结束会清理 Conversation 绑定、UI 物化结果和作用域 Slot 存储,无需单独的 `onBindingRelease` 或 `onRelease` 回调协议。
 
 这种清理方式不要求 Session Controller 了解上层消费者名册。
 
@@ -172,12 +173,12 @@ Session Controller 对外提供三个互不替代的读取面:
 
 `client/ui-session` 是 Session Controller 与 React/Slot 系统之间唯一的 Session adapter。它提供 `ctx.uiSession`,并负责:
 
-- 观察 Session list、current selection 和 per-Session binding
+- 观察会话目录、本地引用元数据和显式提供的绑定
 - 安装 session 与 session-maybe scope adapter;
 - 提供 `SessionProvider` 的呈现语义;
 - 内建 session snapshot、projection 和 sessionId source;
 - 接收其他领域 package 的 Session-scoped source contribution;
-- 聚合业务 package 注册的 pending interaction
+- 在 `sessionStatus` 中组合领域持有的待处理交互、运行事实和完成提醒
 
 它不拥有 Session transport、event folding、Conversation target 或具体业务结果。
 
@@ -193,12 +194,12 @@ Session Controller 对外提供三个互不替代的读取面:
 
 session 与 session-maybe 使用同一个 adapter,但绑定语义不同:
 
-- strict session scope 在没有 current binding 时拒绝渲染;
+- 严格会话作用域在没有显式提供的绑定时拒绝渲染;
 - session-maybe 使用稳定 absent binding,保持 hook 调用顺序;
-- current Session 切换以 `sessionId` 为 key 重建严格 Session subtree
-- root 与 session-maybe entry 可以跨 Session 切换常驻
+- 精确 Context generation 改变时重新挂载已绑定子树,同一 id 的替代 generation 也如此
+- 未绑定的 session-maybe 条目无需重新挂载即可接纳首个绑定;root 条目没有会话绑定
 
-每个真实 materialized binding 保留 Controller binding 的 Context。`ui-session` 通过 `binding.ctx.effect()` 删除缓存项并撤销 current binding
+每个 UI 物化结果借用控制器绑定的 Context。`ui-session` 通过 `binding.ctx.effect()` 移除该 generation 的缓存项并发布空值,不会 retain 会话
 
 Contribution roster 变化会重建已 materialize 的 binding 并发布新的 source 集合。同一 binding 生命周期内,source identity 保持稳定,以满足 `useSyncExternalStore` 的缓存要求。
 
@@ -206,9 +207,9 @@ Contribution roster 变化会重建已 materialize 的 binding 并发布新的 s
 
 `SessionProvider` 是 `PropsRenderSlots` 根据 session-scoped child 声明派生的标准席,不是业务 component 直接 import 的 React Context。
 
-它接收普通 `ReactNode` children,不接收 `(sessionId) => ReactNode` render function;调用方直接用它包裹 `renderSlot('details', {})`。
+它接收普通 `ReactNode` children 和必填的 `session={reference | undefined}`。Provider 借用调用方持有的引用,不获取或释放它;调用方直接包住 `renderSlot('details', {})`。
 
-Session identity 通过 scope binding 和标准 `sessionId` prop 提供。Provider 只负责 absent branch 与按 Session identity 隔离 subtree,组件不得借助 Provider 回调取得 Session 数据
+会话身份通过显式作用域绑定和标准 `sessionId` prop 到达组件。空 Provider 保持未绑定,嵌套 Provider 和 root 条目都不会回退到主区域会话
 
 ### Pending interaction
 
@@ -220,7 +221,7 @@ Session identity 通过 scope binding 和标准 `sessionId` prop 提供。Provid
 
 `ui-session` 使用各 domain 的 precedence 选出每个 Session 当前生效的对象。较高 precedence 胜出,相同 precedence 下后遍历到的有效对象胜出。
 
-聚合结果发布为 `pendingInteractions: ObservableSnapshot<ReadonlyMap<SessionId, SessionPendingInteraction>>`,`useSessionPendingInteraction` 是其 React 读取面
+待处理聚合是 `ui-session` 的私有实现;其有效请求原样出现在 `sessionStatus.getSnapshot().get(id)?.pendingInteraction` 中。`useSessionStatus` 是公开 UI 读取接口
 
 Session 导航状态和 composer takeover 必须读取同一个 effective object,不得分别维护 status map 或 takeover roster。
 
@@ -242,7 +243,7 @@ Session 导航状态和 composer takeover 必须读取同一个 effective object
 
 `client/ui-workspace` 把 Workspace list source 注册为 root 标准 source `workspaces`,renderer 由此提供 `useWorkspaces`。
 
-初始选择、blank Session 复用、新建导航、并发创建合并和归档后导航属于 UI navigation policy。该 policy 可以在决定时同时读取 `ctx.workspaces` 与 `ctx.sessions`,但只调用 Controller command 和 selection action,不发布联合 snapshot
+启动恢复、空白会话复用、新会话导航、并发创建合并与归档后的导航属于 UI 策略。`ui-workspace` 可以读取两个控制器,但把主目标和引用保存在自己的导航 owner 中,不向控制器快照写入 UI 选择
 
 目录 picker、目录浏览和 `openPath` 属于独立目录能力,不进入 Workspace Controller。
 
@@ -286,7 +287,7 @@ Shell phase 由 Session lifecycle 与 Conversation target activity 纯合成。S
 
 ### Input 与 composer
 
-Composer chain 属于 `ui-conversation`,具体 takeover 属于业务 package。`ConversationRoot` 从 `useSessionPendingInteraction` 读取当前 Session 的 effective object,并作为 `ComposerChainProps.pendingInteraction` 交给 chain selector。
+composer chain 属于 `ui-conversation`,具体接管属于业务包。`ConversationRoot` 通过 `useSessionStatus` 读取已绑定会话的有效请求,并作为 `ComposerChainProps.pendingInteraction` 提供给 chain selector。
 
 Selector 是 owner currency 的纯函数,非 null 结果作为 `matched` 传给获选 component。Stable composer entry 与默认 composer 可以同时常驻,chain 只选择一个有效呈现。
 
@@ -334,7 +335,7 @@ Gateway 只要求 Remote Event 参数和结果是合法 JSON 传输值,不复
 
 ### 单一 pending 投影
 
-Sidebar 与 composer 使用相同 `pendingInteractions` snapshot。导航根据 effective object 的 `kind` 显示审批、计划审阅或问题状态,composer entry 根据对象实例选择自己的面板。
+Sidebar 与 composer 消费 `sessionStatus` 中同一个有效待处理请求。导航根据其 `kind` 显示审批、计划审阅或问题状态;每个 composer 条目按对象身份选择自己的面板。
 
 同一请求 identity 同时驱动两处 UI。新请求替换同类型旧请求时使用新 key,因此 selector 与订阅者都观察到身份变化。
 
@@ -366,7 +367,7 @@ Store 只承载 draft、View selection、Chat selection、inspection request 和
 
 一个 plugin 同时提供 source 与 Slot entry 时,先注册 source,再注册 entry。Cordis 反向 disposal 先移除 entry,再移除 source,仍挂载的 entry 因而不会短暂失去必需 hook。
 
-Session binding 释放通过 `binding.ctx.effect()` 清理 UI materialization 与 scoped store。Plugin fiber 释放通过 registration disposer 清理 source、listener 和 Slot entry
+最后一份会话引用释放后,通过 `binding.ctx.effect()` 清理 UI 物化结果和作用域存储。插件 fiber 释放通过注册 disposer 清理数据源、监听器和 Slot 条目
 
 所有 disposer 都可重复调用,不依赖 Cordis 生命周期以外的隐式回调。
 
@@ -386,7 +387,7 @@ UI component 不接收 `ctx`。跨 package 协作使用 Cordis service、standar
 
 新增状态前先按消费语义确定唯一 owner:Host 通信、命令和实体生命周期归 API Controller;由 Session events 形成且与 target 无关的数据归 Conversation core;只服务一种 View 的投影归对应 target package;草稿、选择和面板状态归拥有该交互的 UI package。
 
-同一事实不得同时保存在 Controller snapshot、Conversation snapshot 和 Store。需要跨域决策时读取多个 source 并立即发出 command,不创建联合 snapshot,也不缓存另一领域的对象副本
+同一个事实不能同时保存在控制器快照、Conversation 快照和存储中。跨领域导航在决策时读取数据源。UI 持有的状态数据源可以组合独立的运行、待处理请求和完成提醒事实,但必须保留领域归属与对象身份,不能复制这些领域的状态
 
 以下信号表示 owner 选择错误:Controller 开始 import React;renderer 出现业务类型分支;组件遍历 Session events;Store 保存 Session 或 Workspace 实体;一个 target 的变化要求修改 Session Controller。
 
@@ -421,7 +422,7 @@ Target 不得读取另一个 target 的 snapshot 作为自己的数据源。可
 5. 可处理时创建 Pending 对象,使用 publication function 发布,等待结果,并在 `finally` 中移除。
 6. 测试并发 key、precedence、用户取消、transport abort、plugin disposal 和无 Session delegation。
 
-单次请求不得注册 Slot、声明 child Slot、修改 Session snapshot 或另建状态索引。Sidebar 与 composer 都从 `useSessionPendingInteraction` 读取同一个 effective object
+请求不注册 Slot、不声明子 Slot、不修改会话快照,也不创建独立状态索引。Sidebar 与 composer 从 `useSessionStatus` 读取同一个有效对象
 
 ### Review 检查点
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.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-22-single-dsh-application-launcher.md
-2026-08-22-single-dsh-application-launcher.md: 630040c75c4c20c57b5e26288663265174331ca5
-2026-08-22-single-dsh-application-launcher.zh.md: 9bd51e6c4e8b7ab60cbabc384431bc4e22a6b522
+2026-08-22-single-dsh-application-launcher.md: 46a627eed652a0a0edaf7b900510b5d369efcdf0
+2026-08-22-single-dsh-application-launcher.zh.md: b7fa0f2b35c2fbf6c3a28f463b73170c84547a40

+ 4 - 2
.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md

@@ -14,7 +14,7 @@ The Python SDK distributes a native executable through four platform wheels. Its
 
 ### Launch scope
 
-Every supported Node application starts through the `dsh` CLI and one named profile. The shipped application commands are `dsh web`, `dsh --profile headless`, `dsh --profile sdk`, `dsh --profile sdk-minimal`, and `dsh --profile acp`; `dsh web` is the deliberate convenience alias for `--profile web`, not another application entry.
+Every supported Node application starts through the `dsh` CLI and one named profile. The shipped profiles are `web`, `headless`, `sdk`, `sdk-minimal`, and `acp`, selected with `dsh --profile <name>` or `dsh <name>`. `plugin` names the management command; a profile with that name requires `--profile plugin`.
 
 Vendor CLIs, build-only and test-only executables, direct in-process plugin mounting, and the private browser WebWorker preview are outside the application-launch inventory. A package app bin or root demo that launches a package entry is not an accepted extension point.
 
@@ -46,7 +46,7 @@ Direct SDK use follows normal Harness-home resolution: explicit `dshHome`, inher
 
 ### Python runtime
 
-The Python runtime wheel stages [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the `dsh-python-runtime-closure` entry. Its ordinary branch calls the public CLI export; a provider-private selector dispatches to the internal subprocess runner before CLI parsing and is not an application entry point. The [native-containment decision](2026-08-28-subprocess-native-containment.md) owns that private dispatch. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable example under `python/sdk/examples` selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar and the separately packaged `web` application.
+The Python runtime wheel stages [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the `dsh-python-runtime-closure` entry. Its ordinary branch calls the public CLI export; a provider-private selector dispatches to the internal subprocess runner before CLI parsing and is not an application entry point. The [native-containment decision](2026-08-28-subprocess-native-containment.md) owns that private dispatch. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable example under `python/sdk/examples` selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar, including the `web` profile.
 
 The executable family is `deepseek-harness-sdk-runtime-<platform>-<arch>`. The SDK wire, wheel and import distribution names, sidecar names, and wire identity `deepseek-harness-sdk-runtime` remain stable. The SDK package family is `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and `@deepseek-ai/dsh-sdk-jsonrpc-server`; `@deepseek-ai/dsh-acp` remains the ACP protocol plugin. There is no Python-specific Node application, checked-in complete config, compatibility package, forwarding executable, fallback parser, or SDK/ACP launcher alias. [docs/architecture.md](../../../../docs/architecture.md) owns this launch, and the [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows carrier.
 
@@ -56,6 +56,8 @@ The executable family is `deepseek-harness-sdk-runtime-<platform>-<arch>`. The S
 
 ## Existing decisions and supersession
 
+[Profile command shorthand](../feature/2026-09-15-profile-command-shorthand.md) supersedes this note's Web-only shorthand mechanism; this note retains authority over application composition and lifecycle ownership.
+
 This decision supersedes the application-launch and package-name facts in [profile plugin bundles](2026-08-05-profile-plugin-bundles.md), [TypeScript SDK client and subagent backend](../../archived/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md), [remove the SDK project toolchain](../../archived/simplification/2026-08-11-remove-sdk-project-toolchain.md), and [single-file Python SDK runtime distribution](2026-07-10-single-file-executable-sdk-runtime-distribution.md). Those notes retain independent authority for profile layering, client/wire semantics, deleted project tooling, and native packaging.
 
 The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md) remains authoritative for ACP wire and interaction scope. The [adding-a-package cookbook](../../../../docs/cookbook/adding-a-package.md) owns role-based package names. The [standalone sdk-minimal profile](../../archived/architecture/2026-08-24-standalone-sdk-minimal-profile.md) partially supersedes this note's base-first rule and complete-tree alternative while retaining this note's launcher ownership. No active note is fully superseded or eligible for archival.

+ 4 - 2
.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md

@@ -14,7 +14,7 @@ Python SDK 通过四个平台 wheel 包分发原生可执行文件。其打包
 
 ### 启动范围
 
-所有受支持的 Node 应用都通过 `dsh` CLI 与一个具名 profile 启动。随附应用命令是 `dsh web`、`dsh --profile headless`、`dsh --profile sdk`、`dsh --profile sdk-minimal` 与 `dsh --profile acp`;`dsh web` 是刻意为 `--profile web` 保留的便捷别名,不是另一个应用入口
+所有受支持的 Node 应用都通过 `dsh` CLI 与一个具名 profile 启动。随附 profile 为 `web`、`headless`、`sdk`、`sdk-minimal` 和 `acp`,可通过 `dsh --profile <name>` 或 `dsh <name>` 选择。`plugin` 表示管理命令;同名 profile 必须用 `--profile plugin` 选择
 
 Vendor CLI、仅用于构建和测试的可执行文件、进程内直接挂载插件以及私有浏览器 WebWorker 预览都不属于应用启动清单。包应用 bin 或直接启动包入口的根 demo 都不是可接受的扩展点。
 
@@ -46,7 +46,7 @@ SDK 用户通过 profile 自定义插件。`dsh plugin --profile <name> ...` 管
 
 ### Python 运行时
 
-Python 运行时 wheel 将 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) 暂存为 `dsh-python-runtime-closure` 入口。其普通分支调用公开 CLI export;提供方私有选择会在 CLI 解析前分派到内部子进程 runner,而不是应用入口。[原生 containment 决策](2026-08-28-subprocess-native-containment.zh.md)负责该私有分派。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;`python/sdk/examples` 下的可运行示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同 profile 语法与单独打包的 `web` 应用
+Python 运行时 wheel 将 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) 暂存为 `dsh-python-runtime-closure` 入口。其普通分支调用公开 CLI export;提供方私有选择会在 CLI 解析前分派到内部子进程 runner,而不是应用入口。[原生 containment 决策](2026-08-28-subprocess-native-containment.zh.md)负责该私有分派。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;`python/sdk/examples` 下的可运行示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同的 profile 语法,包括 `web` profile
 
 可执行文件族是 `deepseek-harness-sdk-runtime-<platform>-<arch>`。SDK 协议格式、wheel 与 import 分发名称、伴随文件名称,以及协议 identity `deepseek-harness-sdk-runtime` 保持稳定。SDK 包族是 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol` 与 `@deepseek-ai/dsh-sdk-jsonrpc-server`;`@deepseek-ai/dsh-acp` 继续作为 ACP 协议插件。仓库不保留 Python 专用 Node 应用、检入的完整配置、兼容包、转发可执行文件、后备解析器或 SDK/ACP 启动别名。[docs/architecture.md](../../../../docs/architecture.zh.md)负责该启动方式,[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 载体。
 
@@ -56,6 +56,8 @@ Python 运行时 wheel 将 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../..
 
 ## 既有决策与取代关系
 
+[Profile 命令简写](../feature/2026-09-15-profile-command-shorthand.zh.md)取代本 Note 中仅为 Web 提供简写的机制;本 Note 继续负责应用组合与生命周期的所有权。
+
 本决策取代 [profile 插件组合包](2026-08-05-profile-plugin-bundles.zh.md)、[TypeScript SDK 客户端与 SDK subagent 后端](../../archived/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md)、[移除 SDK 项目工具链](../../archived/simplification/2026-08-11-remove-sdk-project-toolchain.md)和[单文件 Python SDK 运行时分发](2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)中的应用启动与包名事实。这些 Note 对 profile 分层、客户端/协议语义、已删除的项目工具链与原生打包仍分别具有独立权威。
 
 [ACP 仅自动化协议](../simplification/2026-07-23-acp-automation-only-protocol.zh.md)继续负责 ACP 协议格式与交互范围。[添加包实操手册](../../../../docs/cookbook/adding-a-package.zh.md)负责基于角色的包名。[独立 sdk-minimal profile](../../archived/architecture/2026-08-24-standalone-sdk-minimal-profile.md)部分取代本 Note 的 base 优先规则与完整配置树替代方案,同时保留本 Note 对 launcher 所有权的决策。没有任何活跃 Note 被完全取代,也没有 Note 符合归档条件。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.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-25-electron-desktop-packaging-and-updates.md
-2026-08-25-electron-desktop-packaging-and-updates.md: 4a4dfe8910905b3c35fbdfdcaedd34a556b532f3
-2026-08-25-electron-desktop-packaging-and-updates.zh.md: 69877127578ae4d61643653403b736dbb5e7b1e6
+2026-08-25-electron-desktop-packaging-and-updates.md: c4940942a8d5188faf1846158873c052a43e1fc1
+2026-08-25-electron-desktop-packaging-and-updates.zh.md: 011624980f6f7239a33fb2b6c6803646b7d41dd3

Разница между файлами не показана из-за своего большого размера
+ 12 - 19
.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md


+ 37 - 45
.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md

@@ -4,7 +4,9 @@ Status: implemented
 
 [English](2026-08-25-electron-desktop-packaging-and-updates.md) | 中文
 
-profile 修改与恢复遵循[直接修改 profile 决策](2026-09-09-desktop-in-place-profile.zh.md)。
+插件管理和原生恢复遵循[共享 Web 薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)。
+
+[Electron 运行时决策](2026-09-11-desktop-electron-node-runtime.zh.md)替代独立上游 Node 可执行文件的选择;本文其他决策仍然适用。
 
 ## 问题
 
@@ -16,58 +18,50 @@ DeepSeek Harness 需要一个复用 Web UI 的 Electron 桌面应用。该应用
 
 ## 决策
 
-交付一个小型 Electron 壳,其中内置上游 Node.js 可执行文件和固定版本的 pnpm。Electron 把私有 Desktop Host 包作为隔离子进程启动;该包组合已安装的 dsh 后端与匹配的客户端图。Fetch 元数据及有界的原始请求与响应分块通过两条带版本的分帧字节管道传递,Node IPC 只承载就绪、致命失败和关闭,Electron 通过 `dsh-app://` 提供经过验证的资源;它不会打开监听端口。每个帧都包含固定标记、类型、单调 stream id、负载长度和经过验证的负载。串行 writer 遵守 pipe drain,请求或响应 stream 施加背压时 reader 会全局暂停,取消会关闭匹配的 stream,已退役 stream 的迟到响应帧保持无效。Connection 插件无需 `webServer` 即可提供与载体无关的 RPC 与 Fetch 注册表,Client Modules 则向 shell-owned carrier 提供与广告内容完全一致的组合 bundle 响应;Web 组合为两者挂载可选 HTTP route。渲染进程保留相同的 Fetch、RPC 与 Remote-stream 格式,子进程载体则避免 Base64 膨胀,也不依赖 Electron 与内置上游 Node.js 之间的 V8 序列化兼容性。发送 shutdown 后,Electron 会关闭自己持有的请求管道写端,以便在等待子进程退出前释放 Windows 上仍在进行的管道读取。该设计沿用 [GUI 分层与 RPC 协议 Agent Note](../../archived/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)中的 Electron 预留
+交付小型 Electron 壳和固定版本 pnpm;[运行时决策](2026-09-11-desktop-electron-node-runtime.zh.md)持有可执行文件选择。[薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)负责 Host 启动与传输:私有 Host 运行共享 Web profile runner,Electron 加载其认证 HTTP URL,子进程 IPC 承载生命周期消息
 
 Electron 拥有 `.dsh/profiles/desktop` 保留 profile。[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)负责核心资源存储、外部插件依赖、共享包链接和 profile 协调。私有 Desktop Host 保持独立于公共 CLI 包,且不会发布到 npm。
 
 一个 Desktop 发布号同时标识 Electron 产物及其精确的 `@deepseek-ai/dsh` 与 `@deepseek-ai/dsh-desktop-host` 依赖。发布不能在构建或运行时选择不同的核心版本。因此,即使壳代码没有变化,更新 dsh 也必须产生新的 Electron 发布。
 
-浏览器 Web UI、dsh 后端、现有 `dsh plugin` CLI、用户 npm 和用户 pnpm 都不能修改该 profile。CLI 保留 `desktop` 名称的所有大小写变体,并拒绝针对它的启动、配置 dump 和插件管理请求。Electron 在项目恢复或 Host 启动前获取进程生命周期单实例锁;后续启动只会聚焦或重建主窗口,不会接触 profile 状态。Electron-only GUI 通过 preload 发送结构化安装、删除和更新请求;Electron 只调用其内置 pnpm。
+Desktop Host 为保留 profile 提供共享 Web 插件管理器,并通过启动器信息提供内置 pnpm。CLI 保留 `desktop` 名称的所有大小写变体,并拒绝针对它的启动、配置 dump 和插件管理请求。Electron 在 profile 恢复或 Host 启动前获取进程生命周期单实例锁;后续启动只会聚焦或重建主窗口,不会接触 profile 状态。
 
 ## 归属
 
 | Owner | 职责 |
 |---|---|
-| Electron 壳 | 窗口与子进程生命周期、分帧字节管道、生命周期 IPC、自定义协议、保留 desktop profile、插件 GUI、更新协调 |
-| 内置 Node.js 与 pnpm | 执行 dsh 并安装桌面项目的精确依赖,不读取用户 `PATH` 或 pnpm 状态 |
+| Electron 壳 | 窗口与子进程生命周期、保留 desktop profile 准备、原生恢复、更新协调 |
+| Electron RunAsNode 与 pnpm | 执行 dsh 并安装桌面项目依赖,使用 pnpm 的正常配置 |
 | Desktop profile | 由内置运行时决策定义的外部插件依赖、已启用 bundle 顺序和共享链接 |
 | 私有 Desktop Host 包 | 与 dsh 一起安装、但不进入公共 CLI 包或 npm 发布的 Electron 专用子进程入口与组合 overlay |
 | 已安装 dsh 包 | 后端、匹配的 Web UI、启动 manifest、客户端包和产品行为 |
 | 共享 `.dsh` owner | 会话、设置、凭据、工作区和存储,由其现有锁与格式版本保护 |
 | 通过 npm 安装的 dsh | 自己的可执行安装和用户管理的 profile;不能访问保留 desktop profile 或包状态 |
 
-渲染进程使用 `nodeIntegration: false`、`contextIsolation: true` 和 `sandbox: true`。Preload 暴露类型化 RPC、生命周期、更新、locale 与桌面插件操作,而不暴露原始 `ipcRenderer`、文件系统访问、shell 命令或 pnpm 参数。Electron 根据应用 locale 选择类型化的中英文字典,并以英文作为 fallback;菜单、原生对话框与插件管理渲染进程使用这些由 locale 持有的文案。
+渲染进程使用 `nodeIntegration: false`、`contextIsolation: true` 和 `sandbox: true`。Preload 提供启动就绪与失败报告、原生目录选择、主题同步,以及 Windows 菜单和外观适配。它不暴露原始 `ipcRenderer`、文件系统访问、shell 命令或 pnpm 参数。Electron 菜单与原生对话框使用类型化中英文文案,并以英文回退;Windows 跟随主文档语言。共享 Web 插件管理器负责自身客户端文案。
 
 ## 文件系统布局
 
 ```text
 ~/.dsh/
-  desktop/
-    pnpm/
-      store/
-      cache/
-      state/
-      config/
   profiles/
     desktop/
       package.json
       pnpm-lock.yaml
       lock
-      desktop-packages-pending
       pnpm-workspace.yaml
-      desktop-runtime-state.json
       node_modules/
   sessions/
   storages/
 ```
 
-`.dsh/profiles/desktop` 是唯一活动桌面 profile。其可执行包归属及允许解析的目录遵循[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)。插件包内容使用 `.dsh/desktop/pnpm/store`
+`.dsh/profiles/desktop` 是唯一活动桌面 profile。其可执行包归属及允许解析的目录遵循[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)。pnpm 根据正常配置选择 store 和缓存
 
 ## 安装与解析
 
-Desktop 在直接修改 profile 前停止 Host。包操作失败后保留部分修改,供显式修复;profile 修改和包重试的职责遵循[直接修改决策](2026-09-09-desktop-in-place-profile.zh.md)
+共享 Web 插件管理器负责 profile 包操作。Electron 保留启动准备和原生恢复;[Web 薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)负责这一职责划分
 
-进程生命周期 Electron 锁是 Desktop 的权威 owner。包事务锁用于纵深防御,并记录仍能修改包状态的进程:包操作之间记录 Electron,pnpm 运行期间记录已生成的 pnpm PID。Owner 变更通过已经打开的排他锁文件完成截断、写入与同步。如果 Electron 在 pnpm 执行期间终止,后续进程会发现仍存活的 worker,并拒绝启动并发的 包事务;该 worker 退出后,陈旧 PID 才可以恢复
+Electron 进程生命周期锁是 Desktop 的权威所有者。profile 准备和原生恢复使用记录 Electron PID 的排他锁;存活的所有者阻止竞争操作,失效 PID 可被回收。Web 包操作使用共享插件管理器的锁
 
 核心物化、首次启动、插件安装和共享模块解析遵循[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)。实际 Host 启动时会组合已启用且提供 `dsh.client` 代码的桌面插件。
 
@@ -81,56 +75,54 @@ Electron 更新只使用一个 `electron-updater` 发布流和签名 `electron-b
 
 ## 安全与发布策略
 
-核心 dsh 和私有 Desktop Host 只来自签名应用的资源树。插件安装接受桌面策略允许的 registry 包规格,不接受原始 pnpm 命令。激活前要求精确版本、锁文件完整性、经过审查的 `allowBuilds` 集合和仅限用户访问的目录权限
+核心 dsh 和私有 Desktop Host 只来自签名应用的资源树。插件安装把包规格交给 pnpm,包括本地和远程来源,但不接受原始 pnpm 命令。pnpm 负责依赖解析和 profile 的 `allowBuilds` 策略;Host 加载已启用的 bundle
 
-Electron 发布产物必须签名;macOS 产物必须公证。发布自动化必须通过明确的环境变量提供应用 ID、macOS Developer ID 限定名、预期 Team ID 与一套完整的 notarytool 凭据。配置加载会拒绝缺失或格式错误的标识符和不完整的公证凭据,macOS 打包还会强制签名,避免证书发现过程静默选择其他已安装身份或生成未签名发布。运行时准备会验证每个内嵌 Mach-O 文件的精确 Authority 与 Team ID,以及时间戳和 hardened-runtime 标记。签名后钩子会执行 Apple 的深度严格应用验证,并要求同一叶证书 Authority 与 Team ID 完全匹配,验证通过后才继续生成产物。固定目标安装包命令使用[隔离的 App 副本并行公证](../process/2026-09-09-parallel-macos-notarization.zh.md):ZIP 包含已钉票的 App,签名 DMG 则携带覆盖其中未钉票 App 的票据。DMG 的 artifact-completion hook 要求其使用配置的身份、具备有效票据并通过 Gatekeeper。只有两条产物流都成功,命令才会移入其输出并写入发布完成记录;仅生成目录的命令仍会公证 App 并钉票。macOS 更新使用签名 ZIP,因此 DMG 不生成 blockmap;否则钉票会让已经生成的 DMG blockmap 失效。自定义协议提供已安装的前端分发目录和活跃模块图点名的客户端文件,并拒绝路径穿越或访问这些根目录之外的内容。插件安装器 API 只对 Electron 拥有的管理 GUI 可用,不存在于浏览器应用或后端 RPC 中。
+Electron 发布产物必须签名;macOS 产物必须公证。打包与上传命令从 Git 忽略的目标 `.env.windows` 或 `.env.macos` 读取发布配置,子进程通过编排器选择的环境字段接收配置。目标文件是发布字段的唯一来源,避免旧的 shell 或系统凭据覆盖本地选择;配置加载不修改父进程环境。打包在构建、下载或清理发布记录前校验该模式必需的应用 ID、更新地址、签名身份及本地文件,macOS 还要求一套完整公证凭据。单独的 `check:package` 执行同一校验而不访问 Token 或 Apple;凭据真实性仍由实际签名与公证验证。配置加载会拒绝缺失或格式错误的标识符和不完整的公证凭据,macOS 打包还会强制签名,避免证书发现过程静默选择其他已安装身份或生成未签名发布。运行时准备会验证每个内嵌 Mach-O 文件的精确 Authority 与 Team ID,以及时间戳和 hardened-runtime 标记。签名后钩子会执行 Apple 的深度严格应用验证,并要求同一叶证书 Authority 与 Team ID 完全匹配,验证通过后才继续生成产物。固定目标安装包命令使用[隔离的 App 副本并行公证](../process/2026-09-09-parallel-macos-notarization.zh.md):ZIP 包含已钉票的 App,签名 DMG 则携带覆盖其中未钉票 App 的票据。DMG 的 artifact-completion hook 要求其使用配置的身份、具备有效票据并通过 Gatekeeper。只有两条产物流都成功,命令才会移入其输出并写入发布完成记录;仅生成目录的命令仍会公证 App 并钉票。macOS 更新使用签名 ZIP,因此 DMG 不生成 blockmap;否则钉票会让已经生成的 DMG blockmap 失效。
 
 [固定版本的 osx-sign 补丁](../../../../patches/@electron__osx-sign@1.3.3.patch)在两种已发布模块构建中使用 `lstat`,因此 Framework 的文件和目录别名不会触发重复签名。选定的上游版本能够跳过这些别名前,仍需保留该补丁。PAK 文件由外层 bundle 签名记录完整性;逐个签名会增加串行时间戳请求,但不会增加资源完整性保护。Desktop 保留全部语言文件,只跳过其单独签名。可执行代码仍使用 Developer ID 签名、安全时间戳和 hardened runtime。[签名器遍历回归测试](../../../../apps/desktop/tests/macos-signing-walk.spec.ts)使用真实 Framework 别名执行已安装依赖;发布验收仍要求严格应用验证、公证和启动。
 
-Windows 发布打包通过 `/f` 向已配置且与 SafeNet 兼容的 SignTool 提供 `DSH_DESKTOP_WINDOWS_CER_FILE` 指定的公开 EV 叶证书,并通过必需的 `DSH_DESKTOP_WINDOWS_KEY_CONTAINER` 标识匹配的私钥。证书文件保留在源码仓库之外,私钥仍留在 USB Token 上。electron-builder hook 把每个产物交给采用 CRLF 的 `windows-sign.cmd`;该 CMD 只调用一次 SignTool,并指定 SafeNet `/kc "[{{PIN}}]=容器"` 值与 CSP、SHA-256 文件摘要和 DigiCert SHA-256 RFC 3161 时间戳。hook 不会改用其他 SignTool,也不会重试失败的请求。打包编排不会把任何 `DSH_DESKTOP_WINDOWS_*` 字段传给构建与 运行时准备子进程,只会把证书路径、SignTool 路径、密钥容器和 PIN 传入 electron-builder。签名器在已清理的 CMD 环境中只提供经过校验的签名字段;CMD 会禁用延迟展开,在 SignTool 启动前清除这些字段,并仅在 SignTool 必需的命令行中保留 PIN。所有对外诊断都会替换 PIN,而且只能允许专用构建账号和管理员检查该 runner。签名器会在企业 Code Integrity 检查 electron-builder 的临时 NSIS bootstrap 前先为该可执行文件签名;对于生成的可执行文件,只有证书表条目指向文件末尾之外时,才会在最终签名前清除该条目。SignTool、证书、容器、PIN、Token 或签名不可用时,打包会在产生未签名产物前失败。自定义协议提供已安装的前端分发目录和活跃模块图点名的客户端文件,并拒绝路径穿越或访问这些根目录之外的内容。插件安装器 API 只对 Electron 持有的管理 GUI 可用,不存在于浏览器应用或后端 RPC 中。
+Windows 发布打包通过 `/f` 向已配置且与 SafeNet 兼容的 SignTool 提供 `DSH_DESKTOP_WINDOWS_CER_FILE` 指定的公开 EV 叶证书,并通过必需的 `DSH_DESKTOP_WINDOWS_KEY_CONTAINER` 标识匹配的私钥。证书文件保留在源码仓库之外,私钥仍留在 USB Token 上。electron-builder hook 把每个产物交给采用 CRLF 的 `windows-sign.cmd`;该 CMD 只调用一次 SignTool,并指定 SafeNet `/kc "[{{PIN}}]=容器"` 值与 CSP、SHA-256 文件摘要和 DigiCert SHA-256 RFC 3161 时间戳。hook 不会改用其他 SignTool,也不会重试失败的请求。打包编排不会把任何 `DSH_DESKTOP_WINDOWS_*` 字段传给构建与 运行时准备子进程,只会把证书路径、SignTool 路径、密钥容器和 PIN 传入 electron-builder。签名器在已清理的 CMD 环境中只提供经过校验的签名字段;CMD 会禁用延迟展开,在 SignTool 启动前清除这些字段,并仅在 SignTool 必需的命令行中保留 PIN。所有对外诊断都会替换 PIN,而且只能允许专用构建账号和管理员检查该 runner。签名器会在企业 Code Integrity 检查 electron-builder 的临时 NSIS bootstrap 前先为该可执行文件签名;对于生成的可执行文件,只有证书表条目指向文件末尾之外时,才会在最终签名前清除该条目。SignTool、证书、容器、PIN、Token 或签名不可用时,打包会在产生未签名产物前失败。
 
 Windows 打包调用强制设置 `ELECTRON_BUILDER_7Z_FILTER=BCJ`。内置的 7-Zip 24.09 编码器会为 ARM64 PE 文件自动选择 ARM64 过滤器,但 `nsis-resources-3.4.1` 中的 NSIS 解码器会在解压时遗漏这些条目。使用实际 NSIS 插件的原生解压验证表明,自动过滤会丢失两个 `node-pty` ARM64 二进制文件,而 BCJ 可以逐字节还原二者。使用兼容的过滤器能够保留依赖内容与运行时完整性,无需删除特定架构的文件或削弱校验。
 
 本地 Windows 安装测试使用显式的 `--unsigned` 打包调用,并执行相同的构建和运行时准备。它清除证书输入,将产物隔离到 `unsigned-artifacts`,并省略更新器配置和发布完成记录。即使父进程环境请求未签名模式,常规打包命令也会显式选择签名模式。这样既能在没有 EV Token 时诊断安装问题,也能防止本地测试产物通过发布上传校验。
 
-NSIS 先解压到私有的 `7z-out` 目录,再把文件复制到应用目录。Finish 启动应用后,默认退出清理可能与后端的文件读取重叠。[安装器 hook](../../../../apps/desktop/scripts/installer.nsh) 在 `customInstall` 阶段仅删除该解压目录,早于交互和静默启动分支。它保留包归档、插件 DLL、回滚目录、寄存器和错误状态;[原生清理 smoke](../../../../apps/desktop/tests/fixtures/installer-cleanup-smoke.nsi) 检查这些约束。把清理移入安装阶段并不会减少文件系统工作,因此必须分别测量安装总耗时与点击 Finish 到窗口出现的耗时。
-
-安装器不开启直接向应用目录执行 `Nsis7z::Extract`。原生[文件占用探针](../../../../apps/desktop/tests/fixtures/installer-write-failure-smoke.nsi)会在未报错的情况下留下被占用的旧文件和新资源;暂存后执行的 `CopyFiles` 在相同替换失败时会设置错误标志。Windows 上同一份 737,557,488 字节载荷经过解压、复制和清理耗时 172.625 秒,直接解压耗时 28.031 秒,但每条路径的单次样本不足以支持放弃失败检测。计时不包括注册表修改、旧版删除及解压后的验证,也没有清空系统缓存。桌面专用载荷过滤减少需要复制的文件,同时保留安装器的替换错误处理。这种处理并不承诺完整的安装回滚。
+Windows 应用替换遵循[目录安装决策](2026-09-11-windows-directory-installation.zh.md):使用能返回失败状态的命令行工具解压到目标旁边,再在同卷内改名替换完整目录。安装器在暂存期间保留旧目录,正式替换失败时恢复旧目录。注册信息、快捷方式和签名卸载器仍由 electron-builder 持有。
 
-打包应用会忽略开发资源和项目环境变量覆盖。只有未打包的 Electron 进程可以替换 Node.js 可执行文件、pnpm 入口、dsh 资源 或活跃项目。
+打包应用会忽略开发资源和项目环境变量覆盖。只有未打包的 Electron 进程可以替换 pnpm 入口、dsh 资源 或活跃项目。
 
-在种子 store 子集之外,内置上游 Node.js 与 pnpm 预计增加约 35–50 MB 压缩体积和 120–165 MB 安装体积。分架构构建必须报告实际组件级体积增量
+分架构构建报告实际组件级压缩体积和安装体积
 
 ## 实现
 
 | 表面 | 实现 |
 |---|---|
-| 壳 | `apps/desktop` 负责 Electron 窗口、受限 preload、自定义协议、子进程生命周期、项目事务、插件 GUI、更新协调和 electron-builder 配置。 |
-| 已安装运行时 | 私有 `@deepseek-ai/dsh-desktop-host` 从活跃项目启动无端口桌面组合,并通过经过验证的分帧字节管道流式传输 API 与资源响应。 |
-| 包状态 | 内置 Node.js 执行不可变核心资源;内置 pnpm 只修改 Desktop profile 中的外部插件依赖图。 |
+| 壳 | `apps/desktop` 负责 Electron 窗口、受限 preload、自定义协议、子进程生命周期、profile 准备、原生恢复、更新协调和 electron-builder 配置。 |
+| 已安装运行时 | 私有 `@deepseek-ai/dsh-desktop-host` 调用共享 profile runner,并向 Electron 报告认证 Web URL。 |
+| 包状态 | Electron RunAsNode 执行不可变核心资源;内置 pnpm 只修改 Desktop profile 中的外部插件依赖图。 |
 | 资格验证 | macOS 打包要求已配置的公司身份与公证凭据可用,在生成清单前验证每个原生运行时文件,验证完整应用签名,并要求应用和 DMG 都完成公证且通过 Gatekeeper。Windows 打包要求已配置的公开证书、SafeNet 私钥容器、Token Password 与 SignTool,并验证生成的每个签名。更新托管、跨上一版本的已安装产物测试和各平台 GUI 录制仍是发布环境门槛。 |
 
-`dev:desktop` 会构建当前 workspace,把已构建 CLI 包、私有 Desktop Host 包及其依赖链接投影为一次性项目,使用隔离的 Harness home,打开 Main、Renderer 和 Host 调试器,并在不准备发布资源的情况下启动未打包 Electron。该模式的链接依赖图不是由 pnpm 安装的桌面项目,因此会禁用包修改。固定的 macOS arm64、macOS x64 与 Windows x64 打包命令会把同一目标传给运行时准备、dsh 准备和 electron-builder;每条命令还提供未封装安装器的变体,用于在生成安装器前验证发布路径。
+`dev:desktop` 会构建当前 workspace,把已构建 CLI 包、私有 Desktop Host 包及其依赖链接投影为一次性项目,使用隔离的 Harness home,打开 Main、Renderer 和 Host 调试器,并在不准备发布资源的情况下启动未打包 Electron。开发运行时为独立的插件 profile 提供工作区链接;插件管理和恢复使用与打包应用相同的流程。固定的 macOS arm64、macOS x64 与 Windows x64 打包命令会把同一目标传给运行时准备、dsh 准备和 electron-builder;每条命令还提供未封装安装器的变体,用于在生成安装器前验证发布路径。
 
 ## 考虑过的替代方案
 
 **使用 Electron 的 Node.js 执行 dsh。** 这可以减小包体积,但会让 dsh 耦合到 Electron 的 Node 补丁、fuse、原生 ABI、TLS 行为和进程生命周期。内置上游 Node.js 可以让 dsh 继续使用其受支持运行时。
 
-**通过 JSON IPC 以 Base64 承载 Fetch 消息体。** JSON IPC 可以只保留一种消息机制,但会膨胀每个请求与响应消息体、在两个进程中构造大字符串、在分派前缓冲完整请求,还会再次编码 RPC JSON 中已经表示为 Base64 的图片字节。原始分帧管道保留明确的带版本协议,同时不要求 Electron 与上游 Node.js 共享 V8 序列化行为
+**通过 JSON IPC 以 Base64 承载 Fetch 消息体。** 这会膨胀请求与响应消息体、在两个进程中构造大字符串,并在分派前缓冲请求。原始分帧管道避免 Base64 膨胀,但仍需维护第二套传输;[薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)选择已有的 Web HTTP 传输
 
 **把产品 Web UI 永久打包进 Electron。** 独立 UI 与后端更新需要新的版本化兼容计划。从同一个 dsh 包安装后端与 Web UI 可以保持当前发布绑定。
 
-**复用现有 CLI 或浏览器插件安装器。** 这会跨越桌面授权与发布 scope,并可能使用用户的包管理器状态。桌面包修改完全由 Electron 拥有
+**维护 Electron 专用插件安装器。** 独立页面、IPC API 和包服务会重复共享 Web 管理器的功能。共享服务使用保留 Desktop profile 和启动器提供的 pnpm,同时继续禁止 CLI 访问该 profile
 
 **让桌面 profile 使用 CLI 管理的包或插件。** 任一产品都可能改变另一产品的依赖图、Cordis 版本、插件版本或原生模块。Desktop 拒绝通过 CLI profile 回退目录解析包。
 
-**分离核心与插件解析,却不明确共享包归属。** 这会允许宿主模块重复以及不可控的 peer 回退。[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)为分离的资源和 profile 目录提供明确链接及依赖验证
+**分离核心与插件解析,却不提供共享包链接。** pnpm 未安装宿主 peer 包时,插件需要访问内置运行时。[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)通过明确链接提供 profile 缺失的包,同时保留 pnpm 安装的包
 
 **从 registry 包删除非目标 Mach-O 文件。** 架构裁剪可以节省少量 运行时空间,但包可能有意附带多个架构变体,调用方也可以观察安装后的文件集。签署每个实际携带的 Mach-O 对象,无需发明 Desktop 专属包布局就能满足公证要求。
 
 **把 Windows EV 私钥导出到 PFX 文件。** 外部提供的公开叶证书让 SignTool 构造签名,`/csp` 与 `/kc` 则定位硬件密钥。EV 私钥保持不可导出,并留在 Token 上。
 
-**提交包含凭据的签名脚本或持久保存 Token Password。** 包含凭据的 CMD 文件、`.env` 或 Windows 用户/系统环境变量都会让 Token Password 以静态形式被读取。已提交的 CMD 只包含环境变量引用,打包步骤则把密码作为 runner 临时 secret 接收
+**把凭据写进已跟踪脚本或系统环境。** 本地平台文件把配置限制在单个 checkout,并使打包输入明确。代价是凭据以明文落盘:构建账号需要限制文件访问权限,CI 必须清理临时配置,Git 与发布文件映射都必须排除真实配置。已提交的模板不含凭据;Windows CMD 只包含变量引用,签名串行执行并在首次失败后停止,文件格式不会免除 Token 的错误 PIN 计数
 
 **让 electron-builder 或通用目录同步直接发布。** 直接发布可能在所有引用产物就绪前暴露频道元数据,可能把陈旧或其他目标的文件混入发布,也无法证明已完成签名的构建仍与当前 dsh 版本一致。目标专用且经过校验的上传可以明确控制发布顺序与发布身份。
 
@@ -138,16 +130,16 @@ NSIS 先解压到私有的 `7z-out` 目录,再把文件复制到应用目录
 
 - 没有系统 Node.js 或 pnpm 的干净离线机器能够启动内置 dsh,无需安装核心依赖。
 - 签名应用记录最终运行时文件清单;每个 macOS 原生文件都具有发布 Developer ID、安全时间戳和 hardened runtime,每个 Windows 产物都具有配置的硬件 EV 签名。
-- `.dsh/profiles/desktop/node_modules` 能解析共享宿主链接和每个通过 GUI 安装的桌面插件。
-- 每个桌面 pnpm 操作都使用内置可执行文件和 `.dsh/desktop/pnpm/store`;不读取用户 `PATH`、配置、store 或 profile `node_modules`
-- Electron-only GUI 安装、删除和更新普通 npm 插件包,而不暴露原始 pnpm 参数
-- 后端与浏览器应用不能修改桌面包
+- `.dsh/profiles/desktop/node_modules` 保存由共享 Web 插件管理器管理的外部插件。
+- Desktop 包操作使用启动器提供的内置 pnpm,以及共享管理器的子进程环境与 profile 配置
+- 主应用的“插件”页面向共享 Host 服务发送结构化包操作与激活请求
+- 独立 CLI 仍不能访问保留 Desktop profile
 - npm/CLI dsh 与 Electron 绝不从对方的 `node_modules` 解析或安装插件。
 - 在产品 UI 加载前,活跃后端与 Web UI 报告相同 dsh 版本和兼容壳 API。
-- 包操作或 Host 失败后保留部分 profile 修改,并提供恢复控件;不承诺自动回滚 profile
+- 共享插件管理器负责包操作失败处理;原生恢复负责 Host 致命故障
 - 一个 Desktop 版本绑定 Electron 与 dsh;每次 dsh 更新都通过一个 Electron 更新弹窗交付,并产生一次用户可见的重启。
 - 共享 `.dsh` 数据在迁移或修改前拒绝不兼容的读取方。
-- 不打开回环监听端口,沙箱渲染进程不能访问任意文件系统或 Electron API。
+- Web 应用负责 HTTP 认证与服务;沙箱渲染进程不能访问任意文件系统或 Electron API。
 - Workspace 开发无需下载发布资源即可运行当前已构建代码,未封装安装器的应用验证仍保留生产安装路径。
 - Windows 发布打包要求已验证的 SignTool、EV Token、匹配的公开叶证书、Token Password 和明确的密钥容器,绝不会回退到未签名产物或可导出的密钥文件。
 - 目标更新只有在已完成签名的构建及其引用的每个产物通过发布校验后才能暴露新频道元数据;保留的历史产物继续供差分更新使用。
@@ -159,23 +151,23 @@ NSIS 先解压到私有的 `7z-out` 目录,再把文件复制到应用目录
 |---|---|
 | 首次启动 | 检查内置发布元数据并创建 profile 链接,不安装核心依赖 |
 | 桌面 profile | 一个由 Electron 拥有的保留 profile,保存外部插件和共享包链接 |
-| 插件管理 | Electron-only GUI 与包服务;没有 CLI、后端或浏览器安装路径 |
-| 激活 | 直接修改包后启动实际 Host |
+| 插件管理 | 共享 Web“插件”页面与 Host 服务,使用启动器提供的内置 pnpm |
+| 激活 | 启用 HMR 时由共享管理器应用配置;否则变更需要重启 |
 | 初始平台 | macOS arm64/x64 与 Windows x64;Linux 尚无受支持的发布目标 |
 | 更新行为 | 后台检查,差分下载与重启前显式确认,启动时校准 dsh |
 
 ## 风险
 
-插件生命周期脚本会执行第三方代码。在 GUI 安装功能交付前,获准 registry、包策略、精确版本、完整性、`allowBuilds` 和诊断都需要安全评审
+插件生命周期脚本会执行第三方代码。profile 的 `allowBuilds` 配置决定哪些构建获准执行
 
-更新绑定的 dsh 可能使插件 peer dependency 或原生模块失效。校准会验证变化的依赖并重建原生包;失败后需要通过恢复 UI 显式修复。
+更新绑定的 dsh 可能使插件 peer 依赖或原生模块不兼容。原生恢复可以禁用第三方 bundle 并重启应用,之后可通过共享“插件”页面修复。
 
 通过 npm 安装的 dsh 与桌面 dsh 可能在共享持久化数据时使用不同版本。每个共享 owner 都必须在读取、迁移或写入前执行格式版本与进程锁。
 
-中断的包操作保留未完成标记。已安装产物测试必须验证后续启动会重试锁定依赖的安装和获准的原生构建
+包操作失败与取消遵循共享插件管理器的还原规则。原生恢复不会重新安装包
 
 代码签名、公证和更新托管需要生产发布基础设施。只运行仓库测试不能完成这些认证。
 
 ## 相关提案
 
-[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)取代本记录的离线 运行时准备与单项目包归属决策。发布身份、签名、无端口传输、进程归属及仅限 Electron 的包授权仍由本记录负责。
+[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)负责核心资源与外部插件依赖。[薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)负责共享 Web 传输和插件管理。发布身份、签名与进程归属仍由本记录负责。

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md
-2026-09-05-sidebar-tab-types-and-navigation.md: 24332c844d13c41e4f966f39aa7d2fe79fd8553f
-2026-09-05-sidebar-tab-types-and-navigation.zh.md: 3aeb38e2ec9c997b9aaf040ae0cb575fa08fa235
+2026-09-05-sidebar-tab-types-and-navigation.md: 834a26e55f87e5db54d57f06fec592e4fdb9f61c
+2026-09-05-sidebar-tab-types-and-navigation.zh.md: 9884db9eef49162e8883c390dd71bdbf9d45489f

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

@@ -116,6 +116,5 @@ The conversation's `openFile(path, { line? })` — tool-row path links, produced
 ## Deferred
 
 - A navigation protocol beyond `sidebar://<kind>`: sub-routes within a page, naming an implementation, and the ecosystem-facing rules for other navigation schemes.
-- Parameters for the shipped page types, which today declare none.
 - Opening into a session other than the one on screen from the public face, which acts on the mounted session only; a tab's own actions already act on their tab's session.
 - A localized message when an open fails from the conversation; the failure is currently the thrown error's text.

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

@@ -116,6 +116,5 @@ interface SidebarRightTabParamsMap {}        // key: kind — a page type declar
 ## Deferred
 
 - `sidebar://<kind>` 之外的导航协议:页内子路由、点名实现、以及面向生态的其它导航 scheme 规则。
-- 随包页类型的参数,今天未声明任何。
 - 从公开面往屏上会话之外的会话里打开;公开面只作用于已挂载的会话,而 tab 自己的动作已作用于其所在会话。
 - 从会话区打开失败时的本地化提示;目前是抛错文本本身。

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.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-desktop-bundled-runtime-and-external-plugins.md
-2026-09-08-desktop-bundled-runtime-and-external-plugins.md: 581a4ad31121bfda651cb99e550487a0a712e943
-2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md: fbbb25a4a3446d52ac56dc35bb176c0b706d7cb4
+2026-09-08-desktop-bundled-runtime-and-external-plugins.md: a5f9266e0e7a8def3f0ff6fc271bddde3db30163
+2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md: 2cc9fd635102f08e6ca4afb4f086a0cf6b8b05f0

+ 18 - 16
.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.md

@@ -4,7 +4,9 @@ Status: implemented
 
 English | [中文](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)
 
-Profile mutation and recovery follow the [in-place profile decision](2026-09-09-desktop-in-place-profile.md).
+Plugin management and native recovery follow the [shared Web wrapper decision](2026-09-10-desktop-web-wrapper.md).
+
+The [Electron runtime decision](2026-09-11-desktop-electron-node-runtime.md) supersedes the separate upstream Node executable; other decisions in this note remain applicable.
 
 ## Problem
 
@@ -14,48 +16,48 @@ Separate package directories can load duplicate Cordis or service modules. Retai
 
 ## Decision
 
-[Runtime preparation](../../../../apps/desktop/scripts/prepare-dsh.ts) materializes the production graph once at build time and ships it through `extraResources/dsh`. The Electron shell stays in ASAR. A bundled upstream Node process runs the private Desktop Host from resources and loads enabled plugins from `$DSH_HOME/profiles/desktop`.
+[Runtime preparation](../../../../apps/desktop/scripts/prepare-dsh.ts) materializes the production graph once at build time and ships it through `extraResources/dsh`. The Electron shell stays in ASAR. An Electron RunAsNode process runs the private Desktop Host from resources and loads enabled plugins from `$DSH_HOME/profiles/desktop`.
 
-Desktop has not been released. This is its first installation format; there are no readers or migrations for the unpublished seed-based profile. This note supersedes core seed installation and single-project dependency ownership in the [Desktop packaging decision](2026-08-25-electron-desktop-packaging-and-updates.md). That note continues to own release identity, signing, portless transport, process ownership, and Electron-only plugin authorization. No existing note is fully superseded or archived.
+This note owns core resource storage and external plugin dependencies. The [packaging decision](2026-08-25-electron-desktop-packaging-and-updates.md) retains release identity, signing, process ownership, and Electron-only plugin authorization. The [thin-wrapper decision](2026-09-10-desktop-web-wrapper.md) owns shared Web boot and HTTP transport.
 
 ## Package ownership
 
 The resource descriptor records the exact release, Node version, platform, architecture, shared package versions, and final file hashes. The runtime tree contains ordinary files and directories, without links back to pnpm’s build store. Native Mach-O files are signed before hashing; the application signer preserves their bytes and checks the inventory after signing. An explicit `dsh/node_modules` resource mapping bypasses electron-builder’s root `node_modules` exclusion, and the copied tree is verified before any signing or notarization.
 
-The [Desktop file policy](../../../../apps/desktop/scripts/runtime-file-policy.ts) applies after production npm installation and before native signing or descriptor generation. npm publication lists serve library consumers and can include declarations, maps, tests, and native build inputs; they do not identify the files needed by the Desktop process. The Desktop copy omits declarations and recognized source maps because Host execution uses JavaScript and generated Typert artifacts, clears inherited `NODE_OPTIONS`, and does not enable source mapping. Reviewed plugin lifecycle builds cover native dependencies, not arbitrary TypeScript compilation. Published npm packages and external plugin directories retain their own files. Source debugger navigation is a development-package capability.
+The [Desktop file policy](../../../../apps/desktop/scripts/runtime-file-policy.ts) applies after production npm installation and before native signing or descriptor generation. npm publication lists serve library consumers and can include declarations, maps, tests, and native build inputs; they do not identify the files needed by the Desktop process. The Desktop copy omits declarations and recognized source maps because Host execution uses JavaScript and generated Typert artifacts. The Host inherits the user environment. Published npm packages and external plugin directories retain their own files. Source debugger navigation is a development-package capability.
 
-Package-specific exclusions remove Domino tests, fs-ext compilation outputs, Koffi's Windows import library, and non-target node-pty prebuilds and debug symbols. The policy retains native executable dependencies, node-pty's ConPTY source distribution, licenses, and unrecognized assets; broad `src`, `test`, `.ts`, or `.map` exclusions could remove executable code or runtime data. Copy tests preserve sentinel assets and seal the filtered inventory; the bundled-Node [payload smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs) verifies PTY output, native file seeking, FFI, image conversion, and HTML parsing. Runtime preparation still verifies every retained byte and boots the complete Host with an external plugin.
+Package-specific exclusions remove Domino tests, fs-ext compilation outputs, Koffi's Windows import library, and non-target node-pty prebuilds and debug symbols. The policy retains native executable dependencies, node-pty's ConPTY source distribution, licenses, and unrecognized assets; broad `src`, `test`, `.ts`, or `.map` exclusions could remove executable code or runtime data. Copy tests preserve sentinel assets and seal the filtered inventory; the Electron [payload smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs) verifies PTY output, native file seeking, FFI, image conversion, and HTML parsing. Runtime preparation still verifies every retained byte and boots the complete Host with an external plugin.
 
-Every first-party package in the dsh and private Host production closures is shared. The profile contains directory symlinks to those resource packages, or junctions on Windows. Links resolve to real host package directories under normal Node resolution. Host and plugin imports therefore share the same module instance for each resolved export. Distinct ESM and CommonJS conditional exports remain distinct entry points; a link cannot merge a package’s dual implementations.
+The shared profile runner projects missing installation and selected-bundle dependencies within the Desktop profile. pnpm-installed packages take precedence. Links resolve to real package directories under normal Node resolution, so Host and plugin imports reaching the same export share its module instance. Distinct ESM and CommonJS conditional exports remain distinct entry points; a link cannot merge a package’s dual implementations.
 
-External plugins declare shared host packages as peers. Ordinary dependencies remain plugin-owned and may differ from the versions used by dsh. Validation rejects incompatible enabled peers, nested or aliased copies of shared packages, private package links, and dependency resolution through CLI or other ancestor directories. A third-party package requiring host-wide instance identity must be explicitly added to the runtime’s shared inventory; matching version numbers alone are insufficient.
+External plugins use normal Node package resolution. Desktop does not recursively check peer versions, duplicate packages, linked packages, or ancestor dependency resolution. These checks duplicate package-manager and loader responsibilities and reject pnpm-supported installation sources. Shared fallback links supply missing packages, but a plugin can resolve another installed copy; incompatible plugins may fail during Host startup and require recovery through the independent shell UI.
 
-The profile manifest records exact installed plugin dependencies separately from its enabled bundle list. Disabling a plugin preserves its package, lockfile entry, and user configuration. The shared links are Desktop-owned derived state, recorded separately from pnpm; package-manager operations run without those links, then Desktop recreates and validates them.
+The profile manifest records pnpm-installed dependencies separately from its enabled bundle list. Disabling a plugin preserves its package, lockfile entry, and user configuration. The [thin-wrapper decision](2026-09-10-desktop-web-wrapper.md) assigns initialization, bundle reconciliation, and module links to shared app-boot helpers; Desktop holds no separate link ledger or runtime-state identity.
 
 ## Transactions and upgrades
 
-First launch creates profile metadata and host links without running pnpm, preserving unrelated files. Compatible release changes or application relocation refresh links and validate enabled peers in place. Node version, platform, or architecture changes reinstall the locked plugin graph and run approved native builds.
+First launch creates profile metadata and host links without running pnpm, preserving unrelated files. Compatible release changes or application relocation refresh links in place. Node version, platform, or architecture changes preserve installed plugins; pnpm and the loader report installation and compatibility failures.
 
 Native canonical paths identify shared package directories. Windows launchers can vary path casing without moving the application; string equality would trigger unnecessary profile preparation. Profile cleanup explicitly unlinks every nested directory link before removing real directories. A Windows fixture under Electron 44 reproduces recursive `fs.rmSync` deleting files through a nested junction, while bundled upstream Node 24.17 preserves them. Cleanup qualification therefore includes the real Electron runtime; Node-only tests do not establish target preservation.
 
-Dependency mutations install with scripts disabled, validate the plugin graph and host links, run the reviewed pending lifecycle builds, and validate again. This permits approved native dependencies to resolve host peers while preventing accidental duplicate host packages from reaching startup. The `allowBuilds` policy remains explicit; unsupported build-requiring dependencies fail the transaction.
+The shared [plugin manager](../../../../packages/boot/plugin-manager/README.md) owns supported package specifications, bundle validation, activation, and installation failure handling. The bundled pnpm reads normal user and profile settings. Development uses the same Web manager against a separate Desktop profile, with workspace packages supplied by the development runtime.
 
-Desktop stops the Host before package mutations and waits for pnpm exit before restarting it. The [in-place decision](2026-09-09-desktop-in-place-profile.md) owns partial failures and persistent retry state. Recorded host links identify owned directories independently of package-operation completion.
+The shared Web plugin manager owns package mutations and activation; Electron retains profile preparation and native recovery. The [Web wrapper decision](2026-09-10-desktop-web-wrapper.md) owns these responsibilities.
 
-The [immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md) owns direct Host startup and recovery in the main window. Users can update, remove, disable, or re-enable plugins and retry startup. Incompatible plugins are not silently deleted or automatically downgraded. Each backend launch requires the current runtime identity.
+The [immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md) owns direct Host startup. The native recovery dialog can disable third-party bundles and back up the profile patch when the Web application cannot start. Installed plugin files remain available for repair.
 
 ## Alternatives considered
 
-Full runtime verification belongs to packaging. Startup reads the descriptor, checks shared package records and required Host entries, and uses the recorded runtime identity for profile reuse. The [release-validation decision](2026-09-09-desktop-build-release-validation.md) assigns release and target compatibility checks to packaging. It neither enumerates nor hashes installed runtime files, including on first launch or after an upgrade. Reading every file before backend loading adds startup I/O proportional to the distribution size. Installed content changes therefore are not detected by a startup checksum comparison; unusable modules fail when loaded. Build-time verification still rejects changed, missing, extra, or linked files against the recorded inventory.
+Full runtime verification belongs to packaging. Startup reads the resource descriptor and checks shared package records and required Host entries. The [release-validation decision](2026-09-09-desktop-build-release-validation.md) assigns release and target compatibility checks to packaging. Startup neither enumerates nor hashes installed runtime files. Reading every file before backend loading adds I/O proportional to distribution size; unusable modules instead fail when loaded. Build-time verification rejects changed, missing, extra, or linked files against the recorded inventory.
 
 - **Install the bundled offline seed at startup.** This preserves an ordinary pnpm installation procedure but repeats core extraction and installation on every affected machine. Materialized resources remove that work at the cost of more application files and release-builder responsibility.
-- **Link all host dependencies into plugins.** This unnecessarily couples ordinary plugin dependencies to the host. Only the explicit shared inventory is linked; private packages retain independent versions.
+- **Force host dependency versions into plugins.** This unnecessarily couples ordinary plugin dependencies to the host. Shared fallback supplies missing packages while pnpm-owned entries retain independent versions.
 - **Use hardlinks.** They cannot represent directories, may not cross volumes, share writable bytes, and retain old inodes after application replacement. Directory symlinks and Windows junctions express the intended package target.
 - **Use `NODE_PATH` or preserve symlink paths.** These do not provide uniform ESM resolution or shared module identity. Normal package lookup through explicit links is directly testable.
-- **Keep core packages in ASAR.** The backend uses upstream Node rather than Electron’s patched filesystem. Ordinary `extraResources` also preserves native loading and subprocess paths.
+- **Keep core packages in ASAR.** Ordinary `extraResources` preserves native loading and subprocess paths. ASAR requires separate package-resolution qualification.
 
 ## Consequences
 
 Core package installation is absent from first launch and compatible upgrades. Metadata checks and backend loading still cost startup time; no release latency or download-size improvement is claimed without measurement. Plugin preservation is conditional on host API and native runtime compatibility, with a visible recovery path when that condition fails.
 
-The [Desktop README](../../../../apps/desktop/README.md) owns operational guidance. Focused tests cover real pnpm installation and approved builds, shared ESM instance identity, private dependency versions, relocation, disabled plugins, native rebuild selection, activation failures, and transaction locking. Signed installed-artifact upgrades, macOS notarization, Windows junction/native behavior, release size and startup benchmarks, and real-model GUI recordings remain release-environment qualification requirements; unit fixtures do not substitute for them.
+The [Desktop README](../../../../apps/desktop/README.md) owns operational guidance. Focused tests cover real pnpm installation and approved builds, shared ESM instance identity, private dependency versions, relocation, disabled plugins, runtime changes without automatic reinstalls, activation failures, and transaction locking. Signed installed-artifact upgrades, macOS notarization, Windows junction/native behavior, release size and startup benchmarks, and real-model GUI recordings remain release-environment qualification requirements; unit fixtures do not substitute for them.

+ 18 - 16
.agents/notes/implemented/architecture/2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md

@@ -4,7 +4,9 @@ Status: implemented
 
 [English](2026-09-08-desktop-bundled-runtime-and-external-plugins.md) | 中文
 
-profile 修改与恢复遵循[直接修改 profile 决策](2026-09-09-desktop-in-place-profile.zh.md)。
+插件管理和原生恢复遵循[共享 Web 薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)。
+
+[Electron 运行时决策](2026-09-11-desktop-electron-node-runtime.zh.md)替代独立上游 Node 可执行文件的选择;本文其他决策仍然适用。
 
 ## 问题
 
@@ -14,48 +16,48 @@ Desktop 初始化时安装核心依赖图,会重复发布构建器已经完成
 
 ## 决策
 
-[运行时准备](../../../../apps/desktop/scripts/prepare-dsh.ts)在构建时物化一次生产依赖图,并通过 `extraResources/dsh` 分发。Electron 壳保留在 ASAR 中。内置上游 Node 进程从资源启动私有 Desktop Host,并从 `$DSH_HOME/profiles/desktop` 加载已启用插件。
+[运行时准备](../../../../apps/desktop/scripts/prepare-dsh.ts)在构建时物化一次生产依赖图,并通过 `extraResources/dsh` 分发。Electron 壳保留在 ASAR 中。Electron RunAsNode 进程从资源启动私有 Desktop Host,并从 `$DSH_HOME/profiles/desktop` 加载已启用插件。
 
-Desktop 尚未发布。这是它的第一种安装格式;不提供未发布 seed profile 的读取器或迁移。本记录取代 [Desktop 打包决策](2026-08-25-electron-desktop-packaging-and-updates.zh.md)中的核心 seed 安装和单项目依赖归属部分。该记录继续负责发布身份、签名、无端口传输、进程归属和仅限 Electron 的插件授权。没有现有记录被完全取代或归档
+本记录负责核心资源存储与外部插件依赖。[打包决策](2026-08-25-electron-desktop-packaging-and-updates.zh.md)保留发布身份、签名、进程归属和仅限 Electron 的插件授权。[薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)负责共享 Web 启动与 HTTP 传输
 
 ## 包归属
 
 资源描述文件记录精确发布版本、Node 版本、平台、架构、共享包版本和最终文件哈希。运行时树包含普通文件和目录,不包含指回 pnpm 构建 store 的链接。原生 Mach-O 文件先签名再哈希;应用签名器保留其字节,并在签名后检查清单。明确的 `dsh/node_modules` 资源映射绕过 electron-builder 对根 `node_modules` 的排除,并在任何签名或公证前验证复制后的依赖树。
 
-[桌面文件规则](../../../../apps/desktop/scripts/runtime-file-policy.ts)在生产 npm 依赖安装之后、原生签名或描述文件生成之前执行。npm 发布列表服务于库的使用者,可以包含声明、map、测试和原生构建输入,不能直接表示桌面进程需要哪些文件。桌面副本排除声明和已识别的 source map,因为 Host 执行 JavaScript 和生成的 Typert 产物,清除继承的 `NODE_OPTIONS`,且不开启源码映射。经过审核的插件生命周期构建面向原生依赖,不执行任意 TypeScript 编译。已发布的 npm 包和外部插件目录保留各自的文件。源码调试导航由开发包提供。
+[桌面文件规则](../../../../apps/desktop/scripts/runtime-file-policy.ts)在生产 npm 依赖安装之后、原生签名或描述文件生成之前执行。npm 发布列表服务于库的使用者,可以包含声明、map、测试和原生构建输入,不能直接表示桌面进程需要哪些文件。桌面副本排除声明和已识别的 source map,因为 Host 执行 JavaScript 和生成的 Typert 产物。Host 继承用户环境。已发布的 npm 包和外部插件目录保留各自的文件。源码调试导航由开发包提供。
 
-包专用排除项包括 Domino 测试、fs-ext 编译产物、Koffi 的 Windows 导入库,以及非目标平台的 node-pty 预构建文件和调试符号。规则保留原生可执行依赖、node-pty 的 ConPTY 源分发内容、许可证和未知资源;宽泛排除 `src`、`test`、`.ts` 或 `.map` 可能移除可执行代码或运行时数据。复制测试保留哨兵资源并封存过滤后的清单;内置 Node 的[产物 smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs)验证 PTY 输出、原生文件定位、FFI、图像转换和 HTML 解析。运行时准备仍会验证每个保留字节,并携带外部插件启动完整 Host。
+包专用排除项包括 Domino 测试、fs-ext 编译产物、Koffi 的 Windows 导入库,以及非目标平台的 node-pty 预构建文件和调试符号。规则保留原生可执行依赖、node-pty 的 ConPTY 源分发内容、许可证和未知资源;宽泛排除 `src`、`test`、`.ts` 或 `.map` 可能移除可执行代码或运行时数据。复制测试保留哨兵资源并封存过滤后的清单;Electron 的[产物 smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs)验证 PTY 输出、原生文件定位、FFI、图像转换和 HTML 解析。运行时准备仍会验证每个保留字节,并携带外部插件启动完整 Host。
 
-dsh 与私有 Host 生产闭包中的每个第一方包都共享。profile 包含指向这些资源包的目录软链接,在 Windows 上使用 junction。正常 Node 解析会把链接解析到实际宿主包目录。因此,宿主与插件对每个已解析导出的导入共享同一模块实例。不同的 ESM 与 CommonJS 条件导出仍是不同入口;链接不能合并包的两套实现。
+共享 profile runner 在 Desktop profile 内补全安装包与选中 bundle 缺失的依赖。pnpm 安装的包优先。链接通过正常 Node 解析指向真实包目录,因此 Host 与插件导入同一导出时共享其模块实例。不同的 ESM 与 CommonJS 条件导出仍是不同入口;链接不能合并包的双重实现。
 
-外部插件把共享宿主包声明为 peer。普通依赖由插件拥有,可以不同于 dsh 使用的版本。验证拒绝已启用插件的不兼容 peer、共享包的嵌套或别名副本、私有包链接,以及通过 CLI 或其他祖先目录解析依赖。如果第三方包需要宿主范围的实例身份,必须明确加入运行时共享清单;版本号相同并不足够
+外部插件使用正常 Node 包解析。Desktop 不递归检查 peer 版本、重复包、链接包或上级目录依赖解析。这些检查重复包管理器及加载器的职责,并拒绝 pnpm 支持的安装来源。共享补全链接提供缺失的包,但插件可以解析到另一份已安装副本;不兼容插件可能在 Host 启动时失败,需要通过独立壳 UI 恢复
 
-profile manifest 分别记录精确的已安装插件依赖和已启用 bundle 列表。停用插件会保留其包、锁文件条目和用户配置。共享链接是 Desktop 拥有的派生状态,独立于 pnpm 记录;包管理器操作不携带这些链接,随后 Desktop 重建并验证它们
+profile manifest 分别记录 pnpm 安装的依赖及已启用 bundle 列表。禁用插件保留其包、锁文件条目及用户配置。[薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)将初始化、bundle 协调和模块链接交给共享 app-boot helper;Desktop 不保存独立链接账本或运行时状态身份
 
 ## 事务与升级
 
-首次启动创建 profile 元数据和宿主链接,不运行 pnpm,并保留无关文件。兼容的发布变化或应用移动会直接刷新链接并验证已启用的 peer。Node 版本、平台或架构变化时,会重新安装锁定的插件依赖图并运行获准的原生构建
+首次启动创建 profile 元数据和宿主链接,不运行 pnpm,并保留无关文件。兼容的发布变化或应用移动会直接刷新链接。Node 版本、平台或架构变化时保留已安装插件;安装和兼容性错误由 pnpm 与加载器报告
 
 共享包目录使用原生规范路径识别。Windows 启动器可能改变路径大小写而不移动应用;字符串相等判断会触发不必要的 profile 准备。profile 清理在移除真实目录前,显式解除每一个嵌套目录链接。Windows 夹具在 Electron 44 下复现了递归 `fs.rmSync` 沿嵌套 junction 删除目标文件,而内置上游 Node 24.17 会保留它们。因此清理验收包含真实 Electron 运行时;仅在 Node 下测试不能证明目标文件会保留。
 
-依赖修改先禁用脚本安装,验证插件依赖图和宿主链接,运行经过审查的待执行生命周期构建,再次验证。这允许已批准的原生依赖解析宿主 peer,同时阻止意外的重复宿主包进入启动过程。`allowBuilds` 策略保持明确;不受支持且需要构建的依赖会使事务失败
+共享[插件管理器](../../../../packages/boot/plugin-manager/README.zh.md)负责支持的包规格、bundle 验证、激活和安装失败处理。内置 pnpm 读取正常的用户和 profile 设置。开发模式使用相同的 Web 管理器操作独立 Desktop profile,工作区包由开发运行时提供
 
-Desktop 在包修改前停止 Host,并等待 pnpm 退出后再重启它。[直接修改决策](2026-09-09-desktop-in-place-profile.zh.md)规定部分失败和持久重试状态的处理方式。记录的宿主链接用于识别自有目录,与包操作是否完成相互独立
+共享 Web 插件管理器负责包变更和激活;Electron 保留 profile 准备和原生恢复。[Web 薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)负责这些职责
 
-[立即显示窗口决策](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)规定实际 Host 启动和主窗口恢复。用户可以更新、删除、禁用或重新启用插件并重试启动。不兼容插件不会被静默删除或自动降级。每次后端启动都要求当前运行时标识
+[立即显示窗口决策](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)负责直接启动 Host。Web 应用无法启动时,原生恢复对话框可禁用第三方 bundle 并备份 profile patch。已安装插件文件保留以供修复
 
 ## 考虑过的替代方案
 
-完整运行时验证属于打包流程。启动读取描述文件,检查共享包记录和必要的 Host 入口,并使用记录的运行时身份复用 profile。[发布验证决策](2026-09-09-desktop-build-release-validation.zh.md)把发布与目标兼容性检查交给打包流程。首次启动和升级后启动都不枚举已安装运行时文件或计算其哈希。在后端加载前读取每个文件,会增加与分发体积成正比的启动 I/O。因此,启动不会通过校验和比较检测已安装内容的变化;不可用模块在加载时失败。构建时验证仍按记录的清单拒绝内容变化、缺失、多余或链接文件。
+完整运行时验证属于打包流程。启动读取资源描述文件,并检查共享包记录及必要的 Host 入口。[发布验证决策](2026-09-09-desktop-build-release-validation.zh.md)将发布与目标兼容性检查交给打包流程。启动既不枚举已安装运行时文件,也不计算其哈希。后端加载前读取每个文件会增加与分发体积成正比的 I/O;不可用模块改由加载时失败暴露。构建时验证按记录清单拒绝内容变化、缺失、多余或链接文件。
 
 - **启动时安装内置离线 seed。** 这保留普通 pnpm 安装流程,但会在每台受影响机器上重复核心解压与安装。物化资源消除了这部分工作,代价是更多应用文件和发布构建器责任。
-- **把所有宿主依赖链接给插件。** 这会让普通插件依赖与宿主产生不必要的耦合。只链接明确的共享清单;私有包保留独立版本。
+- **强制插件使用 Host 依赖版本。** 这会让普通插件依赖不必要地耦合于 Host。共享模块补全提供缺失的包,pnpm 拥有的条目保留独立版本。
 - **使用硬链接。** 它不能表示目录,可能无法跨卷,共享可写字节,并在应用替换后保留旧 inode。目录软链接和 Windows junction 能表达预期的包目标。
 - **使用 `NODE_PATH` 或保留软链接路径。** 它们不能提供统一的 ESM 解析或共享模块身份。通过明确链接进行正常包查找可以直接测试。
-- **把核心包留在 ASAR。** 后端使用上游 Node,而不是 Electron 修改过的文件系统。普通 `extraResources` 也能保留原生加载和子进程路径。
+- **把核心包留在 ASAR。** 普通 `extraResources` 保留原生加载和子进程路径。ASAR 需要单独验证包解析。
 
 ## 影响
 
 首次启动和兼容升级不安装核心包。元数据检查和后端加载仍需要启动时间;没有测量前,不声称发布启动延迟或下载体积改善。插件保留以宿主 API 和原生运行时兼容为条件,条件不满足时提供可见的恢复入口。
 
-[Desktop README](../../../../apps/desktop/README.zh.md)负责操作说明。定向测试覆盖真实 pnpm 安装与已批准构建、共享 ESM 实例身份、私有依赖版本、应用移动、停用插件、原生重建选择、激活失败和事务锁。签名安装产物升级、macOS 公证、Windows junction 与原生行为、发布体积与启动基准,以及真实模型 GUI 录制仍是发布环境验收要求;单元夹具不能替代这些验证。
+[Desktop README](../../../../apps/desktop/README.zh.md)负责操作说明。定向测试覆盖真实 pnpm 安装与已批准构建、共享 ESM 实例身份、私有依赖版本、应用移动、停用插件、运行时变化时保留插件、激活失败和事务锁。签名安装产物升级、macOS 公证、Windows junction 与原生行为、发布体积与启动基准,以及真实模型 GUI 录制仍是发布环境验收要求;单元夹具不能替代这些验证。

+ 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: 43cc8d935763512a53379466bb796b5cac469793
-2026-09-08-document-preview-operations.zh.md: 44eccba894b3748a1d8640561a9eb760de481919
+2026-09-08-document-preview-operations.md: 980e2e443db13d6a956d554172bc765a5c11b435
+2026-09-08-document-preview-operations.zh.md: 364ef9ced6f8d98313f9bc287588c98daa408837

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

@@ -16,12 +16,16 @@ 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 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.
+[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 supported alternatives and remembers a manual choice per tab. Plain text remains available for unknown extensions and text-compatible renderers. Registered binary suffixes determine text compatibility independently of loading mode; HTML and SVG remain outside those suffixes and retain source viewing. A single candidate renders no viewer control. Registered binary suffixes omit plain text; known binary suffixes without a renderer show an unsupported state without reading the file ([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. 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.
 
+PDF.js's official TextLayerBuilder owns selection boundaries and copy normalization over the width-fitted canvas, with shared page cleanup and a component-owned resize observer. Responsive sizing uses the CSS `scale` property independently of PDF.js's page rotation and translation transforms. Its end-of-content marker and stacking rules constrain selection in blank regions; line-break highlighting is suppressed. Per-page cancellation uses the builder's cleanup rather than aborting the first page's signal, because the official selection listeners are shared across pages.
+
 ## Alternatives considered
 
+**Load converted content through callbacks in preview metadata.** A callback makes the shared file store hold both original file bytes and format-specific conversion results. Renderer-owned loading keeps conversion caches, failures, and font metadata with Office while preserving shared file identity and toolbar controls. Definitions declare `loading: 'renderer'`; the body receives a format-independent loading revision and reports only its displayed source version. Reload or implementation replacement advances the revision, stale reports are ignored, and the body cancels pending work on replacement or unmount. Office retains settled contents for the tab lifetime and composes its own PDF child slot.
+
 **Methods attached to an Iterator or its values.** This conflates observation with commands and repeats capability identity in data frames. Frames carry data and failures; explicit Preview RPC callbacks perform reads.
 
 **A core public-projection factory, or the same assembly inside `open`.** Separate stream values, operations bundles, and public interfaces add assembly without another current consumer that needs it. Preview's shared RPC adapter already keeps Session decoding and base64 out of renderers. Resource offers no provider-agnostic command interface or opening-bound command lifetime; adding either needs consumer evidence beyond file preview.

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

@@ -16,12 +16,16 @@ 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 记住手动选择;纯文本是兜底,但注册声明为二进制的后缀和 owner 的 unviewable 清单所列后缀除外([侧边栏预览打磨](../feature/2026-09-11-sidebar-document-preview-polish.zh.md))。子组件收到累积文本或完整原生字节、原始资源地址,以及标准 `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 记住手动选择。未知扩展名及文本兼容的渲染器仍可使用纯文本。文本兼容性由注册的二进制后缀决定,与加载方式无关;HTML 和 SVG 不属于二进制后缀,保留源码查看。只有一个候选时不显示查看器控件。注册声明为二进制的后缀不提供纯文本;已知二进制后缀没有注册渲染器时不发起读取,并显示不支持预览的空态([侧边栏预览打磨](../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 居中,较高的图片扩展共享滚动区的纵向范围([侧边栏预览打磨](../feature/2026-09-11-sidebar-document-preview-polish.zh.md))。渲染器不提供缩放或拖拽平移。SVG 标记绝不进入应用 DOM 或 iframe,因此脚本保持不可执行,也无法访问父页面。替换 HTML 或图片时会撤销其根 Blob URL。
 
+PDF.js 官方 TextLayerBuilder 在适配宽度的 canvas 上负责选择边界和复制规范化,共享页面清理,并使用由组件拥有的 resize observer。响应式尺寸适配使用独立的 CSS `scale` 属性,与 PDF.js 的页面旋转和平移变换组合。其内容结束标记和堆叠规则限制空白区域中的选择;换行高亮被抑制。逐页取消使用 builder 的清理操作,而不 abort 第一页的信号,因为官方选择监听器跨页面共享。
+
 ## 考虑过的替代方案
 
+**通过预览元数据中的回调加载转换内容。** 这种回调会让共享文件 store 同时持有源文件字节和格式专属的转换结果。由渲染器自行加载可将转换缓存、错误和字体元数据留在 Office,同时保留共享文件身份和工具栏控件。定义声明 `loading: 'renderer'`;正文接收与格式无关的加载 revision,只报告已展示的源版本。重新加载或替换实现会增加 revision,过期报告会被忽略,正文在替换或卸载时取消待处理工作。Office 在 tab 生命周期内保留已完成的内容,并组合自己的 PDF 子 slot。
+
 **把方法挂到 Iterator 或其值上。** 这会混淆观察与命令,并在数据帧中重复能力身份。帧携带数据和失败;显式 Preview RPC 回调负责读取。
 
 **核心公开投影工厂,或在 `open` 内做同样的组装。** 分开的流值、operations 组合与公开接口增加了组装步骤,没有另一个当前消费方需要它。Preview 的共享 RPC 适配已让渲染器无需解码 Session 和 base64。Resource 不提供与提供方无关的命令接口,也不提供绑定于打开实例的命令生命周期;增加任一种都需要文件预览之外的消费方证据。

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-08-global-main-panels.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-global-main-panels.md
-2026-09-08-global-main-panels.md: 75be68ac1bf6dceb812a5aedbca74ce58df929b3
-2026-09-08-global-main-panels.zh.md: 343f183367a5cfbd13127e32542f689c50306c89
+2026-09-08-global-main-panels.md: 63530a6b498bd8099cf627d82c888846d3d96767
+2026-09-08-global-main-panels.zh.md: 776787c3de3b79ca7948dcf949ac0393f87e7808

+ 3 - 3
.agents/notes/implemented/architecture/2026-09-08-global-main-panels.md

@@ -10,13 +10,13 @@ Plugins need application-wide views that do not belong to a Session. A Session-s
 
 ## Decision
 
-The layout declares a root-scoped keyed `main` slot. The reserved `conversation` key belongs to the Conversation plugin, whose `main.conversation` child retains optional-Session binding. Other main entries receive no implicit Session binding.
+The layout declares a root-scoped keyed `main` slot. The reserved `conversation` key belongs to Conversation. `ui-session` derives the root Session binding from `uiWorkspace`'s `mainView` ownership marker; `main.conversation` and its associated right Sidebar inherit that Provider binding. Other main entries receive no implicit Session binding. [Client Session references](2026-09-15-client-session-references.md) owns acquisition and source metadata; this note owns global panel selection.
 
-The sidebar owns the root-scoped `sidebar.panellist` list. Each list entry supplies its icon and an id matching its main entry; its string or locale-aware label provides plain visible text, the accessible name, and the collapsed tooltip. The shipped composition registers no panel entry, so the empty list has no DOM or spacing. Selection validates the live main entry and rejects a missing key without replacing the current panel.
+The sidebar owns the root-scoped `sidebar.panellist` list. Each list entry supplies its icon and an id matching its main entry; its string or locale-aware label provides plain visible text, the accessible name, and the collapsed tooltip. The shipped composition registered no panel entry when this landed, so an empty list has no DOM or spacing; the web bundle's plugin manager now registers the first one ([plugin management moves to the Web sidebar](2026-09-09-plugin-management-in-the-web-sidebar.md)). Selection validates the live main entry and rejects a missing key without replacing the current panel.
 
 One eagerly created root store is shared by the renderer and layout controller. Its `panelInfo` and `layoutInfo` objects preserve independent references. The framework supplies `usePanelInfo`; individual rows and main content subscribe to their required selection values, while AppFrame reads only layout information. The right Sidebar's root controller decides whether to mount its Session subtree and reports the resulting track requirements to the frame.
 
-`uiWorkspace.openSession(id)` selects the Session before returning the main area to the Conversation, including when the same Session is selected again. `openWorkspace` and `forkSession` use the layout's `beginNavigation()` abort signal and their own service lifetime to commit only the latest navigation. The Workspace preparation callback moves drafts synchronously only while the request remains current. Supersession prevents a late UI commit, not Session creation. Panel navigation neither cancels the retained Session nor writes a Session event.
+`uiWorkspace.openSession(target)` acquires the explicit target before replacing its main reference and returning the main area to Conversation. `openWorkspace` and `forkSession` keep the layout's existing `beginNavigation()` signal and service lifetime. `openWorkspace` runs its existing synchronous preparation after acquisition and before replacing the main reference. Supersession prevents a late UI commit, not Session creation. Direct Session opening adds no global navigation cancellation. Panel navigation neither releases the retained main Session nor writes a Session event.
 
 DOM focus is not navigation selection. Search and directory-picker controls can receive focus while the global panel and its selected sidebar row remain visible; opening a Session changes the main selection.
 

+ 3 - 3
.agents/notes/implemented/architecture/2026-09-08-global-main-panels.zh.md

@@ -10,13 +10,13 @@ Status: implemented
 
 ## 决策
 
-布局声明 root 作用域的 keyed `main` slot。保留的 `conversation` key 属于 Conversation 插件,其 `main.conversation` 子 slot 保留可选的会话绑定。其他主面板条目不获得隐式会话绑定。
+布局声明 root 作用域的 keyed `main` slot。保留的 `conversation` key 属于 Conversation。`ui-session` 根据 `uiWorkspace` 的 `mainView` 所有权标记得出根 Session binding;`main.conversation` 及其关联右 Sidebar 继承该 Provider binding。其他主面板条目不获得隐式会话绑定。[Client 会话引用](2026-09-15-client-session-references.zh.md)拥有引用获取与来源元数据;本篇拥有全局面板选择。
 
-侧栏拥有 root 作用域的 `sidebar.panellist` list。每个 list 条目提供图标,以及与主面板条目匹配的 id;字符串或随语言变化的标签提供普通可见文字、无障碍名称和折叠提示。默认组合不注册面板条目,因此空列表没有 DOM 或间距。选中操作检查实时主面板条目,对缺失的 key 报错而不替换当前面板。
+侧栏拥有 root 作用域的 `sidebar.panellist` list。每个 list 条目提供图标,以及与主面板条目匹配的 id;字符串或随语言变化的标签提供普通可见文字、无障碍名称和折叠提示。本决定落地时默认组合不注册面板条目,因此空列表没有 DOM 或间距;现在 web bundle 的插件管理器注册了第一个条目([插件管理移到 Web 侧栏](2026-09-09-plugin-management-in-the-web-sidebar.zh.md))。选中操作检查实时主面板条目,对缺失的 key 报错而不替换当前面板。
 
 渲染器与布局控制器共享一个直接创建的 root 存储。其 `panelInfo` 和 `layoutInfo` 对象保持独立的引用。框架提供 `usePanelInfo`;各行和中央内容订阅所需的选中态值,AppFrame 仅读取布局信息。右侧 Sidebar 的 root 控制器决定是否挂载其会话子树,并把最终所需的列宽报告给框架。
 
-`uiWorkspace.openSession(id)` 先选中会话,再将中央区域切回 Conversation,包括再次选中同一个会话的情况。`openWorkspace` 和 `forkSession` 使用布局的 `beginNavigation()` abort signal 与自身 service 生命周期,只提交最新导航。工作区准备回调仅在请求仍有效时同步搬移草稿。请求过期会阻止晚到的 UI 提交,但不阻止会话创建。面板导航既不取消保留的会话,也不写入会话事件。
+`uiWorkspace.openSession(target)` 先获取显式目标,再替换主引用并让中央区域返回 Conversation。`openWorkspace` 和 `forkSession` 保留布局既有的 `beginNavigation()` 信号与服务生命周期。`openWorkspace` 在获取完成后、替换主引用前执行既有的同步准备动作。请求被替代会阻止迟到的 UI 提交,不阻止会话创建。直接打开会话不增加全局导航取消。面板导航既不释放所持主会话,也不写入会话事件。
 
 DOM 焦点不是导航选中态。搜索和目录选择控件可以获得焦点,同时保留全局面板及其侧栏行的选中态;打开会话才改变中央区域的选中态。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.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-09-consumer-owned-startup-strictness.md
-2026-09-09-consumer-owned-startup-strictness.md: e009e66ead25ef0a5e6001d33663e32bc04d19d2
-2026-09-09-consumer-owned-startup-strictness.zh.md: 58c364056f5b0dc41e018cd5488be983662401d4
+2026-09-09-consumer-owned-startup-strictness.md: 2f16078c7051b4038c1d48120b500f0fcd465aef
+2026-09-09-consumer-owned-startup-strictness.zh.md: c0fe61e5f708f79f6a5b903f3157ee358c3086bc

+ 4 - 2
.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.md

@@ -28,11 +28,13 @@ This policy governs [Web host boot](2026-07-24-web-config-tree-boot-and-transpor
 
 ## Consequences
 
-Stable required entry ids are part of application assembly. Renaming one requires updating the list and its tests. Optional plugin failures remain visible in Loader state and stderr without tearing down active siblings. Required failures use the same detailed import, activation, or pending-service diagnostic before app-boot disposes the root.
+Stable required entry ids are part of application assembly. Renaming one requires updating the list and its tests. Optional plugin failures remain visible in Loader state and stderr without tearing down active siblings. Required failures combine every inactive entry into one diagnostic, separating failed plugins from pending services and marking required entries. `StartupError` retains the original failures as its cause after app-boot disposes the root. The CLI prints its message once and exits with code 1, avoiding duplicate wrapper stacks while preserving plugin stacks, nested causes, and aggregate members. The CLI saves original errors, inactive-entry metadata, and startup warning/error records in a unique report directly under `$DSH_HOME/logs/`. This preserves import errors and error properties that the concise terminal output omits. Failed writes fall back to the full report on stderr and keep exit code 1. Unrelated exceptions remain unhandled.
+
+The compact terminal report keeps the failing plugins visible; a separate file retains raw diagnostics without the default logger buffer's record limit. Raw error values remain intact, so a sharing warning accompanies the report rather than silently redacting fields. An independent exporter lifetime covers asynchronous application disposal. The CLI awaits stderr completion before explicitly exiting, because failed plugins can leave stdin or other handles open.
 
 ## Testing
 
-App-boot unit tests cover absent and disabled required ids, optional import failure, config evaluation failure, synchronous and asynchronous `apply()` failure, pending dependencies, and required failure teardown. The built Web-profile acceptance serves the full UI with optional failures and exits nonzero without readiness when the required HTTP port is occupied or `modules` or `connection` cannot activate.
+App-boot unit tests cover absent and disabled required ids, optional import failure, config evaluation failure, synchronous and asynchronous `apply()` failure, pending dependencies, and required failure teardown. Unit expectations pin diagnostic grouping, preservation of original error objects and import logs, exporter cleanup, complete diagnostic values, private file creation, concurrent report names, and failed-write fallback. The built Web-profile acceptance asserts a single port-conflict stack without Node wrapper output, verifies the saved diagnostic file and its stderr fallback, serves the full UI with optional failures and exits nonzero without readiness when the required HTTP port is occupied or `modules` or `connection` cannot activate.
 
 The [Web process matrix](../../../../apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts) independently exercises optional and required failures at startup and after native patch-file edits. Authenticated HTTP requests and plugin lifecycle files distinguish a usable application from a surviving process. These keyless process checks complement the [controlled-delivery unit tests](../testing/2026-09-09-user-patch-hmr-test-delivery.md): unit tests isolate reconciliation failures, while the process tests also require the shipped launcher, native watcher, and bounded shutdown to work together.
 

+ 4 - 2
.agents/notes/implemented/architecture/2026-09-09-consumer-owned-startup-strictness.zh.md

@@ -28,11 +28,13 @@ Required id 为 `agent-loop`、`webserver`、`modules`、`connection`、`headles
 
 ## 后果
 
-稳定的 required entry id 是应用 assembly 的一部分。重命名时必须同步更新 list 与测试。Optional plugin failure 会保留在 Loader state 和 stderr 中,但不会拆卸 active sibling。Required failure 使用相同的详细 import、activation 或 pending-service 诊断,然后由 app-boot 拆卸 root。
+稳定的 required entry id 是应用 assembly 的一部分。重命名时必须同步更新 list 与测试。Optional plugin failure 会保留在 Loader state 和 stderr 中,但不会拆卸 active sibling。Required failure 将所有 inactive entry 合并到一份诊断中,区分失败插件与等待服务的插件,并标记 required entry。App-boot 拆卸 root 后,`StartupError` 仍以 cause 保留原始失败。CLI 仅输出其消息一次,并以退出码 1 结束,避免重复的包装堆栈,同时保留插件堆栈、嵌套原因和聚合错误成员。CLI 将原始错误、未激活条目的元数据及启动警告、错误记录保存到直接位于 `$DSH_HOME/logs/` 下的唯一报告中,保留简洁终端输出省略的导入错误和错误属性。写入失败时,完整报告回退到 stderr,退出码仍为 1。其他异常继续作为未处理异常抛出。
+
+简洁的终端报告突出失败插件;单独文件保存原始诊断,不受默认 logger 缓冲区记录数限制。原始错误值保持完整,因此报告附带分享提醒,不会静默脱敏字段。独立的 exporter 生命周期覆盖应用的异步资源释放。CLI 等待 stderr 写入完成后明确退出,因为失败插件可能留下 stdin 或其他打开的句柄。
 
 ## 测试
 
-App-boot 单元测试覆盖缺失和禁用的 required id、optional import failure、config evaluation failure、同步和异步 `apply()` failure、pending dependency,以及 required failure teardown。构建后的 Web-profile acceptance 会在 optional failure 存在时继续提供完整 UI,并在 required HTTP port 被占用或 `modules`、`connection` 无法激活时以非零码退出,且不报告就绪。
+App-boot 单元测试覆盖缺失和禁用的 required id、optional import failure、config evaluation failure、同步和异步 `apply()` failure、pending dependency,以及 required failure teardown。单元预期输出固定诊断分组、原始错误对象与导入日志的保留、exporter 清理、完整诊断值、私有文件创建、并发报告命名以及写入失败回退行为。构建后的 Web-profile acceptance 断言端口冲突堆栈只输出一次且不包含 Node 包装输出,验证已保存的诊断文件及其 stderr 回退,并会在 optional failure 存在时继续提供完整 UI,并在 required HTTP port 被占用或 `modules`、`connection` 无法激活时以非零码退出,且不报告就绪。
 
 [Web 进程矩阵](../../../../apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts)分别验证启动时和原生补丁文件修改后的 optional 与 required 失败。经过认证的 HTTP 请求和插件生命周期文件区分可用应用与仅存活的进程。这些无需密钥的进程检查与[受控事件投递单元测试](../testing/2026-09-09-user-patch-hmr-test-delivery.zh.md)互补:单元测试隔离配置协调失败,进程测试还要求随附启动器、原生监听器和有界关闭流程协同工作。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.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-09-desktop-immediate-window-and-direct-start.md
-2026-09-09-desktop-immediate-window-and-direct-start.md: 1c2209c044495de1fb9d3410e5ac777759e8fa6b
-2026-09-09-desktop-immediate-window-and-direct-start.zh.md: 49b08e4eeea2703c9bcf868d7b290d56eba79542
+2026-09-09-desktop-immediate-window-and-direct-start.md: e7ccf9b7456213d69795a40d9cd2a8509adfc44e
+2026-09-09-desktop-immediate-window-and-direct-start.zh.md: c59cc91f0013293cbaf2c086822085000dd3afd2

+ 6 - 6
.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.md

@@ -4,7 +4,7 @@ Status: implemented
 
 English | [中文](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)
 
-Profile mutation and recovery follow the [in-place profile decision](2026-09-09-desktop-in-place-profile.md).
+Plugin management and native recovery follow the [shared Web wrapper decision](2026-09-10-desktop-web-wrapper.md).
 
 ## Problem
 
@@ -12,13 +12,13 @@ Waiting for backend readiness leaves users without a window during profile prepa
 
 ## Decision
 
-Electron creates the main window with a local loading page before profile reconciliation or Host startup. The page depends only on packaged shell assets and receives starting, ready, or error state through the owned preload. Readiness loads the product UI in that window; startup failures display diagnostics and available recovery actions. Closing during loading cancels further startup work and waits for the pending child to exit.
+Electron creates the main window with the packaged Web loading page before profile reconciliation or Host startup. The Web entry draws its boot page before awaiting Host readiness. The owned preload delivers structured boot injections, and the existing document activates its client plugins after they are applied; startup failures display diagnostics and available recovery actions. Closing during loading cancels further startup work and waits for the pending child to exit.
 
-The main window owns recovery because the failed Host cannot supply its own controls. Error pages retain diagnostics, restart, and reinstallation guidance. Disabling plugins and resetting Desktop are available only in a packaged application with loaded runtime metadata and available resources. Reset removes all profile contents except its held lock, without a backup; shared product data and the Harness-home environment file remain intact. The profile directory remains in place so another transaction cannot acquire a replacement lock during cleanup. Self-contained recovery controls use intercepted form navigation when preload is unavailable. A crashed renderer invalidates the navigation cache so the startup page loads again.
+Fatal presentation follows [native Desktop recovery](2026-09-15-desktop-native-fatal-recovery.md). Window timing, direct Host startup, and shutdown ownership remain governed here.
 
-Desktop starts the actual Host after preparing the profile in place, without booting a separate health-check backend. Package mutations retain dependency validation, approved lifecycle builds, runtime identity checks, and locking. Failures retain partial changes for explicit repair; there is no automatic profile rollback.
+Desktop starts the actual Host through the [shared Web runner](2026-09-10-desktop-web-wrapper.md) after preparing the profile in place. Readiness supplies the authenticated Host URL and boot injections. The shell exchanges the URL for a Host cookie, forwards application HTTP requests, and authenticates direct WebSocket requests only for the owned application origin. This carrier adaptation preserves Web route and stream semantics while allowing static HTML to appear before the Host.
 
-This partially supersedes staged backend probes and waiting to create the main window in the [packaging decision](2026-08-25-electron-desktop-packaging-and-updates.md) and [bundled-runtime decision](2026-09-08-desktop-bundled-runtime-and-external-plugins.md). Those notes retain release, signing, transport, resource ownership, and dependency-transaction rationale. Full runtime file verification remains a packaging operation.
+This partially supersedes staged backend probes and waiting to create the main window in the [packaging decision](2026-08-25-electron-desktop-packaging-and-updates.md) and [bundled-runtime decision](2026-09-08-desktop-bundled-runtime-and-external-plugins.md). Those notes retain release, signing, transport, and resource ownership rationale. Full runtime file verification remains a packaging operation.
 
 ## Alternatives considered
 
@@ -30,4 +30,4 @@ This partially supersedes staged backend probes and waiting to create the main w
 
 Users can see startup progress and recover from failures before the product UI is available. A responsive window does not imply that the backend is ready, and startup latency still requires installed-artifact measurement. Profile changes remain in place after activation fails.
 
-Verification covers a delayed Host with a visible loading page, one serving startup for a fresh profile, failure and retry in the same window, plugin management during recovery, and closing while a child is starting. Installed GUI evidence complements lifecycle and transaction tests.
+Verification covers a delayed Host with a visible loading page, one serving startup for a fresh profile, failure and retry in the same window, native recovery, and closing while a child is starting. Installed GUI evidence complements lifecycle tests.

+ 6 - 6
.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.zh.md

@@ -4,7 +4,7 @@ Status: implemented
 
 [English](2026-09-09-desktop-immediate-window-and-direct-start.md) | 中文
 
-profile 修改与恢复遵循[直接修改 profile 决策](2026-09-09-desktop-in-place-profile.zh.md)。
+插件管理和原生恢复遵循[共享 Web 薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)。
 
 ## 问题
 
@@ -12,13 +12,13 @@ profile 修改与恢复遵循[直接修改 profile 决策](2026-09-09-desktop-in
 
 ## 决策
 
-Electron 在 profile 校准或 Host 启动前创建带本地加载页的主窗口。该页面仅依赖已打包的壳资源,并通过自有 preload 接收 starting、ready 或 error 状态。就绪后在同一窗口加载产品 UI;启动失败时显示诊断和可用恢复操作。加载期间关闭窗口会取消后续启动工作,并等待正在启动的子进程退出。
+Electron 在 profile 校准或 Host 启动前创建带打包 Web 加载页的主窗口。Web 入口先显示启动页,再等待 Host 就绪。自有 preload 交付结构化启动注入,现有文档应用注入后激活客户端插件;启动失败时显示诊断和可用恢复操作。加载期间关闭窗口会取消后续启动工作,并等待正在启动的子进程退出。
 
-主窗口提供恢复操作,因为失败的 Host 无法提供自身控件。错误页保留诊断、重启和重装指导。只有已打包应用加载了运行时元数据且资源可用时,才提供禁用插件和重置 Desktop。重置会删除 profile 中除所持锁文件外的所有内容,不保留备份;共享产品数据和 Harness-home 环境文件保持完整。profile 目录保持原位,避免清理期间另一事务获取替代锁。preload 不可用时,独立恢复控件使用被拦截的表单导航。渲染进程崩溃会使导航缓存失效,以重新加载启动页
+致命错误展示遵循[原生 Desktop 恢复](2026-09-15-desktop-native-fatal-recovery.zh.md)。窗口展示时机、直接启动 Host 和关闭所有权仍由本文规定
 
-Desktop 直接准备 profile 后启动实际 Host,不另行启动健康检查后端。包修改保留依赖验证、获准生命周期构建、运行时标识检查和锁。失败后保留部分修改,供显式修复;不自动回滚 profile
+Desktop 原位准备 profile 后,通过[共享 Web runner](2026-09-10-desktop-web-wrapper.zh.md)启动实际 Host。就绪信息提供认证后的 Host URL 和启动注入。桌面壳用该 URL 换取 Host cookie,转发应用 HTTP 请求,并仅为所属应用源认证直接 WebSocket 请求。此承载适配保留 Web 路由和流语义,同时允许静态 HTML 在 Host 就绪前显示
 
-本决策部分取代[打包决策](2026-08-25-electron-desktop-packaging-and-updates.zh.md)和[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)中的 staging 后端探针与延迟创建主窗口。这两份记录仍保留发布、签名、传输、资源归属与依赖事务的理由。完整运行时文件验证仍属于打包操作。
+本决策部分取代[打包决策](2026-08-25-electron-desktop-packaging-and-updates.zh.md)和[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)中的 staging 后端探针与延迟创建主窗口。这两份记录仍保留发布、签名、传输与资源归属的理由。完整运行时文件验证仍属于打包操作。
 
 ## 考虑过的替代方案
 
@@ -30,4 +30,4 @@ Desktop 直接准备 profile 后启动实际 Host,不另行启动健康检查
 
 用户可以在产品 UI 可用前看到启动进度并从失败中恢复。窗口能够响应不代表后端已经就绪,启动延迟仍需通过已安装产物测量。激活失败后,profile 修改保留在原位。
 
-验证覆盖 Host 延迟时可见的加载页、新 profile 只启动一次服务进程、同一窗口中的失败与重试、恢复期间的插件管理,以及子进程正在启动时关闭应用。安装后 GUI 证据补充生命周期与事务测试。
+验证覆盖 Host 延迟时可见的加载页、新 profile 仅启动一次服务、同一窗口中的失败与重试、原生恢复,以及子进程启动时关闭。安装后 GUI 证据补充生命周期测试。

+ 0 - 27
.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.md

@@ -1,27 +0,0 @@
-# Agent Note: Modify the Desktop profile in place
-
-Status: implemented
-
-English | [中文](2026-09-09-desktop-in-place-profile.zh.md)
-
-## Problem
-
-Staging preserves an old plugin installation but adds profile copying, directory moves, a recovery journal, and rollback state. Local plugin changes accept explicit repair after failure instead of this complexity.
-
-## Decision
-
-Desktop stops the Host and modifies the current profile directly. Shared host links are detached for package changes and restored when the operation settles. Package locking, dependency validation, and approved native builds remain. Compatible upgrades refresh links without copying plugin files.
-
-Package or Host failures retain partial changes for repair and retry. There is no staging profile, activation journal, directory-swap recovery, or automatic rollback. Existing scratch directories are not interpreted or deleted.
-
-This supersedes staging and rollback in [2026-08-25-electron-desktop-packaging-and-updates](2026-08-25-electron-desktop-packaging-and-updates.md), [2026-09-08-desktop-bundled-runtime-and-external-plugins](2026-09-08-desktop-bundled-runtime-and-external-plugins.md), [2026-09-09-desktop-immediate-window-and-direct-start](2026-09-09-desktop-immediate-window-and-direct-start.md). Other release, module-identity, and window-lifecycle decisions remain active.
-
-A persistent `desktop-packages-pending` marker precedes package writes or native-runtime rebuilding and is removed only after installation, approved builds, and validation succeed. A later launch with that marker reinstalls the locked graph and retries pending builds even when recorded runtime metadata already matches. Ordinary unchanged startups reuse the profile without scanning the plugin dependency graph; package mutations and runtime reconciliation retain validation.
-
-## Alternatives considered
-
-Staging protects the previous installation at the cost of copying and crash recovery. Versioned directories still need preparation, selection, and cleanup. Direct writes give up automatic recovery; reintroduction requires an unattended-recovery product requirement that justifies these costs.
-
-## Consequences
-
-Tests cover offline initialization, in-place upgrades, failure before writes, retained changes after Host failure, partial pnpm failure, restored host links, and exclusive package ownership. Signed application and GUI acceptance remain release-environment checks.

+ 0 - 27
.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.zh.md

@@ -1,27 +0,0 @@
-# Agent Note: 直接修改 Desktop profile
-
-Status: implemented
-
-[English](2026-09-09-desktop-in-place-profile.md) | 中文
-
-## 问题
-
-staging 能保留旧插件安装,但增加 profile 复制、目录移动、恢复日志和回滚状态。本地插件变更接受失败后显式修复,以避免这些复杂度。
-
-## 决策
-
-Desktop 停止 Host 后直接修改当前 profile。修改包前解除宿主共享链接,操作结束后恢复链接。保留包锁、依赖验证和已批准的原生构建。兼容升级只刷新链接,不复制插件文件。
-
-包操作或 Host 失败会保留部分修改,供修复和重试。不使用 staging profile、激活日志、目录切换恢复或自动回滚。已有临时目录不会被解释或删除。
-
-本决策取代以下记录中的 staging 和回滚:[2026-08-25-electron-desktop-packaging-and-updates](2026-08-25-electron-desktop-packaging-and-updates.zh.md), [2026-09-08-desktop-bundled-runtime-and-external-plugins](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md), [2026-09-09-desktop-immediate-window-and-direct-start](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)。其他发布、模块实例和窗口生命周期决策继续有效。
-
-持久的 `desktop-packages-pending` 标记先于包写入或原生运行时重建,仅在安装、获准构建和验证成功后删除。后续启动发现该标记时,会重新安装锁定的依赖图并重试待执行构建,即使记录的运行时元数据已经匹配。普通未变化的启动复用 profile,不扫描插件依赖图;包修改和运行时校准保留验证。
-
-## 考虑过的替代方案
-
-staging 以复制和崩溃恢复为代价保护旧安装。版本化目录仍需要准备、选择和清理。直接写入放弃自动恢复;只有无人值守恢复的产品要求能证明这些成本合理时,才重新引入。
-
-## 后果
-
-测试覆盖离线初始化、原地升级、写入前失败、Host 失败后保留修改、pnpm 部分失败、宿主链接恢复及独占包操作。签名应用和 GUI 验收仍由发布环境负责。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.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-plugin-management-in-the-web-sidebar.md
+2026-09-09-plugin-management-in-the-web-sidebar.md: d2a3329e7c878f25ad5e50d98fc2e3ab39722887
+2026-09-09-plugin-management-in-the-web-sidebar.zh.md: aae035b3a93aec72e84e2792334c42c54d2bca62

+ 31 - 0
.agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.md

@@ -0,0 +1,31 @@
+# Agent Note: Plugin management moves to the Web sidebar
+
+Status: implemented
+
+English | [中文](2026-09-09-plugin-management-in-the-web-sidebar.zh.md)
+
+## Problem
+
+Installed packages belong to the running profile, while Settings is a modal over a Session. The management page needs room for package details and installation output. The layout's [global main panels](2026-09-08-global-main-panels.md) provide that lifetime and space.
+
+## Decision
+
+**Management is a sidebar entry; configuration stays in Settings.** `ui-plugin-manager` registers a `sidebar.panellist` entry and the `main` panel it opens under `plugins`. The page manages the profile's bundles and their rows through the [plugin manager](2026-09-14-current-profile-plugin-management.md) Remote, displays install output and confirms uninstalls. The page lists installed bundles only. The Settings Plugins section keeps the global configuration cards beside the read-only Plugin list tab, where the installation's own bundles (`dsh-base`, `dsh-web-app`) are inspected; both of that tab's groups start collapsed, and the tab carries no management controls of its own.
+
+**One store follows Host state.** The manager controller joins `listBundles` with `listPlugins` into one view per bundle, decides availability from the inventory's `managementAvailable`, refreshes after management operations, on `plugin-manager/changed`, and on reconnect, and keeps installation progress under the owning job. Configuration cards use the existing global settings bindings.
+
+**Installation results belong to a request.** The dialog generates a fresh request id for every install or retry and filters Host progress, logs, and responses by that id. A cancellation acknowledgement can arrive before the original add response, so that response cannot settle a subsequent retry. Cancellation uses the manager's explicit cleanup acknowledgement; local RPC cancellation and connection loss never imply that pnpm has stopped. The application phase closes the cancellation window.
+
+## Alternatives considered
+
+**A settings section that opens the management page.** Rejected: the dialog covers the main column, so such an entry would have to close Settings to show the page.
+
+**Configuration on the plugin's page.** Rejected for now, for the reasons in the decision; it becomes a link from the plugin's page once Settings can be opened on one section.
+
+## Consequences
+
+The web bundle's panel list is no longer empty: the **Plugins** entry sits between New Session and the workspaces. The Settings Plugins section keeps two tabs: the configuration page and the read-only Plugin list. `apps/web/tests/plugin-manager.e2e.ts` reaches the manager through the sidebar, and the `plugin-config` and `settings-chrome` scenarios and goldens follow.
+
+## Testing
+
+`packages/client/ui-plugin-manager/tests` pin the two registrations under one id and the page's rendering; `packages/client/ui-settings-plugins/tests` the tab-less single contribution; the web e2e scenarios above drive the panel and the section over a scaffold.

+ 31 - 0
.agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.zh.md

@@ -0,0 +1,31 @@
+# Agent Note:插件管理移到 Web 侧栏
+
+Status: implemented
+
+[English](2026-09-09-plugin-management-in-the-web-sidebar.md) | 中文
+
+## 问题
+
+已安装的包属于运行中的 profile,而设置是覆盖在 Session 上的弹窗。管理页面需要容纳包详情与安装输出。布局的[全局主面板](2026-09-08-global-main-panels.zh.md)提供对应的生命周期与空间。
+
+## 决定
+
+**管理位于侧栏,配置保留在设置中。** `ui-plugin-manager` 在 `plugins` 下注册 `sidebar.panellist` 入口与它打开的 `main` 面板。页面通过[插件管理器](2026-09-14-current-profile-plugin-management.zh.md)的 Remote 管理 profile 的组合包及其行、展示安装输出,并确认卸载。页面只列出已安装的组合包。设置的插件分区保留全局配置卡片,旁边是只读的「插件列表」标签页,随安装提供的组合包(`dsh-base`、`dsh-web-app`)在那里查看;该标签页的两个分组默认收起,且不带任何管理控件。
+
+**一个 store 跟随 Host 状态。** 管理器控制器把 `listBundles` 与 `listPlugins` 合成每个组合包一份视图,按清单的 `managementAvailable` 判定可用性,在管理操作后、收到 `plugin-manager/changed` 时以及重连后刷新,并按所属 job 保存安装进度。配置卡片使用原有全局 settings 绑定。
+
+**安装结果属于一次请求。** 每次安装或重试都生成新的请求 ID,对话框按该 ID 筛选 Host 进度、日志和返回结果。取消确认可能先于原 add 响应到达,因此旧响应不能结束随后的重试。取消采用管理器的明确清理确认;本地 RPC 取消和断线均不表示 pnpm 已停止。进入配置应用阶段后关闭取消窗口。
+
+## 考虑过的替代方案
+
+**用一个设置分区打开管理页。** 否决:对话框盖住主列,这样的入口必须先关掉设置才能显示页面。
+
+**把配置放在插件页面上。** 暂不采纳,理由见决定;等设置能按分区打开后,它会变成插件页面上的一个链接。
+
+## 后果
+
+web bundle 的面板列表不再为空:**插件**入口位于新建会话与工作区之间。设置的「插件」分区保留两个标签页:配置页与只读的「插件列表」。`apps/web/tests/plugin-manager.e2e.ts` 经侧栏到达管理器,`plugin-config` 与 `settings-chrome` 的场景与 golden 随之更新。
+
+## 测试
+
+`packages/client/ui-plugin-manager/tests` 钉住同一 id 下的两处注册与页面的渲染;`packages/client/ui-settings-plugins/tests` 钉住没有标签条的单一贡献;上述 web e2e 场景在脚手架上驱动面板与分区。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-10-component-factories-and-local-slots.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-10-component-factories-and-local-slots.md
+2026-09-10-component-factories-and-local-slots.md: 801e77644d9d2c80c4a9f3cad263420d1deda163
+2026-09-10-component-factories-and-local-slots.zh.md: 3ccd5f01633a1aa4ae830e1ee41243f5d1b65cc5

+ 105 - 0
.agents/notes/implemented/architecture/2026-09-10-component-factories-and-local-slots.md

@@ -0,0 +1,105 @@
+# Agent Note: Reusable component factories with local slots
+
+Status: implemented
+
+English | [中文](2026-09-10-component-factories-and-local-slots.zh.md)
+
+## Problem
+
+The browser Slot system starts with a parent-owned extension position. A parent entry declares a child through `children`, and unrelated plugins may then register implementations into that position. The declaration fixes the child Slot's kind, scope, render authority, and lifetime.
+
+A reusable component assembly has the opposite ownership direction. One package defines the assembly, unrelated parents render independent occurrences, and each parent may choose a different Component for a named internal region. An ordinary Slot cannot represent this relationship because its definition belongs to one parent position in the global Slot tree.
+
+The defining and consuming packages compile independently. TypeScript cannot infer a selected Component's props from a runtime registration in another package, and a separately maintained flattened props type would duplicate the store, injection, locale, child-render, and scope declarations.
+
+## Decision
+
+`ui-slots` and `ui-renderer` provide named Component Factories beside the [ordinary Slot system](2026-07-22-slot-type-chain-implementation.md). `registerFactory()` installs one reusable definition, `renderFactorySlot()` renders an occurrence, and the definition reads caller-selected local Components through `useFactorySlot()`.
+
+### Opposite registration directions
+
+Ordinary Slots and Component Factories retain distinct ownership models.
+
+| Property | Ordinary Slot | Component Factory |
+|---|---|---|
+| First declaration | Parent declares a child Slot | Definition owner declares the Factory |
+| Later operation | Child registers into the parent position | Parent renders an occurrence |
+| Static authority | `SlotMap` describes the position | `SlotFactoryMap` describes the complete definition |
+| Live definitions | Multiple entries may occupy cells | One definition owns one Factory name |
+| Parent input | `renderSlot()` owner and keyed props | `renderFactorySlot()` occurrence props |
+| Parent-selected Component | Registry routing selects entries | Caller selects one Component per local slot |
+| Descendant extension points | Entry-owned ordinary `children` | Definition-owned ordinary `children` |
+
+`SlotFactoryMap` declaration-merges the complete static definition:
+
+```text
+interface SlotFactoryDef {
+  scope: SlotScope
+  props?: object
+  children?: ChildrenDecl
+  store?: StoreDecl
+  inject?: object
+  locale?: keyof LocaleNamespaceMap & string
+  slots?: Record<string, { scope: SlotScope; props?: object }>
+}
+```
+
+The map is the only type authority. `registerFactory()` checks the runtime definition and main Component against its map entry. Store declarations normalize through `HandleOf`, so registration accepts one shared handle or one factory that produces that handle, never a nested factory. `FactoryComponentPropsOf<F>` and `FactoryLocalComponentPropsOf<F, N>` derive the complete Component props from the same entry; definition owners and consumers do not restate a flattened shared type.
+
+### Definition and occurrence lifecycle
+
+A Factory name has one live definition in a registry ledger separate from ordinary Slot cells. Registration follows the caller's Cordis effect. Disposal removes the definition, collapses its ordinary child declarations, notifies mounted outlets, and invalidates retained child-render or local-Component authority.
+
+Each `renderFactorySlot()` call creates an occurrence whose identity follows its React position and `key`. Definition replacement remounts occurrences. Definition Components and their fallback local Components report failures against the definition, while caller-selected local Components report against the caller registration; nested Factory renders preserve the same owner. All failures remain contained to that occurrence without retiring the shared definition. Assembly errors propagate, while ownership and stale-authorization failures report like ordinary component failures. The outer error boundary resets with the Factory scope incarnation, and each local boundary resets with its local slot's scope incarnation.
+
+Factory scope follows the scope binding at the occurrence's React position. `renderFactorySlot()` accepts no Session id or scope target. A strict `session` Factory requires a current binding and remounts when its identity changes; a `session-maybe` Factory preserves its first empty-to-Session adoption and remounts on later identity changes, matching ordinary Slot behavior.
+
+A shared store handle keeps the ordinary handle-by-scope behavior, including requiring a Session binding for either non-root scope. An exclusive store factory creates one handle when an occurrence first materializes and rejects that handle if it declares `spec.persist`, because registration must remain lazy and independently live occurrences cannot share one persistence key safely. Render-time records stay weak until an idempotent effect setup retains the committed occurrence; effect cleanup removes the strong mounted reference, while the occurrence-keyed WeakMap preserves identity during React effect replay and permits collection after unmount.
+
+### Local slots and ordinary children
+
+The caller may select one Component for each name in the Factory's `slots` declaration. The Factory calls `useFactorySlot(name, fallback)` and receives an identity-stable bound Component that accepts only that local slot's occurrence props. The renderer supplies the Factory's store, injection, locale, child renderers, and the local slot's own standard scope props when the bound Component renders.
+
+Local slots have no list, keyed, or chain routing and no independent registration lifetime. Multi-contributor extension points remain ordinary child Slots declared by the Factory. Those child declarations are global to the definition, while every occurrence renders their registered entries under its inherited scope.
+
+Live inspection represents each definition as a `type: 'factory'` node and nests its ordinary child Slots beneath it. Ordinary nodes retain `type: 'slot'` and their existing `kind`; callers select a Factory root with `factory:<name>`.
+
+Every renderer-created Component receives `renderFactorySlot`, so a Factory occurrence needs no parent-side use declaration. Local selection remains per occurrence and does not introduce a runtime value import from the defining package.
+
+### First shipped use
+
+`ui-conversation` registers the optional-Session `conversation.content` Factory around the shared body and Composer. Its strict-Session `views` local position defaults to an adapter that renders the existing `conversation.session` Slot; another occurrence can select a different view Component without mounting the main Conversation Header.
+
+The Factory does not own the Conversation store. The ordinary `conversation.session` body and `conversation.session.header` retain one shared strict-Session handle, preserving draft and View-selection identity without mounting that handle under both `session` and `session-maybe` scopes.
+
+### Type and runtime enforcement
+
+The type chain rejects unknown Factory names, missing or extra occurrence props, child specs that disagree with `SlotMap`, definition fields that disagree with `SlotFactoryMap`, nested store factories, unknown local names, incompatible selected Components, and overlapping ownership among input, registration, injection, and scope props.
+
+Runtime checks cover dynamically assembled and plain-JavaScript callers: duplicate definitions, child-declaration conflicts, undeclared local names, recursive rendering, prop collisions, stale authority, strict-scope absence, and component failure isolation. Type and runtime tests also pin independent stores per occurrence, shared handles by scope, fallback behavior before registration, definition replacement, local scope projection, and ordinary child rendering.
+
+## Alternatives considered
+
+**Reuse an ordinary Slot as a portable definition.** An ordinary Slot belongs to one parent declaration and one position in the global tree. Reusing it elsewhere would borrow the wrong ownership and lifetime.
+
+**Move the main Conversation Header into the Factory.** Only the main host renders that Header. Keeping it outside the Factory lets embedded occurrences omit it without another local selection and preserves its existing strict-Session Slot lifecycle.
+
+**Move the shared Conversation store onto the optional-Session Factory.** The Header and Session body share one strict-Session handle. Mounting that handle on both the `session-maybe` Factory and the `session` Header would violate the one-handle-one-scope rule.
+
+**Register the same assembly under every parent.** Separate registrations would duplicate the definition and its child declarations. Global contributors would need parallel child names or conflicting declarations.
+
+**Maintain a flattened shared props type.** This repeats facts already present in `children`, `store`, `inject`, `locale`, and scope fields, allowing the declaration and Component props to drift.
+
+**Pass React nodes or render callbacks as business props.** Those values bypass renderer-supplied scope props, store and injection assembly, stale-authority checks, and local Component type checking.
+
+**Give local slots ordinary Slot routing.** Ordinary Slots already own multi-contributor routing. A local slot represents one caller choice for one occurrence.
+
+**Pass Session identity to `renderFactorySlot()`.** The occurrence inherits its render-position scope like an ordinary Slot. A second identity argument would create two authorities that can disagree; independently addressed Session providers are a separate capability.
+
+## Consequences
+
+Feature packages can publish one reusable UI assembly without runtime-importing its Component from consumers. Each occurrence gets independently selected local Components and exclusive state while preserving global ordinary child contributions and the existing scope, locale, injection, and store rules.
+
+The additional registry ledger and occurrence bookkeeping increase renderer complexity. Factory definitions must be globally unique, local slots intentionally support only one selected Component, and the API does not itself create an independently addressed Session scope.
+
+The Factory type and runtime tests are the executable compatibility record. The Slots subsystem reference and the `ui-slots` and `ui-renderer` package references document the consumer API.

+ 105 - 0
.agents/notes/implemented/architecture/2026-09-10-component-factories-and-local-slots.zh.md

@@ -0,0 +1,105 @@
+# Agent Note: 带局部 slot 的可复用组件 Factory
+
+Status: implemented
+
+[English](2026-09-10-component-factories-and-local-slots.md) | 中文
+
+## 问题
+
+浏览器 Slot 系统从 parent-owned 扩展位置开始。parent entry 通过 `children` 声明 child,之后互不相关的插件可以向该位置注册实现。声明固定 child Slot 的 kind、scope、渲染权限与生命周期。
+
+可复用组件装配采用相反的所有权方向。一个包定义装配,互不相关的 parents 渲染独立 occurrence,并且每个 parent 可以为具名内部区域选择不同 Component。普通 Slot 无法表示这种关系,因为它的 definition 属于全局 Slot 树中的一个 parent 位置。
+
+定义包与消费包独立编译。TypeScript 无法从另一个包中的运行时 registration 推导所选 Component 的 props,另行维护的扁平 props 类型则会重复 store、injection、locale、child-render 与 scope 声明。
+
+## 决策
+
+`ui-slots` 与 `ui-renderer` 在[普通 Slot 体系](2026-07-22-slot-type-chain-implementation.zh.md)之外提供具名 Component Factory。`registerFactory()` 安装一个可复用 definition,`renderFactorySlot()` 渲染一个 occurrence,definition 通过 `useFactorySlot()` 读取调用方选择的局部 Component。
+
+### 相反的注册方向
+
+普通 Slot 与 Component Factory 保留不同的所有权模型。
+
+| 属性 | 普通 Slot | Component Factory |
+|---|---|---|
+| 首个声明 | Parent 声明 child Slot | Definition owner 声明 Factory |
+| 后续操作 | Child 注册进 parent 位置 | Parent 渲染 occurrence |
+| 静态权威 | `SlotMap` 描述位置 | `SlotFactoryMap` 描述完整 definition |
+| 有效 definitions | 多个 entries 可以占据 cells | 一个 definition 独占一个 Factory 名 |
+| Parent 输入 | `renderSlot()` owner 与 keyed props | `renderFactorySlot()` occurrence props |
+| Parent 选择的 Component | Registry routing 选择 entries | 调用方为每个局部 slot 选择一个 Component |
+| 后代扩展点 | Entry-owned 普通 `children` | Definition-owned 普通 `children` |
+
+`SlotFactoryMap` 通过声明合并给出完整静态 definition:
+
+```text
+interface SlotFactoryDef {
+  scope: SlotScope
+  props?: object
+  children?: ChildrenDecl
+  store?: StoreDecl
+  inject?: object
+  locale?: keyof LocaleNamespaceMap & string
+  slots?: Record<string, { scope: SlotScope; props?: object }>
+}
+```
+
+该 map 是唯一类型权威。`registerFactory()` 根据对应 map 条目检查运行时 definition 与主 Component。Store 声明通过 `HandleOf` 规范化,因此 registration 只接受一个共享 handle 或一个生成该 handle 的 factory,绝不接受嵌套 factory。`FactoryComponentPropsOf<F>` 和 `FactoryLocalComponentPropsOf<F, N>` 从同一条目推导完整 Component props;definition owner 与消费方无需重述扁平共享类型。
+
+### Definition 与 occurrence 生命周期
+
+每个 Factory 名在独立于普通 Slot cells 的 registry ledger 中只有一个有效 definition。registration 遵循调用方的 Cordis effect。dispose 会移除 definition、折叠其普通 child 声明、通知已挂载 outlets,并令保留的 child-render 或局部 Component 权限失效。
+
+每次 `renderFactorySlot()` 调用都会创建 occurrence,其 identity 由 React 位置和 `key` 决定。替换 definition 会重新挂载 occurrence。Definition Component 及其 fallback 局部 Component 的失败归属 definition,调用方所选局部 Component 的失败归属调用方 registration;嵌套 Factory 渲染保留同一 owner。所有失败都限制在当前 occurrence 内,不会移除共享 definition。装配错误继续向外传播,所有权与陈旧授权错误则像普通组件失败一样上报。外层错误边界随 Factory scope incarnation 重置,每个局部边界随自身局部 slot 的 scope incarnation 重置。
+
+Factory scope 跟随 occurrence 所在 React 位置的 scope binding。`renderFactorySlot()` 不接受 Session id 或 scope target。严格 `session` Factory 要求当前 binding 存在,并在 identity 变化时重新挂载;`session-maybe` Factory 保留首次从空状态采纳 Session 的过程,并在后续 identity 变化时重新挂载,与普通 Slot 行为一致。
+
+共享 store handle 保留普通模式的 handle-by-scope 行为,包括两个非 root scope 均要求 Session binding。独占 store factory 在 occurrence 首次物化时创建一个 handle;若该 handle 声明 `spec.persist`,renderer 会拒绝它,因为 registration 必须保持 lazy,且多个同时存活的独立 occurrence 无法安全共享一个 persistence key。渲染期记录在幂等 effect setup 保留已 commit occurrence 前只持有弱引用;effect cleanup 会移除 mounted 强引用,而 occurrence-keyed WeakMap 在 React effect replay 期间保留 identity,并允许实例在卸载后被回收。
+
+### 局部 slots 与普通 children
+
+调用方可以为 Factory `slots` 声明中的每个名称选择一个 Component。Factory 调用 `useFactorySlot(name, fallback)`,取得 identity 稳定的绑定 Component;该 Component 只接受对应局部 slot 的 occurrence props。绑定 Component 渲染时,renderer 提供 Factory 的 store、injection、locale、child renderers 与该局部 slot 自身的标准 scope props。
+
+局部 slots 没有 list、keyed 或 chain routing,也没有独立 registration 生命周期。多贡献方扩展点仍使用 Factory 声明的普通 child Slots。这些 child 声明在 definition 范围内全局共享,而每个 occurrence 都在继承的 scope 下渲染其 registered entries。
+
+实时检查将每个 definition 表示为 `type: 'factory'` 节点,并把其普通 child Slots 嵌套在该节点下。普通节点保留 `type: 'slot'` 与现有 `kind`;调用方通过 `factory:<name>` 选择 Factory 根节点。
+
+每个由 renderer 创建的 Component 都会收到 `renderFactorySlot`,因此 Factory occurrence 不需要 parent-side use declaration。局部选择仍然属于单个 occurrence,并且不会引入对定义包的运行时 value import。
+
+### 首个交付用途
+
+`ui-conversation` 在共享正文与 Composer 外注册 optional-Session `conversation.content` Factory。其 strict-Session `views` 局部位置默认使用一个渲染现有 `conversation.session` Slot 的 adapter;其他 occurrence 可以选择不同的 View Component,且不会挂载主 Conversation Header。
+
+Factory 不拥有 Conversation store。普通 `conversation.session` body 与 `conversation.session.header` 保留同一个 strict-Session handle,在保持草稿与 View 选择 identity 的同时,避免将该 handle 同时挂到 `session` 和 `session-maybe` scope。
+
+### 类型与运行时强制规则
+
+类型链拒绝未知 Factory 名、缺失或多余的 occurrence props、与 `SlotMap` 不一致的 child spec、与 `SlotFactoryMap` 不一致的 definition 字段、嵌套 store factory、未知局部名称、不兼容的选中 Component,以及 input、registration、injection 与 scope props 之间的所有权重叠。
+
+运行时检查覆盖动态装配与纯 JavaScript 调用方:重复 definitions、child 声明冲突、未声明的局部名称、递归渲染、prop 冲突、陈旧权限、严格 scope 缺失与组件失败隔离。类型和运行时测试还固定了各 occurrence 的独占 store、按 scope 共享的 handles、注册前 fallback 行为、definition 替换、局部 scope 投影与普通 child 渲染。
+
+## 考虑过的替代方案
+
+**把普通 Slot 复用为可移植 definition。** 普通 Slot 属于一个 parent 声明以及全局树中的一个位置。在其他位置复用它会借用错误的所有权与生命周期。
+
+**把主 Conversation Header 移进 Factory。** 只有主 host 渲染该 Header。将其留在 Factory 外,使嵌入式 occurrence 无需另一个局部选择即可省略 Header,并保留其现有 strict-Session Slot 生命周期。
+
+**把共享 Conversation store 移到 optional-Session Factory。** Header 与 Session body 共享一个 strict-Session handle。将该 handle 同时挂到 `session-maybe` Factory 和 `session` Header 会违反 one-handle-one-scope 规则。
+
+**在每个 parent 下分别注册同一装配。** 独立 registrations 会重复 definition 及其 child 声明。全局贡献方需要使用平行 child 名称,或造成声明冲突。
+
+**维护扁平共享 props 类型。** 这会重复 `children`、`store`、`inject`、`locale` 与 scope 字段中已有的事实,使声明与 Component props 可以发生漂移。
+
+**把 React node 或 render callback 作为业务 props 传递。** 这些值绕过 renderer 提供的 scope props、store 与 injection 装配、陈旧权限检查和局部 Component 类型检查。
+
+**让局部 slots 具备普通 Slot routing。** 普通 Slots 已经负责多贡献方 routing。局部 slot 表示一次 occurrence 的一个调用方选择。
+
+**向 `renderFactorySlot()` 传递 Session identity。** occurrence 像普通 Slot 一样继承渲染位置的 scope。第二个 identity 参数会产生两个可能不一致的权威;独立定址的 Session provider 属于另一项能力。
+
+## 影响
+
+功能包可以发布一份可复用 UI 装配,而消费方无需运行时导入其 Component。每个 occurrence 可以获得独立选择的局部 Components 与独占状态,同时保留全局普通 child 贡献以及现有 scope、locale、injection 和 store 规则。
+
+新增的 registry ledger 与 occurrence 记录增加了 renderer 复杂度。Factory definitions 必须全局唯一,局部 slots 有意只支持一个选中 Component,并且该 API 本身不会创建可独立定址的 Session scope。
+
+Factory 类型与运行时测试是可执行的兼容性记录。Slots 子系统参考以及 `ui-slots` 和 `ui-renderer` 包参考记录消费方 API。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.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-10-desktop-web-wrapper.md
+2026-09-10-desktop-web-wrapper.md: 05210e2ab85f0afe6d0275e25f31ce8addf11332
+2026-09-10-desktop-web-wrapper.zh.md: 84e72c9261250b103245f974e1a0546a4898e9ca

+ 51 - 0
.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md

@@ -0,0 +1,51 @@
+# Agent Note: Run Desktop through the shared Web application
+
+Status: implemented
+
+English | [中文](2026-09-10-desktop-web-wrapper.zh.md)
+
+The [Electron runtime decision](2026-09-11-desktop-electron-node-runtime.md) supersedes the separate upstream Node executable; other decisions in this note remain applicable.
+
+## Problem
+
+Separate Desktop composition and request transport require their own configuration, module loading, streaming, and asset-serving behavior. Those implementations can omit Web features even when the renderer is shared. Desktop needs its own installation and native controls without maintaining a second application backend.
+
+## Decision
+
+The private Desktop Host invokes the CLI's shared profile runner against the independently owned Desktop profile. The complete Web composition owns authentication, HTTP routes, client assets, RPC, and response streaming. Electron loads packaged static Web assets before the child is ready. Child IPC carries readiness, structured boot injections, and shutdown. The [immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md) owns local-document HTTP forwarding and authenticated WebSocket access; Web retains application dispatch and stream framing.
+
+The shared runner owns profile and Harness-home patches, proxy setup, telemetry defaults, module fallbacks, configuration reload, and application lifecycle. Desktop initializes profiles from the shared Web template and uses its Plugin Manager in the main application. Electron owns windows, menus, native directory selection, recovery, and release updates.
+
+The [bundled-runtime decision](2026-09-08-desktop-bundled-runtime-and-external-plugins.md) retains separate runtime and plugin storage, bundled pnpm and explicit package ownership. The public CLI continues to reject the reserved Desktop profile. Electron profile preparation and native recovery remain available without a working Host.
+
+Independent package ownership prevents CLI and Desktop from modifying each other’s installations; it does not define a stricter Desktop plugin policy. Desktop delegates registry, store, Git, tarball, local-path, and ordinary-package installation to pnpm with normal user and profile configuration. The Host inherits `NODE_OPTIONS`, `NODE_PATH`, and npm/pnpm environment variables. User build configuration determines which dependency lifecycle scripts execute. This replaces Desktop-specific source, environment, and build restrictions with the same package-manager and loader responsibilities used by Web.
+
+The shared Web plugin manager owns installation, activation, errors, and restart requirements. Electron has no separate plugin-management renderer, preload, shell asset route, plugin IPC, or package-mutation runner. Packaging explicitly selects the main entry and application preload so stale build outputs cannot restore the removed bridge.
+
+Shared `initProfile` creates missing profile files and preserves existing content. The Host’s `healIsolatedProfileModuleFallback` is the sole owner of installation and bundle projections; package operations use shared `unlinkProfileModuleFallback` to detach only its own links before pnpm. pnpm-managed directories retain priority. Desktop maintains no second runtime-state, lockfile hash, or link reconciliation mechanism. One-time cleanup of `desktop-runtime-state.json` removes only matching recorded links and retires that metadata.
+
+This partially supersedes the private composition and portless transport in the [packaging decision](2026-08-25-electron-desktop-packaging-and-updates.md). That design avoided listening ports and used framed byte pipes to avoid Base64 expansion and cross-version V8 serialization. Shared HTTP gives up the portless guarantee and assigns serving and authentication to the existing Web implementation. Release identity, signing, process ownership, and native shell features remain active decisions.
+
+Native directory selection in the local application uses a narrow preload IPC call to Electron’s window-owned dialog. Main admits only the current application window’s main frame at `dsh-app://app`; shell, remote, and child frames cannot request it. Concurrent requests share the pending dialog and destroyed windows discard selections. Web backend selection and Host browse are shared.
+
+## Alternatives considered
+
+**Use the Host OS chooser in Electron.** The Host’s macOS AppleScript dialog has no Electron parent window and cannot reliably follow application focus. Electron owns the local dialog while Web keeps its Host chooser; cancellation and errors do not launch a second chooser.
+
+**Maintain a second backend composition and carrier.** This permits a portless application, but every Web route, reload behavior, authentication change, and stream capability needs a Desktop implementation or explicit omission. Reintroduction requires a desktop product requirement that cannot use the Web implementation and justifies that continuing cost.
+
+**Merge CLI and Desktop plugin installations.** Shared boot code does not require shared executable dependencies. Separate installations allow independently qualified releases and plugin versions while their existing data owners govern shared sessions and settings.
+
+**Keep a Desktop link ledger and manifest reconciler.** These duplicate shared profile mechanisms and can reject otherwise usable installations when derived metadata drifts. A single fallback owner can protect pnpm directories without maintaining release identity in the plugin profile.
+
+**Keep a separate native plugin-management page.** It can operate while the Web Host is unavailable, but duplicates package operations, renderer IPC, localization, and backend restart handling. Native recovery already supports disabling third-party bundles and backing up the profile patch without the Host. Reintroduction requires a repair operation that this recovery cannot provide.
+
+**Pin registry and store settings, filter runtime environment, and admit only approved plugin sources.** Those rules constrain execution and package selection, but make the same user configuration behave differently in Desktop and Web. Separate installation ownership remains useful without those restrictions. A Desktop-only restriction requires a distinct product requirement instead of following automatically from packaging or plugin isolation.
+
+## Consequences
+
+Desktop inherits Web features through the same boot and serving path. HTTP listener ownership and authentication remain part of application startup. Electron uses the reported Host address and preserves the existing Web document through readiness. The shared Web loading page is available before the Host starts; native recovery remains available when startup fails.
+
+User-selected runtime options, package sources, and permitted lifecycle scripts can affect Host execution, load third-party code, or cause startup failure. The shared Web manager owns package-failure handling; Electron retains native recovery for fatal startup failures. The signed core runtime does not attest to user-installed plugin code.
+
+Verification requires shared-runner coverage, authenticated HTTP asset and API delivery, configuration reload, native directory selection, child shutdown, and recovery after plugin failure. Installed-platform and real-model GUI qualification remain distinct from unit tests; this note records no measured startup or transfer improvement.

+ 51 - 0
.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.zh.md

@@ -0,0 +1,51 @@
+# Agent Note: 通过共享 Web 应用运行 Desktop
+
+Status: implemented
+
+[English](2026-09-10-desktop-web-wrapper.md) | 中文
+
+[Electron 运行时决策](2026-09-11-desktop-electron-node-runtime.zh.md)替代独立上游 Node 可执行文件的选择;本文其他决策仍然适用。
+
+## Problem
+
+独立的 Desktop 组合与请求传输需要分别维护配置、模块加载、流式响应与资源服务行为。即使共享渲染界面,这些实现也可能遗漏 Web 功能。Desktop 需要独立安装与原生控件,但不需要第二套应用后端。
+
+## Decision
+
+私有 Desktop Host 针对独立归属的 Desktop profile 调用 CLI 的共享 profile runner。完整 Web 组合负责认证、HTTP 路由、客户端资源、RPC 与响应流。Electron 在子进程就绪前加载打包静态 Web 资源。子进程 IPC 承载就绪、结构化启动注入与关闭。[立即显示窗口决策](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)规定本地文档 HTTP 转发与认证 WebSocket 访问;Web 保留应用分派与流帧处理。
+
+共享 runner 负责 profile 与 Harness-home patch、代理设置、遥测默认值、模块补全、配置重载和应用生命周期。Desktop 从共享 Web 模板初始化 profile,并在主应用中使用其插件管理器。Electron 负责窗口、菜单、原生目录选择、恢复和发布更新。
+
+[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)保留独立运行时与插件存储、内置 pnpm,以及明确的包归属。公开 CLI 继续拒绝保留的 Desktop profile。Electron profile 准备和原生恢复在 Host 不可用时仍可执行。
+
+独立包归属防止 CLI 与 Desktop 修改彼此的安装,不代表 Desktop 采用更严格的插件策略。Desktop 将 registry、store、Git、tarball、本地路径及普通包安装交给 pnpm,并遵循正常用户与 profile 配置。Host 继承 `NODE_OPTIONS`、`NODE_PATH` 及 npm/pnpm 环境变量。用户构建配置决定哪些依赖生命周期脚本可以执行。这以 Web 使用的相同包管理器和加载器职责取代 Desktop 专用的来源、环境及构建限制。
+
+共享 Web 插件管理器负责安装、激活、错误和重启要求。Electron 不提供独立插件管理渲染器、preload、shell 资源路由、插件 IPC 或包变更执行器。打包明确选择主入口和应用 preload,避免旧构建产物重新带入已删除的桥接。
+
+共享 `initProfile` 创建缺失的 profile 文件并保留现有内容。Host 的 `healIsolatedProfileModuleFallback` 是安装包与 bundle 投影的唯一归属方;包操作在 pnpm 前通过共享 `unlinkProfileModuleFallback` 仅分离它自己拥有的链接。pnpm 管理的目录保持优先。Desktop 不维护第二套运行时状态、锁文件哈希或链接协调机制。`desktop-runtime-state.json` 的一次性清理仅移除与记录匹配的链接,并清除该元数据。
+
+本记录部分取代[打包决策](2026-08-25-electron-desktop-packaging-and-updates.zh.md)中的私有组合与无端口传输。该设计避免监听端口,并使用分帧字节管道避免 Base64 膨胀与跨版本 V8 序列化。共享 HTTP 放弃无端口保证,将服务与认证交给已有 Web 实现。发布身份、签名、进程归属及原生壳功能仍是有效决策。
+
+本地应用的原生目录选择通过窄 preload IPC 调用 Electron 的窗口所属对话框。Main 仅接受当前应用窗口中位于 `dsh-app://app` 的主框架请求;shell、远程页面和子框架均不能调用。并发请求共用待完成的对话框,窗口销毁后丢弃选择结果。Web 后端选择和 Host 浏览由共享实现负责。
+
+## Alternatives considered
+
+**在 Electron 中使用 Host 操作系统选择器。** Host 的 macOS AppleScript 对话框没有 Electron 父窗口,无法可靠跟随应用焦点。Electron 负责本地对话框,Web 保留 Host 选择器;取消和错误不会启动第二个选择器。
+
+**维护第二套后端组合与传输。** 这允许应用不监听端口,但每项 Web 路由、重载行为、认证变化和流式能力都需要 Desktop 实现或明确省略。只有无法使用 Web 实现、且足以承担持续维护成本的桌面产品需求,才支持重新引入这种方案。
+
+**合并 CLI 与 Desktop 插件安装。** 共享启动代码不要求共享可执行依赖。独立安装允许分别验收发布与插件版本,共享会话和设置则仍由已有数据归属方负责。
+
+**保留 Desktop 链接账本与 manifest 协调器。** 这些机制重复共享 profile 逻辑,并可能因派生元数据漂移而拒绝原本可用的安装。单一模块补全归属方可以保护 pnpm 目录,无需在插件 profile 中维护发布身份。
+
+**保留独立原生插件管理页。** 它可以在 Web Host 不可用时运行,但重复维护包操作、渲染器 IPC、本地化和后端重启处理。原生恢复已能在没有 Host 时禁用第三方 bundle 并备份 profile patch。只有出现此恢复方式无法提供的修复操作时,才应重新引入。
+
+**固定 registry 与 store、过滤运行时环境,并仅允许批准的插件来源。** 这些规则限制执行和包选择,却使同一用户配置在 Desktop 与 Web 中产生不同行为。独立安装归属无需这些限制仍然有用。Desktop 专用限制需要独立的产品需求,不能仅由打包或插件隔离推导而来。
+
+## Consequences
+
+Desktop 通过相同启动与服务路径继承 Web 功能。HTTP 监听归属与认证仍属于应用启动。Electron 使用报告的 Host 地址,并在就绪前后保留现有 Web 文档。共享 Web 加载页在 Host 启动前可用;原生恢复在启动失败时仍可用。
+
+用户选择的运行时选项、包来源和允许的生命周期脚本可能影响 Host 执行、加载第三方代码或造成启动失败。共享 Web 管理器负责包操作失败处理;Electron 为致命启动失败保留原生恢复。签名核心运行时不为用户安装的插件代码背书。
+
+验证需要覆盖共享 runner、认证 HTTP 资源与 API 传输、配置重载、原生目录选择、子进程关闭及插件失败恢复。安装后平台验收与真实模型 GUI 验收独立于单元测试;本记录不声称已测得启动或传输提升。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-10-windows-native-installer-pages.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-10-windows-native-installer-pages.md
+2026-09-10-windows-native-installer-pages.md: 93aa4839620207447ad85b1027a68fcfdc5d3e2c
+2026-09-10-windows-native-installer-pages.zh.md: da28e22f82c6e403696904362bcbe6c8b96881b8

+ 31 - 0
.agents/notes/implemented/architecture/2026-09-10-windows-native-installer-pages.md

@@ -0,0 +1,31 @@
+# Agent Note: Native Windows installer pages
+
+Status: implemented
+
+English | [中文](2026-09-10-windows-native-installer-pages.zh.md)
+
+## Problem
+
+The Windows installation interface needs branded light and dark pages without introducing another application runtime or replacing the release mechanisms that extract, register, upgrade, and uninstall Desktop.
+
+## Decision
+
+The installer adds custom welcome, progress, and completion pages through electron-builder's NSIS include. The stock script remains responsible for installation and uninstaller generation. A custom full script bypasses electron-builder's separate uninstaller signing path and is therefore unsuitable for this interface change. The [Desktop release decision](2026-08-25-electron-desktop-packaging-and-updates.md) continues to own release identity, update distribution, and signing requirements.
+
+NSIS native controls preserve directory editing, folder selection, checkbox state, and keyboard interaction. An x86 Win32/GDI+ helper retains DWM shadows and draws installation progress on the UI thread while the stock installation worker runs. Stock page visibility is suppressed even when NSIS shows the page after MUI's callback. Windows 11 supplies the outer corner radius; Windows 10 retains its supported frame appearance.
+
+Installation is per-user. Welcome-page leave validation reads the current edit control for mouse and keyboard navigation; the debounced inline hint is not an installation authority. Running-process checks match the affected executable path, leaving other installations independent. Completion-page leave honors the launch checkbox for both mouse and keyboard navigation. Electron-builder resolves the registered directory before custom initialization, so silent updates without `/D=` retain that directory. Directory staging and promotion retain the release installer’s rollback behavior; first-launch profile preparation remains outside the installer.
+
+## Alternatives considered
+
+**An Electron installer interface** adds a runtime before the application exists and requires a separate installation bridge. Native controls provide the required interaction within the existing installer.
+
+**Replacing the full NSIS script with the lightweight prototype** also replaces upgrade, registry, uninstaller, and signing behavior. The prototype's transaction mechanism requires separate release qualification and is excluded from the UI integration.
+
+**A window region for rounded corners** disables the DWM frame shadow. The system frame preserves the shadow while accepting the platform's corner radius.
+
+## Consequences
+
+Windows packaging additionally requires the x86 Visual C++ compiler and Windows SDK. The helper is signed before embedding by the same signer as other Windows artifacts. The preparation hook returns true on every platform so electron-builder collects production dependencies. The directory installer owns staging, promotion, registration, and recovery. Its extraction hook invokes the pinned 7-Zip executable with a dedicated progress pipe and a separate diagnostic file. The child inherits only its standard streams and joins a kill-on-close job at creation, so installer termination also stops extraction. Only a zero exit code permits promotion. The helper parses percentages across pipe-read boundaries; it does not implement archive extraction or recursive copying. Preparation, promotion, registration, and cleanup retain bounded estimates. Stage weights express completed work, not remaining time. Displayed progress never regresses, and captions follow the displayed stage. NSIS success authorizes a 600 ms fill animation and a brief 100% frame, with a 750 ms transition target; timers cannot authorize success. The frame stays hidden until branded controls are ready and is initialized once; page transitions preserve its position. Finish hides the window before creating the installed process directly under the current user. An explicitly elevated installer retains shell-mediated launch. Launch failure restores the finish page. Application startup time is independent of installer dismissal.
+
+The native installer regression builds English-only and Chinese-only variants and identifies their language from the visible welcome button; the Windows installation language does not determine test labels. Sequential runs use a unique product identity and private installation directories to verify path rejection, folder selection, launch choices, upgrade, running-process preservation, hidden stock progress, first-show readiness, and uninstall. Registered paths with trailing separators remain upgrade destinations, while drive roots remain invalid. Deterministic native progress tests cover fast and slow extraction, stalls, source resets, fragmented progress tokens, short cleanup, and success between UI ticks. Directory tests exercise extraction through the helper, long paths, new installation, replacement, locked-file recovery, missing staged directories, broken archives, and cancellation. Screenshots and expected behavior belong to Desktop tests rather than recorded Session snapshots. Signed release qualification still requires the configured certificate and token, and actual Windows update artifacts.

+ 31 - 0
.agents/notes/implemented/architecture/2026-09-10-windows-native-installer-pages.zh.md

@@ -0,0 +1,31 @@
+# Agent Note: 原生 Windows 安装页面
+
+Status: implemented
+
+[English](2026-09-10-windows-native-installer-pages.md) | 中文
+
+## Problem
+
+Windows 安装界面需要符合品牌设计的亮暗页面,同时避免引入额外的应用运行时,也不能替换 Desktop 的解压、注册、升级和卸载发布机制。
+
+## Decision
+
+安装程序通过 electron-builder 的 NSIS include 接入自定义欢迎页、进度页和完成页。原生脚本继续负责安装和卸载程序生成。完整自定义脚本会绕过 electron-builder 单独生成并签名卸载程序的流程,因此不适合这次界面改动。[Desktop 发布决策](2026-08-25-electron-desktop-packaging-and-updates.zh.md)继续负责发布身份、更新分发和签名要求。
+
+NSIS 原生控件保留目录编辑、文件夹选择、复选状态和键盘交互。x86 Win32/GDI+ 辅助库保留 DWM 阴影,并在原生安装工作线程运行期间,由界面线程绘制安装进度。即使 NSIS 在 MUI 回调之后重新显示页面,原生页面仍保持隐藏。Windows 11 提供外框圆角半径;Windows 10 保留其支持的窗口外观。
+
+安装面向当前用户。欢迎页离开校验在鼠标和键盘导航时均读取当前编辑框;防抖显示的行内提示不决定实际安装路径。进程检查匹配受影响的可执行文件路径,使其他安装保持独立。完成页离开回调在鼠标和键盘导航时均遵循启动复选框。electron-builder 在自定义初始化之前解析已登记目录,因此不带 `/D=` 的静默更新会保留该目录。目录暂存和替换保留发布安装器的回滚行为;首次启动的配置档案准备仍不属于安装程序。
+
+## Alternatives considered
+
+**Electron 安装界面**会在应用安装前引入运行时,并要求额外的安装通信机制。原生控件能在现有安装程序内提供所需交互。
+
+**使用轻量原型替换完整 NSIS 脚本**还会替换升级、注册表、卸载和签名行为。原型的事务机制需要单独进行发布验证,不纳入界面接入。
+
+**使用窗口区域裁剪圆角**会禁用 DWM 窗口阴影。系统窗口框架保留阴影,同时接受平台提供的圆角半径。
+
+## Consequences
+
+Windows 打包额外要求 x86 Visual C++ 编译器和 Windows SDK。辅助库在嵌入前使用与其他 Windows 产物相同的签名器签名。准备钩子在所有平台返回 true,使 electron-builder 收集生产依赖。目录安装器负责暂存、替换、注册和恢复。其解压钩子通过独立进度管道和单独的诊断文件调用锁定版本的 7-Zip 可执行文件。子进程仅继承标准流句柄,并在创建时加入关闭即终止的 Job,因此终止安装程序也会停止解压。只有退出码为零才允许替换目录。辅助库跨管道读取边界解析百分比,不自行实现压缩包解压或递归复制。准备、替换、注册和清理仍使用有界估算。阶段权重表示已完成工作量,而非剩余时间。显示进度不回退,文案跟随显示阶段。NSIS 成功信号允许执行 600 毫秒补满动画并短暂显示 100%,切换目标时长为 750 毫秒;计时器不能宣告成功。窗口框架在品牌控件准备完成前保持隐藏,且只初始化一次,页面切换保留其位置。点击完成后先隐藏窗口,再以当前用户直接创建已安装应用的进程;显式提权的安装程序保留通过用户桌面启动的方式。启动失败会恢复完成页。应用启动耗时与安装窗口关闭分别处理。
+
+原生安装回归构建仅英文和仅中文的变体,并从可见的欢迎页按钮识别语言;Windows 安装语言不决定测试文案。顺序执行的测试使用独立产品身份和私有安装目录,验证路径拒绝、文件夹选择、启动选项、升级、运行中进程保留、原生进度条隐藏、首次显示时就绪和卸载。末尾带分隔符的已登记路径仍可用于升级,磁盘根目录仍然无效。确定性的原生进度测试覆盖快速和慢速解压、停滞、进度来源重置、分片进度文本、短暂清理,以及两次界面刷新之间成功的情况。目录测试通过辅助库执行解压,覆盖长路径、新装、替换、文件占用恢复、暂存目录缺失、损坏压缩包和取消。截图和预期行为归属 Desktop 测试,不放入录制 Session 快照。签名发布仍需使用已配置的证书、Token 和真实 Windows 更新产物进行验证。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.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-11-desktop-electron-node-runtime.md
+2026-09-11-desktop-electron-node-runtime.md: 71516bb930f8eee22c1c6a988b6dab596a53a215
+2026-09-11-desktop-electron-node-runtime.zh.md: 162908392b98ee6a666ca414b602207ad8344937

+ 27 - 0
.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.md

@@ -0,0 +1,27 @@
+# Agent Note: Use Electron as the Desktop Node runtime
+
+Status: implemented
+
+English | [中文](2026-09-11-desktop-electron-node-runtime.zh.md)
+
+## Problem
+
+Shipping an upstream Node executable alongside Electron duplicates the JavaScript runtime. Desktop needs one runtime for its Host and package scripts without requiring users to install Node.
+
+## Decision
+
+Desktop runs the shared Web Host and bundled pnpm through its own Electron executable with `ELECTRON_RUN_AS_NODE=1`. It ships no separate upstream Node executable. The target Electron distribution supplies both the packaging input and the runtime used to prepare and verify production dependencies; release metadata records its actual Node version. Development uses the installed Electron distribution.
+
+This supersedes the separate-Node choice in the [packaging decision](2026-08-25-electron-desktop-packaging-and-updates.md) and [bundled-runtime decision](2026-09-08-desktop-bundled-runtime-and-external-plugins.md). Their independent plugin storage and ordinary resource-directory layout remain applicable. The [Web wrapper](2026-09-10-desktop-web-wrapper.md) retains the shared profile runner and HTTP transport.
+
+## Consequences
+
+Host and pnpm launches pass `--expose-internals`: the bundled Cordis loader uses Node's internal ESM loader, while its native builtin accessor cannot locate the required symbol in Electron 44. The explicit flag makes that loader available without modifying Cordis. The RunAsNode fuse remains enabled.
+
+The Host inherits the caller PATH without Desktop’s private `bin` directory, so PTC and agent shells cannot resolve internal launchers through that directory. `DSH_DESKTOP_NODE_EXECUTABLE` is injected only for package installation. Package-script environments prepend a small `node` shell launcher that forwards arguments to the current Electron executable. This supports shell lifecycle scripts without a system Node installation. On Windows it is `node.cmd`, not a replacement `node.exe`; third-party code that directly spawns the literal `node` executable without a shell must use `process.execPath` or provide its own runtime. Child processes inherit RunAsNode; worker threads inherit the Host's arguments. Desktop does not emulate upstream OpenSSL behavior or rebuild arbitrary third-party native addons automatically.
+
+Electron's Node patches and native ABI are release compatibility obligations. The packaged native smoke exercises pnpm shell scripts without system Node on PATH, terminal output through the Windows shell, Koffi, Sharp, and HTML conversion. The Host smoke loads an external plugin sharing Cordis and serves its route through the real Web application. Platform signing and installed-application qualification remain required; Windows results do not establish macOS compatibility. Windows token signing runs serially and retains the first failure, preventing queued tasks from repeating a rejected PIN.
+
+## Alternatives considered
+
+A separate Node executable decouples the Host from Electron's runtime but adds another binary, download, signature, and version selection. Electron RunAsNode removes that duplication. Moving production packages into ASAR is a separate change involving native modules, package resolution, and subprocess paths; the Host continues to load ordinary resource files.

+ 27 - 0
.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.zh.md

@@ -0,0 +1,27 @@
+# Agent Note: 使用 Electron 作为 Desktop 的 Node 运行时
+
+Status: implemented
+
+[English](2026-09-11-desktop-electron-node-runtime.md) | 中文
+
+## 问题
+
+在 Electron 之外携带上游 Node 可执行文件会重复分发 JavaScript 运行时。Desktop 需要让 Host 和包脚本共用一个运行时,无需用户安装 Node。
+
+## 决策
+
+Desktop 通过自己的 Electron 可执行文件运行共享 Web Host 和内置 pnpm,并设置 `ELECTRON_RUN_AS_NODE=1`。应用不携带独立的上游 Node 可执行文件。目标 Electron 分发包同时作为打包输入,以及准备和验证生产依赖的运行时;发布元数据记录其实际 Node 版本。开发模式使用已安装的 Electron 分发包。
+
+此决策替代[打包决策](2026-08-25-electron-desktop-packaging-and-updates.zh.md)和[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)中的独立 Node 选择。独立插件存储和普通资源目录布局仍然适用。[Web 薄壳](2026-09-10-desktop-web-wrapper.zh.md)保留共享 profile runner 和 HTTP 传输。
+
+## 后果
+
+Host 和 pnpm 启动时传入 `--expose-internals`:内置 Cordis 加载器使用 Node 内部 ESM 加载器,而其原生 builtin 访问器无法在 Electron 44 中找到所需符号。显式参数使加载器可用,无需修改 Cordis。RunAsNode fuse 保持启用。
+
+Host 继承调用者的 PATH,不加入 Desktop 私有的 `bin` 目录,因此 PTC 和 agent shell 不会通过该目录解析内部启动器。`DSH_DESKTOP_NODE_EXECUTABLE` 仅为包安装注入。包脚本环境在 PATH 前添加一个小型 `node` shell 启动器,把参数转发给当前 Electron 可执行文件。这支持没有系统 Node 的 shell 生命周期脚本。Windows 上它是 `node.cmd`,并非替代的 `node.exe`;第三方代码若绕过 shell 直接启动名为 `node` 的可执行文件,必须使用 `process.execPath` 或提供自己的运行时。子进程继承 RunAsNode;worker 线程继承 Host 参数。Desktop 不模拟上游 OpenSSL 行为,也不自动重编译任意第三方原生扩展。
+
+Electron 的 Node 补丁和原生 ABI 属于发布兼容性责任。打包原生 smoke 在 PATH 不含系统 Node 的情况下验证 pnpm shell 脚本,并验证 Windows shell 终端输出、Koffi、Sharp 和 HTML 转换。Host smoke 加载共享 Cordis 的外部插件,通过真实 Web 应用提供其路由。各平台仍需完成签名和已安装应用验收;Windows 结果不能证明 macOS 兼容性。Windows Token 签名串行执行并保留首次失败,阻止排队任务重复提交被拒绝的 PIN。
+
+## 考虑过的替代方案
+
+独立 Node 可执行文件可以让 Host 与 Electron 运行时分离,但会增加另一份二进制文件、下载、签名和版本选择。Electron RunAsNode 消除这一重复。将生产包移入 ASAR 是另一项涉及原生模块、包解析和子进程路径的改动;Host 继续加载普通资源文件。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-11-node-office-kit.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-11-node-office-kit.md
+2026-09-11-node-office-kit.md: d77d6c5392532f39a9993af60faad6474bc8f36c
+2026-09-11-node-office-kit.zh.md: a0cbd22e4f06bd4f2878adf27852f9d2378b8b33

+ 65 - 0
.agents/notes/implemented/architecture/2026-09-11-node-office-kit.md

@@ -0,0 +1,65 @@
+# Agent Note: Node Office conversion with independently packaged engines
+
+Status: implemented
+
+English | [中文](2026-09-11-node-office-kit.zh.md)
+
+## Problem
+
+Binary Office and OOXML files require document layout before they can be previewed. Conversion must stay on the device without opening an Office application or adding source bytes to a model conversation. Native engines need platform-specific distribution, while browser conversion duplicates font transport, worker ownership, and resource limits across the Host and Client.
+
+## Decision
+
+The [office-to-pdf capability](../../../../packages/document/README.md) delegates conversion to the independently released `@deepseek-ai/libreoffice-kit` Node API. The [kit ownership decision](2026-09-14-independent-libreoffice-kit.md) owns source maintenance, compatibility versions, and npm distribution. DSH owns Session file authorization, conversion concurrency, private scratch files, output limits, and Remote transport. The [Web bundle](../../../../packages/bundle/web-app/README.md) declares separate provider and controller entries and the shared Document Preview entry with stable IDs. Conversion and authorized transport remain independently configurable; Office UI shares the Document Preview Loader lifetime.
+
+The [platform engine decision](2026-09-15-platform-office-engines.md) requires the kit’s declared native target engine, or WASM when no native target is declared. Missing or invalid required engines reject conversion. The shared [bounded provider](2026-09-15-bounded-office-conversion.md) owns admission, conversion reuse, and cancellation through scratch cleanup. Preview consumes that provider without registering another converter or requiring Office authoring skills.
+
+The service, Remote methods, and Client registration accept DOC, DOCX, XLS, XLSX, PPT, and PPTX. The kit verifies bounded ZIP membership and content types for OOXML inputs and the OLE compound-file header for binary Office inputs before LibreOffice imports them. Renaming text to an Office suffix does not admit it. Binary formats return no missing-font diagnostics because the kit does not extract their font tables. It writes a fresh, exclusively created PDF in a caller-owned private directory. DSH reads and validates the complete output before removing scratch files. The [service’s Remote method](../../../../packages/document/office-to-pdf/README.md) authorizes source access through [Workspace Files](2026-09-09-workspace-file-read-authority.md), preserves the source path/version, and returns PDF bytes. Source read caps and generated PDF caps remain independent. One source path/version snapshot governs the access probe and deferred read, including size-failure rechecks, so conversion cannot publish bytes under another identity. Preview bytes do not enter Session storage or persistent caches.
+
+A converter reuses font metadata obtained by its first conversion Worker; original font buffers and decoded glyph coverage remain conversion-local. Workers validate indexed files when reading them. Exact installed families precede configured alternatives, and complete family/style/weight/italic/width/pitch/language/code-point requests retain their distinct matches. WASM callbacks import original font files, including complete collections, into MEMFS. Native engines also retain their platform's font discovery. Neither path downloads or installs fonts; native OS-managed font memory is outside the explicit import budget. Recreating the converter refreshes its metadata after font changes.
+
+The kit owns default serif, sans-serif, and monospace preference groups, including Chinese text families. Missing Chinese glyphs in Western text try the corresponding common text families before searching the remaining catalog, which avoids choosing a handwriting face solely because its file sorts first. The provider's optional `fontFallbacks` replaces these ordered groups without duplicating their defaults. Preferences preserve exact installed fonts and retain other covering fonts as a last resort; they are not a font whitelist. The native adapter writes missing-family choices into its private VCL profile. Installed metric-compatible fonts can resolve before that table, and platform glyph fallback remains available. Native platform selection and whole collection imports require inspecting the fonts actually used in exported PDFs.
+
+DSH exports raster images at configurable resolution, defaulting to 192 DPI for the shared PDF canvas's 96 CSS DPI at device-pixel ratio 2. Text and vectors remain scalable; explicit bookmark export preserves the engine default when JSON filter options replace it. Node WASM downscales images with LibreOffice's CPU filter. Native conversion uses its separate platform engine.
+
+The [Office viewer](../../../../packages/client/ui-sidebar-documentpreview/README.md#office-preview) lives under Document Preview’s `client/office/` directory, alongside the loading lifecycle, PDF body, and reader types it uses. Keeping these components in one package removes an independent UI boot entry without creating cross-plugin runtime imports. Its bounded cache validates authorized source metadata, shares pending conversions between readers, cancels only when the final reader leaves, excludes failures, and clears on connection reset. Conversion starts when a user opens a preview. Missing declared font families accompany the PDF and appear in a dismissible notice above the Office scrollport; font-table inventories and unrelated engine defaults are not warnings. The shared preview entry’s `office` cache settings use the existing page-global injection channel because the module boot graph carries package identities, not Loader configuration. Reloading the page adopts updated YAML values.
+
+Ordinary file reads and Office responses share `documentFileBytes()`, which decodes into one typed byte buffer rather than materializing a JavaScript element array from the binary string. Element-array expansion can exhaust the browser heap for a PDF that the configured Host limits permit. A child-process regression checks byte equality within a fixed heap, while the built browser scenario verifies the transport and PDF Worker together. Cache byte limits do not bound transient transport or viewer memory.
+
+The [kit ownership decision](2026-09-14-independent-libreoffice-kit.md) defines npm distribution and bundled offline conversion.
+
+Desktop installs the kit through its existing target-Node pnpm dependency installation and retains the complete dependency tree. Worker paths and executable permissions remain ordinary package files. The [Desktop build guide](../../../../apps/desktop/README.md) owns target selection and packaging; each signed application requires qualification on its target platform.
+
+The [Python executable distribution](2026-07-10-single-file-executable-sdk-runtime-distribution.md) keeps the kit, target engine, and their dependency closure beside the executable. Its installed-wheel smoke relocates the payload, requires exactly the target backend, and converts DOCX once through that engine. Platform packaging and publication constraints belong to the [platform engine decision](2026-09-15-platform-office-engines.md).
+
+The notices gate permits only the exact API and engine package names at MPL-2.0 and continues to reject unrelated MPL or changed non-permissive terms. Each recipient must retain access to the kit’s corresponding LibreOffice source pin, patches, build instructions, and license notices; the engine packages retain their third-party notices. [MPL source availability](https://www.mozilla.org/en-US/MPL/2.0/FAQ/) applies when distributing covered executables outside the organization.
+
+The [browser-only preview](../../../../packages/experimental/webworker-runtime/README.md) replaces the kit entry with an unavailable converter and excludes its engine dependency tree from the VFS image. Keeping the Host provider loadable preserves the ordinary Office error presentation without shipping Node Workers, native helpers, or WASM conversion resources to the browser.
+
+## Alternatives considered
+
+**Keep conversion in browser Workers.** This requires shipping the engine to the Client, browser font RPC, and shared-memory response headers. Node already owns authorized disk access and can serve the resulting PDF to every Client.
+
+**Use the installed soffice CLI or automate Microsoft Office.** Executable discovery and ambient versions weaken reproducibility; Office GUI automation additionally changes focus and requires application permissions. The distributed helper owns a fixed engine without requiring either application installation.
+
+**Load a native addon in the Host process.** A parser crash or synchronous stall would affect the Host. The separate native helper provides an independently terminable lifetime; its disk exchange also works with the WASM adapter.
+
+**Fall back after any native error.** Retrying a damaged package or failed document with another engine hides release defects, duplicates work, and makes output depend on failure timing. Platform selection requires the kit’s declared native target engine, or WASM when no native target is declared; it does not retry native failures with WASM.
+
+**Return a temporary PDF path, or rasterize pages to PNG.** The Client needs PDF bytes for its existing viewer and selectable text. Exposing temporary paths would add authorization and lease ownership; PNG would create another rendering pipeline and remove PDF controls.
+
+**Index every font for every render, or cache only family names.** Repeated indexing dominates small-document work, while family-only keys lose style and glyph distinctions. Shared metadata and complete per-request keys remove repeated parsing without retaining font buffers or introducing a filesystem watcher.
+
+**Compile at install time, download engines or fonts at runtime, or use an online converter.** These add toolchain or network requirements and may move private content off device. Prebuilt tarballs keep installation and conversion independent of those operations.
+
+**Keep an independent Office UI package.** Its reader, cache, and font notice have the same preview lifecycle and PDF presentation consumers. A separate plugin adds a package, boot wiring, and a Loader switch without an independently evolving UI responsibility. Host conversion and Remote authorization retain their separate plugins.
+
+**Remove the shared Client conversion cache.** Per-tab state cannot share pending conversion or retained PDFs across readers. A bounded cache avoids that repeated work while preserving per-reader cancellation and authorized version checks.
+
+**Add a model-facing rendering tool or durable preview events.** Preview provides no model input. Such a tool would require logged facts and both SDK projections and remains a separate consumer.
+
+## Consequences
+
+Native and WASM fidelity still depends on the build, source formatting, installed fonts, and platform font discovery. A missing glyph cannot be recovered without a covering font. Image resolution limits do not cap image decoding or total process memory. WASM retains bounded memory growth and checked stack space for large collections and CFF fonts; a fatal runtime abort prevents subsequent C++ cleanup calls. Macros and document-link updates are disabled by supported LOKit options and the pinned source patch; this does not constitute an OS sandbox.
+
+The [provider tests](../../../../packages/document/office-to-pdf/tests/provider.spec.ts), [Loader composition](../../../../packages/bundle/web-app/tests/document-preview.spec.ts), and [browser scenario](../../../../apps/web/tests/document-preview.e2e.ts) own DSH lifecycle, authorization, and presentation evidence. Engine qualification additionally requires real DOC/DOCX/XLS/XLSX/PPT/PPTX conversion, external PDF text/font/page/image inspection, relocation and corrupted-package rejection, and same-input native/WASM performance samples. Mock helpers and microbenchmarks do not establish these outcomes. Each target's real builder and Desktop package need independent qualification; a successful local architecture does not prove the full matrix.

+ 65 - 0
.agents/notes/implemented/architecture/2026-09-11-node-office-kit.zh.md

@@ -0,0 +1,65 @@
+# Agent Note: 独立打包引擎的 Node Office 转换
+
+Status: implemented
+
+[English](2026-09-11-node-office-kit.md) | 中文
+
+## 问题
+
+二进制 Office 和 OOXML 文件需要经过文档排版才能预览。转换必须留在设备上,不能打开 Office 应用,也不能将源字节加入模型对话。原生引擎需要按平台分发,而浏览器转换会在 Host 和 Client 两侧重复字体传输、worker 生命周期与资源限额管理。
+
+## 决策
+
+[文档渲染能力](../../../../packages/document/README.zh.md)将转换委托给独立发布的 `@deepseek-ai/libreoffice-kit` Node API。[kit 归属决策](2026-09-14-independent-libreoffice-kit.zh.md)负责源码维护、兼容版本和 npm 分发。DSH 负责 Session 文件授权、转换并发、私有临时文件、输出限制和 Remote 传输。[Web bundle](../../../../packages/bundle/web-app/README.zh.md)使用稳定 ID 声明独立的 provider、controller 入口和共享文档预览入口。转换与授权传输仍可独立配置;Office UI 共享文档预览的 Loader 生命周期。
+
+[平台引擎决策](2026-09-15-platform-office-engines.zh.md)要求使用 kit 已声明的原生目标引擎,未声明原生目标时使用 WASM。缺失或无效的必需引擎会拒绝转换。共享的[有界提供方](2026-09-15-bounded-office-conversion.zh.md)负责准入、转换复用以及持续到临时文件清理完成的取消。预览消费该提供方,不注册另一个转换器,也不依赖 Office 创作 skills。
+
+服务、Remote 方法和 Client 注册接受 DOC、DOCX、XLS、XLSX、PPT 和 PPTX。LibreOffice 导入前,kit 校验 OOXML 输入的有界 ZIP 成员和内容类型,以及二进制 Office 输入的 OLE 复合文件头。将文本改为 Office 后缀不能通过校验。kit 不提取二进制格式的字体表,因此这些格式不返回缺失字体诊断。kit 在调用方拥有的私有目录中独占创建新的 PDF。DSH 读取并校验完整输出后才删除临时文件。[服务的 Remote 方法](../../../../packages/document/office-to-pdf/README.zh.md)通过 [Workspace Files](2026-09-09-workspace-file-read-authority.zh.md)授权源文件访问,保留源路径和版本,并返回 PDF 字节。源文件读取上限与生成 PDF 上限相互独立。读取权限探测和延迟读取(包括超限失败后的复查)采用同一个源路径/版本快照,防止转换将字节发布到另一个源身份下。预览字节不会进入 Session 存储或持久缓存。
+
+converter 复用首个转换 Worker 返回的字体元数据;原始字体缓冲区和解码后的字符覆盖范围仍只属于单次转换。Worker 读取字体时校验索引中的文件。已安装字体族的精确匹配优先于配置的替代字体,完整的字体族、样式、字重、斜体、宽度、字距、语言与码点请求保留各自的匹配结果。WASM 回调将包含完整字体集合的原始字体文件导入 MEMFS。原生引擎还保留各平台的字体发现能力。两条路径均不下载或安装字体;原生操作系统管理的字体内存不受显式导入预算约束。字体变化后,重新创建 converter 会刷新元数据。
+
+kit 维护 serif、sans-serif 和 monospace 的默认优先组,其中包含中文正文字体。西文文本缺少中文字形时,先尝试同类的常用正文字体,再搜索其余字体目录,避免仅因文件排序靠前而选用手写体。provider 的可选 `fontFallbacks` 替换这些有序组,不重复维护默认值。优先规则保留已安装原字体的精确匹配,并以其他覆盖字体作为最后兜底;它们不是字体白名单。原生适配器将缺失字体的选择写入私有 VCL profile。已安装的度量兼容字体可能在查询该表前被选中,平台的字形回退仍然可用。原生平台的选择及完整字体集合的导入要求检查导出 PDF 实际使用的字体。
+
+DSH 按可配置分辨率导出栅格图片,默认 192 DPI,对应共享 PDF 画布在设备像素比 2 时的 96 CSS DPI。文本与矢量仍可缩放;JSON 过滤选项替代隐式选项时,显式书签导出保留引擎默认行为。Node WASM 使用 LibreOffice 的 CPU 过滤器降采样图片。原生转换使用独立的平台引擎。
+
+[Office 查看器](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md#office-preview)位于文档预览的 `client/office/` 目录,与其使用的加载生命周期、PDF 正文和读取器类型同属一个包。这些组件放在同一包中,既减少一个独立 UI 启动入口,也无需跨插件运行时导入。其有界缓存校验已授权的源元数据,在读取方之间共享待完成转换,仅在最后一个读取方离开时取消,不缓存失败,并在连接重置时清空。用户打开预览时才开始转换。缺失的已声明字体族随 PDF 返回,在 Office 滚动区上方显示可关闭的提示;字体表清单与无关的引擎默认字体不构成警告。共享预览入口的 `office` 缓存设置复用页面全局注入通道,因为模块启动图携带包标识而不传递 Loader 配置。重新加载页面后采用更新的 YAML 值。
+
+普通文件读取与 Office 响应共享 `documentFileBytes()`,解码使用一个类型化字节缓冲区,不将二进制字符串物化为 JavaScript 元素数组。即使 PDF 符合 Host 配置的大小限制,元素数组展开也可能耗尽浏览器堆内存。子进程回归测试在固定堆容量内检查字节一致性,构建后的浏览器场景则一起验证传输和 PDF Worker。缓存字节限制不约束临时传输或查看器内存。
+
+[kit 归属决策](2026-09-14-independent-libreoffice-kit.zh.md)定义 npm 分发和随应用打包的离线转换。
+
+Desktop 通过现有的目标 Node pnpm 依赖安装流程安装 kit,并保留完整依赖树。Worker 路径和可执行权限仍由普通包文件承载。[Desktop 构建指南](../../../../apps/desktop/README.zh.md) 负责目标选择与打包;每个签名应用仍需在目标平台验收。
+
+[Python 可执行分发](2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)将 kit、目标引擎及其依赖闭包保留在可执行文件旁。安装后的 wheel 冒烟测试会迁移载荷,要求仅存在目标后端,并通过该引擎转换一次 DOCX。各平台的打包与发布限制由[平台引擎决策](2026-09-15-platform-office-engines.zh.md)说明。
+
+声明检查仅放行精确的 API 与引擎包名及 MPL-2.0 条款,继续拒绝无关 MPL 包或变更后的非宽松条款。每位接收者都必须保有访问 kit 对应 LibreOffice 源码版本、补丁、构建说明与许可证声明的权限;引擎包保留各自的第三方声明。向组织外部分发受覆盖的可执行文件时,须满足 [MPL 源码可用性要求](https://www.mozilla.org/en-US/MPL/2.0/FAQ/)。
+
+[纯浏览器预览](../../../../packages/experimental/webworker-runtime/README.zh.md)用返回不可用错误的转换器替代 kit 入口,并从 VFS 镜像排除其引擎依赖树。保留 Host provider 的可加载性,可以沿用正常的 Office 错误展示,而无需向浏览器分发 Node Worker、原生辅助程序或 WASM 转换资源。
+
+## 考虑过的替代方案
+
+**保留浏览器 Worker 转换。** 这需要向 Client 分发引擎、提供浏览器字体 RPC 和配置共享内存响应头。Node 已经拥有授权磁盘访问能力,可以向所有 Client 提供转换后的 PDF。
+
+**使用已安装的 soffice CLI 或自动化 Microsoft Office。** 可执行文件发现和环境中的版本削弱可复现性;Office GUI 自动化还会改变焦点并需要应用权限。随包辅助进程拥有固定引擎,不要求安装这两类应用。
+
+**在 Host 进程中加载原生 addon。** 解析器崩溃或同步阻塞会影响 Host。独立的原生辅助进程具有可单独终止的生命周期,其磁盘交换方式也适用于 WASM 适配层。
+
+**任何原生错误都触发回退。** 使用另一引擎重试损坏的包或失败文档会隐藏发布缺陷、重复计算,并使输出取决于失败时机。平台选择要求使用 kit 已声明的原生目标引擎,未声明原生目标时使用 WASM;原生失败后不会使用 WASM 重试。
+
+**返回临时 PDF 路径,或将页面栅格化为 PNG。** Client 需要 PDF 字节来使用现有查看器和可选择文本。暴露临时路径会增加授权与租约管理;PNG 则会建立另一条渲染管线并丢失 PDF 控件。
+
+**每次渲染重新索引所有字体,或只按字体族缓存。** 重复索引主导小文档成本,而仅以字体族为键会丢失样式和字形差异。共享元数据与完整请求键减少重复解析,无需保留字体缓冲区或引入文件系统监听器。
+
+**安装时编译、运行时下载引擎或字体,或使用在线转换器。** 这些方案增加工具链或网络要求,并可能将私有内容移出设备。预构建 tarball 使安装与转换不依赖这些操作。
+
+**保留独立的 Office UI 包。** 其读取器、缓存和字体提示共享预览生命周期与 PDF 展示消费者。独立插件增加包、启动接线和 Loader 开关,却没有独立演进的 UI 职责。Host 转换与 Remote 授权仍保留独立插件。
+
+**移除共享的 Client 转换缓存。** tab 内状态无法在读取方之间共享进行中的转换或已保留的 PDF。有界缓存减少这些重复工作,同时保留各读取方的取消和已授权版本检查。
+
+**添加面向模型的渲染工具或持久预览事件。** 预览不会提供模型输入。这样的工具需要日志事实及两套 SDK 投影,仍属于独立消费者。
+
+## 后果
+
+原生与 WASM 的保真度仍取决于构建、源文件格式、已安装字体及平台字体发现。没有覆盖字体就无法恢复缺失字形。图片分辨率限额不限制图片解码或总进程内存。WASM 为大型字体集合和 CFF 字体保留有界内存增长与受检查的栈空间;致命运行时中止会阻止后续 C++ 清理调用。宏与文档链接更新由实际支持的 LOKit 选项和固定源码补丁禁用;这不构成操作系统沙箱。
+
+[提供方测试](../../../../packages/document/office-to-pdf/tests/provider.spec.ts)、[Loader 组合](../../../../packages/bundle/web-app/tests/document-preview.spec.ts)和[浏览器场景](../../../../apps/web/tests/document-preview.e2e.ts)负责 DSH 生命周期、授权与展示证据。引擎验收还需要真实 DOC/DOCX/XLS/XLSX/PPT/PPTX 转换、外部 PDF 文本、字体、页数与图片检查、迁移安装和损坏包拒绝,以及同输入的原生/WASM 性能样本。模拟辅助进程和微基准不能证明这些结果。各目标的真实构建机与 Desktop 安装包需要独立验收;一个本地架构成功不能证明整个矩阵。

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.md
-2026-09-11-sandboxed-node-ptc-runtime.md: 242b3cfedb49b7ab60c47c6ee03005ddb821bb33
-2026-09-11-sandboxed-node-ptc-runtime.zh.md: d1ab47550e49129ee3e1fefbdfa54cda397374a3
+2026-09-11-sandboxed-node-ptc-runtime.md: f851775460c5bd01760d31552bde8c691807ec06
+2026-09-11-sandboxed-node-ptc-runtime.zh.md: 94f3a37eb702a97161c1d08cec531fcffb3460f2

+ 2 - 0
.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.md

@@ -14,6 +14,8 @@ The [PTC foundation](../feature/2026-06-15-ptc.md) remains responsible for regis
 
 `dsh-ptc-runtime-node` runs each program in one fresh Node process. The host resolves execution choices, confines the launch through the same `ctx.sandbox` provider as Bash, and gives process lifetime to `ctx.subprocess`. The child evaluates erasable TypeScript with direct Node APIs, an empty model environment and host-provided asynchronous bindings. No worker or persistent kernel remains inside this provider.
 
+The host preserves `ELECTRON_RUN_AS_NODE` for child startup; the bootstrap removes it from the native environment before evaluation, and model-visible `process.env` stays empty. Nested Electron launches require their own explicit Node-mode selection. Desktop uses Electron as its Node executable; removing this selector launches Electron's application path instead of the PTC bootstrap. Sandbox permission changes cannot repair that launch mismatch. The macOS Desktop regression uses real Electron to verify binding writes, direct workspace writes, and rejection of writes outside the workspace under restricted policy. It requires an installed Electron binary; ordinary runtime tests cover environment filtering without that dependency.
+
 ### Resolved inputs and policy
 
 `PtcRuntime.resolve(request)` validates supported options and supplies a complete `PtcRunSpec`; `run(spec)` does not introduce defaults. PTC passes the calling Session's cwd and resolved standing policy. Direct runtime callers receive deployment defaults through the same resolver. The filesystem and subprocess providers share one execution world, and bootstrap paths cross through the filesystem's explicit host-file mapping or a configured preinstalled bootstrap.

+ 2 - 0
.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md

@@ -14,6 +14,8 @@ Node worker 隔离 JavaScript 状态,但不应用调用 Session 的 OS 沙箱
 
 `dsh-ptc-runtime-node` 在一个全新 Node 进程中运行每个程序。Host 解析执行选择,通过与 Bash 相同的 `ctx.sandbox` 提供方约束启动,并将进程生命周期交给 `ctx.subprocess`。子进程以直接 Node API、空模型环境和 Host 提供的异步绑定求值可擦除 TypeScript。本提供方不保留 worker 或持久内核。
 
+Host 在子进程启动时保留 `ELECTRON_RUN_AS_NODE`;bootstrap 在求值前将其从原生环境中删除,模型可见的 `process.env` 仍为空。嵌套启动 Electron 需要自行显式选择 Node 模式。桌面端使用 Electron 作为 Node 可执行文件;删除此选择变量会启动 Electron 应用路径,而不是 PTC bootstrap。更改沙箱权限无法修复这一启动模式不匹配。macOS 桌面端回归测试使用真实 Electron 验证绑定写入、直接工作区写入以及受限策略对工作区外写入的拒绝。该测试需要已安装的 Electron 二进制文件;普通运行时测试无需此依赖即可覆盖环境过滤。
+
 ### 已解析输入与策略
 
 `PtcRuntime.resolve(request)` 验证支持的选项并补全 `PtcRunSpec`;`run(spec)` 不引入默认值。PTC 传入调用 Session 的 cwd 与已解析常设策略。直接运行时调用方通过同一解析器取得部署默认值。文件系统与子进程提供方共享一个执行世界,bootstrap 路径通过文件系统的显式宿主文件映射或配置的预安装 bootstrap 传递。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-11-windows-directory-installation.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-11-windows-directory-installation.md
+2026-09-11-windows-directory-installation.md: 8208c6cc4b28de8d1ab947cb8e6afba7bfa4f673
+2026-09-11-windows-directory-installation.zh.md: d337397e8ea654d14cccd0839460763b66a19457

+ 29 - 0
.agents/notes/implemented/architecture/2026-09-11-windows-directory-installation.md

@@ -0,0 +1,29 @@
+# Agent Note: Replace Windows application directories after staging
+
+Status: implemented
+
+English | [中文](2026-09-11-windows-directory-installation.zh.md)
+
+## Problem
+
+The Desktop distribution contains thousands of small files. Extracting to the temporary directory, copying every file to the installation, and deleting the temporary tree repeats filesystem work. A 616,701,792-byte, 11,735-file payload took 101.235, 79.203, and 114.391 seconds through those three phases on Windows; copying accounted for 70.296, 56.688, and 85.281 seconds. These component measurements exclude old-version removal, registration, startup, and cache clearing.
+
+## Decision
+
+The [NSIS adapter](../../../../apps/desktop/scripts/windows-directory-installer.mjs) retains electron-builder's installer, signed uninstaller generation, registration, shortcuts, and updater cache. It stages the complete new application beside the destination before stopping the old application. The [directory transaction](../../../../apps/desktop/scripts/installer-directories.nsh) renames the old directory to a unique backup, renames the new directory to the destination, and removes the backup before launch. Both renames stay on the destination volume. This replaces the copy-based installation decision in the [packaging note](2026-08-25-electron-desktop-packaging-and-updates.md).
+
+The installer embeds its pinned 7-Zip command-line tool, signs its private copy for signed releases, and includes its license texts. Windows records the copied runtime inventory after its executable resources have been signed. A nonzero extraction exit leaves the old application intact. Explicit cleanup before upstream installer exits also covers silent cancellation. Same-path upgrades bypass the old uninstaller so it cannot delete the rollback copy or registration prematurely. Different-path and installation-scope migrations retain electron-builder's old-uninstaller behavior; the directory rollback does not undo those uninstall operations.
+
+## Consequences
+
+Directory cleanup, including uninstallation, uses extended-length Windows paths for deeply nested dependencies and backup suffixes. Failed promotion attempts restore the renamed old directory. If another process prevents restoration, the complete backup remains available. Abrupt process termination or power loss can leave staging or backup directories; the installer does not claim crash-atomic replacement across two renames. The transaction changes installation files, not the external Desktop plugin profile or product data.
+
+## Alternatives considered
+
+- **Direct extraction over the running installation.** The existing locked-file probe demonstrates that `Nsis7z::Extract` can retain an old file without setting the NSIS error flag. Staging through a tool with an exit status avoids accepting a mixed installation.
+- **Delete the old version before extraction.** A corrupt archive or failed write would remove the only usable version before replacement is ready.
+- **Put the Host in ASAR.** Native modules, package resolution, and subprocess paths require separate qualification; directory replacement preserves the resource layout.
+
+## Verification
+
+The [native directory smoke](../../../../apps/desktop/scripts/smoke-installer-directories.ps1) calls production macros against private directories. It covers fresh installation, obsolete-file removal on upgrade, locked-directory failure, restoration after the second rename fails, corrupt archives, and cancellation cleanup. Template tests pin staging before shutdown and promotion before registration. The signed 0.1.5-rc.2 installer passed fresh installation, same-path upgrade with obsolete-file removal, locked-file failure preserving the old installation, installation-location migration, and complete uninstallation on Windows. Each installation check compared all 11,736 payload files against the packaged tree and verified executable/uninstaller signatures and registration. The installed runtime passed native dependency, Web frontend, external plugin route, and actual Electron window startup/exit checks. The final installer sample took 38.0 seconds for fresh installation and 27.5 seconds for same-path upgrade, excluding verification and launch; caches were not cleared. These are full-installer observations, separate from the component baseline above. Hosted updater download and upgrades from historical published releases remain release qualification.

+ 29 - 0
.agents/notes/implemented/architecture/2026-09-11-windows-directory-installation.zh.md

@@ -0,0 +1,29 @@
+# Agent Note: 暂存完成后替换 Windows 应用目录
+
+Status: implemented
+
+[English](2026-09-11-windows-directory-installation.md) | 中文
+
+## 问题
+
+Desktop 分发包包含数千个小文件。先解压到临时目录,再将每个文件复制到安装位置,最后删除临时树,会重复执行文件系统操作。Windows 上一份 616,701,792 字节、11,735 个文件的载荷经过这三个阶段分别耗时 101.235、79.203 和 114.391 秒,其中复制占 70.296、56.688 和 85.281 秒。这些组件测量不包括旧版卸载、注册、启动,也没有清空缓存。
+
+## 决策
+
+[NSIS 适配器](../../../../apps/desktop/scripts/windows-directory-installer.mjs)保留 electron-builder 的安装器、签名卸载器生成、注册、快捷方式和更新缓存。它在停止旧应用前,将完整新应用暂存到目标目录旁。[目录事务](../../../../apps/desktop/scripts/installer-directories.nsh)把旧目录改名为唯一备份,将新目录改名为正式目标,并在启动前删除备份。两次改名都位于目标卷。这替代了[打包记录](2026-08-25-electron-desktop-packaging-and-updates.zh.md)中基于复制的安装决策。
+
+安装器嵌入固定版本的 7-Zip 命令行工具,为签名发布签署其私有副本,并携带许可证文本。Windows 在复制后的运行时可执行资源签名完成后记录文件清单。解压退出码非零时,旧应用保持完整。上游安装器退出前显式清理目录,也覆盖静默取消。同路径升级跳过旧卸载器,避免其提前删除回滚副本或注册信息。不同路径和安装范围迁移保留 electron-builder 的旧卸载器行为;目录回滚不会撤销这些卸载操作。
+
+## 影响
+
+目录清理(包括卸载)使用 Windows 扩展长度路径,覆盖深层依赖与备份后缀。新目录替换失败时,安装器尝试恢复已改名的旧目录。如果另一个进程阻止恢复,完整备份仍保留。进程被强制结束或断电可能留下暂存或备份目录;安装器不承诺跨两次改名的崩溃原子性。事务只改变安装文件,不改变外部 Desktop 插件 profile 或产品数据。
+
+## 考虑过的替代方案
+
+- **直接覆盖运行中的安装目录。** 现有文件占用探针证明,`Nsis7z::Extract` 可能保留旧文件而不设置 NSIS 错误标志。使用具有退出状态的工具进行暂存,可以避免接受混合版本安装。
+- **解压前删除旧版本。** 压缩包损坏或写入失败时,会在替换就绪前移除唯一可用版本。
+- **把 Host 放入 ASAR。** 原生模块、包解析和子进程路径需要单独验证;目录替换保留资源布局。
+
+## 验证
+
+[原生目录 smoke](../../../../apps/desktop/scripts/smoke-installer-directories.ps1)在私有目录中调用生产宏。它覆盖首次安装、升级时删除过时文件、目录占用失败、第二次改名失败后的恢复,损坏归档,以及取消清理。模板测试固定暂存早于关闭应用、正式替换早于注册。签名的 0.1.5-rc.2 安装器在 Windows 上通过了首次安装、同路径升级并移除过时文件、文件占用失败后保留旧安装、变更安装位置及完整卸载。每次安装检查都将全部 11,736 个载荷文件与打包目录比对,并验证程序、卸载器签名及注册信息。已安装运行时通过了原生依赖、Web 前端、外部插件路由及实际 Electron 窗口启动和退出检查。最终安装器的一次样本中,首次安装耗时 38.0 秒,同路径升级耗时 27.5 秒,不包含校验和启动,未清空缓存。这些完整安装器观测与上面的组件基线分别记录。线上更新下载和从历史已发布版本升级仍属于发布验收。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.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-current-profile-plugin-management.md
+2026-09-14-current-profile-plugin-management.md: 8d4d4c40f033e490f09ebf8d367e1fb621037a6d
+2026-09-14-current-profile-plugin-management.zh.md: a8dce606d00321bd96e643a9ba842581c961d6c1

+ 35 - 0
.agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.md

@@ -0,0 +1,35 @@
+# Agent Note: Current-profile plugin management shares CLI transactions
+
+Status: implemented
+
+English | [中文](2026-09-14-current-profile-plugin-management.zh.md)
+
+## Problem
+
+Web and agent controls need to change a running profile without creating an independent package installer or overwriting user-authored YAML. A file watcher can otherwise load the intermediate manifest written during package installation, or report success before removed plugins finish releasing resources.
+
+## Decision
+
+[Plugin Manager](../../../../packages/boot/plugin-manager/README.md) and `dsh plugin` call the same asynchronous package operations. The launcher supplies `ctx.profileContext` as data: profile and resolution locations, startup bundles and invocation overlays. Shared functions compose the current files; this interface contains no callbacks or mutation methods. CLI and service mutations hold the profile manifest's writer lock; [DSH HMR](../../../../packages/boot/hmr/README.md) serializes module replacement, Include refresh, profile recomposition and manager configuration changes through one queue. HMR registers the profile watches during its own initialization, then waits for application readiness before processing edits. Manifest notifications compare only the ordered bundle list; dependency-only changes do not trigger a configuration reload. The final YAML composition controls whether HMR runs; the launcher installs no fallback. Pnpm runs outside `hmr.runExclusive()`; only configuration changes and Loader updates enter that queue. HMR does not acquire the package writer lock, so installation cannot block unrelated file-driven configuration changes. Each generation re-reads the manifest, bundle layers and user patches while retaining invocation overlay precedence.
+
+Configuration watches use Chokidar write stabilization by default. Its ordinary change handler discards a second event within 50 ms, so a write immediately after activation can leave the previous bundle running. Stabilized delivery observes the final file instead; file-driven updates pay the stability delay, while direct manager transactions do not. A regression feeds consecutive changes through Chokidar’s real normalization and verifies both applied states.
+
+Profile files remain the persisted state: entry toggles edit only `disabled` in the last override matching the entry id and any module-name assertion, appending when none matches, and bundle toggles edit the ordered string list. Dependency updates do not reactivate retained disabled bundles. A service removal first applies the composition without the bundle and waits for old fibers to finish before deleting the dependency. Saved configuration, pnpm completion and runtime activation have separate outcomes; a failed removal preserves the actual partial state and a diagnostic path, while a failed or cancelled installation restores the profile files it snapshotted.
+
+This extends the [profile bundle composition decision](2026-08-05-profile-plugin-bundles.md). Profiles without HMR keep their process composition, and Desktop package management remains shell-owned. Web controls and explicitly enabled agent tools call the same service. Management operations return results to callers without adding messages to live Agents. The agent tool is enabled in Creator mode and disabled by default in the base bundle and other shipped presets. The browser-only worker preview has no host package installer; its module-proxy table refuses `execa` calls explicitly while retaining the management module for inventory discovery.
+
+CLI calls inherit the terminal and authentication environment; service calls retain the subprocess credential scrub and bounded diagnostics. Management records carry error codes and parameters for locale-owned Web presentation. Reconciliation compares entry identity, fiber identity, configuration and diagnostics before and after updating: unchanged inactive entries remain warnings, while newly affected failures reject the operation. Explicit enablement targets must activate.
+
+Build approvals update pnpm 11's unresolved `allowBuilds` entries under the same profile lock and preserve unrelated YAML. They persist by exact package name rather than applying an unrestricted script policy. The retry accepts only names still pending, so stale requests cannot override a subsequent denial. Package cleanup leaves the approval settings intact; a later retry can use them without retaining partially installed dependencies. The service reports policy-only changes. Agent tools may grant approval on the user's behalf; their instructions require explicit conversational consent, while the service validates only pending package names. Approval rejects anchors and aliases inside `allowBuilds` to prevent shared YAML nodes from changing unrequested permissions.
+
+## Alternatives considered
+
+**Spawning another dsh process from the service.** This duplicates lifecycle coordination and cannot establish that the current Loader finished unloading before pnpm removes files. Sharing the operation module retains one implementation while letting each caller own its presentation.
+
+**Restoring existing packages after failure.** Package versions, dependency trees and install-script effects cannot be reconstructed reliably from the previous manifest, so existing dependencies and successful installations whose activation fails remain in place. A failed or cancelled installation restores only the manifest and lockfile text snapshotted before pnpm ran ([guided plugin installation](2026-09-15-guided-plugin-installation.md)); downloaded files stay until the next package operation prunes them.
+
+**Source-module hot replacement for package updates.** Configuration changes can reuse the loaded module cache, whereas replacing installed JavaScript needs a new process generation. Replacing an existing dependency reports a required restart.
+
+## Consequences
+
+The same profile can be managed through CLI, Web and tools, with file-level coordination and preserved patch precedence. Operators must repair failed package operations using the reported files and diagnostics. A startup process must stop before its loaded packages can be removed through CLI. Management components remain protected against service-initiated removal.

+ 35 - 0
.agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.zh.md

@@ -0,0 +1,35 @@
+# Agent Note:当前 profile 插件管理共享 CLI 事务
+
+Status: implemented
+
+[English](2026-09-14-current-profile-plugin-management.md) | 中文
+
+## 问题
+
+Web 和 Agent 控件需要修改运行中的 profile,同时避免另建包安装器或覆盖用户编写的 YAML。文件监听器可能读到安装期间写入的中间 manifest,也可能在被删除插件尚未释放资源时报告成功。
+
+## 决策
+
+[插件管理器](../../../../packages/boot/plugin-manager/README.zh.md)与 `dsh plugin` 调用同一套异步包操作。launcher 通过纯数据 `ctx.profileContext` 提供 profile 与解析位置、启动时组合包和调用级 overlay。共享函数组合当前文件;该接口不包含回调或修改方法。CLI 与 service 修改持有 profile manifest 的写锁;[DSH HMR](../../../../packages/boot/hmr/README.zh.md) 通过同一队列串行执行模块替换、Include 刷新、profile 重新组合与管理器配置变更。HMR 在自身初始化时注册 profile 监听,等待应用就绪后再处理编辑。manifest 通知只比较有序组合包列表;仅依赖字段变化不会触发配置重载。最终 YAML 组合决定是否运行 HMR,启动器不安装回退实例。pnpm 在 `hmr.runExclusive()` 外执行;只有配置变更和 Loader 更新进入该队列。HMR 不获取包操作写锁,因此安装不会阻塞其他由文件变化触发的配置更新。每次重载重新读取 manifest、组合包层与用户 patch,同时保留调用级 overlay 的优先级。
+
+配置监听默认使用 Chokidar 写入稳定检测。普通变化处理器会丢弃 50 ms 内的第二个事件,因此激活后立即再次写入可能让之前的组合包继续运行。稳定后交付事件会观察最终文件;文件驱动的更新承担稳定等待,直接管理器事务则不需要。回归测试通过 Chokidar 的真实规范化路径交付连续变化,验证两个状态均被应用。
+
+profile 文件保持为持久状态:条目开关只修改最后一条符合条目 id 及模块名称断言的覆盖项中的 `disabled`,没有匹配项时追加,组合包开关修改有序字符串列表。更新依赖不会重新激活保留的已停用组合包。service 删除组合包时,先应用去掉该组合包的配置,等待旧 fiber 完成卸载后再删除依赖。已保存配置、pnpm 完成状态与运行时激活分别报告;失败的删除保留实际的部分状态与诊断路径,失败或被取消的安装则恢复它快照的 profile 文件。
+
+这扩展了[profile 组合包决策](2026-08-05-profile-plugin-bundles.zh.md)。startup profile 保留进程组合,Desktop 包管理仍由 shell 持有。Web 控件与显式启用的 Agent 工具调用同一 service。管理操作向调用方返回结果,不向存活 Agent 添加消息。创造模式启用该 Agent 工具;base 组合包和其他内置预设默认禁用。纯浏览器 worker 预览没有宿主包安装器;其模块代理表明确拒绝 `execa` 调用,同时保留管理模块用于清单发现。
+
+CLI 调用继承终端和认证环境;service 调用保留子进程凭据清理与有界诊断。管理结果提供错误码和参数,由 Web 词典呈现文案。重载前后比较 entry、fiber、配置与诊断:未变化的已有故障保留为警告,本次影响到的新故障使操作失败。显式启用的目标必须成功激活。
+
+构建审批在同一个 profile 写锁内更新 pnpm 11 尚未决定的 `allowBuilds` 条目,并保留无关 YAML。授权按准确包名持久化,不采用无条件允许脚本的策略。重试只接受仍在待审批列表中的包名,因此过期请求不能覆盖后续拒绝。包清理保留审批设置,后续重试可以复用授权而不必保留部分安装的依赖。service 报告仅涉及策略的变化。Agent 工具可以代表用户授权;工具说明要求用户在对话中明确同意,而 service 仅验证待审批包名。审批拒绝 `allowBuilds` 内的锚点和别名,避免共享 YAML 节点改变未请求的权限。
+
+## 考虑过的替代方案
+
+**由 service 启动另一个 dsh 进程。** 这会重复生命周期协调,也无法确认当前 Loader 已完成卸载后才让 pnpm 删除文件。共享操作模块保留单一实现,同时让调用方持有各自的呈现方式。
+
+**失败后恢复已有包。** 无法仅凭原 manifest 可靠重建包版本、依赖树和安装脚本的副作用,因此已有依赖及安装成功但激活失败的包保留原处。失败或被取消的安装只恢复 pnpm 运行前快照的 manifest 与 lockfile 文本([引导式插件安装](2026-09-15-guided-plugin-installation.zh.md));已下载文件保留到下一次包操作清理为止。
+
+**包更新时热替换源码模块。** 配置变化可以复用已加载模块缓存,替换已安装 JavaScript 则需要新的进程。替换已有依赖会报告需要重启。
+
+## 影响
+
+同一 profile 可以通过 CLI、Web 和工具管理,操作通过文件锁协调并保留 patch 优先级。运维人员需要根据报告的文件和诊断修复失败的包操作。startup 进程必须先停止,才能通过 CLI 删除其加载的包。管理组件受保护,不能通过 service 删除。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.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-independent-libreoffice-kit.md
+2026-09-14-independent-libreoffice-kit.md: 13efc41e56a4810273fbb6d9314eeda6053dbe92
+2026-09-14-independent-libreoffice-kit.zh.md: 720ca2ba3f57546825ae14b5b5f0b086fbb7f287

+ 29 - 0
.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md

@@ -0,0 +1,29 @@
+# Agent Note: Independent LibreOffice kit ownership
+
+Status: implemented
+
+English | [中文](2026-09-14-independent-libreoffice-kit.zh.md)
+
+## Problem
+
+LibreOffice compilation, source patches, platform qualification, and large binary releases have a different maintenance cycle from Harness plugins. Keeping them in the application workspace expands routine CI and couples engine repairs to monorepo package rules.
+
+## Decision
+
+The `deepseek-harness/libreoffice-kit` repository owns the reusable `@deepseek-ai/libreoffice-kit` Node API, its Worker, font handling, engine selection, build recipes, patches, tests, and releases. The API has no Cordis dependency. Harness owns the adapter from its `OfficeToPdf` service to this API, Session authorization, conversion lifetime, transport, Web UI, and application packaging.
+
+Kit releases run independently of Harness releases. The kit repository qualifies and publishes the Node API and engine npm packages at a shared version, starting at `0.0.1`. Harness consumes an exact npm version and commits its dependency resolution in `pnpm-lock.yaml`; Harness releases neither build nor publish kit packages.
+
+The upstream API declares platform engines as optional dependencies. The [platform engine decision](2026-09-15-platform-office-engines.md) supersedes the original required-WASM fallback policy. Desktop installs the kit as an external npm dependency. Python sidecars keep the Worker, selected engine, and their dependency closure on the real filesystem, outside the executable’s virtual filesystem. Conversion requires neither downloads nor GitHub credentials.
+
+The kit repository owns engine qualification and corresponding source materials. MPL-2.0 declarations, source availability, and redistribution notices accompany the API and engines; Harness retains these materials when packaging them. The notices check accepts the exact API, WASM, macOS ARM64/x64, and Windows ARM64/x64 package identities only at MPL-2.0; unrelated packages and changed license terms still reject.
+
+## Alternatives considered
+
+**Co-locate the public API and Core build in Harness.** This synchronizes source changes but makes application maintenance own long engine builds and special package rules. The Cordis provider is the application integration point; the reusable conversion API belongs with its engine tests.
+
+**Prepare GitHub Release archives before installation.** This requires separate authentication, hashes, decompression, workspace overrides, and distribution repacking. Published npm packages use the application's ordinary dependency installation and platform selection.
+
+## Consequences
+
+Changing the kit requires qualifying a release in its own repository and updating the Harness dependency versions and lockfile. A missing required npm package fails installation; Harness does not compile an engine to recover. Native and WASM conversion smokes validate the installed packages, while Desktop and Python checks cover application packaging.

+ 29 - 0
.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md

@@ -0,0 +1,29 @@
+# Agent Note: 独立 LibreOffice kit 的归属
+
+Status: implemented
+
+[English](2026-09-14-independent-libreoffice-kit.md) | 中文
+
+## Problem
+
+LibreOffice 编译、源码补丁、平台资格验证和大型二进制发布的维护周期不同于 Harness 插件。将它们放在应用工作区会扩大常规 CI,并让引擎修复依赖 monorepo 包规则。
+
+## Decision
+
+`deepseek-harness/libreoffice-kit` 仓库维护可复用的 `@deepseek-ai/libreoffice-kit` Node API、Worker、字体处理、引擎选择、构建配方、补丁、测试和发布。API 不依赖 Cordis。Harness 负责将自己的 `OfficeToPdf` 服务适配到此 API,以及 Session 授权、转换生命周期、传输、Web UI 和应用打包。
+
+kit 发布流程独立于 Harness 发布流程。kit 仓库验证并以统一版本发布 Node API 和引擎 npm 包,起始版本为 `0.0.1`。Harness 消费精确的 npm 版本,并在 `pnpm-lock.yaml` 中提交依赖解析结果;Harness 发布既不构建也不发布 kit 包。
+
+上游 API 将平台引擎声明为可选依赖。[平台引擎决策](2026-09-15-platform-office-engines.zh.md)取代最初强制携带 WASM 回退引擎的策略。Desktop 将 kit 作为外部 npm 依赖安装。Python sidecar 将 Worker、所选引擎及其依赖闭包保留在真实文件系统中,位于可执行文件的虚拟文件系统之外。转换无需下载或 GitHub 凭据。
+
+kit 仓库负责引擎资格验证和对应源码材料。API 和引擎携带 MPL-2.0 声明、可访问源码及再分发声明;Harness 打包时保留这些材料。再分发声明检查仅在许可为 MPL-2.0 时接受 API、WASM、macOS ARM64/x64 和 Windows ARM64/x64 的精确包标识;无关包和改变后的许可条款仍被拒绝。
+
+## Alternatives considered
+
+**将公开 API 和 Core 构建放在 Harness。** 这能同步源码变更,却让应用维护承担耗时的引擎构建和特殊包规则。Cordis provider 是应用集成点;可复用转换 API 应与引擎测试放在一起。
+
+**安装前准备 GitHub Release 归档。** 这需要单独的鉴权、哈希、解压、工作区 overrides 和分发重打包。已发布的 npm 包使用应用常规的依赖安装与平台选择流程。
+
+## Consequences
+
+更新 kit 需要在其独立仓库验证并发布新版本,再更新 Harness 依赖版本和锁文件。必需的 npm 包缺失时安装失败;Harness 不通过编译引擎恢复。原生和 WASM 转换冒烟验证已安装的包,Desktop 与 Python 检查覆盖应用打包。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.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-15-bounded-office-conversion.md
+2026-09-15-bounded-office-conversion.md: 78a2fc46d97500575262d2263bf5549037872264
+2026-09-15-bounded-office-conversion.zh.md: 88cb15c8cbf2200a9365b9a42cb5802c1bfb65d8

Некоторые файлы не были показаны из-за большого количества измененных файлов