Merge remote-tracking branch 'origin/master' into worktree/present-changes-dedup
This commit is contained in:
commit
c19d1f12fb
2234 files changed
+68687
-19141
No files matched your search
@@ -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: 6872de0a1390fa5a1707b9f464c9e0edeb2da4be
|
||||
README.zh.md: ced81f77fa904ad0b5cb5219383ef651d87392b8
|
||||
@@ -16,7 +16,7 @@ Every Agent Note has two axes, both encoded in its **path** — `{lifecycle}/{cl
|
||||
|
||||
The date in the filename is when the topic was **first proposed** (per git history). Cross-references between Agent Notes use relative markdown links (`[topic](../../implemented/architecture/2026-…-….md)`) — never bare prose or numbers — so they are mechanically checkable and survive moves between folders.
|
||||
|
||||
The active lifecycle tree is the working inventory: browse its lifecycle/class folders or search the repository. Do not add a centralized `INDEX.md`; the [no-index Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.md) owns the rationale. Low-future-value implemented records move to the separate frozen [`archived/`](archived/AGENTS.md) tree described below.
|
||||
The active lifecycle tree is the working inventory: browse its lifecycle/class folders or search the repository. Do not add a centralized `INDEX.md`; the [no-index Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.md) owns the rationale. Implemented records with historical decision value but little future guidance move to the separate frozen [`archived/`](archived/AGENTS.md) tree described below.
|
||||
|
||||
## Classification
|
||||
|
||||
@@ -35,17 +35,17 @@ The `architecture` / `process` line: **architecture** is about the source we shi
|
||||
|
||||
## Archiving and deletion
|
||||
|
||||
Archive an implemented Agent Note when the shipped decision is complete and its rationale is unlikely to guide future work. Keep it active when its alternatives, ownership boundary, negative guarantee, durable or wire semantics, security rule, or reintroduction condition remains useful. Never archive a proposed note: reject an obsolete proposal. Keep a rejected note only while it prevents a plausible mistake; otherwise delete its English, Chinese, and sidecar files together. Use the calibrated [`dsh-archive-agent-notes`](../skills/dsh-archive-agent-notes/SKILL.md) workflow rather than word count, age, or a target quota.
|
||||
Delete an implemented Agent Note that only describes small UI adjustments or purely mechanical changes; delete its English, Chinese, and sidecar files together and repair or remove inbound links. A local bug fix, performance change, new capability, or substantive behavior decision does not qualify merely because its implementation is small. Archive other implemented notes only when the shipped decision is complete, its rationale is unlikely to guide future work, and the record still has historical decision value. Keep it active when its alternatives, ownership boundary, negative guarantee, durable or wire semantics, security rule, or reintroduction condition remains useful. Never archive a proposed note: reject an obsolete proposal. Keep a rejected note only while it prevents a plausible mistake; otherwise delete its English, Chinese, and sidecar files together. Use the calibrated [`dsh-archive-agent-notes`](../skills/dsh-archive-agent-notes/SKILL.md) workflow rather than word count, age, or a target quota.
|
||||
|
||||
The archive is path-encoded as `archived/{class}/yyyy-mm-dd-topic-title.md`; `implemented` is deliberately absent because only implemented notes can enter it. An archival change moves the complete English/Chinese/sidecar triplet, retains `Status: implemented`, inserts the same `Archived: YYYY-MM-DD` line immediately below that status in both language files, re-records the sidecar, and repairs or deletes inbound links. These are the only permitted content changes during archival.
|
||||
The archive is path-encoded as `archived/{class}/yyyy-mm-dd-topic-title.md`; `implemented` is deliberately absent because only implemented notes can enter it. An archival change moves the complete English/Chinese/sidecar triplet, retains `Status: implemented`, inserts the same `Archived: YYYY-MM-DD` line immediately below that status in both language files, re-records the sidecar, and repairs or deletes inbound links. These are the only permitted content changes during archival. Existing title punctuation, blank-line layout, and language-switcher wording do not block archival; preserve them with the body.
|
||||
|
||||
Once sealed, every archived triplet is permanently frozen. Do not edit, translate, reformat, update, move, or delete it, and do not treat it as authority for current behavior. Documentation gates skip archived sources, including their outbound links; active prose may still link into an archived note when it intentionally cites history. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) enforces the closed class tree, complete triplets, archive metadata, sidecar hashes, and the append-only frozen-content manifest. The [archive-policy Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.md) owns the rationale.
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
文件名中的日期是该主题**首次提出**的时间(以 git 历史为准)。Agent Note 之间的交叉引用使用相对 Markdown 链接(`[topic](../../implemented/architecture/2026-…-….md)`),从不使用纯文字或编号,这样既可机械检查,也能在文件夹间移动时保持有效。
|
||||
|
||||
活跃生命周期目录树就是工作清单:浏览其生命周期/类别文件夹,或搜索仓库即可。请勿添加集中式 `INDEX.md`;设计理由见[不设索引的 Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.zh.md)。未来指导价值较低的已实施记录会移至下文所述、单独冻结的 [`archived/`](archived/AGENTS.md) 目录树。
|
||||
活跃生命周期目录树就是工作清单:浏览其生命周期/类别文件夹,或搜索仓库即可。请勿添加集中式 `INDEX.md`;设计理由见[不设索引的 Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.zh.md)。具有历史决策价值但未来指导价值较低的已实施记录会移至下文所述、单独冻结的 [`archived/`](archived/AGENTS.md) 目录树。
|
||||
|
||||
<a id="classification"></a>
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
|
||||
## 归档与删除
|
||||
|
||||
当一份 implemented Agent Note 记录的交付决策已经完整落地,且其决策依据不太可能再指导未来工作时,将其归档。如果其中的备选方案、归属边界、否定性保证、持久化语义或协议语义、安全规则,或者重新引入条件仍有价值,则继续作为活跃记录保留。绝不归档 proposed Agent Note:过时的提案应转为 rejected。仅当 rejected Agent Note 仍能避免一种可能发生的错误时保留;否则一并删除其英文、中文和伴随记录文件。请使用经过校准的 [`dsh-archive-agent-notes`](../skills/dsh-archive-agent-notes/SKILL.md) 工作流,不要根据字数、存续时间或目标配额来判断。
|
||||
仅描述小型 UI 调整或纯机械变更的 implemented Agent Note 直接删除;一并删除英文、中文和伴随记录文件,并修复或移除入站链接。局部 bug 修复、性能变更、新能力或实质行为决策,不会仅因实现规模小就符合删除条件。其他已实施记录只有在交付决策完整落地、其依据不太可能再指导未来工作,但仍具有历史决策价值时才归档。如果其中的备选方案、归属边界、否定性保证、持久化语义或协议语义、安全规则,或者重新引入条件仍有价值,则继续作为活跃记录保留。绝不归档 proposed Agent Note:过时的提案应转为 rejected。仅当 rejected Agent Note 仍能避免一种可能发生的错误时保留;否则一并删除其英文、中文和伴随记录文件。请使用经过校准的 [`dsh-archive-agent-notes`](../skills/dsh-archive-agent-notes/SKILL.md) 工作流,不要根据字数、存续时间或目标配额来判断。
|
||||
|
||||
归档路径编码为 `archived/{class}/yyyy-mm-dd-topic-title.md`;其中有意省略 `implemented`,因为只有 implemented Agent Note 可以进入归档。归档变更会移动完整的英文、中文和伴随记录三个文件,保留 `Status: implemented`,在两种语言的文件中紧接该状态行插入相同的 `Archived: YYYY-MM-DD` 行,重新记录伴随记录,并修复或删除入站链接。归档时只允许对内容做这些更改。
|
||||
归档路径编码为 `archived/{class}/yyyy-mm-dd-topic-title.md`;其中有意省略 `implemented`,因为只有 implemented Agent Note 可以进入归档。归档变更会移动完整的英文、中文和伴随记录三个文件,保留 `Status: implemented`,在两种语言的文件中紧接该状态行插入相同的 `Archived: YYYY-MM-DD` 行,重新记录伴随记录,并修复或删除入站链接。归档时只允许对内容做这些更改。 既有标题标点、空行布局和语言切换栏措辞不阻止归档;与正文一起保留。
|
||||
|
||||
封存后,每组归档文件都永久冻结。禁止编辑、翻译、重新格式化、更新、移动或删除,也不得将其视为当前行为的权威依据。文档门禁会跳过归档源文件,包括其中的出站链接;当活跃文档有意引用历史时,仍可链接到归档 Agent Note。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 强制执行封闭的类别目录树、完整的三文件配对、归档元数据、伴随记录 hash,以及仅追加的冻结内容 manifest(元数据清单)。[归档政策 Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.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
@@ -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: 6f0e9598903d922e4b765d785be6dd3aa79ce93c
|
||||
2026-09-09-desktop-in-place-profile.zh.md: bec79b3b9d4ce5439664959b16695932be4af4cd
|
||||
2026-09-09-desktop-in-place-profile.md: 57dabc0d03c999b90dc18baa9d833aa90320863a
|
||||
2026-09-09-desktop-in-place-profile.zh.md: 2b57c49ce45b1937552596cf4f59e0251b2d5774
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
# Agent Note: Modify the Desktop profile in place
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-09-17
|
||||
|
||||
English | [中文](2026-09-09-desktop-in-place-profile.zh.md)
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
# Agent Note: 直接修改 Desktop profile
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-09-17
|
||||
|
||||
[English](2026-09-09-desktop-in-place-profile.md) | 中文
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-turn-duration-hour-unit.md
|
||||
2026-09-09-turn-duration-hour-unit.md: f0f5cb478dafa7938614bdeeec3a6de9f58aed8d
|
||||
2026-09-09-turn-duration-hour-unit.zh.md: c9c94eef2a88a0f58d5f5a05332b4b5034ef977a
|
||||
# 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
|
||||
@@ -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.
|
||||
@@ -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,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
|
||||
@@ -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,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 参数转发无法证明过滤器行为。
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.md
|
||||
2026-08-03-web-sticky-collapsible-headers.md: 6e8b4b94c9c6aa40bc7190e6705b84caa8189c52
|
||||
2026-08-03-web-sticky-collapsible-headers.zh.md: d4e1f69539db6870ae60d5c4b05cd52c7d129360
|
||||
# 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
|
||||
@@ -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.
|
||||
@@ -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 自动高度不属于本次改动,因为任意文档布局具有不同的测量与隔离要求。
|
||||
@@ -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",
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-09-composer-placeholder-whitespace.md
|
||||
2026-09-09-composer-placeholder-whitespace.md: c5d778f8fb1fdfc742636819a9ff27ddeeb0234e
|
||||
2026-09-09-composer-placeholder-whitespace.zh.md: b5f82bcd86f8567976bba96095d4de9d42bae9b1
|
||||
# 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
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
+9
-5
@@ -42,17 +42,19 @@ The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-ru
|
||||
|
||||
The deploy root includes `@deepseek-ai/dsh-mcp-client` as an explicitly supported custom-configuration plugin even though no shipped preset mounts it. An external config can therefore connect to user-supplied stdio and Streamable HTTP MCP servers and register their tools; the distribution does not carry those servers. The optional resource service provides MCP Resources; MCP Prompts remain unsupported. The executable and installed-wheel smokes start a temporary stdio server, discover its tool, and complete one model-requested call.
|
||||
|
||||
The [Office kit](2026-09-14-independent-libreoffice-kit.md) and its installed dependency tree live in `<executable-stem>-office/node_modules`, preserving complete package contents and helper permissions. Top-level `pkg.ignore` excludes kit packages from the SEA archive, including assets selected by dependency `files` fields; small dependencies shared with other consumers remain available in the VFS. The private Python bootstrap uses Node `registerHooks` to resolve the kit entry and its package manifest from the real directory. Native executables and URL Workers therefore retain their ordinary paths without modifying the kit or the public CLI. Missing required packages and missing installed package manifests fail the copy; absent optionals preserve the kit's engine selection.
|
||||
|
||||
### Build pipeline and artifacts
|
||||
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment decision](2026-08-28-subprocess-native-containment.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it with its target-specific Office directory into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment decision](2026-08-28-subprocess-native-containment.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
|
||||
|
||||
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) runs [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) on Linux/Windows x64 for pull requests and Linux ARM64 plus both macOS architectures for master pushes. The [public publication workflow](../../archived/process/2026-08-11-python-publication-workflow.md) calls it for all five targets; `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / macos-x64 (`macos-15-intel`) / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, verifies their package and executable origin, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests and master pushes additionally run a real DeepSeek two-turn tool smoke on their selected targets; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts `python-v<repository-version>` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows target and the explicit exclusion of Windows arm64.
|
||||
|
||||
### Python SDK distribution: two carriers, exe for production, node for development
|
||||
|
||||
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` is the client and `python/sdk-runtime` is the runtime carrier package. The runtime package's data directory holds the build-injected platform executable with its required `-rg` sidecar and optional macOS helper, plus the build-injected `runtime/node/` closure tree for repository development. `resolve_bundled_launch_args()` selects the executable by default; explicit `DSH_RUNTIME_MODE=node` runs `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. The node carrier never enters wheel distributions, and neither carrier uses a checked-in complete `cordis.yml`.
|
||||
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` is the client and `python/sdk-runtime` is the runtime carrier package. The runtime package's data directory holds the build-injected platform executable with its required `-rg` sidecar, `-office/` directory, and optional macOS helper, plus the build-injected `runtime/node/` closure tree for repository development. `resolve_bundled_launch_args()` selects the executable by default; explicit `DSH_RUNTIME_MODE=node` runs `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. The node carrier never enters wheel distributions, and neither carrier uses a checked-in complete `cordis.yml`.
|
||||
|
||||
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative `X.Y.Z` or prerelease version from the repository root `package.json`, converts prereleases to their PEP 440 spelling, and stages both packages at that wheel version, with `deepseek-harness-sdk` depending exactly on the matching `deepseek-harness-runtime-bin`. An optional `python-v<repository-version>` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. Staging also carries the repository license into both wheels and the third-party notices into the bundled runtime wheel. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe and its architecture-matched ripgrep sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, `py3-none-macosx_14_0_arm64`, `py3-none-macosx_14_0_x86_64`, or `py3-none-win_amd64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms. Both macOS tags deliberately declare a conservative 14.0 installation floor: the packaged Node 24 executables declare macOS 13.5, and the x64 PTY helper declares 10.7, but release validation proves the complete payload only against the 14.0 wheel claim rather than promising each observed component minimum as a supported host. The two macOS wheels remain architecture-specific; no universal2 wheel is published.
|
||||
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative `X.Y.Z` or prerelease version from the repository root `package.json`, converts prereleases to their PEP 440 spelling, and stages both packages at that wheel version, with `deepseek-harness-sdk` depending exactly on the matching `deepseek-harness-runtime-bin`. An optional `python-v<repository-version>` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. Staging also carries the repository license into both wheels and the third-party notices into the bundled runtime wheel. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe, its Office directory, and its architecture-matched ripgrep sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, `py3-none-macosx_14_0_arm64`, `py3-none-macosx_14_0_x86_64`, or `py3-none-win_amd64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms. Both macOS tags deliberately declare a conservative 14.0 installation floor: the packaged Node 24 executables declare macOS 13.5, and the x64 PTY helper declares 10.7, but release validation proves the complete payload only against the 14.0 wheel claim rather than promising each observed component minimum as a supported host. The two macOS wheels remain architecture-specific; no universal2 wheel is published.
|
||||
|
||||
The Python client launches the packaged `dsh` command with the selected profile (`sdk` by default), ordered patch files, and an explicit Harness home. The profile owns JSON-RPC serving and application composition; missing homes, profiles, bundles, patches, and server rows fail without an external complete-config fallback.
|
||||
|
||||
@@ -70,6 +72,8 @@ The verification surface has three tiers. Mechanism tier: the measured conclusio
|
||||
|
||||
Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disposes immediately, so a short-lived pipe aborts an in-flight turn — pipe-driven runs must keep stdin open until the turn ends.
|
||||
|
||||
The installed-wheel `sdk-office` scenario copies the target's complete payload to a private temporary directory, requires exactly one target backend (the declared native target, or WASM when no native target is declared), and converts DOCX once through the shipped SDK profile with that engine. It checks that the kit module resolves inside the relocated directory, that conversion reports the expected backend, and that the output contains PDF bytes. Wheel validation checks the declared engine assets and native helper execute bit; CI uploads the completed wheel, so nested helper permissions stay inside the ZIP archive.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Bare Node native SEA.** The injected main script must be a single CJS file, and the blob carries no filesystem and no module resolution, so a dynamic import of a bare specifier has nothing to resolve against; the only option is compiling plugins statically into the main script and registering them by hand — bypassing standard module resolution and hardcoding the plugin set, contrary to "configuration decides everything". The final route is in fact "the official SEA foundation + pkg's VFS/module-hook layer"; what was rejected is the bare use, not SEA itself.
|
||||
@@ -84,6 +88,6 @@ Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disp
|
||||
|
||||
## Consequences
|
||||
|
||||
**Bought**: zero-dependency single-file distribution on target platforms; plugin semantics strictly identical to running from source (the same real package tree, no transpilation, no registry); the serving interface, the plugin set, and the configuration all converge on two sources of truth — `cordis.yml` plus one dependency manifest; the exe and node carriers share one tree and one semantics, so development verification never waits for packaging; official Node binaries remove the patched-binary supply-chain concern.
|
||||
**Bought**: no system Node.js dependency on supported targets; plugin semantics strictly identical to running from source (the same real package tree, no transpilation, no registry); the serving interface, the plugin set, and the configuration all converge on two sources of truth — `cordis.yml` plus one dependency manifest; the exe and node carriers share one tree and one semantics, so development verification never waits for packaging; official Node binaries remove the patched-binary supply-chain concern.
|
||||
|
||||
**Paid**: artifacts on the order of 174MB with source entering the blob as-is (no bytecode obfuscation; a closed-source distribution requirement needs a separate evaluation); pkg's VFS/module-hook layer remains community-maintained (`@yao-pkg/pkg` is an exact-pinned, pnpm-patched root devDependency; upgrading re-records the patch and is an explicit change); `--sea` is one invocation per target (matching CI's one leg per platform; local multi-platform builds are serial).
|
||||
**Paid**: large Office engine resources remain external to the SEA archive, and the executable must travel with its required sidecars; source enters the distribution as-is (no bytecode obfuscation; a closed-source distribution requirement needs a separate evaluation); pkg's VFS/module-hook layer remains community-maintained (`@yao-pkg/pkg` is an exact-pinned, pnpm-patched root devDependency; upgrading re-records the patch and is an explicit change); `--sea` is one invocation per target (matching CI's one leg per platform; local multi-platform builds are serial).
|
||||
+9
-5
@@ -42,17 +42,19 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真
|
||||
|
||||
部署根目录显式包含 `@deepseek-ai/dsh-mcp-client`,将其作为自定义配置可用的插件,即使随附 preset 均未挂载该插件。外部配置因此可以连接由用户提供的 stdio 与 Streamable HTTP MCP server 并注册其工具;分发物不包含这些 server。可选资源服务提供 MCP Resources;MCP Prompts 仍不受支持。可执行程序与已安装 wheel 包的冒烟测试会启动临时 stdio server,发现其工具,并完成一次由模型请求的调用。
|
||||
|
||||
[Office kit](2026-09-14-independent-libreoffice-kit.zh.md) 及其已安装依赖树位于 `<executable-stem>-office/node_modules`,保留完整包内容与辅助程序权限。顶层 `pkg.ignore` 从 SEA 归档排除 kit 包,包括依赖 `files` 字段选中的资源;与其他使用方共享的小型依赖仍可在 VFS 中使用。Python 私有 bootstrap 使用 Node `registerHooks` 从真实目录解析 kit 入口及其包清单。因此原生可执行程序与 URL Worker 保留普通文件路径,无需修改 kit 或公开 CLI。必需包缺失或已安装包缺少清单会使复制失败;未安装的可选包保留 kit 自身的引擎选择规则。
|
||||
|
||||
### 构建流水线与产物
|
||||
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/` 并拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 决策](2026-08-28-subprocess-native-containment.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/`,并将它与对应目标的 Office 目录一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 决策](2026-08-28-subprocess-native-containment.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。
|
||||
|
||||
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel 包验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)在拉取请求上运行 Linux/Windows x64,在 master 推送上运行 Linux ARM64 与两种 macOS 架构。[公开发布工作流](../../archived/process/2026-08-11-python-publication-workflow.md)调用它构建全部五个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64、macos-x64(`macos-15-intel`)与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求与 master 推送还会在各自选定的目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则检查 runtime、ripgrep 与 PTY helper 的架构,并验证三个载荷的部署目标都符合 wheel 包标签。完整构建五个目标时保留 6 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 5 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v<repository-version>` 标签流水线,构建一个 SDK wheel 包和 5 个原生运行时 wheel 包,再由单个串行任务校验并将这 6 个文件发布到项目的 PyPI 注册表。[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 目标及对 Windows arm64 的明确排除。
|
||||
|
||||
### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发
|
||||
|
||||
Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件和可选的 macOS helper,以及供仓库开发使用的构建注入 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 默认选择可执行文件;显式设置 `DSH_RUNTIME_MODE=node` 会在系统 Node 22.19 或更高版本上运行 `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。node 载体从不进入 wheel 分发,两种载体都不使用检入的完整 `cordis.yml`。
|
||||
Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件、`-office/` 目录和可选的 macOS helper,以及供仓库开发使用的构建注入 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 默认选择可执行文件;显式设置 `DSH_RUNTIME_MODE=node` 会在系统 Node 22.19 或更高版本上运行 `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。node 载体从不进入 wheel 分发,两种载体都不使用检入的完整 `cordis.yml`。
|
||||
|
||||
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的 `X.Y.Z` 或预发布版本,把预发布版本转换为 PEP 440 写法,并以该 wheel 包版本暂存两个包,让 `deepseek-harness-sdk` 精确依赖匹配版本的 `deepseek-harness-runtime-bin`。可选的 `python-v<repository-version>` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。暂存过程还会把仓库许可证放入两个 wheel 包,并把第三方声明放入内置运行时 wheel 包。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe 及其架构匹配的 ripgrep 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`、`py3-none-macosx_14_0_arm64`、`py3-none-macosx_14_0_x86_64` 或 `py3-none-win_amd64`;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。两个 macOS 标签都会特意声明保守的 14.0 安装下限:打包后的 Node 24 可执行文件声明 macOS 13.5,x64 PTY helper 声明 10.7,但发布验证只按照 14.0 wheel 声明证明完整载荷,不会把各组件实测的最低版本承诺为受支持宿主。两个 macOS wheel 包仍按架构分别发布,不发布 universal2 wheel 包。
|
||||
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的 `X.Y.Z` 或预发布版本,把预发布版本转换为 PEP 440 写法,并以该 wheel 包版本暂存两个包,让 `deepseek-harness-sdk` 精确依赖匹配版本的 `deepseek-harness-runtime-bin`。可选的 `python-v<repository-version>` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。暂存过程还会把仓库许可证放入两个 wheel 包,并把第三方声明放入内置运行时 wheel 包。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe、其 Office 目录及架构匹配的 ripgrep 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`、`py3-none-macosx_14_0_arm64`、`py3-none-macosx_14_0_x86_64` 或 `py3-none-win_amd64`;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。两个 macOS 标签都会特意声明保守的 14.0 安装下限:打包后的 Node 24 可执行文件声明 macOS 13.5,x64 PTY helper 声明 10.7,但发布验证只按照 14.0 wheel 声明证明完整载荷,不会把各组件实测的最低版本承诺为受支持宿主。两个 macOS wheel 包仍按架构分别发布,不发布 universal2 wheel 包。
|
||||
|
||||
Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和显式 Harness home 启动打包后的 `dsh` 命令。Profile 负责 JSON-RPC 服务和应用组合;缺失 home、profile、bundle、patch 或 server 配置项都会失败,不存在外部完整配置回退。
|
||||
|
||||
@@ -70,6 +72,8 @@ Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和
|
||||
|
||||
手工驱动注意:`bin` 将 stdin EOF 视为「客户端已离开」并立即 dispose,生命周期较短的管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。
|
||||
|
||||
安装后 wheel 的 `sdk-office` 场景会将目标的完整载荷复制到私有临时目录,要求仅存在一个目标后端(使用已声明的原生目标,未声明原生目标时使用 WASM),并通过随附 SDK profile 使用该引擎转换一次 DOCX。它检查 kit 模块确实解析到迁移后的目录、转换报告预期后端,且输出包含 PDF 字节。wheel 校验会检查声明的引擎资源与原生辅助程序执行位;CI 上传已完成的 wheel,因此嵌套辅助程序权限保留在 ZIP 归档中。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**裸用 Node 原生 SEA。** 注入的主脚本必须是 CJS 单文件,blob 内没有文件系统与模块解析,因此动态 `import()` 无法解析裸包名;只能把插件静态编译进主脚本并手工注册。这会绕过标准模块解析并硬编码插件集合,与「配置决定一切」相悖。最终路线实际是「官方 SEA 基础 + pkg 的 VFS/模块钩子层」;否决的是裸用方式,而不是 SEA 本身。
|
||||
@@ -84,6 +88,6 @@ Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和
|
||||
|
||||
## 后果
|
||||
|
||||
**买到的**:目标平台零依赖的单文件分发;插件语义与源码运行严格一致(同一棵真实包树,无转译、无注册表);对外服务接口、插件集与配置全部收敛到 `cordis.yml` 和一份依赖 manifest 这两个真源;exe 与 `node` 双载体使用同一棵树和相同语义,开发验证无需等待打包;官方 Node 二进制消除了补丁版二进制的供应链顾虑。
|
||||
**买到的**:受支持目标无需依赖系统 Node.js;插件语义与源码运行严格一致(同一棵真实包树,无转译、无注册表);对外服务接口、插件集与配置全部收敛到 `cordis.yml` 和一份依赖 manifest 这两个真源;exe 与 `node` 双载体使用同一棵树和相同语义,开发验证无需等待打包;官方 Node 二进制消除了补丁版二进制的供应链顾虑。
|
||||
|
||||
**付出的**:产物约 174MB,且源码原样进入 blob(没有字节码混淆;闭源分发诉求需要另行评估);pkg 的 VFS/模块钩子层仍由社区维护(`@yao-pkg/pkg` 为精确钉版、带 pnpm 补丁的根 devDependency,升级需重录补丁,属显式改动);`--sea` 每个构建目标调用一次(与 CI 每个平台一个任务相匹配,本地多平台构建串行执行)。
|
||||
**付出的**:大型 Office 引擎资源保留在 SEA 归档之外,可执行文件必须与必需的伴随文件一起迁移;源码原样进入分发物(没有字节码混淆;闭源分发诉求需要另行评估);pkg 的 VFS/模块钩子层仍由社区维护(`@yao-pkg/pkg` 为精确钉版、带 pnpm 补丁的根 devDependency,升级需重录补丁,属显式改动);`--sea` 每个构建目标调用一次(与 CI 每个平台一个任务相匹配,本地多平台构建串行执行)。
|
||||
+2
-2
@@ -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: 0f003edc4a53e7a04ea4f5613abe42c2d3926566
|
||||
2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 1bae16cfaa1c54eebc6c8709ac6db5323b68d222
|
||||
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
|
||||
+16
-14
@@ -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,23 +79,23 @@ 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.
|
||||
|
||||
|
||||
+16
-14
@@ -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,23 +79,23 @@ 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 记录。
|
||||
|
||||
|
||||
+2
-2
@@ -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-09-client-conversation-node-assembly.md
|
||||
2026-08-09-client-conversation-node-assembly.md: 43f9f0359822f7d7c5b52c09646fd3ffbf80f783
|
||||
2026-08-09-client-conversation-node-assembly.zh.md: d7475f5ff8218419743448d455d5d5ffe3cc4091
|
||||
2026-08-09-client-conversation-node-assembly.md: bb1d68cc045710d46a44ffb295c2307628338521
|
||||
2026-08-09-client-conversation-node-assembly.zh.md: 6291211d589b3389e7dc9a42d15fe6ac0d9e7ab9
|
||||
+1
-1
@@ -291,7 +291,7 @@ Page size, record packing, the number of history loads, and RAF coalescing affec
|
||||
|
||||
Inbox demonstrates that every Event can be a start-only instantaneous-state Context; not every business requires a start/update pair. Reader links each next-step state to the prior same-kind Context instead of inventing a lifecycle ID for the entire Inbox. The state itself shares immutable pending splice nodes and one current claimed-batch Set, while unconsumed next-turn input remains outside Conversation because no Chat or Trajectory classification reads it.
|
||||
|
||||
Request Prompt demonstrates shared pure interpretation without shared target State: Chat and Trajectory call `inspectRequestPrompt()` from their own Definitions. The function canonicalizes the full header and classifies model-visible system/tool differences; each target then chooses its own output. Chat materializes a non-empty initial system field, a real system change, and each `series` snapshot that explicitly begins a message series or follows a surface replacement. An unchanged `resume` remains in Trajectory and reconstruction state but does not repeat the visible Chat row once its predecessor is loaded. Ordinary append-only later Turns do not write another unchanged header. The first header in a Step follows the provider envelope rather than the header Event position: step one uses the owning Turn start and later steps use their Step start, placing the system field before the request's user-role messages; a later header in the same Step stays at its own Event after the surface rewrite that began the new series. When the preceding header is outside a partial window, a non-`initial` header stays at its own Event and renders conservatively. Prepending an identical predecessor hides an unchanged resume without withdrawing its stable Node key; a real change remains visible. Every header is a full snapshot, so a first loaded `resume`, `change`, or `series` header can render its system field without fabricating a comparison to unloaded history ([resume presentation decision](../bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.md)).
|
||||
Request Prompt demonstrates shared pure interpretation without shared target State: Chat and Trajectory call `inspectRequestPrompt()` from their own Definitions. The function canonicalizes the full header and classifies model-visible system/tool differences; each target then chooses its own output. Chat materializes a non-empty initial system field, a real system change, and each `series` snapshot that explicitly begins a message series or follows a surface replacement. Resume headers retain a Chat prompt card even when the system text is unchanged. Ordinary append-only later Turns do not write another unchanged header. The first header in a Step follows the provider envelope rather than the header Event position: step one uses the owning Turn start and later steps use their Step start, placing the system field before the request's user-role messages; a later header in the same Step stays at its own Event after the surface rewrite that began the new series. When the preceding header is outside a partial window, a non-`initial` header stays at its own Event and renders conservatively. Prepending an identical predecessor can hide an unchanged `change` header without withdrawing its stable Node key; resume and explicit series-start cards remain visible. Every header is a full snapshot, so a first loaded `resume`, `change`, or `series` header can render its system field without fabricating a comparison to unloaded history ([request-prompt Definition](../../../../packages/client/ui-chat/src/client/conversation-nodes/request-prompt.ts)).
|
||||
|
||||
Retry, Assistant, and Turn Tail demonstrate independent claims on one Event. Each Definition updates only its own State and produces its own atomic Chat Node.
|
||||
|
||||
|
||||
+1
-1
@@ -291,7 +291,7 @@ Chat `order` 的结构性变化仍可能重排当前可见 key;纯 data 更新
|
||||
|
||||
Inbox 展示了“每条 Event 都是一个 start-only 瞬间态 Context”,不是所有业务都需要 start/update 配对。每个 next-step state 通过 Reader 与前一个同 kind Context 形成连续 fold,而非给整个 Inbox 人工制造生命周期 ID。state 自身共享不可变 pending splice 节点和一个当前 claimed-batch Set;未消费的 next-turn input 不进入 Conversation,因为 Chat 与 Trajectory 都不读取它来分类。
|
||||
|
||||
Request Prompt 展示了如何在不共享 target State 的前提下共用纯解释逻辑:Chat 与 Trajectory 各自在自己的 Definition 中调用 `inspectRequestPrompt()`。该函数规范化完整 header,并判定面向模型的 system/tool 差异;随后每个 target 自行选择产物。Chat 会物化非空的初始 system 字段、真实 system 变化,以及每个显式开启消息序列或紧随表层替换的 `series` 快照。未变化的 `resume` 会留在 Trajectory 和重建状态中,但前序 header 已加载时不会重复可见的 Chat 行。普通的仅追加后续 Turn 不会再次写入未变 header。一个 Step 中的首条 header 遵循提供方信封,而不是 header Event 位置:step one 使用所属 Turn start,后续 step 使用各自的 Step start,把 system 字段放到该请求的 user-role 消息之前;同一 Step 的后续 header 保留在开启新序列的表层改写之后。部分窗口未包含前序 header 时,非 `initial` header 会保留在自身 Event 并保守渲染。prepend 补入相同的前序 header 后,内容未变的 resume 会隐藏但不撤回其稳定 Node key;真实变化仍然可见。每条 header 都是完整快照,因此已加载窗口中的首条 `resume`、`change` 或 `series` header 无需凭空构造与未加载历史的比较,也能渲染其 system 字段([resume 展示决策](../bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.zh.md))。
|
||||
Request Prompt 展示了如何在不共享 target State 的前提下共用纯解释逻辑:Chat 与 Trajectory 各自在自己的 Definition 中调用 `inspectRequestPrompt()`。该函数规范化完整 header,并判定面向模型的 system/tool 差异;随后每个 target 自行选择产物。Chat 会物化非空的初始 system 字段、真实 system 变化,以及每个显式开启消息序列或紧随表层替换的 `series` 快照。即使 system 文本未变化,resume header 仍保留 Chat prompt 卡片。普通的仅追加后续 Turn 不会再次写入未变 header。一个 Step 中的首条 header 遵循提供方信封,而不是 header Event 位置:step one 使用所属 Turn start,后续 step 使用各自的 Step start,把 system 字段放到该请求的 user-role 消息之前;同一 Step 的后续 header 保留在开启新序列的表层改写之后。部分窗口未包含前序 header 时,非 `initial` header 会保留在自身 Event 并保守渲染。prepend 补入相同的前序 header 后,内容未变的 `change` header 可以隐藏,但不撤回其稳定 Node key;resume 和显式序列起点的卡片仍然可见。每条 header 都是完整快照,因此已加载窗口中的首条 `resume`、`change` 或 `series` header 无需凭空构造与未加载历史的比较,也能渲染其 system 字段([request-prompt Definition](../../../../packages/client/ui-chat/src/client/conversation-nodes/request-prompt.ts))。
|
||||
|
||||
Retry、Assistant 和 Turn Tail 展示了同一 Event 被多个 Definition 独立认领。每个 Definition 只更新自己的 State,最终分别生成原子 Chat Node。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md
|
||||
2026-08-18-experimental-agent-teams-packages.md: 4a78a60c2ad7463c06b670e6ec664579ffd767b9
|
||||
2026-08-18-experimental-agent-teams-packages.zh.md: 8056cd8194695e8ce41acb359e51c040d4ed3aec
|
||||
2026-08-18-experimental-agent-teams-packages.md: a1d29b5e96b185a5539d664e19f2f5cc342a6aba
|
||||
2026-08-18-experimental-agent-teams-packages.zh.md: f4c4eb481ae6ad6319ddac50eda82fa9b9feebf7
|
||||
+5
-5
@@ -12,17 +12,17 @@ Moving the packages into product-role groups would remove their experimental nam
|
||||
|
||||
## Decision
|
||||
|
||||
`packages/experimental/agent-team`, `packages/experimental/tool-agent-team`, `packages/experimental/agent-team-profile`, `packages/experimental/client-ui-agent-team`, and `packages/experimental/agent-team-web-profile` are public workspace packages. They retain their existing `@deepseek-ai/dsh-experimental-*` names and join the dsh release family. The [publication denylist decision](../process/2026-09-12-experimental-publication-denylist.md) owns the public default and private exceptions; the [experimental package rules](../../../../packages/experimental/AGENTS.md) own dependency isolation and later promotion.
|
||||
`packages/experimental/agent-team`, `packages/experimental/tool-agent-team`, `packages/experimental/agent-team-profile`, and `packages/experimental/client-ui-agent-team` are public workspace packages. They retain their existing `@deepseek-ai/dsh-experimental-*` names and join the dsh release family. The [publication denylist decision](../process/2026-09-12-experimental-publication-denylist.md) owns the public default and private exceptions; the [experimental package rules](../../../../packages/experimental/AGENTS.md) own dependency isolation and later promotion.
|
||||
|
||||
The dsh pack and publish set and the local baseline publisher include these five Agent Teams directories and the [Cua Driver providers](2026-09-12-computer-use-provider-registration.md). Workspace constraints require them to omit `private`, set `publishConfig.access` to `public`, and keep the experimental npm prefix. Release packages and apps outside the experimental group, plus the Python runtime, cannot name experimental packages in `dependencies`, `optionalDependencies`, or `peerDependencies`; experimental packages may depend on release packages and each other.
|
||||
The dsh pack and publish set and the local baseline publisher include these four Agent Teams directories and the [Cua Driver providers](2026-09-12-computer-use-provider-registration.md). Workspace constraints require them to omit `private`, set `publishConfig.access` to `public`, and keep the experimental npm prefix. Release packages and apps outside the experimental group, plus the Python runtime, cannot name experimental packages in `dependencies`, `optionalDependencies`, or `peerDependencies`; experimental packages may depend on release packages and each other.
|
||||
|
||||
The generic caller-reserved continuable child identity and selective direct-child drain remain in the stable Subagent service. They own Subagent identity and Activation lifecycle without importing or naming Agent Teams; the experimental Team service consumes them in the permitted direction.
|
||||
|
||||
The published Host-side Agent Teams profile bundle depends on the Team packages and applies after `dsh-base`. It inserts the Team rows and disables the global continuable-child controls whose model-visible names overlap the Team tools. The separate published Web profile applies after `dsh-web-app` and the Host profile; it inserts the Team UI, which mounts the Remote contribution generated by the Team package. Both layers remain opt-in and leave the shipped base, CLI, Web, and Python runtime dependency graphs unchanged.
|
||||
The [single-bundle decision](2026-09-18-agent-teams-single-bundle.md) supersedes the separate Host and Web bundle composition. This note retains publication, dependency isolation, and promotion rationale. The current Team profile remains opt-in and disables the global continuable-child controls whose model-visible names overlap the Team tools; the [optional-bundle decision](../process/2026-09-15-shipped-optional-bundles.md) owns its inclusion in the installation.
|
||||
|
||||
Profile startup resolves selected bundles before computing the [immutable profile resolution generation](2026-09-09-profile-resolution-generations.md). The generation retains installation-first precedence, traverses each explicit bundle root completely in profile order, and keeps pnpm-managed profile packages authoritative. Runtime mode enforces the result in memory; retained link and dual modes materialize the same result as shared and profile-owned projections. A private profile layer can therefore carry experimental plugin rows without adding those plugins to a release app, requiring profile users to install transitive packages directly, weakening packaged-runtime module identity, or changing another profile's resolution.
|
||||
|
||||
Experimental status changes compatibility and support expectations, not publication for these five packages. They retain the repository's ordinary documentation, invariant, lifecycle, security, unit, real-composition, and snapshot requirements. Promotion still requires review of the public contracts, limitations, test evidence, runtime dependents, and a named owner accepting stable-package obligations.
|
||||
Experimental status changes compatibility and support expectations, not publication for these four packages. They retain the repository's ordinary documentation, invariant, lifecycle, security, unit, real-composition, and snapshot requirements. Promotion still requires review of the public contracts, limitations, test evidence, runtime dependents, and a named owner accepting stable-package obligations.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -36,6 +36,6 @@ Experimental status changes compatibility and support expectations, not publicat
|
||||
|
||||
## Consequences
|
||||
|
||||
Agent Teams publishes as five installable tarballs in the dsh release family without changing package names or enabling Team in a shipped profile. Public availability does not make the packages stable or supported by default, and stable release packages cannot take runtime dependencies on them.
|
||||
Agent Teams publishes as four installable tarballs in the dsh release family without changing package names or enabling Team in a shipped profile. Public availability does not make the packages stable or supported by default, and stable release packages cannot take runtime dependencies on them.
|
||||
|
||||
The release family carries the experimental npm names. Promotion still creates path and npm-name churn as specified by the experimental package rules.
|
||||
+5
-5
@@ -12,17 +12,17 @@ Agent Teams 的服务与工具约定仍在变化,但它需要使用真实 Sess
|
||||
|
||||
## 决策
|
||||
|
||||
`packages/experimental/agent-team`、`packages/experimental/tool-agent-team`、`packages/experimental/agent-team-profile`、`packages/experimental/client-ui-agent-team` 与 `packages/experimental/agent-team-web-profile` 是公开 workspace 包。它们保留现有 `@deepseek-ai/dsh-experimental-*` 名称并加入 dsh 发布系列。[发布拒绝列表决策](../process/2026-09-12-experimental-publication-denylist.zh.md)负责默认公开与私有例外;[实验性包规则](../../../../packages/experimental/AGENTS.md)负责依赖隔离与后续 promotion。
|
||||
`packages/experimental/agent-team`、`packages/experimental/tool-agent-team`、`packages/experimental/agent-team-profile` 与 `packages/experimental/client-ui-agent-team` 是公开 workspace 包。它们保留现有 `@deepseek-ai/dsh-experimental-*` 名称并加入 dsh 发布系列。[发布拒绝列表决策](../process/2026-09-12-experimental-publication-denylist.zh.md)负责默认公开与私有例外;[实验性包规则](../../../../packages/experimental/AGENTS.md)负责依赖隔离与后续 promotion。
|
||||
|
||||
dsh 打包与发布集合以及本地基线发布器包含这五个 Agent Teams 目录和 [Cua Driver 提供方](2026-09-12-computer-use-provider-registration.zh.md)。workspace 约束要求它们省略 `private`、将 `publishConfig.access` 设为 `public`,并保留实验性 npm 前缀。实验组之外的发布包、应用和 Python 运行时不能在 `dependencies`、`optionalDependencies` 或 `peerDependencies` 中引用实验包;实验包可以依赖发布包和彼此。
|
||||
dsh 打包与发布集合以及本地基线发布器包含这四个 Agent Teams 目录和 [Cua Driver 提供方](2026-09-12-computer-use-provider-registration.zh.md)。workspace 约束要求它们省略 `private`、将 `publishConfig.access` 设为 `public`,并保留实验性 npm 前缀。实验组之外的发布包、应用和 Python 运行时不能在 `dependencies`、`optionalDependencies` 或 `peerDependencies` 中引用实验包;实验包可以依赖发布包和彼此。
|
||||
|
||||
通用的调用方预留 continuable child 身份和精确 direct-child drain 仍属于稳定 Subagent 服务。它们负责 Subagent 身份与 Activation 生命周期,不 import 或命名 Agent Teams;实验性 Team 服务沿允许的方向消费这些能力。
|
||||
|
||||
公开发布的 Host 侧 Agent Teams profile bundle 依赖 Team 包,并在 `dsh-base` 之后应用。它会插入 Team 配置行,并禁用模型可见名称与 Team 工具重叠的全局 continuable-child control。独立公开发布的 Web profile 在 `dsh-web-app` 与 Host profile 之后应用;它会插入 Team UI,后者挂载 Team package 生成的 Remote contribution。两个层都保持显式启用,不改变随附 base、CLI、Web 与 Python runtime 的依赖图。
|
||||
[单组合包决策](2026-09-18-agent-teams-single-bundle.zh.md)取代了独立 Host 与 Web 组合包的组合方式。本记录保留发布、依赖隔离与 promotion 的理由。当前 Team profile 仍须显式启用,并禁用模型可见名称与 Team 工具重叠的全局 continuable-child control;[可选组合包决策](../process/2026-09-15-shipped-optional-bundles.zh.md)负责其随安装交付的规则。
|
||||
|
||||
profile 启动会先解析所选 bundle,再计算[不可变 profile resolution generation](2026-09-09-profile-resolution-generations.zh.md)。generation 保留安装优先顺序,按 profile 顺序完整遍历每个显式 bundle 根,并让 pnpm 管理的 profile 包保持优先。runtime 模式在内存中强制该结果;保留的 link 与 dual 模式把同一结果物化为共享和 profile 自有投影。因此,私有 profile 层可以携带实验性 plugin 配置行,而无需把这些 plugin 加入发布 app、要求 profile 用户直接安装传递依赖、破坏 packaged-runtime 的模块身份,或改变其他 profile 的解析结果。
|
||||
|
||||
对这五个包而言,实验性状态改变兼容性与支持预期,而不阻止发布。这些包仍须满足仓库的一般文档、不变式、生命周期、安全、单元测试、真实组合测试和快照要求。promotion 前仍须评审公开约定、限制、测试证据、运行时依赖方,并由一名具名 owner 接受稳定包义务。
|
||||
对这四个包而言,实验性状态改变兼容性与支持预期,而不阻止发布。这些包仍须满足仓库的一般文档、不变式、生命周期、安全、单元测试、真实组合测试和快照要求。promotion 前仍须评审公开约定、限制、测试证据、运行时依赖方,并由一名具名 owner 接受稳定包义务。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -36,6 +36,6 @@ profile 启动会先解析所选 bundle,再计算[不可变 profile resolution
|
||||
|
||||
## 后果
|
||||
|
||||
Agent Teams 会作为 dsh 发布系列中的五个可安装 tarball 发布,同时保持包名不变,也不会在随附 profile 中启用 Team。公开可用不代表这些包稳定或默认受支持,稳定发布包也不能对其建立运行时依赖。
|
||||
Agent Teams 会作为 dsh 发布系列中的四个可安装 tarball 发布,同时保持包名不变,也不会在随附 profile 中启用 Team。公开可用不代表这些包稳定或默认受支持,稳定发布包也不能对其建立运行时依赖。
|
||||
|
||||
发布系列保留实验性 npm 名称。promotion 仍会按照实验性包规则产生路径和 npm 名改动。
|
||||
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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-23-client-derived-tool-presentation.md
|
||||
2026-08-23-client-derived-tool-presentation.md: 703960cb24f90eb678fda381f929efb3f94bbd28
|
||||
2026-08-23-client-derived-tool-presentation.zh.md: 7f161379553f3aca33c72c2edf93e114d27eb7f9
|
||||
2026-08-23-client-derived-tool-presentation.md: 931276fa6d4c0590746b6c59eeae9f998f05629c
|
||||
2026-08-23-client-derived-tool-presentation.zh.md: b930f98da17a1cb6ecbf605e3a9f37255ba75fc9
|
||||
+4
-4
@@ -22,7 +22,7 @@ The required result is one raw Session journal and one Client presentation owner
|
||||
|
||||
## Decision
|
||||
|
||||
The visual-equivalence requirements below exclude the separately approved [nested terminal-card fix](../bug-fix/2026-09-05-nested-terminal-cards.md); all other presentation and ownership constraints remain.
|
||||
The visual-equivalence requirements below exclude the separately approved [nested terminal-card fix](../bug-fix/2026-09-05-nested-terminal-cards.md) and [compact tool details](2026-09-10-compact-tool-details.md); all other presentation and ownership constraints remain.
|
||||
|
||||
The Session Remote journal sends only raw, validated, persistable Session events. `session.page` and `session.follow` do not parse tool arguments, query the Tools registry, restore a presenter scope, execute `presentCall` or `presentResult`, or construct or clone any tool view.
|
||||
|
||||
@@ -51,7 +51,7 @@ The Host `ToolDefinition.presentCall`, `ToolDefinition.presentResult`, `ToolCall
|
||||
| Retained | the Session log format, Remote journal lifecycle, and Conversation identity/topology |
|
||||
| Retained | the existing keyed slot, Generic fallback, and Chat, Details, and Trajectory structure |
|
||||
| Forbidden | a new Client presenter service, parallel registry, or wire renderer id |
|
||||
| Forbidden | new cards, visual redesign, interaction redesign, or PTC dispatch rich-card enhancements except the [nested terminal-card exception](../bug-fix/2026-09-05-nested-terminal-cards.md) |
|
||||
| Forbidden | new cards, visual redesign, interaction redesign, or PTC dispatch rich-card enhancements except the [nested terminal-card exception](../bug-fix/2026-09-05-nested-terminal-cards.md) and [compact tool details](2026-09-10-compact-tool-details.md) |
|
||||
| Forbidden | compatibility dual-writing, version negotiation, or retention of the old `view` field |
|
||||
|
||||
## Terminology
|
||||
@@ -64,7 +64,7 @@ The Host `ToolDefinition.presentCall`, `ToolDefinition.presentResult`, `ToolCall
|
||||
|
||||
**Client card model** means the pure props data under `ui-tool/src/client/tool/models/` consumed directly by `TerminalBlock`, `DiffBlock`, `ReadBlock`, `SearchBlock`, `WebBlock`, or `ToolRow`.
|
||||
|
||||
**Specialized card** means the structured terminal, diff, read, search, or web body. Titles, summaries, status dots, and ordinary IN/OUT text remain part of the generic tool row.
|
||||
**Specialized card** means the structured terminal, diff, read, search, or web body. Titles, summaries, lifecycle styling, business glyphs, and ordinary IN/OUT text remain part of the generic tool row.
|
||||
|
||||
**Equivalent** means that the same supported input produces the user-visible result and interaction pinned by the existing component, assembly, and browser evidence. It does not require the same intermediate TypeScript types or internal calls.
|
||||
|
||||
@@ -301,7 +301,7 @@ The Client terminal model derives existing `TerminalBlock` props from the tool n
|
||||
| Input | Preserved result |
|
||||
|---|---|
|
||||
| running standard `bash`/`pwsh` foreground call | terminal prompt, description, cwd, and running state |
|
||||
| successful standard foreground call | terminal output, exit code/signal, and success or failure status dot |
|
||||
| successful standard foreground call | terminal output, exit code/signal, and settled state presentation |
|
||||
| `run_in_background:true` | Generic row and raw result |
|
||||
| tool execution error | Generic IN/OUT and error summary |
|
||||
| running persistent `bash`/`pwsh` | terminal prompt |
|
||||
|
||||
+4
-4
@@ -22,7 +22,7 @@ Host presenter 与 Client keyed renderer 分担展示会形成对同一事件的
|
||||
|
||||
## Decision
|
||||
|
||||
下述展示对等要求不包含已独立批准的[嵌套 terminal 卡片修复](../bug-fix/2026-09-05-nested-terminal-cards.zh.md);其他展示与所有权约束全部保留。
|
||||
下述展示对等要求不包含已独立批准的[嵌套 terminal 卡片修复](../bug-fix/2026-09-05-nested-terminal-cards.zh.md)和[紧凑工具详情](2026-09-10-compact-tool-details.zh.md);其他展示与所有权约束全部保留。
|
||||
|
||||
Session Remote journal 只下发原始、已验证、可持久化的 Session event。`session.page` 和 `session.follow` 不解析工具参数,不查询 Tools registry,不恢复 presenter scope,不执行 `presentCall`/`presentResult`,也不构造或克隆任何 tool view。
|
||||
|
||||
@@ -51,7 +51,7 @@ Host 的 `ToolDefinition.presentCall`、`ToolDefinition.presentResult`、`ToolCa
|
||||
| 保留 | Session 日志格式、Remote journal 生命周期与 Conversation identity/topology |
|
||||
| 保留 | 现有 keyed slot、Generic fallback、Chat、Details 与 Trajectory 结构 |
|
||||
| 禁止 | 新 Client presenter service、平行 registry 或 wire renderer id |
|
||||
| 禁止 | 新卡片、视觉改版、交互改版或 PTC dispatch rich-card 增强,[嵌套 terminal 卡片例外](../bug-fix/2026-09-05-nested-terminal-cards.zh.md)除外 |
|
||||
| 禁止 | 新卡片、视觉改版、交互改版或 PTC dispatch rich-card 增强,[嵌套 terminal 卡片例外](../bug-fix/2026-09-05-nested-terminal-cards.zh.md)和[紧凑工具详情](2026-09-10-compact-tool-details.zh.md)除外 |
|
||||
| 禁止 | 为兼容保留双写、版本协商或旧 `view` 字段 |
|
||||
|
||||
## 术语
|
||||
@@ -64,7 +64,7 @@ Host 的 `ToolDefinition.presentCall`、`ToolDefinition.presentResult`、`ToolCa
|
||||
|
||||
**Client card model**指 `ui-tool/src/client/tool/models/` 下直接供 `TerminalBlock`、`DiffBlock`、`ReadBlock`、`SearchBlock`、`WebBlock` 或 `ToolRow` 使用的纯 props 数据。
|
||||
|
||||
**专用卡片**指 terminal、diff、read、search 与 web 的结构化正文;标题、摘要、状态点和普通 IN/OUT 文本仍属于通用工具行。
|
||||
**专用卡片**指 terminal、diff、read、search 与 web 的结构化正文;标题、摘要、生命周期样式、业务图形和普通 IN/OUT 文本仍属于通用工具行。
|
||||
|
||||
**对等**指同一受支持输入产生由现有组件、组装与浏览器证据固定的用户可见结果和交互,不要求相同的中间 TypeScript 类型或内部函数调用。
|
||||
|
||||
@@ -301,7 +301,7 @@ Client terminal model 从工具名称、调用参数、结果 content、error
|
||||
| 输入 | 保持的结果 |
|
||||
|---|---|
|
||||
| 标准 `bash`/`pwsh` 前台 running | terminal prompt、description、cwd、running 状态 |
|
||||
| 标准前台 success | terminal output、exit code/signal、成功或失败状态点 |
|
||||
| 标准前台 success | terminal output、exit code/signal 与结算状态呈现 |
|
||||
| `run_in_background:true` | Generic 行与原始结果 |
|
||||
| 工具执行 error | Generic IN/OUT 与错误摘要 |
|
||||
| persistent `bash`/`pwsh` running | terminal prompt |
|
||||
|
||||
+2
-2
@@ -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: c532db9c3078a8c0cc67935f88457bf06b850141
|
||||
2026-08-25-electron-desktop-packaging-and-updates.zh.md: fad71d59ea720542bfdcd15a010ece60b295f2d8
|
||||
2026-08-25-electron-desktop-packaging-and-updates.md: c4940942a8d5188faf1846158873c052a43e1fc1
|
||||
2026-08-25-electron-desktop-packaging-and-updates.zh.md: 011624980f6f7239a33fb2b6c6803646b7d41dd3
|
||||
+20
-20
@@ -4,7 +4,7 @@ Status: implemented
|
||||
|
||||
English | [中文](2026-08-25-electron-desktop-packaging-and-updates.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.
|
||||
|
||||
@@ -24,13 +24,13 @@ Electron owns the reserved profile at `.dsh/profiles/desktop`. The [bundled-runt
|
||||
|
||||
One Desktop release number identifies the Electron artifact and its exact `@deepseek-ai/dsh` and `@deepseek-ai/dsh-desktop-host` dependencies. A release cannot select a different core version at build or runtime. Updating dsh therefore requires a new Electron release even when shell code is unchanged.
|
||||
|
||||
The browser Web UI, dsh backend, existing `dsh plugin` CLI, user npm, and user pnpm cannot mutate this profile. The CLI reserves every case variant of the `desktop` name and rejects boot, config-dump, and plugin-management requests for it. Electron acquires its process-lifetime single-instance lock before project recovery or Host startup; later launches focus or recreate the primary window without touching profile state. An Electron-only GUI sends structured install, remove, and update requests through preload; Electron invokes only its bundled pnpm.
|
||||
The Desktop Host exposes the shared Web plugin manager for its reserved profile and supplies bundled pnpm through launcher facts. The CLI reserves every case variant of the `desktop` name and rejects boot, config-dump, and plugin-management requests for it. Electron acquires its process-lifetime single-instance lock before profile recovery or Host startup; later launches focus or recreate the primary window without touching profile state.
|
||||
|
||||
## Ownership
|
||||
|
||||
| Owner | Responsibility |
|
||||
|---|---|
|
||||
| Electron shell | Window and child lifecycle, local shell pages, reserved desktop profile, plugin GUI, update coordination |
|
||||
| Electron shell | Window and child lifecycle, reserved desktop profile preparation, native recovery, update coordination |
|
||||
| Electron RunAsNode and pnpm | Execute dsh and install desktop-project dependencies using pnpm’s normal configuration |
|
||||
| Desktop profile | External plugin dependencies, ordered enabled bundles, and shared links defined by the bundled-runtime decision |
|
||||
| Private Desktop Host package | Electron-only child-process entry and composition overlay installed with dsh but excluded from the public CLI package and npm publication |
|
||||
@@ -38,7 +38,7 @@ The browser Web UI, dsh backend, existing `dsh plugin` CLI, user npm, and user p
|
||||
| Shared `.dsh` owners | Sessions, settings, credentials, workspaces, and storage, guarded by their existing locks and format versions |
|
||||
| npm-installed dsh | Its own executable installation and user-managed profiles; no access to the reserved desktop profile or package state |
|
||||
|
||||
The renderer uses `nodeIntegration: false`, `contextIsolation: true`, and `sandbox: true`. Preload exposes typed RPC, lifecycle, update, locale, and desktop-plugin actions rather than raw `ipcRenderer`, filesystem access, shell commands, or pnpm arguments. Electron selects a typed English or Chinese dictionary from its application locale and falls back to English; menus, native dialogs, and the plugin-management renderer use that locale-owned copy.
|
||||
The renderer uses `nodeIntegration: false`, `contextIsolation: true`, and `sandbox: true`. Preload provides boot readiness and failure reporting, native directory selection, theme synchronization, and Windows menu and appearance adapters. It exposes no raw `ipcRenderer`, filesystem access, shell commands, or pnpm arguments. Electron menus and native dialogs use typed English or Chinese copy with English fallback; Windows follows the main document’s language. The shared Web plugin manager owns its client copy.
|
||||
|
||||
## Filesystem layout
|
||||
|
||||
@@ -59,9 +59,9 @@ The renderer uses `nodeIntegration: false`, `contextIsolation: true`, and `sandb
|
||||
|
||||
## Installation and resolution
|
||||
|
||||
Desktop stops the Host before modifying its profile in place. Package failures retain partial changes for explicit repair; profile mutation and package-retry ownership follow the [in-place decision](2026-09-09-desktop-in-place-profile.md).
|
||||
The shared Web plugin manager owns profile package operations. Electron retains startup preparation and native recovery; the [Web wrapper decision](2026-09-10-desktop-web-wrapper.md) owns this separation.
|
||||
|
||||
The process-lifetime Electron lock is the authoritative Desktop owner. The package transaction lock is depth defense and records the process that can still mutate package state: Electron between package operations and the spawned pnpm PID while pnpm runs. The owner change is truncated, written, and synchronized through the already-open exclusive lock file. If Electron terminates during pnpm execution, a later process observes the live worker and refuses to start a competing package transaction; after that worker exits, the stale PID can be recovered.
|
||||
The process-lifetime Electron lock is the authoritative Desktop owner. Profile preparation and native recovery use an exclusive lock recording the Electron PID; a live owner blocks a competing operation, while a stale PID can be recovered. Web package operations use the shared plugin manager’s locking.
|
||||
|
||||
Core materialization, first launch, plugin installation, and shared-module resolution follow the [bundled-runtime decision](2026-09-08-desktop-bundled-runtime-and-external-plugins.md). The actual Host composes enabled desktop plugins contributing `dsh.client` code when it starts.
|
||||
|
||||
@@ -77,11 +77,11 @@ The [immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-s
|
||||
|
||||
Core dsh and the private Desktop Host come only from the signed application resource tree. Plugin installation forwards package specs to pnpm, including local and remote sources, but never accepts raw pnpm commands. pnpm owns dependency resolution and the profile’s `allowBuilds` policy; the Host loads activated bundles.
|
||||
|
||||
Electron release artifacts are signed; macOS artifacts are notarized. Package and upload commands read release settings from the Git-ignored target `.env.windows` or `.env.macos`; subprocesses receive the fields selected by orchestration. The target file exclusively owns release fields so stale shell or system credentials cannot override local selection, and loading does not mutate the parent environment. Packaging validates the mode-specific application ID, update origin, signing identity, and local files before builds, downloads, or release-record cleanup; macOS also requires one complete notarization strategy. The separate `check:package` command runs the same validation without accessing the token or Apple; actual signing and notarization still verify authentication. Configuration loading rejects missing or malformed identifiers and incomplete notarization credentials, while macOS packaging requires signing so certificate discovery cannot silently select another installed identity or emit an unsigned release. Runtime preparation verifies the exact Authority and Team ID plus the timestamp and hardened-runtime flags on every embedded Mach-O file. An after-sign hook performs Apple's deep strict application verification and requires the same leaf Authority and Team ID before artifact creation continues. The fixed-target installer command uses [isolated App copies for parallel notarization](../process/2026-09-09-parallel-macos-notarization.md): the ZIP contains a stapled App, while the signed DMG carries the ticket covering its unstapled inner App. The DMG artifact-completion hook requires the configured identity, a valid ticket, and Gatekeeper acceptance. Both artifact lanes must succeed before the command promotes their outputs and writes the release completion record; directory-only commands still notarize and staple the App. DMG blockmaps are disabled because macOS updates consume the signed ZIP, and stapling would otherwise invalidate an already-generated DMG blockmap. The shared Web server owns frontend and client-module responses. The plugin installer API is available only to the Electron-owned management GUI and is absent from the browser application and backend RPC.
|
||||
Electron release artifacts are signed; macOS artifacts are notarized. Package and upload commands read release settings from the Git-ignored target `.env.windows` or `.env.macos`; subprocesses receive the fields selected by orchestration. The target file exclusively owns release fields so stale shell or system credentials cannot override local selection, and loading does not mutate the parent environment. Packaging validates the mode-specific application ID, update origin, signing identity, and local files before builds, downloads, or release-record cleanup; macOS also requires one complete notarization strategy. The separate `check:package` command runs the same validation without accessing the token or Apple; actual signing and notarization still verify authentication. Configuration loading rejects missing or malformed identifiers and incomplete notarization credentials, while macOS packaging requires signing so certificate discovery cannot silently select another installed identity or emit an unsigned release. Runtime preparation verifies the exact Authority and Team ID plus the timestamp and hardened-runtime flags on every embedded Mach-O file. An after-sign hook performs Apple's deep strict application verification and requires the same leaf Authority and Team ID before artifact creation continues. The fixed-target installer command uses [isolated App copies for parallel notarization](../process/2026-09-09-parallel-macos-notarization.md): the ZIP contains a stapled App, while the signed DMG carries the ticket covering its unstapled inner App. The DMG artifact-completion hook requires the configured identity, a valid ticket, and Gatekeeper acceptance. Both artifact lanes must succeed before the command promotes their outputs and writes the release completion record; directory-only commands still notarize and staple the App. DMG blockmaps are disabled because macOS updates consume the signed ZIP, and stapling would otherwise invalidate an already-generated DMG blockmap.
|
||||
|
||||
The [pinned osx-sign patch](../../../../patches/@[email protected]) uses `lstat` in both published module builds, so Framework file and directory aliases do not trigger duplicate signing. The patch remains necessary until the selected upstream release skips those aliases. PAK files are resources sealed by the enclosing bundle; individual signatures add serial timestamp requests without additional resource integrity. Desktop preserves all locale files and skips only their standalone signatures. Executable code retains Developer ID signatures, secure timestamps, and hardened runtime. The [signer traversal regression](../../../../apps/desktop/tests/macos-signing-walk.spec.ts) exercises the installed dependency with real Framework aliases; release qualification still requires strict application verification, notarization, and startup.
|
||||
|
||||
Windows release packaging supplies the public EV leaf certificate named by `DSH_DESKTOP_WINDOWS_CER_FILE` to the configured SafeNet-compatible SignTool through `/f` and identifies its matching private key through the required `DSH_DESKTOP_WINDOWS_KEY_CONTAINER`. The certificate file remains outside source control, and the private key remains on the USB token. The electron-builder hook passes each artifact to the CRLF `windows-sign.cmd`, whose single SignTool invocation uses the SafeNet `/kc "[{{PIN}}]=container"` value and CSP, a SHA-256 file digest, and a DigiCert SHA-256 RFC 3161 timestamp. The hook never substitutes another SignTool and never retries a failed request. Package orchestration withholds every `DSH_DESKTOP_WINDOWS_*` field from build and runtime-preparation children and passes only the certificate path, SignTool path, key container, and PIN into electron-builder. The signer supplies only validated signing fields in an otherwise scrubbed CMD environment; the CMD disables delayed expansion, clears those fields before SignTool starts, and preserves the PIN only in the required SignTool command line. Every surfaced diagnostic replaces the PIN, and only the dedicated build account and administrators may inspect the runner. The signer signs electron-builder's temporary NSIS bootstrap before enterprise Code Integrity evaluates that executable and clears a generated executable's certificate-table entry only when it points beyond the file before applying the final signature. Packaging fails before producing unsigned artifacts when the SignTool, certificate, container, PIN, token, or signature is unavailable. The shared Web server owns frontend and client-module responses. The plugin installer API is available only to the Electron-owned management GUI and is absent from the browser application and backend RPC.
|
||||
Windows release packaging supplies the public EV leaf certificate named by `DSH_DESKTOP_WINDOWS_CER_FILE` to the configured SafeNet-compatible SignTool through `/f` and identifies its matching private key through the required `DSH_DESKTOP_WINDOWS_KEY_CONTAINER`. The certificate file remains outside source control, and the private key remains on the USB token. The electron-builder hook passes each artifact to the CRLF `windows-sign.cmd`, whose single SignTool invocation uses the SafeNet `/kc "[{{PIN}}]=container"` value and CSP, a SHA-256 file digest, and a DigiCert SHA-256 RFC 3161 timestamp. The hook never substitutes another SignTool and never retries a failed request. Package orchestration withholds every `DSH_DESKTOP_WINDOWS_*` field from build and runtime-preparation children and passes only the certificate path, SignTool path, key container, and PIN into electron-builder. The signer supplies only validated signing fields in an otherwise scrubbed CMD environment; the CMD disables delayed expansion, clears those fields before SignTool starts, and preserves the PIN only in the required SignTool command line. Every surfaced diagnostic replaces the PIN, and only the dedicated build account and administrators may inspect the runner. The signer signs electron-builder's temporary NSIS bootstrap before enterprise Code Integrity evaluates that executable and clears a generated executable's certificate-table entry only when it points beyond the file before applying the final signature. Packaging fails before producing unsigned artifacts when the SignTool, certificate, container, PIN, token, or signature is unavailable.
|
||||
|
||||
Windows package invocations force `ELECTRON_BUILDER_7Z_FILTER=BCJ`. The bundled 7-Zip 24.09 encoder automatically selects ARM64 filters for ARM64 PE files, but the NSIS decoder from `nsis-resources-3.4.1` omits those entries during extraction. A native extraction probe with the actual NSIS plugin loses both `node-pty` ARM64 binaries under automatic filtering and restores both byte-for-byte with BCJ. Keeping a compatible filter preserves dependency contents and runtime integrity instead of removing architecture-specific files or weakening verification.
|
||||
|
||||
@@ -97,7 +97,7 @@ Architecture-specific builds report actual component-level compressed and instal
|
||||
|
||||
| Surface | Implementation |
|
||||
|---|---|
|
||||
| Shell | `apps/desktop` owns Electron windows, restricted preloads, the custom protocol, child lifecycle, project transactions, the plugin GUI, update coordination, and electron-builder configuration. |
|
||||
| Shell | `apps/desktop` owns Electron windows, restricted preloads, the custom protocol, child lifecycle, profile preparation, native recovery, update coordination, and electron-builder configuration. |
|
||||
| Installed runtime | Private `@deepseek-ai/dsh-desktop-host` invokes the shared profile runner and reports the authenticated Web URL to Electron. |
|
||||
| Package state | Electron RunAsNode executes immutable core resources; bundled pnpm modifies only the external plugin graph in the Desktop profile. |
|
||||
| Qualification | macOS packaging requires the configured company identity and notary credentials, verifies every native runtime file before inventory generation, verifies the completed application signature, and requires notarization plus Gatekeeper acceptance for both the application and DMG. Windows packaging requires the configured public certificate, SafeNet private-key container, Token Password, and SignTool, and verifies every produced signature. Update hosting, previous-version installed-artifact tests, and platform GUI recordings remain release-environment gates. |
|
||||
@@ -112,7 +112,7 @@ Architecture-specific builds report actual component-level compressed and instal
|
||||
|
||||
**Bake the product Web UI into Electron.** Independent UI and backend updates would require a new versioned compatibility program. Installing backend and Web UI from the same dsh package preserves the current release binding.
|
||||
|
||||
**Reuse the existing CLI or browser plugin installer.** That crosses the desktop authorization and release scope and can use the user's package-manager state. Desktop package mutation remains exclusively Electron-owned.
|
||||
**Maintain an Electron-only plugin installer.** A separate page, IPC API, and package service duplicate the shared Web manager. The shared service uses the reserved Desktop profile and launcher-supplied pnpm while keeping CLI profile access disabled.
|
||||
|
||||
**Let the desktop profile use CLI-managed packages or plugins.** Either product could change the other’s dependency graph, Cordis version, plugin version, or native module. Desktop rejects package resolution through the CLI profile fallback.
|
||||
|
||||
@@ -130,13 +130,13 @@ Architecture-specific builds report actual component-level compressed and instal
|
||||
|
||||
- A clean offline machine without system Node.js or pnpm starts bundled dsh without installing core dependencies.
|
||||
- The signed application inventories final runtime files; every macOS native file has the release Developer ID, secure timestamp, and hardened runtime, and every Windows artifact has the configured hardware-backed EV signature.
|
||||
- `.dsh/profiles/desktop/node_modules` resolves shared host links and every GUI-installed desktop plugin.
|
||||
- Every Desktop package operation uses bundled pnpm with the user’s normal environment and profile configuration.
|
||||
- The Electron-only GUI installs, removes, and updates ordinary npm plugin packages without exposing raw pnpm arguments.
|
||||
- The backend and browser application cannot mutate desktop packages.
|
||||
- `.dsh/profiles/desktop/node_modules` stores external plugins managed by the shared Web plugin manager.
|
||||
- Desktop package operations use launcher-supplied bundled pnpm and the shared manager’s subprocess environment and profile configuration.
|
||||
- The main application’s Plugins page sends structured package and activation requests to the shared Host service.
|
||||
- The reserved Desktop profile remains unavailable to the standalone CLI.
|
||||
- npm/CLI dsh and Electron never resolve or install plugins from each other's `node_modules`.
|
||||
- The active backend and Web UI report the same dsh version and a compatible shell API before the product UI loads.
|
||||
- Package or Host failures retain partial profile changes and expose recovery controls; no automatic profile rollback is promised.
|
||||
- The shared plugin manager owns package-failure handling; native recovery handles fatal Host failures.
|
||||
- One Desktop version binds Electron and dsh; every dsh update arrives through one Electron update dialog and one user-visible restart.
|
||||
- Shared `.dsh` data rejects incompatible readers before migration or mutation.
|
||||
- The Web application owns HTTP authentication and serving; the sandboxed renderer cannot access arbitrary filesystem or Electron APIs.
|
||||
@@ -151,8 +151,8 @@ Architecture-specific builds report actual component-level compressed and instal
|
||||
|---|---|
|
||||
| First launch | Check bundled release metadata and create profile links without installing core dependencies |
|
||||
| Desktop profile | One Electron-owned reserved profile for external plugins and shared package links |
|
||||
| Plugin management | Electron-only GUI and package service; no CLI, backend, or browser installation path |
|
||||
| Activation | In-place package changes followed by actual Host startup |
|
||||
| Plugin management | Shared Web Plugins page and Host service with launcher-supplied bundled pnpm |
|
||||
| Activation | Shared manager applies configuration through HMR when enabled; otherwise changes require restart |
|
||||
| Initial platforms | macOS arm64/x64 and Windows x64; Linux has no supported release target |
|
||||
| Update behavior | Background check, explicit confirmation before differential download and restart, startup dsh reconciliation |
|
||||
|
||||
@@ -160,14 +160,14 @@ Architecture-specific builds report actual component-level compressed and instal
|
||||
|
||||
Plugin lifecycle scripts execute third-party code. The profile’s `allowBuilds` configuration determines which builds may execute.
|
||||
|
||||
Updating the bound dsh can invalidate plugin peer dependencies or native modules. Installed plugin files remain in place; loading failures require explicit repair through the recovery UI.
|
||||
Updating the bound dsh can invalidate plugin peer dependencies or native modules. Native recovery can disable third-party bundles and restart the application so the shared Plugins page can be used for repair.
|
||||
|
||||
An npm-installed dsh and desktop dsh may have different versions while sharing durable data. Each shared owner must enforce its format version and process lock before reading, migrating, or writing.
|
||||
|
||||
Interrupted package operations retain partial changes. Recovery retries the actual Host or permits explicit plugin repair without an automatic package reinstall.
|
||||
Package-operation failure and cancellation follow the shared plugin manager’s restoration rules. Native recovery does not reinstall packages.
|
||||
|
||||
Code signing, notarization, and update hosting require production release infrastructure. Repository tests alone cannot complete that qualification.
|
||||
|
||||
## Related proposals
|
||||
|
||||
The [bundled-runtime decision](2026-09-08-desktop-bundled-runtime-and-external-plugins.md) owns core resources and external plugin dependencies. The [thin-wrapper decision](2026-09-10-desktop-web-wrapper.md) supersedes portless transport and private backend composition. Release identity, signing, process ownership, and Electron-only package authorization remain owned here.
|
||||
The [bundled-runtime decision](2026-09-08-desktop-bundled-runtime-and-external-plugins.md) owns core resources and external plugin dependencies. The [thin-wrapper decision](2026-09-10-desktop-web-wrapper.md) owns shared Web transport and plugin management. Release identity, signing, and process ownership remain owned here.
|
||||
+20
-20
@@ -4,7 +4,7 @@ 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 可执行文件的选择;本文其他决策仍然适用。
|
||||
|
||||
@@ -24,13 +24,13 @@ Electron 拥有 `.dsh/profiles/desktop` 保留 profile。[内置运行时决策]
|
||||
|
||||
一个 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 壳 | 窗口与子进程生命周期、本地壳页面、保留 desktop profile、插件 GUI、更新协调 |
|
||||
| Electron 壳 | 窗口与子进程生命周期、保留 desktop profile 准备、原生恢复、更新协调 |
|
||||
| Electron RunAsNode 与 pnpm | 执行 dsh 并安装桌面项目依赖,使用 pnpm 的正常配置 |
|
||||
| Desktop profile | 由内置运行时决策定义的外部插件依赖、已启用 bundle 顺序和共享链接 |
|
||||
| 私有 Desktop Host 包 | 与 dsh 一起安装、但不进入公共 CLI 包或 npm 发布的 Electron 专用子进程入口与组合 overlay |
|
||||
@@ -38,7 +38,7 @@ Electron 拥有 `.dsh/profiles/desktop` 保留 profile。[内置运行时决策]
|
||||
| 共享 `.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 插件管理器负责自身客户端文案。
|
||||
|
||||
## 文件系统布局
|
||||
|
||||
@@ -59,9 +59,9 @@ Electron 拥有 `.dsh/profiles/desktop` 保留 profile。[内置运行时决策]
|
||||
|
||||
## 安装与解析
|
||||
|
||||
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` 代码的桌面插件。
|
||||
|
||||
@@ -77,11 +77,11 @@ Electron 更新只使用一个 `electron-updater` 发布流和签名 `electron-b
|
||||
|
||||
核心 dsh 和私有 Desktop Host 只来自签名应用的资源树。插件安装把包规格交给 pnpm,包括本地和远程来源,但不接受原始 pnpm 命令。pnpm 负责依赖解析和 profile 的 `allowBuilds` 策略;Host 加载已启用的 bundle。
|
||||
|
||||
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 失效。共享 Web server 负责前端与客户端模块响应。插件安装器 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/@[email protected])在两种已发布模块构建中使用 `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 或签名不可用时,打包会在产生未签名产物前失败。共享 Web server 负责前端与客户端模块响应。插件安装器 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 可以逐字节还原二者。使用兼容的过滤器能够保留依赖内容与运行时完整性,无需删除特定架构的文件或削弱校验。
|
||||
|
||||
@@ -97,7 +97,7 @@ Windows 应用替换遵循[目录安装决策](2026-09-11-windows-directory-inst
|
||||
|
||||
| 表面 | 实现 |
|
||||
|---|---|
|
||||
| 壳 | `apps/desktop` 负责 Electron 窗口、受限 preload、自定义协议、子进程生命周期、项目事务、插件 GUI、更新协调和 electron-builder 配置。 |
|
||||
| 壳 | `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 录制仍是发布环境门槛。 |
|
||||
@@ -112,7 +112,7 @@ Windows 应用替换遵循[目录安装决策](2026-09-11-windows-directory-inst
|
||||
|
||||
**把产品 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 回退目录解析包。
|
||||
|
||||
@@ -130,13 +130,13 @@ Windows 应用替换遵循[目录安装决策](2026-09-11-windows-directory-inst
|
||||
|
||||
- 没有系统 Node.js 或 pnpm 的干净离线机器能够启动内置 dsh,无需安装核心依赖。
|
||||
- 签名应用记录最终运行时文件清单;每个 macOS 原生文件都具有发布 Developer ID、安全时间戳和 hardened runtime,每个 Windows 产物都具有配置的硬件 EV 签名。
|
||||
- `.dsh/profiles/desktop/node_modules` 能解析共享宿主链接和每个通过 GUI 安装的桌面插件。
|
||||
- 每次 Desktop 包操作都使用内置 pnpm,并遵循用户的正常环境与 profile 配置。
|
||||
- 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` 数据在迁移或修改前拒绝不兼容的读取方。
|
||||
- Web 应用负责 HTTP 认证与服务;沙箱渲染进程不能访问任意文件系统或 Electron API。
|
||||
@@ -151,8 +151,8 @@ Windows 应用替换遵循[目录安装决策](2026-09-11-windows-directory-inst
|
||||
|---|---|
|
||||
| 首次启动 | 检查内置发布元数据并创建 profile 链接,不安装核心依赖 |
|
||||
| 桌面 profile | 一个由 Electron 拥有的保留 profile,保存外部插件和共享包链接 |
|
||||
| 插件管理 | Electron-only GUI 与包服务;没有 CLI、后端或浏览器安装路径 |
|
||||
| 激活 | 直接修改包后启动实际 Host |
|
||||
| 插件管理 | 共享 Web“插件”页面与 Host 服务,使用启动器提供的内置 pnpm |
|
||||
| 激活 | 启用 HMR 时由共享管理器应用配置;否则变更需要重启 |
|
||||
| 初始平台 | macOS arm64/x64 与 Windows x64;Linux 尚无受支持的发布目标 |
|
||||
| 更新行为 | 后台检查,差分下载与重启前显式确认,启动时校准 dsh |
|
||||
|
||||
@@ -160,14 +160,14 @@ Windows 应用替换遵循[目录安装决策](2026-09-11-windows-directory-inst
|
||||
|
||||
插件生命周期脚本会执行第三方代码。profile 的 `allowBuilds` 配置决定哪些构建获准执行。
|
||||
|
||||
更新绑定的 dsh 可能使插件 peer 依赖或原生模块不兼容。已安装插件文件保留原位;加载失败需要通过恢复 UI 显式修复。
|
||||
更新绑定的 dsh 可能使插件 peer 依赖或原生模块不兼容。原生恢复可以禁用第三方 bundle 并重启应用,之后可通过共享“插件”页面修复。
|
||||
|
||||
通过 npm 安装的 dsh 与桌面 dsh 可能在共享持久化数据时使用不同版本。每个共享 owner 都必须在读取、迁移或写入前执行格式版本与进程锁。
|
||||
|
||||
包操作中断会保留部分变更。恢复操作重试实际 Host,或允许显式修复插件,不会自动重新安装包。
|
||||
包操作失败与取消遵循共享插件管理器的还原规则。原生恢复不会重新安装包。
|
||||
|
||||
代码签名、公证和更新托管需要生产发布基础设施。只运行仓库测试不能完成这些认证。
|
||||
|
||||
## 相关提案
|
||||
|
||||
[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)负责核心资源与外部插件依赖。[薄壳决策](2026-09-10-desktop-web-wrapper.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
@@ -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-session-observations-and-projection-owned-client-state.md
|
||||
2026-08-25-session-observations-and-projection-owned-client-state.md: ff5628c3051086159d532b71ca3e98c1b41a51f8
|
||||
2026-08-25-session-observations-and-projection-owned-client-state.zh.md: e433df28fec86564fe1c1e9e88686a5abe6b9549
|
||||
2026-08-25-session-observations-and-projection-owned-client-state.md: 0ff8cbf50c18285092432850ce5222430f5acf0d
|
||||
2026-08-25-session-observations-and-projection-owned-client-state.zh.md: f3fca078d089a9ee1e329fdbb661653053053a91
|
||||
+1
-1
@@ -120,7 +120,7 @@ Client-local interaction state also remains local: loading and error status, an
|
||||
|
||||
### Domain applications
|
||||
|
||||
- **Title and list metadata.** Cached projection hints may render an existing title and determine blankness or recency. Missing hints leave those facts unknown; only the bounded small-log policy may resolve them during listing.
|
||||
- **Title and list metadata.** Cached projection hints may render an existing title and determine blankness or recency. Missing hints leave those facts unknown; only the bounded small-log policy may resolve them during listing. The Client reconciles list rows with the current metadata projection: nonblank evidence excludes a Session from blank reuse, and the later prompt timestamp supplies recency. Projection stores outlive lazy Client Session instances, so instantiation reads retained metadata even before a list row is available. A stale list response cannot override a newer history or control projection.
|
||||
- **Model selection.** `model/selection` records a complete provider, model, and optional reasoning effort. `modelSelection` distinguishes the last request's route from a later selection pending consumption by a request header.
|
||||
- **Agent preset.** The projection initializes from immutable Session metadata and advances on preset-selection events. A missing or `null` value is not replaced with the deployment default for an existing Session.
|
||||
- **Subagent identity.** The `subagent` unit remains the sole descriptor interpreter. Listing obtains candidates from the shared corpus and resolves values through live state, projection cache, or an observation rather than scanning events itself.
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ Client 本地交互状态也继续留在本地:loading 和 error 状态、打
|
||||
|
||||
### 领域应用
|
||||
|
||||
- **Title 与 list metadata。** Cached projection hints 可以渲染已有 title,并判断 blankness 或 recency。Hints 缺失时这些事实保持未知;listing 期间只有有界小日志策略可以解析它们。
|
||||
- **Title 与 list metadata。** Cached projection hints 可以渲染已有 title,并判断 blankness 或 recency。Hints 缺失时这些事实保持未知;listing 期间只有有界小日志策略可以解析它们。Client 使用当前 metadata projection 对账列表行:非空白事实会将 Session 排除在空白复用之外,较晚的 prompt 时间用于最近活动排序。Projection store 的生命周期长于惰性创建的 Client Session 实例,因此实例化时即使尚无列表行,也会读取已保留的元数据。过期列表响应不能覆盖较新的 history 或 control projection。
|
||||
- **Model selection。** `model/selection` 记录完整 provider、model 和可选 reasoning effort。`modelSelection` 区分上一请求使用的 route,以及等待 request header 消费的较晚 selection。
|
||||
- **Agent preset。** Projection 从不可变 Session metadata 初始化,并随 preset-selection event 推进。对于现有 Session,缺失或 `null` 值不会替换成部署默认值。
|
||||
- **Subagent identity。** `subagent` unit 仍是唯一 descriptor interpreter。Listing 从共享 corpus 获得 candidate,并通过 live state、projection cache 或 observation 解析值,不自行扫描 event。
|
||||
|
||||
+2
-2
@@ -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-28-subprocess-native-containment.md
|
||||
2026-08-28-subprocess-native-containment.md: 1b0fd162be77356001bcd9224bb2ee2889e6d6c0
|
||||
2026-08-28-subprocess-native-containment.zh.md: f33643f98a6ea6a90c0780432cbe11a35a868562
|
||||
2026-08-28-subprocess-native-containment.md: 64fd59e13ea38d7f6e066d5dadec4be830c9c0af
|
||||
2026-08-28-subprocess-native-containment.zh.md: 59e51c5a5c19f55058eb3881928d4f218a22a59c
|
||||
@@ -54,6 +54,8 @@ This note owns the current native-containment mechanism. It partially updates th
|
||||
|
||||
## Verification
|
||||
|
||||
The host-exit disposal fixture compares the provider's listener identities before and after disposal and verifies that an independently registered exit listener remains installed. Total process listener counts cannot establish provider cleanup because other process-lifetime handlers may register after provider construction.
|
||||
|
||||
- Provider and Linux protocol suites pin synchronous NUL rejection before launch side effects, strict request/error decoding, target cwd and complete environment restoration, private-variable collision, symlink-sensitive PATH traversal with preserved argv, close-on-exec removal for inherited stdio, pre-exec error ownership, failed-deep-probe retry plus successful-deep-probe caching with per-call manager checks, the three scope-establishment states including requested versus unexpected exits with an unconsumed request, `LoadState`/`ActiveState`/`TasksCurrent` parsing, releasing a leftover scope left active with no processes beside the live-client, unterminated, and unset-count cases, `reloading`, terminate wake-up with losing-delay cancellation, bounded established-scope backoff, and exactly-once PTY managed-owner cleanup.
|
||||
- Windows protocol and Win32 suites pin exactly two result branches, numeric-only target exits, ordinary-error start cancellation with raw parent-local reasons, the reduced `name`/`message`/`code`/`syscall`/`path` error record, the fixed `2`/`3`/`267` to `ENOENT`, `740` to `EACCES`, `5` to `EPERM`, `193` to `EFTYPE`, and remaining-code to `UNKNOWN` mapping, start delivery after runner spawn, empty-range settlement after pre-spawn failure, explicit ordinally sorted target environment blocks with `=C:` preservation and double-NUL termination, `uv_get_osfhandle()` carrier mapping and unsigned invalid-sentinel rejection, the null-device ignored-stdin carrier and piped non-ignored stdin, result-send and IPC-disconnect failures, direct-result latching before stdio settlement, active-process quiescence, and unique handle cleanup.
|
||||
- A keyless [`bash-startup-timeout`](../../../../snapshots/session/bash-startup-timeout/snapshot.yml) Session snapshot pins the model-facing timeout result. A Linux user-systemd fixture holds the launch request unconsumed at an input barrier and verifies cancellation plus range settlement.
|
||||
|
||||
@@ -54,6 +54,8 @@ selector 是 per-spawn locator 或 sentinel,不是凭据或持久格式。Linu
|
||||
|
||||
## Verification
|
||||
|
||||
宿主退出清理 fixture 对比 provider 在 disposal 前后的监听器身份,并验证独立注册的退出监听器仍然存在。进程监听器总数无法证明 provider 清理完成,因为其他进程生命周期处理器可能在 provider 构造之后注册。
|
||||
|
||||
- provider 与 Linux 协议测试套件固定同步 NUL 拒绝发生在启动副作用之前、严格 request/error 解码、target cwd 与完整环境恢复、私有变量碰撞、保留 argv 且对 symlink 敏感的 PATH 遍历、为继承 stdio 清除 close-on-exec、pre-exec error ownership、失败深度 probe 重试与成功深度 probe 缓存及逐调用 manager 检查、三种 scope 建立状态(包括 request 未消费时的请求终止与意外退出)、`LoadState`/`ActiveState`/`TasksCurrent` 解析、释放被留在 active 且没有任何进程的遗留 scope(连同 client 仍存活、未请求终止与进程数未上报三种情形)、`reloading`、带未胜出 delay 取消的 terminate wake-up、建立后有上限的退避,以及 PTY managed-owner 恰好一次 cleanup。
|
||||
- Windows 协议与 Win32 测试套件固定恰好两个 result 分支、只含数字的 target exit、使用普通 error 的 start cancellation 与 parent 原样保留的本地 reason、缩减到 `name`/`message`/`code`/`syscall`/`path` 的 error record、固定的 `2`/`3`/`267` 到 `ENOENT`、`740` 到 `EACCES`、`5` 到 `EPERM`、`193` 到 `EFTYPE` 及其余 code 到 `UNKNOWN` 的映射、runner spawn 后才发送 start、spawn 前 failure 的 empty-range settlement、按序数显式排序的 target 环境块及 `=C:` 保留和双 NUL 结尾、`uv_get_osfhandle()` carrier 映射与 unsigned invalid sentinel 拒绝、null-device ignored-stdin carrier 与非 ignore stdin pipe、result-send 与 IPC-disconnect failure、stdio settlement 前的 direct-result 锁存、active-process 完全停稳,以及唯一 handle cleanup。
|
||||
- 无需密钥的 [`bash-startup-timeout`](../../../../snapshots/session/bash-startup-timeout/snapshot.yml) Session 快照固定模型可见的超时结果。Linux user-systemd fixture 通过输入屏障保持启动请求未消费,并验证取消与 range settlement。
|
||||
|
||||
+2
-2
@@ -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-shared-client-control-primitives.md
|
||||
2026-09-05-shared-client-control-primitives.md: 14128497cf4152659a62136cbdeb74b92ee3fbf0
|
||||
2026-09-05-shared-client-control-primitives.zh.md: 66268b48f81887181c3981bf89ead30eb74250af
|
||||
2026-09-05-shared-client-control-primitives.md: db8cb9a8bc515918867b27b216fe8edb50c392db
|
||||
2026-09-05-shared-client-control-primitives.zh.md: 6d4b04a21bf224eaa32b5fbdfb2bf1c3a2cd804d
|
||||
+9
-10
@@ -18,15 +18,15 @@ Neither half of the fix was available to an author. No checklist told them to lo
|
||||
|
||||
`Switch` is the two-state toggle, at the 36×20 track and 16px thumb `ui-settings-plugins` established. `label` is required and has no default, so a render site cannot omit the accessible name; `title` carries a lock reason where a deployment disables the control.
|
||||
|
||||
`StateDotState` gains `idle`, a static grey dot in the same halo-and-core construction as `done`, `warning`, and `error`. The plugin inventory's `pending` and `unloading` phases mean no activity is in progress, and the four existing states had no member for that; without `idle` those two phases lose their marker entirely. The addition is safe for the eight packages already consuming `StateDot` because each of them produces a `StateDotState` from its own closed status union and none switches over `StateDotState` itself.
|
||||
`StateDotState` includes `idle` for a tracked subject with no activity in progress. The plugin inventory's `pending` and `unloading` phases need that semantic member; without it those two phases lose their marker entirely. The five-state union remains safe for consumers because each package produces a `StateDotState` from its own closed status union and none switches over `StateDotState` itself. The current solid-dot palette and rotating `ongoing` loader are owned by the later [StateDot visual language decision](../feature/2026-09-17-state-dot-visual-language.md).
|
||||
|
||||
**The component catalog in the `ui-primitives` README is what makes the rule usable.** It lists every exported component with its purpose and the case it is wrong for, and it names the three pairs that are easy to confuse: `Tag` against `Pill`, `DisclosureRow` against card-shaped disclosure, and the package-internal `FoldToggle` against the exported surface. `Pill` is the selectable capsule button — it takes `active` and `onClick` and drives view switchers and filters; `Tag` is the read-only badge and takes neither. `Pill`'s own header comment previously advertised itself for badges, which contradicted the catalog, and no longer does.
|
||||
**The component catalog in the `ui-primitives` README is what makes the rule usable.** It lists every exported component with its purpose and the case it is wrong for, and it names the three pairs that are easy to confuse: `Tag` against `Pill`, `DisclosureRow` against card-shaped disclosure, and the package-internal `FoldToggle` against the exported surface. `Pill` is the selectable capsule button — it takes `active` and `onClick` and drives view switchers and filters; `Tag` is the read-only badge and takes neither. The component comments use the same distinction.
|
||||
|
||||
The rule is stated in [packages/client/AGENTS.md](../../../../packages/client/AGENTS.md) as the first step of the new-component checklist, and [docs/web-styling.md](../../../../docs/web-styling.md) points at the catalog so an author arriving from the styling side reaches it too.
|
||||
|
||||
## Finding the duplicates
|
||||
|
||||
A name-based search undercounts. `.badge`, `.tag`, `.chip`, and `.configTag` miss a capsule named for its role rather than its appearance — `PluginCard`'s unsaved marker is `.pending`, and its rule was byte-identical to the badge two files away. What finds them is the geometry: a CSS Modules rule carrying both `border-radius: 999px` and `padding: 1px 8px`. After this change that signature matches exactly two rules: `Tag` itself and the broken badge below. The unsaved marker keeps only a `flex: none` placement class, which the signature no longer matches.
|
||||
A name-based search undercounts. `.badge`, `.tag`, `.chip`, and `.configTag` miss a capsule named for its role rather than its appearance — `PluginCard`'s unsaved marker is `.pending`. Geometry is the reliable probe: search CSS Modules rules carrying both `border-radius: 999px` and `padding: 1px 8px`. That signature belongs to `Tag` and the deliberately local broken badge below; the unsaved marker keeps only its `flex: none` placement class.
|
||||
|
||||
<a id="what-stays-local"></a>
|
||||
## What stays local
|
||||
@@ -34,7 +34,6 @@ A name-based search undercounts. `.badge`, `.tag`, `.chip`, and `.configTag` mis
|
||||
A mechanical search groups these controls with the promoted three. They stay in their own packages, because the grouping is superficial:
|
||||
|
||||
- **`ui-trajectory`'s toolbar toggle** carries `role="switch"` but is an 88px labelled control with an inline track, and it currently renders `hidden`. It is not the same widget as `Switch`.
|
||||
- **`ui-schedule`'s status dot** is a static blue dot for the next run that turns amber when overdue. `StateDot` has no static blue — its only blue is `ongoing`, an animated pixel matrix — and one animation per row in a schedule list would misstate the meaning as well as the appearance.
|
||||
- **`ui-plan`'s mode chip** and **`ui-conversation`'s `ReferenceChip`** are interactive: the first is a warning-toned button with hover, focus, disabled, and a close affordance; the second is a Lexical atom node with its own truncation. Neither is a read-only badge.
|
||||
- **`ui-trajectory`'s cell tag** and **`ui-user-questions`'s recommendation badge** use their own geometry — a 6px radius at table density, and a 6px radius at 600 weight on the sidebar accent. Forcing either into the capsule baseline would change a deliberate design, not an accidental one.
|
||||
- **`TerminalBlock`'s exit-status pill** stays a static `Pill`. It sits on a 24px command line at the pill's own geometry, and `Tag`'s 11px capsule would not fit that row. The read-only/selectable split is the usual guide, but size decides this one, and the catalog says so.
|
||||
@@ -52,7 +51,7 @@ A mechanical search groups these controls with the promoted three. They stay in
|
||||
|
||||
**A two-axis `variant × tone` API for `Tag`.** Rejected. Three variants against six tones describes eighteen combinations of which eight ship, and it lets a caller request combinations with no defined appearance. The flat eight-member union maps each value to exactly one shipped appearance.
|
||||
|
||||
**Rendering no dot for `pending` and `unloading` instead of adding `idle`.** Rejected. Those rows show a grey dot today, and dropping it would remove information from the inventory in a change whose purpose is to consolidate presentation.
|
||||
**Rendering no dot for `pending` and `unloading` instead of adding `idle`.** Rejected. Those phases need a compact inactive or transitional marker; dropping it would remove information from the inventory in a change whose purpose is to consolidate presentation.
|
||||
|
||||
**Putting the reuse rule in the root `AGENTS.md`.** Rejected. The rule governs `packages/client` alone, and the root file sits exactly at its 1950-word ceiling in `scripts/doc-budgets.manifest.json`, so stating it there would have to displace an unrelated repository-wide rule.
|
||||
|
||||
@@ -60,15 +59,15 @@ A mechanical search groups these controls with the promoted three. They stay in
|
||||
|
||||
`Tag`, `Switch`, and the extended `StateDot` carry component specs in `packages/client/ui-primitives/tests`, inside the per-file 100% coverage gate. `StateDot`'s palette is pinned by reading its stylesheet: CSS Modules resolve to class-name maps in the component suites, so a state whose color rule is missing renders on the inherited color and no render assertion notices.
|
||||
|
||||
The migrated render sites keep their existing package specs unchanged. The web e2e goldens are ARIA snapshots, and the full replayed web suite passes without re-recording, because the migration preserves every role, accessible name, and state — `Switch` keeps `role="switch"` with `aria-checked`, and the inventory's phase dot keeps its `role="img"` name on a wrapper, since `StateDot` is `aria-hidden`.
|
||||
The migrated render sites keep package specs for their role, accessible name, state mapping, and interaction. Web ARIA snapshots cover the assembled presentations — `Switch` keeps `role="switch"` with `aria-checked`, and the inventory's phase dot keeps its `role="img"` name on a wrapper because `StateDot` is `aria-hidden`.
|
||||
|
||||
That is also the limit of the automated evidence. No gate in this repository compares pixels, so the capsule geometry, the dot halo, and the font-weight change are verified by review against the light and dark screenshots in the pull request.
|
||||
The component and stylesheet specs pin the capsule geometry, StateDot artwork, animation, tokens, and font weight; Web snapshots pin accessible structure. Manual visual inspection remains responsible for the rendered result across themes.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A new client control now has one place to check and one place to add, and the catalog makes the check a single file read rather than a `grep` over forty exports.
|
||||
- `TagTone` is eight members wide because eight appearances shipped. Adding a ninth requires a render site that needs it, not a symmetry argument.
|
||||
- The plugin inventory's tags change from a 5px rectangle to a capsule, and the disabled tag's fill moves from `--dsw-alias-bg-layer-1` to the `neutral` tone's `--dsw-alias-bg-module-platform`, which is a visible grey where it used to be near-transparent. Its phase dots gain a halo and animate through `loading` and `unloading`. The unconfigured-secret badge in the plugin settings fields moves from 400 to the baseline 500 weight. These are deliberate visual changes, recorded here so a later reader does not treat them as regressions.
|
||||
- The migration removes a literal `#b45309`. The plugin inventory's `conditional` tag read `var(--dsw-alias-state-warning-primary, #b45309)`, and no such alias exists — the real token is `--dsw-alias-state-warn-primary` — so both themes had been painting the hardcoded fallback that [docs/web-styling.md](../../../../docs/web-styling.md) forbids.
|
||||
- The plugin inventory uses capsule tags; the disabled tag uses the `neutral` tone's visible `--dsw-alias-bg-module-platform` fill. Its phase markers use the shared `StateDot`, including the rotating loader for active transitions. The unconfigured-secret badge in the plugin settings fields uses the baseline 500 weight.
|
||||
- The plugin inventory palette contains no literal `#b45309`. The conditional tag uses the real `--dsw-alias-state-warn-primary` token rather than the nonexistent `--dsw-alias-state-warning-primary` alias and its hardcoded fallback.
|
||||
- The rule cannot be checked mechanically. A future author can still copy a control, and only review will catch it. That is the accepted cost of not gating: the alternative rejects legitimate work, and the packages that stay local above are proof that legitimate work exists.
|
||||
- `ui-primitives` grows two components that one package each consumes today. The switch in particular is a single-consumer primitive, promoted because it is a general control and because the plugin-management work already in flight will adopt it rather than adding two further copies.
|
||||
- `Tag` and `Switch` are shared across settings and preset packages; future consumers extend these primitives only when an existing prop cannot express a deliberate difference.
|
||||
+9
-10
@@ -18,15 +18,15 @@ Status: implemented
|
||||
|
||||
`Switch` 是双态开关,采用 `ui-settings-plugins` 确立的 36×20 轨道与 16px 滑块。`label` 必填且没有默认值,渲染点无法省略无障碍名称;`title` 在部署禁用该控件时承载锁定原因。
|
||||
|
||||
`StateDotState` 增加 `idle`,一个静态灰点,与 `done`、`warning`、`error` 采用同样的光晕加实心核结构。插件清单的 `pending` 与 `unloading` 相位表示没有进行中的活动,原有四态没有对应成员;没有 `idle`,这两个相位会彻底失去标记。这次增补对已经消费 `StateDot` 的八个包是安全的:它们各自从自己的闭合状态联合**产生** `StateDotState`,没有任何一个对 `StateDotState` 本身做 switch。
|
||||
`StateDotState` 包含 `idle`,表示一个被跟踪的对象当前没有进行中的活动。插件清单的 `pending` 与 `unloading` 相位需要这个语义成员;没有 `idle`,这两个相位会彻底失去标记。五态联合对消费方保持安全,因为每个包都从自己的闭合状态联合**产生** `StateDotState`,没有任何一个对 `StateDotState` 本身做 switch。当前的纯色点配色与旋转 `ongoing` loading 由后续的 [StateDot 视觉语言决策](../feature/2026-09-17-state-dot-visual-language.zh.md)负责。
|
||||
|
||||
**`ui-primitives` README 里的组件目录,是这条规则得以可用的前提。** 它列出每个导出组件的用途,以及它不适用的场合,并点名三组容易混淆的配对:`Tag` 与 `Pill`、`DisclosureRow` 与卡片式折叠、包内的 `FoldToggle` 与对外导出面。`Pill` 是可选中的胶囊按钮——它接受 `active` 与 `onClick`,用于视图切换与筛选器;`Tag` 是只读徽章,两者都不接受。`Pill` 自己的头注释此前把自身宣传为可用作徽章,与目录互相矛盾,现已改掉。
|
||||
**`ui-primitives` README 里的组件目录,是这条规则得以可用的前提。** 它列出每个导出组件的用途,以及它不适用的场合,并点名三组容易混淆的配对:`Tag` 与 `Pill`、`DisclosureRow` 与卡片式折叠、包内的 `FoldToggle` 与对外导出面。`Pill` 是可选中的胶囊按钮——它接受 `active` 与 `onClick`,用于视图切换与筛选器;`Tag` 是只读徽章,两者都不接受。组件注释使用同一套区分。
|
||||
|
||||
规则写在 [packages/client/AGENTS.md](../../../../packages/client/AGENTS.md) 新组件清单的第一步,[docs/web-styling.md](../../../../docs/web-styling.zh.md) 指向该目录,让从样式一侧进来的作者也能到达。
|
||||
|
||||
## 如何找出重复
|
||||
|
||||
按名字搜会漏。`.badge`、`.tag`、`.chip`、`.configTag` 找不到以角色而非外观命名的胶囊——`PluginCard` 的未保存标记叫 `.pending`,而它的规则与两个文件之外的徽章逐字节相同。能找出它们的是几何特征:同时带有 `border-radius: 999px` 与 `padding: 1px 8px` 的 CSS Modules 规则。本次改动后,这个特征恰好匹配两条规则:`Tag` 本身与下面那个损坏徽章。未保存标记只剩一个 `flex: none` 布局类,已不再匹配该特征。
|
||||
按名字搜会漏。`.badge`、`.tag`、`.chip`、`.configTag` 找不到以角色而非外观命名的胶囊——`PluginCard` 的未保存标记叫 `.pending`。几何特征才是可靠探针:搜索同时带有 `border-radius: 999px` 与 `padding: 1px 8px` 的 CSS Modules 规则。这个特征属于 `Tag` 与下面那个有意保留在原包的损坏徽章;未保存标记只保留自己的 `flex: none` 布局类。
|
||||
|
||||
<a id="what-stays-local"></a>
|
||||
## 保留在原包的控件
|
||||
@@ -34,7 +34,6 @@ Status: implemented
|
||||
机械搜索会把下面这些控件归到被提升的那三类里。它们留在各自的包中,因为那种归并只是表面相似:
|
||||
|
||||
- **`ui-trajectory` 的工具栏开关**带 `role="switch"`,但它是 88px 宽的带标签控件、轨道内联,而且当前渲染为 `hidden`。它和 `Switch` 不是同一个部件。
|
||||
- **`ui-schedule` 的状态点**是表示下次运行的静态蓝点,逾期转为琥珀色。`StateDot` 没有静态蓝——它唯一的蓝是 `ongoing`,一个动画像素方阵——日程列表里每行一个动画,既说错了含义也说错了观感。
|
||||
- **`ui-plan` 的模式 chip** 与 **`ui-conversation` 的 `ReferenceChip`** 都是可交互的:前者是警告色调的按钮,带 hover、focus、disabled 与关闭操作;后者是带自有截断逻辑的 Lexical 原子节点。两者都不是只读徽章。
|
||||
- **`ui-trajectory` 的单元格 tag** 与 **`ui-user-questions` 的推荐徽章**使用各自的几何——一个是表格密度下的 6px 圆角,一个是侧栏强调色上 600 字重的 6px 圆角。把它们塞进胶囊基准,改掉的是有意的设计,不是意外的分歧。
|
||||
- **`TerminalBlock` 的退出状态胶囊**保持为静态 `Pill`。它落在 24px 的命令行上、用的是 pill 自己的几何,`Tag` 的 11px 胶囊放不进那一行。只读与可选中的区分是通常的判据,但这一处由尺寸决定,目录里也是这么写的。
|
||||
@@ -52,7 +51,7 @@ Status: implemented
|
||||
|
||||
**给 `Tag` 设计 `variant × tone` 双轴 API。** 否决。三个 variant 乘六个 tone 描述十八种组合,其中八种会发布,而且它允许调用方请求没有定义外观的组合。扁平的八成员联合把每个值映射到恰好一种已发布外观。
|
||||
|
||||
**让 `pending` 与 `unloading` 不渲染点,而不是新增 `idle`。** 否决。这两行今天显示灰点,在一个目的是统一呈现的改动里把它删掉,等于从清单中拿走信息。
|
||||
**让 `pending` 与 `unloading` 不渲染点,而不是新增 `idle`。** 否决。这些阶段需要紧凑的未活动或过渡标记;在一个目的是统一呈现的改动里把它删掉,等于从清单中拿走信息。
|
||||
|
||||
**把复用规则写进根 `AGENTS.md`。** 否决。这条规则只管 `packages/client`,而根文件正好处在 `scripts/doc-budgets.manifest.json` 给它的 1950 词上限上,写在那里就必须挤掉一条不相干的仓库级规则。
|
||||
|
||||
@@ -60,15 +59,15 @@ Status: implemented
|
||||
|
||||
`Tag`、`Switch` 与扩展后的 `StateDot` 在 `packages/client/ui-primitives/tests` 中各有组件测试,处于每文件 100% 覆盖率门禁之内。`StateDot` 的配色通过读取其样式表来钉住:CSS Modules 在组件测试里解析为类名映射,因此某个状态缺了配色规则时会落到继承色上,而任何渲染断言都不会察觉。
|
||||
|
||||
迁移后的渲染点保留各自包内原有的测试,未作改动。web e2e 的 golden 是 ARIA 快照,完整的 replay web 套件无需重录即可通过,因为这次迁移保住了每一个 role、无障碍名称与状态——`Switch` 保留带 `aria-checked` 的 `role="switch"`,而由于 `StateDot` 是 `aria-hidden`,插件清单的相位点把 `role="img"` 名称保留在外层包裹元素上。
|
||||
迁移后的渲染点保留包级测试,覆盖 role、无障碍名称、状态映射与交互。Web ARIA 快照覆盖装配后的呈现——`Switch` 保留带 `aria-checked` 的 `role="switch"`,而由于 `StateDot` 是 `aria-hidden`,插件清单的相位点把 `role="img"` 名称保留在外层包裹元素上。
|
||||
|
||||
这同时也是自动化证据的边界。本仓库没有任何门禁比较像素,因此胶囊几何、状态点光晕与字重变化,只能由评审对照 PR 中的明暗两套截图来确认。
|
||||
组件与样式表测试固定胶囊几何、StateDot 图稿、动画、token 与字重;Web 快照固定无障碍结构。明暗主题下的最终渲染仍由人工视觉检查负责。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 新的客户端控件现在有一处可查、一处可加,而目录把这次查询变成读一个文件,而不是在四十多个导出上 `grep`。
|
||||
- `TagTone` 有八个成员,因为发布了八种外观。加第九个需要一个真的需要它的渲染点,而不是一个对称性论证。
|
||||
- 插件清单的标签从 5px 矩形变为胶囊,其中"已停用"标签的底色从 `--dsw-alias-bg-layer-1` 变为 `neutral` 色调的 `--dsw-alias-bg-module-platform`,由近乎透明变成可见的灰底。相位点获得光晕,并在 `loading` 与 `unloading` 两个相位动画。插件设置字段中"未配置"徽章的字重从 400 改为基准的 500。这些都是有意的视觉变更,记录在此,以免后来的读者把它们当成回归。
|
||||
- 本次迁移移除了一个 `#b45309` 字面量。插件清单的 `conditional` 标签写的是 `var(--dsw-alias-state-warning-primary, #b45309)`,而这个别名并不存在——真正的 token 是 `--dsw-alias-state-warn-primary`——所以明暗两套主题一直在绘制那个被 [docs/web-styling.md](../../../../docs/web-styling.zh.md) 禁止的硬编码兜底色。
|
||||
- 插件清单使用胶囊标签;"已停用"标签使用 `neutral` 色调下可见的 `--dsw-alias-bg-module-platform` 填充。相位标记使用共享 `StateDot`,活动转换采用旋转 loading。插件设置字段中的"未配置"徽章使用基准 500 字重。
|
||||
- 插件清单调色板不含 `#b45309` 字面量。`conditional` 标签使用真实的 `--dsw-alias-state-warn-primary` token,而不是不存在的 `--dsw-alias-state-warning-primary` 别名及其硬编码兜底色。
|
||||
- 这条规则无法被机械检查。将来的作者仍然可以复制一个控件,只有评审能拦住。这是不设门禁所接受的代价:另一条路会拒绝正当的工作,而上面那份保留在原包的清单就是正当工作确实存在的证据。
|
||||
- `ui-primitives` 多出两个各自只有一个消费方的组件。开关尤其如此,提升它是因为它是一个通用控件,也因为正在进行中的插件管理工作会采用它,而不是再加两份拷贝。
|
||||
- `Tag` 与 `Switch` 由多个设置和预设包共享;只有现有 prop 无法表达明确差异时,后续消费方才扩展这些原语。
|
||||
+2
-2
@@ -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: 599ce36ed4bb1dfc2cce063a461903e1a4aeb40e
|
||||
2026-09-05-sidebar-tab-types-and-navigation.zh.md: e4e8de5a389fea77aae705d63cc4fb16f88af647
|
||||
+1
-2
@@ -36,7 +36,7 @@ interface SidebarRightTabDefinition {
|
||||
|
||||
`priority` is one of three literal bands, spelled as strings so that a type from another package needs no runtime import: `extension` is the band of a type from outside the product and the highest, so a type that declares nothing outranks every viewer shipped here; `builtin` is the ordinary band for shipped types; `fallback` is the plain-content position that anything more specific should beat, which VS Code's text editor holds implicitly and our text preview holds explicitly. `candidates(address)` returns every type whose globs match and whose `canOpen` does not veto, ranked by band, then by the length of the longest pattern that matched, then by registration order. `claim(address, kind?)` takes the best candidate, or the named kind's type in force when the caller overrides (its globs are not consulted; naming the type is the decision), and throws for an address nothing will open — a wiring mistake, not a user error. `get(kind)` returns the type in force; `entries()` and `guide()` list the types and their guide boxes in force; `subscribe` observes changes.
|
||||
|
||||
`title(address)`, `guide[].title()`, and optional `guide[].description()` are thunks read on every use, so a language change needs no re-registration. [Guide start page and stat pill refinements](../feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the current description visibility and fallback-glyph rules. The registry itself is a plain object provided at `apply`'s top level **without** `Service.tracker`: a tracker would rebind `this.ctx` to the caller's context, and a cross-package `register()` would then add its effect to the caller's fiber while that fiber is the active scope, stalling the browser boot with no error.
|
||||
`title(address)`, `guide[].title()`, and optional `guide[].description()` are thunks read on every use, so a language change needs no re-registration. The [Sidebar README](../../../../packages/client/ui-sidebar-right/README.md#the-guide) defines description visibility and fallback-glyph rules. The registry itself is a plain object provided at `apply`'s top level **without** `Service.tracker`: a tracker would rebind `this.ctx` to the caller's context, and a cross-package `register()` would then add its effect to the caller's fiber while that fiber is the active scope, stalling the browser boot with no error.
|
||||
|
||||
### Bodies and titles: keyed Slot seats under the definition's `id`
|
||||
|
||||
@@ -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.
|
||||
+1
-2
@@ -36,7 +36,7 @@ interface SidebarRightTabDefinition {
|
||||
|
||||
`priority` 是三个字面量档位之一,写成字符串,好让别的包的类型不需要任何运行时引入:`extension` 是来自产品之外的类型的档位也是最高档,所以什么都不声明的类型压过这里随包交付的每个查看器;`builtin` 是随包类型的常规档;`fallback` 是任何更具体的东西都应压过的纯内容位置,VS Code 的文本编辑器隐含地占据它,我们的文本预览明确地占据它。`candidates(address)` 返回 glob 命中且 `canOpen` 未否决的每个类型,按档位、再按命中的最长 pattern 长度、再按注册顺序排序。`claim(address, kind?)` 取最佳候选,或在调用方指定时取该 kind 生效的类型(不查它的 glob;点名即决定),对无人愿开的地址抛错——这是接线错误,不是用户错误。`get(kind)` 返回生效类型;`entries()` 与 `guide()` 列出生效类型及其引导入口;`subscribe` 观察变化。
|
||||
|
||||
`title(address)`、`guide[].title()` 与可选的 `guide[].description()` 都是每次使用时重读的 thunk,语言切换无需重新注册。[引导起始页与统计 pill 的细化](../feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责当前的描述显示与兜底图标规则。注册表本身是 `apply` 顶层提供的普通对象,**不带** `Service.tracker`:tracker 会把 `this.ctx` 重绑到调用方上下文,跨包 `register()` 就会在调用方 fiber 仍是活动作用域时往它上加 effect,浏览器启动会无声卡死。
|
||||
`title(address)`、`guide[].title()` 与可选的 `guide[].description()` 都是每次使用时重读的 thunk,语言切换无需重新注册。[Sidebar README](../../../../packages/client/ui-sidebar-right/README.zh.md#the-guide)定义描述显示与兜底图标规则。注册表本身是 `apply` 顶层提供的普通对象,**不带** `Service.tracker`:tracker 会把 `this.ctx` 重绑到调用方上下文,跨包 `register()` 就会在调用方 fiber 仍是活动作用域时往它上加 effect,浏览器启动会无声卡死。
|
||||
|
||||
### 正文与标题:按定义 `id` keyed 的 Slot 坑位
|
||||
|
||||
@@ -116,6 +116,5 @@ interface SidebarRightTabParamsMap {} // key: kind — a page type declar
|
||||
## Deferred
|
||||
|
||||
- `sidebar://<kind>` 之外的导航协议:页内子路由、点名实现、以及面向生态的其它导航 scheme 规则。
|
||||
- 随包页类型的参数,今天未声明任何。
|
||||
- 从公开面往屏上会话之外的会话里打开;公开面只作用于已挂载的会话,而 tab 自己的动作已作用于其所在会话。
|
||||
- 从会话区打开失败时的本地化提示;目前是抛错文本本身。
|
||||
+2
-2
@@ -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: b49ae76bfd8c468e9c971f803cf714755978e5b4
|
||||
2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md: 58dd72124c0fdeb53652a600d0d3c143feb1ff3d
|
||||
2026-09-08-desktop-bundled-runtime-and-external-plugins.md: a5f9266e0e7a8def3f0ff6fc271bddde3db30163
|
||||
2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md: 2cc9fd635102f08e6ca4afb4f086a0cf6b8b05f0
|
||||
+4
-4
@@ -4,7 +4,7 @@ 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.
|
||||
|
||||
@@ -40,11 +40,11 @@ First launch creates profile metadata and host links without running pnpm, prese
|
||||
|
||||
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 use ordinary pnpm add and remove commands, including their configured lifecycle scripts. Installation sources and dependency resolution belong to pnpm. Bundle declarations select automatic activation; patch validation belongs to the bundle loader. Plain dependencies remain installed without activation. The bundled pnpm reads normal user and profile settings, including registry and build permissions. Desktop supplies no runtime build allowlist, strict-build setting, version-range restriction, or fixed profile identity. Unreadable package metadata does not prevent listing, disabling, or removing a dependency. Development uses the same manager against a separate plugin profile, with workspace packages supplied by the development runtime.
|
||||
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. Shared cleanup removes only fallback-owned links, preserving pnpm entries. The [in-place decision](2026-09-09-desktop-in-place-profile.md) owns partial failures and explicit recovery.
|
||||
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.
|
||||
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
|
||||
|
||||
|
||||
+4
-4
@@ -4,7 +4,7 @@ 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 可执行文件的选择;本文其他决策仍然适用。
|
||||
|
||||
@@ -40,11 +40,11 @@ profile manifest 分别记录 pnpm 安装的依赖及已启用 bundle 列表。
|
||||
|
||||
共享包目录使用原生规范路径识别。Windows 启动器可能改变路径大小写而不移动应用;字符串相等判断会触发不必要的 profile 准备。profile 清理在移除真实目录前,显式解除每一个嵌套目录链接。Windows 夹具在 Electron 44 下复现了递归 `fs.rmSync` 沿嵌套 junction 删除目标文件,而内置上游 Node 24.17 会保留它们。因此清理验收包含真实 Electron 运行时;仅在 Node 下测试不能证明目标文件会保留。
|
||||
|
||||
依赖修改使用普通的 pnpm add 和 remove 命令,并遵循其生命周期脚本配置。安装来源和依赖解析由 pnpm 负责。bundle 声明决定自动启用;patch 验证由 bundle 加载器负责。普通依赖安装后不自动启用。内置 pnpm 读取正常的用户和 profile 设置,包括 registry 和构建权限。Desktop 不提供运行时构建许可列表、严格构建设置、版本范围限制或固定 profile 身份。包元数据不可读时,仍可列出、禁用或删除依赖。开发模式使用同一管理器操作独立的插件 profile,工作区包由开发运行时提供。
|
||||
共享[插件管理器](../../../../packages/boot/plugin-manager/README.zh.md)负责支持的包规格、bundle 验证、激活和安装失败处理。内置 pnpm 读取正常的用户和 profile 设置。开发模式使用相同的 Web 管理器操作独立 Desktop profile,工作区包由开发运行时提供。
|
||||
|
||||
Desktop 在包变更前停止 Host,并等待 pnpm 退出后重新启动。共享清理仅移除模块补全逻辑拥有的链接,保留 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。已安装插件文件保留以供修复。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
+2
-2
@@ -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: be3a835a6296cb28d9a90c5c5e8b9bf7367598c2
|
||||
2026-09-08-document-preview-operations.zh.md: efb87e16cc126001c1a6eb2bf938124d7e00044a
|
||||
@@ -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.
|
||||
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. With [Developer tools](../feature/2026-09-17-developer-tools-settings.md) enabled, 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.
|
||||
|
||||
@@ -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。
|
||||
Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML、PDF 和图片读取完整 `Uint8Array<ArrayBuffer>` 数据;Host 传输保持 base64。发布后的缓冲区只读借用,绝不持久化进布局或 Session JSON。PDF.js 在自有 Worker 中运行,字体和解码数据以相同版本随包发布,转移输入前先复制,以保留 Preview 的缓冲区。开启[开发者工具](../feature/2026-09-17-developer-tools-settings.zh.md)时,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,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: 6aa7df2dcdd28b94cc0084a0044764efb2e749e2
|
||||
2026-09-08-global-main-panels.zh.md: bbb10b2ef5784748a165ad54c907fac2a63c9b17
|
||||
2026-09-08-global-main-panels.md: 63530a6b498bd8099cf627d82c888846d3d96767
|
||||
2026-09-08-global-main-panels.zh.md: 776787c3de3b79ca7948dcf949ac0393f87e7808
|
||||
@@ -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 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.
|
||||
|
||||
|
||||
@@ -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 或间距;现在 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
@@ -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
@@ -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
@@ -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
@@ -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: 2d325d5c9f9c3fc8dd83816538d0a1b4b4c5480b
|
||||
2026-09-09-desktop-immediate-window-and-direct-start.zh.md: 8aebe6bc8c86cc14b78d9dc979eeb902493de8ae
|
||||
2026-09-09-desktop-immediate-window-and-direct-start.md: e7ccf9b7456213d69795a40d9cd2a8509adfc44e
|
||||
2026-09-09-desktop-immediate-window-and-direct-start.zh.md: c59cc91f0013293cbaf2c086822085000dd3afd2
|
||||
+4
-4
@@ -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
|
||||
|
||||
@@ -16,9 +16,9 @@ Electron creates the main window with the packaged Web loading page before profi
|
||||
|
||||
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 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. Package mutations retain pnpm lifecycle scripts 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.
|
||||
+4
-4
@@ -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)。
|
||||
|
||||
## 问题
|
||||
|
||||
@@ -16,9 +16,9 @@ Electron 在 profile 校准或 Host 启动前创建带打包 Web 加载页的主
|
||||
|
||||
致命错误展示遵循[原生 Desktop 恢复](2026-09-15-desktop-native-fatal-recovery.zh.md)。窗口展示时机、直接启动 Host 和关闭所有权仍由本文规定。
|
||||
|
||||
Desktop 原位准备 profile 后,通过[共享 Web runner](2026-09-10-desktop-web-wrapper.zh.md)启动实际 Host。就绪消息提供认证 Host URL 与启动注入。壳使用该 URL 换取 Host cookie,转发应用 HTTP 请求,并仅为归属的应用 origin 认证直接 WebSocket 请求。这一载体适配保留 Web 路由与流语义,同时允许静态 HTML 在 Host 之前显示。包变更保留 pnpm 生命周期脚本与锁。失败保留部分变更以供显式修复,不会自动回滚 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 后,通过[共享 Web runner](2026-09-10-desktop-
|
||||
|
||||
用户可以在产品 UI 可用前看到启动进度并从失败中恢复。窗口能够响应不代表后端已经就绪,启动延迟仍需通过已安装产物测量。激活失败后,profile 修改保留在原位。
|
||||
|
||||
验证覆盖 Host 延迟时可见的加载页、新 profile 只启动一次服务进程、同一窗口中的失败与重试、恢复期间的插件管理,以及子进程正在启动时关闭应用。安装后 GUI 证据补充生命周期与事务测试。
|
||||
验证覆盖 Host 延迟时可见的加载页、新 profile 仅启动一次服务、同一窗口中的失败与重试、原生恢复,以及子进程启动时关闭。安装后 GUI 证据补充生命周期测试。
|
||||
+2
-2
@@ -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-profile-resolution-generations.md
|
||||
2026-09-09-profile-resolution-generations.md: 09ea70e49a4c876eac88e58262644e76c058fca5
|
||||
2026-09-09-profile-resolution-generations.zh.md: 0f852e9d31ef4c362f341d6b8fb6d5560225240f
|
||||
2026-09-09-profile-resolution-generations.md: ad6f05ba88b25ea52da97834cf4638c8feb961ba
|
||||
2026-09-09-profile-resolution-generations.zh.md: ffa9abc5a6a04bc9553878946fa3fb422328312c
|
||||
@@ -6,23 +6,23 @@ English | [中文](2026-09-09-profile-resolution-generations.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A profile loads plugin rows from its own package project, while Harness packages and packages carried by selected bundles can live outside that project's ordinary dependency tree. The current launcher bridges the trees by calculating package precedence at startup and materializing that result as shared symlinks, profile-owned links, or packaged-executable proxy packages. The files persist across processes and installations, require reconciliation and locking, expose generated proxy manifests to metadata readers, and cannot represent a process-local change atomically.
|
||||
A profile loads plugin rows from its own package project, while Harness packages and packages carried by selected bundles can live outside that project's ordinary dependency tree. Bridging the trees through shared symlinks, profile-owned links, or packaged-executable proxy packages persists package selections across processes and installations. Those files require reconciliation and locking, expose generated proxy manifests to metadata readers, and cannot represent a process-local change atomically.
|
||||
|
||||
The runtime design preserves the existing selection rules rather than introducing a second package policy. It covers imports performed by plugin modules as well as Loader row imports and works in the main thread and Harness-owned Workers. Generation replacement accepts only additive package sets and never mutates a live table entry by entry.
|
||||
|
||||
## Decision
|
||||
|
||||
Profile startup computes one immutable `ResolutionGeneration` from the same dependency traversal that supplies the disk module fallback. The launcher defaults to link mode and preserves the existing materialized lookup behavior. Internal callers and tests can select runtime mode, which installs the generation into Node's ESM and CommonJS resolvers, or dual mode, which materializes and verifies the same generation. `PluginPackages.replace()` publishes a complete additive successor with one reference replacement.
|
||||
Profile startup computes one immutable `ResolutionGeneration` from the same dependency traversal that supplies the disk module fallback. The launcher defaults to runtime mode, which installs the generation into Node's ESM and CommonJS resolvers without materializing fallback links. Plain Node callers and tests can explicitly select link mode to materialize the generation or dual mode to materialize and verify it. `PluginPackages.replace()` publishes a complete additive successor with one reference replacement.
|
||||
|
||||
### One selection algorithm
|
||||
|
||||
The package traversal remains in `@deepseek-ai/dsh-app-boot` beside profile loading. The disk materializer and the runtime resolver consume one pure plan; neither owns a copy of the precedence algorithm. Ordinary Node callers can select link, dual, or runtime mode, while an omitted mode selects link. Packaged executables and the Electron Host select runtime mode because their dependency trees may live in a virtual filesystem; dual remains an internal comparison path.
|
||||
The package traversal remains in `@deepseek-ai/dsh-app-boot` beside profile loading. The disk materializer and the runtime resolver consume one pure plan; neither owns a copy of the precedence algorithm. Ordinary Node callers can select link, dual, or runtime mode, while an omitted mode selects runtime. Packaged executables and the Electron Host select runtime mode because their dependency trees may live in a virtual filesystem; dual remains an internal comparison path.
|
||||
|
||||
The installation manifest is the first root. Its graph traverses `dependencies` followed by `peerDependencies` breadth-first, resolving each edge from the manifest that declares it. The first installed package reached under a name owns that name. Selected bundle roots then run in profile order, with each earlier root's complete graph taking precedence over every later root. Names supplied by the installation are reserved, and bundle package roots themselves do not become plugin fallbacks. Missing declared packages are skipped as before.
|
||||
|
||||
Profile-local and plugin-private `node_modules` entries stay outside the fallback entries, and Node checks them before the virtual fallback position. The generation records only installed direct profile package names for a no-I/O native fast path. Each fallback entry records the package name, version, selected lookup directory, declaring manifest anchor, and scope needed to rerun Node's native resolution from the selected package and validate that a successor preserves existing mappings.
|
||||
|
||||
The existing `healProfilesModuleFallback()` remains as the disk materializer for the same computed result, which permits direct comparison without rewriting the selection rules. The launcher uses it by default. Runtime mode computes the generation without materializing it, while dual mode materializes and installs that generation for comparison.
|
||||
The existing `healProfilesModuleFallback()` remains as the disk materializer for the same computed result, which permits direct comparison without rewriting the selection rules. Named profile launches use it only in explicit link or dual mode. Runtime mode computes the generation without materializing it, while dual mode materializes and installs that generation for comparison.
|
||||
|
||||
### Immutable generations
|
||||
|
||||
@@ -74,11 +74,11 @@ Legacy disk state remains available to link-only launches, old processes, and ro
|
||||
|
||||
Link, dual, and runtime modes use the same generation schema and dependency-selection policy. Link mode persists the computed result, runtime mode installs it only in the process, and dual mode requires Node's materialized result to equal the generation route.
|
||||
|
||||
The `dsh` launcher selects link mode when an ordinary Node caller omits `resolutionMode`, so existing npm-installed profile startup keeps its filesystem behavior. A pkg executable always selects runtime mode, and the Electron Host installs its runtime generation before any profile row mounts. Tests and low-level embedders can still select runtime or dual explicitly.
|
||||
The `dsh` launcher selects runtime mode when an ordinary Node caller omits `resolutionMode`. A pkg executable always selects runtime mode, and the Electron Host explicitly selects runtime mode in both development and packaged builds before any profile row mounts. Plain Node tests and low-level embedders can explicitly select link, dual, or runtime.
|
||||
|
||||
Runtime mode requires a supported Node Internal loader interface and does not create, update, or retire fallback links. Dual mode retains link writes and fails when Node's disk result differs from the generation. Writable profile state and package-manager transactions remain outside the resolver.
|
||||
|
||||
Pkg and packaged Electron carriers force runtime resolution. The Electron Host runs through the Electron executable with `ELECTRON_RUN_AS_NODE=1`, reads its dsh tree from ASAR, and maps executable ASAR entries to electron-builder's unpacked tree. Neither carrier creates, updates, or removes legacy resolution links.
|
||||
Pkg and Electron carriers force runtime resolution. The Electron Host runs through the Electron executable with `ELECTRON_RUN_AS_NODE=1`; packaged builds read the dsh tree from ASAR and map executable ASAR entries to electron-builder's unpacked tree. Their runtime resolvers do not create, update, or remove legacy resolution links.
|
||||
|
||||
### Performance and verification
|
||||
|
||||
@@ -115,4 +115,4 @@ Behavior tests compare the runtime generation with the disk materializer over th
|
||||
|
||||
## Consequences
|
||||
|
||||
Runtime startup avoids disk mutation and proxy manifests while preserving the existing package-selection algorithm. It accepts the maintenance cost of Node Internal compatibility tests and an early, self-contained bootstrap in each owned Worker. Link remains the ordinary Node launcher default, dual keeps a migration comparison path, and pkg plus Electron carriers force runtime resolution without retiring old links. Generation replacement remains additive until the product owns module-cache invalidation and Worker restart.
|
||||
Runtime startup avoids disk mutation and proxy manifests while preserving the existing package-selection algorithm. It accepts the maintenance cost of Node Internal compatibility tests and an early, self-contained bootstrap in each owned Worker. Runtime is the ordinary Node launcher default, link and dual remain explicit comparison options, and pkg plus Electron carriers force runtime resolution without the resolver retiring old links. Generation replacement remains additive until the product owns module-cache invalidation and Worker restart.
|
||||
+7
-7
@@ -6,23 +6,23 @@ Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
profile 从自己的包项目加载插件配置项,而 Harness 包和所选 bundle 携带的包可能位于该项目普通依赖树之外。当前启动器在启动时计算包优先级,再将结果物化为共享 symlink、profile 自有链接或打包可执行文件的代理包。文件跨进程和安装版本持续存在,需要协调和锁来维护,并向元数据读取方暴露生成的代理 manifest,也无法原子表示进程内变更。
|
||||
profile 从自己的包项目加载插件配置项,而 Harness 包和所选 bundle 携带的包可能位于该项目普通依赖树之外。通过共享 symlink、profile 自有链接或打包可执行文件的代理包连接两棵依赖树,会让选包结果跨进程和安装版本持续存在。这些文件需要协调和锁来维护,并向元数据读取方暴露生成的代理 manifest,也无法原子表示进程内变更。
|
||||
|
||||
运行时设计保留现有选包规则,不另建一套包策略。它覆盖插件模块内部的 import 以及 Loader 配置项的 import,并在主线程和 Harness 自有 Worker 中工作。generation 替换只接受新增包的集合,不会逐项修改正在使用的表。
|
||||
|
||||
## Decision
|
||||
|
||||
profile 启动从磁盘 module fallback 使用的同一套依赖遍历生成一个不可变 `ResolutionGeneration`。launcher 默认使用 link 模式,保留现有的物化查找行为。内部调用方和测试可以选择 runtime 模式,把 generation 安装到 Node 的 ESM 与 CommonJS resolver;也可以选择 dual 模式,同时物化并校验同一份 generation。`PluginPackages.replace()` 通过一次引用替换发布完整的新增型后继 generation。
|
||||
profile 启动从磁盘 module fallback 使用的同一套依赖遍历生成一个不可变 `ResolutionGeneration`。launcher 默认使用 runtime 模式,把 generation 安装到 Node 的 ESM 与 CommonJS 解析器,不物化 fallback 链接。普通 Node 调用方和测试可以显式选择 link 模式以物化 generation,或选择 dual 模式以物化并校验它。`PluginPackages.replace()` 通过一次引用替换发布完整的新增型后继 generation。
|
||||
|
||||
### 唯一选包算法
|
||||
|
||||
包遍历继续放在 `@deepseek-ai/dsh-app-boot` 的 profile 加载代码旁。磁盘 materializer 和运行时解析器消费同一个纯计划;两者都不持有另一份优先级算法。普通 Node 调用方可以选择 link、dual 或 runtime 模式,省略模式时使用 link。打包可执行文件与 Electron Host 会选择 runtime,因为其依赖树可能位于虚拟文件系统;dual 保留为内部对比路径。
|
||||
包遍历继续放在 `@deepseek-ai/dsh-app-boot` 的 profile 加载代码旁。磁盘 materializer 和运行时解析器消费同一个纯计划;两者都不持有另一份优先级算法。普通 Node 调用方可以选择 link、dual 或 runtime 模式,省略模式时使用 runtime。打包可执行文件与 Electron Host 会选择 runtime,因为其依赖树可能位于虚拟文件系统;dual 保留为内部对比路径。
|
||||
|
||||
安装 manifest 是第一个根。它按 BFS 依次遍历 `dependencies` 和 `peerDependencies`,每条边从声明它的 manifest 解析,同名包由第一次找到的已安装包占有。所选 bundle 随后按 profile 顺序逐根遍历;每个较早根的完整依赖图优先于所有较晚根。安装闭包中的名称被保留,bundle 包根本身不成为插件 fallback。与旧行为相同,已声明但未安装的包会被跳过。
|
||||
|
||||
profile 本地和插件私有 `node_modules` 不进入 fallback entries,由 Node 在虚拟 fallback 位置之前选择。generation 只记录已安装的 profile 直接包名用于 native 快速分流;每个 fallback 记录包名、版本、旧规则选中的查找目录、声明该边的 manifest 锚点和作用域,足以从选定包重新进入 Node 原生解析并验证换代保持既有映射。
|
||||
|
||||
旧 `healProfilesModuleFallback()` 保留为相同纯计算结果的磁盘 materializer,便于直接比较并避免重写旧规则。launcher 默认调用它。runtime 模式只计算 generation 而不物化,dual 模式会物化并安装该 generation 进行比较。
|
||||
旧 `healProfilesModuleFallback()` 保留为相同纯计算结果的磁盘 materializer,便于直接比较并避免重写旧规则。具名 profile 启动只在显式 link 或 dual 模式下调用它。runtime 模式只计算 generation 而不物化,dual 模式会物化并安装该 generation 进行比较。
|
||||
|
||||
### 不可变 generation
|
||||
|
||||
@@ -74,11 +74,11 @@ runtime-only 启动流程不创建、更新或退休 symlink 和代理包。reso
|
||||
|
||||
link、dual 与 runtime 模式使用同一种 generation schema 和依赖选择策略。link 模式持久化计算结果,runtime 模式只在进程内安装,dual 模式要求 Node 的磁盘结果与 generation 路由一致。
|
||||
|
||||
普通 Node 调用方省略 `resolutionMode` 时,`dsh` launcher 选择 link 模式,因此既有 npm 安装的 profile 启动保留文件系统行为。pkg 可执行文件始终选择 runtime,Electron Host 则在挂载任何 profile 条目前安装 runtime generation。测试与底层嵌入方仍可显式选择 runtime 或 dual。
|
||||
普通 Node 调用方省略 `resolutionMode` 时,`dsh` launcher 选择 runtime 模式。pkg 可执行文件始终选择 runtime,Electron Host 在开发与打包构建中也会在挂载任何 profile 条目前显式选择 runtime。普通 Node 测试与底层嵌入方可以显式选择 link、dual 或 runtime。
|
||||
|
||||
runtime 模式要求受支持的 Node Internal loader 接口,并且不会创建、更新或退休 fallback 链接。dual 模式保留链接写入,并在 Node 的磁盘结果与 generation 不同时失败。可写 profile 状态和包管理器事务不属于 resolver。
|
||||
|
||||
pkg 与打包 Electron 载体强制使用 runtime 解析。Electron Host 通过设置 `ELECTRON_RUN_AS_NODE=1` 的 Electron 可执行文件运行,从 ASAR 读取 dsh 依赖树,并把 ASAR 中的可执行条目映射到 electron-builder 的 unpacked 目录。两种载体都不会创建、更新或删除旧解析链接。
|
||||
pkg 与 Electron 载体强制使用 runtime 解析。Electron Host 通过设置 `ELECTRON_RUN_AS_NODE=1` 的 Electron 可执行文件运行;打包构建从 ASAR 读取 dsh 依赖树,并把 ASAR 中的可执行条目映射到 electron-builder 的 unpacked 目录。它们的运行时解析器不会创建、更新或删除旧解析链接。
|
||||
|
||||
### 性能与验证
|
||||
|
||||
@@ -115,4 +115,4 @@ generation 构造发生在启动或显式更新阶段,不属于单次 resolve
|
||||
|
||||
## Consequences
|
||||
|
||||
runtime 启动避免磁盘修改和代理 manifest,同时保留既有选包算法。代价是持续维护 Node Internal 兼容测试,并在每个自有 Worker 中最早执行自包含 bootstrap。link 保持普通 Node launcher 的默认值,dual 保留迁移比较路径,pkg 与 Electron 载体则强制使用 runtime 且不退休旧链接。在产品拥有模块缓存失效和 Worker 重启前,generation 替换只能新增映射。
|
||||
runtime 启动避免磁盘修改和代理 manifest,同时保留既有选包算法。代价是持续维护 Node Internal 兼容测试,并在每个自有 Worker 中最早执行自包含 bootstrap。runtime 是普通 Node launcher 的默认值,link 与 dual 保留为显式对比选项;pkg 与 Electron 载体强制使用 runtime,解析器不退休旧链接。在产品拥有模块缓存失效和 Worker 重启前,generation 替换只能新增映射。
|
||||
+2
-2
@@ -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-workspace-file-read-authority.md
|
||||
2026-09-09-workspace-file-read-authority.md: 35ac15f5e463910be3b3a0cfe7b80dc69c129ad3
|
||||
2026-09-09-workspace-file-read-authority.zh.md: 559a59af0bfa988ff880fad22470397e4e74f296
|
||||
2026-09-09-workspace-file-read-authority.md: d1a5b9d9634381cb74b17651c67f2b701402ff1c
|
||||
2026-09-09-workspace-file-read-authority.zh.md: f8b0fae4325a90548c6b9dc33ac46b17248b04e7
|
||||
@@ -14,7 +14,7 @@ Workspace Files serves both file content and workspace navigation. Applying work
|
||||
|
||||
`list` and `changes` remain workspace-scoped because they expose workspace navigation and observation rather than a named file read. `list` rejects a directory outside the root, and `changes` filters observations through the backend's workspace-containment predicate.
|
||||
|
||||
`readRelated` resolves a relative path from the base file's directory. A `..` path may therefore read JavaScript or CSS outside the workspace when the Session backend permits it. Document Preview packages bounded, statically declared local scripts and stylesheets into an HTML Blob iframe with `sandbox="allow-scripts"`; the opaque origin blocks parent access, but the browser retains normal network access. This exposure is an intentional security trade-off for rendering static generated HTML.
|
||||
`readRelated` resolves a relative path from the base file's directory. A `..` path may therefore read JavaScript or CSS outside the workspace when the Session backend permits it. With [Developer tools](../feature/2026-09-17-developer-tools-settings.md) enabled, Document Preview packages bounded, statically declared local scripts and stylesheets into an HTML Blob iframe with `sandbox="allow-scripts"`; the opaque origin blocks parent access, but the browser retains normal network access. This exposure is an intentional security trade-off for rendering static generated HTML.
|
||||
|
||||
The [Workspace Files service](2026-09-05-workspace-files-service.md) owns paging, file checks, listing, and observation. [Document Preview](2026-09-08-document-preview-operations.md) owns which related files are packaged and the iframe sandbox.
|
||||
|
||||
@@ -26,4 +26,4 @@ The [Workspace Files service](2026-09-05-workspace-files-service.md) owns paging
|
||||
|
||||
## Consequences
|
||||
|
||||
Any caller holding a valid Session file address can receive bytes from every regular file that the Session filesystem backend permits it to read, including files outside the workspace. A previewed HTML document can execute packaged local JavaScript and make network requests. Outside files do not produce `changes` frames, so their previews require explicit refresh to observe updates.
|
||||
Any caller holding a valid Session file address can receive bytes from every regular file that the Session filesystem backend permits it to read, including files outside the workspace. With Developer tools enabled, a previewed HTML document can execute packaged local JavaScript and make network requests. Outside files do not produce `changes` frames, so their previews require explicit refresh to observe updates.
|
||||
+2
-2
@@ -14,7 +14,7 @@ Workspace Files 同时提供文件内容与工作区导航。对所有操作应
|
||||
|
||||
`list` 与 `changes` 仍限于工作区,因为它们暴露工作区导航和观察,而不是读取一个具名文件。`list` 拒绝根外目录,`changes` 通过后端的工作区包含判定过滤观察。
|
||||
|
||||
`readRelated` 从基准文件所在目录解析相对路径。因此,只要 Session 后端允许,`..` 路径就可以读取工作区外的 JavaScript 或 CSS。Document Preview 把有界、静态声明的本地脚本与样式表打包进带 `sandbox="allow-scripts"` 的 HTML Blob iframe;不透明源阻止访问父应用,但浏览器保留正常网络访问。这种暴露是为渲染静态生成 HTML 而有意接受的安全取舍。
|
||||
`readRelated` 从基准文件所在目录解析相对路径。因此,只要 Session 后端允许,`..` 路径就可以读取工作区外的 JavaScript 或 CSS。开启[开发者工具](../feature/2026-09-17-developer-tools-settings.zh.md)时,Document Preview 把有界、静态声明的本地脚本与样式表打包进带 `sandbox="allow-scripts"` 的 HTML Blob iframe;不透明源阻止访问父应用,但浏览器保留正常网络访问。这种暴露是为渲染静态生成 HTML 而有意接受的安全取舍。
|
||||
|
||||
[Workspace Files 服务](2026-09-05-workspace-files-service.zh.md)负责分页、文件检查、列举和观察。[Document Preview](2026-09-08-document-preview-operations.zh.md)负责选择要打包的关联文件及 iframe sandbox。
|
||||
|
||||
@@ -26,4 +26,4 @@ Workspace Files 同时提供文件内容与工作区导航。对所有操作应
|
||||
|
||||
## Consequences
|
||||
|
||||
持有有效 Session 文件地址的调用方可以接收 Session 文件系统后端允许读取的每个普通文件的字节,包括工作区外文件。预览的 HTML 文档可以执行已打包的本地 JavaScript,并发起网络请求。工作区外文件不会产生 `changes` 帧,因此其预览需要显式刷新才能观察更新。
|
||||
持有有效 Session 文件地址的调用方可以接收 Session 文件系统后端允许读取的每个普通文件的字节,包括工作区外文件。开启开发者工具时,预览的 HTML 文档可以执行已打包的本地 JavaScript,并发起网络请求。工作区外文件不会产生 `changes` 帧,因此其预览需要显式刷新才能观察更新。
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.md
|
||||
2026-09-04-web-clickable-link-styles.md: 6fa61945143853a2812468638dafd23ebcd456dc
|
||||
2026-09-04-web-clickable-link-styles.zh.md: 0b15a6c72df9151ffb79f7fb41d582e191e79be6
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-10-compact-tool-details.md
|
||||
2026-09-10-compact-tool-details.md: 6c1f29c5554c74b01fc2a86c2dee3750a529a6b5
|
||||
2026-09-10-compact-tool-details.zh.md: d0822a48f100f9f21ef4706204087fcd1e627da4
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Compact Tool Details from Recorded Results
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-10-compact-tool-details.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Goal, todo, and schedule results contain a small set of user-facing facts that take more effort to read as JSON. Dedicated expanded bodies need to fit the existing conversation rows without adding a second presentation registry or live state to historical calls.
|
||||
|
||||
## Decision
|
||||
|
||||
The `ui-tool` package registers goal and schedule toolviews through the existing keyed slot and shares one package-internal fields/list component with the todo view. Successful, recognized results display recorded values; incomplete calls, errors, malformed values, and unsupported result formats retain generic input/output. Schedule domain errors, including uncertain persistence and an unknown deletion target, retain their original result.
|
||||
|
||||
Tool rows keep their disclosure and Inspect interaction. Checklist marks are static and have accessible status labels. Schedule times use absolute dates with the viewer's time zone; states come from the recorded result. Goal details distinguish an active goal awaiting continuation from an armed goal. These display values do not subscribe to the current goal, todo, or schedule projection.
|
||||
|
||||
Todo changes compare each root or nested call with its preceding durable `todo/write`, so later writes cannot alter an earlier card. The conversation assembler repairs predecessors when older history loads. Until the predecessor is known, the card shows the submitted list without inferred additions or removals.
|
||||
|
||||
This scoped visual change is an exception to the visual-equivalence requirement in [Client-derived tool presentation](2026-08-23-client-derived-tool-presentation.md). Its raw-event ownership, generic fallback, and single keyed registry remain authoritative.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**A shared presenter framework:** the existing keyed slot already dispatches tools. Reusing only the detail body keeps the abstraction local until another package needs it.
|
||||
|
||||
**A dashboard inside each card:** interactive controls, progress graphics, and live states add visual weight and can misrepresent what a historical call returned. Compact read-only fields and lists preserve the conversation's hierarchy.
|
||||
|
||||
## Consequences
|
||||
|
||||
Registered tools gain readable details without changing Host tools, Session events, or public Client exports. Tool-specific adapters interpret recorded JSON and text; unrecognized formats retain generic output. The authored tool-details Session replay covers their assembled rendering.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Compact Tool Details from Recorded Results
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-10-compact-tool-details.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
目标、任务清单和定时任务的结果包含少量面向用户的事实,以 JSON 阅读时较费力。专用展开内容需要融入现有对话行,不能为历史调用新增一套呈现注册表或实时状态。
|
||||
|
||||
## 决策
|
||||
|
||||
`ui-tool` 包通过现有按键分发的插槽注册目标和定时任务视图,并与任务清单视图共用一个包内字段/列表组件。成功且可识别的结果显示记录值;未完成调用、错误、格式异常和不支持的结果格式保留通用输入/输出。定时任务领域错误(包括持久化不确定和删除目标不存在)保留原始结果。
|
||||
|
||||
工具行保留展开和 Inspect 交互。任务清单标记是静态的,并带有可访问的状态标签。定时任务使用查看者时区的绝对日期,状态取自记录的结果。目标详情区分等待继续的活动目标和已启动的目标。这些显示值不订阅当前目标、任务清单或定时任务投影。
|
||||
|
||||
任务清单的变化将每个根调用或嵌套调用与之前的持久化 `todo/write` 比较,因此后续写入不会改变较早的卡片。对话组装器在加载更早的历史时修复前序记录。前序记录尚不明确时,卡片展示提交的列表,不推断新增或删除。
|
||||
|
||||
这项有限的视觉调整是[客户端派生工具呈现](2026-08-23-client-derived-tool-presentation.zh.md)中视觉等价要求的例外。该决策中的原始事件所有权、通用回退和唯一按键注册表仍然有效。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**共享呈现框架:**现有按键插槽已负责工具分发。仅复用详情内容,可以在其他包出现需求前将抽象限制在包内。
|
||||
|
||||
**在每张卡片中嵌入仪表盘:**交互控件、进度图形和实时状态会增加视觉负担,也可能歪曲历史调用返回的事实。紧凑的只读字段和列表保留了对话的层级。
|
||||
|
||||
## 影响
|
||||
|
||||
已注册的工具获得可读详情,无需改变 Host 工具、Session 事件或公开的 Client 导出。工具专用适配器解读记录中的 JSON 和文本;未识别的格式保留通用输出。手写的 tool-details Session 回放覆盖这些工具组装后的渲染。
|
||||
+6
@@ -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,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,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。
|
||||
@@ -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-10-desktop-web-wrapper.md
|
||||
2026-09-10-desktop-web-wrapper.md: 445c574f86d6e465763603e7d447d92ffefb4f99
|
||||
2026-09-10-desktop-web-wrapper.zh.md: a41ce3c2f18237ddf580777e88fe05507998982f
|
||||
2026-09-10-desktop-web-wrapper.md: 05210e2ab85f0afe6d0275e25f31ce8addf11332
|
||||
2026-09-10-desktop-web-wrapper.zh.md: 84e72c9261250b103245f974e1a0546a4898e9ca
|
||||
@@ -14,13 +14,13 @@ Separate Desktop composition and request transport require their own configurati
|
||||
|
||||
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's bundles and uses Web's automatic directory-picker selection, keeping application defaults under one owner. Desktop uses a separate default listener port so both applications can run concurrently; profile configuration can override it. Shell windows, menus, plugin management, recovery, and updates remain Electron responsibilities.
|
||||
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 [in-place decision](2026-09-09-desktop-in-place-profile.md) retains package transactions and partial-failure recovery. The public CLI continues to reject the reserved Desktop profile.
|
||||
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.
|
||||
|
||||
App-boot owns installed-dependency discovery, installation-first bundle declaration resolution, and bundle-list updates after pnpm succeeds. CLI selects automatic activation; Desktop explicitly preserves bundles disabled through its UI. The policy difference belongs to the visible activation control, while metadata handling and reconciliation remain shared.
|
||||
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.
|
||||
|
||||
@@ -38,12 +38,14 @@ Native directory selection in the local application uses a narrow preload IPC ca
|
||||
|
||||
**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. Desktop accepts these effects under the same configuration ownership as Web; the signed core runtime does not attest to user-installed plugin code. Package or loading failures retain explicit repair and the native recovery dialog rather than triggering stricter admission checks or automatic rollback.
|
||||
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.
|
||||
@@ -14,13 +14,13 @@ Status: implemented
|
||||
|
||||
私有 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 模板的 bundle 列表初始化 profile,并使用 Web 的自动目录选择机制,让应用默认值由一处维护。Desktop 使用独立的默认监听端口,使两个应用可以同时运行;profile 配置可以覆盖该端口。壳窗口、菜单、插件管理、恢复及更新仍由 Electron 负责。
|
||||
共享 runner 负责 profile 与 Harness-home patch、代理设置、遥测默认值、模块补全、配置重载和应用生命周期。Desktop 从共享 Web 模板初始化 profile,并在主应用中使用其插件管理器。Electron 负责窗口、菜单、原生目录选择、恢复和发布更新。
|
||||
|
||||
[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)保留独立运行时与插件存储、内置 pnpm,以及明确的包归属。[原位修改决策](2026-09-09-desktop-in-place-profile.zh.md)保留包事务与部分失败恢复。公开 CLI 继续拒绝保留的 Desktop profile。
|
||||
[内置运行时决策](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 专用的来源、环境及构建限制。
|
||||
|
||||
App-boot 负责已安装依赖发现、安装目录优先的 bundle 声明解析及 pnpm 成功后的 bundle 列表更新。CLI 选择自动激活;Desktop 显式保留通过 UI 禁用的 bundle。策略差异属于可见的启用控件,元数据处理与协调逻辑仍然共享。
|
||||
共享 Web 插件管理器负责安装、激活、错误和重启要求。Electron 不提供独立插件管理渲染器、preload、shell 资源路由、插件 IPC 或包变更执行器。打包明确选择主入口和应用 preload,避免旧构建产物重新带入已删除的桥接。
|
||||
|
||||
共享 `initProfile` 创建缺失的 profile 文件并保留现有内容。Host 的 `healIsolatedProfileModuleFallback` 是安装包与 bundle 投影的唯一归属方;包操作在 pnpm 前通过共享 `unlinkProfileModuleFallback` 仅分离它自己拥有的链接。pnpm 管理的目录保持优先。Desktop 不维护第二套运行时状态、锁文件哈希或链接协调机制。`desktop-runtime-state.json` 的一次性清理仅移除与记录匹配的链接,并清除该元数据。
|
||||
|
||||
@@ -38,12 +38,14 @@ App-boot 负责已安装依赖发现、安装目录优先的 bundle 声明解析
|
||||
|
||||
**保留 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 执行、加载第三方代码或导致启动失败。Desktop 按与 Web 相同的配置归属接受这些影响;签名核心运行时不为用户安装的插件代码背书。包操作或加载失败保留显式修复及原生恢复对话框,不触发更严格的准入检查或自动回滚。
|
||||
用户选择的运行时选项、包来源和允许的生命周期脚本可能影响 Host 执行、加载第三方代码或造成启动失败。共享 Web 管理器负责包操作失败处理;Electron 为致命启动失败保留原生恢复。签名核心运行时不为用户安装的插件代码背书。
|
||||
|
||||
验证需要覆盖共享 runner、认证 HTTP 资源与 API 传输、配置重载、原生目录选择、子进程关闭及插件失败恢复。安装后平台验收与真实模型 GUI 验收独立于单元测试;本记录不声称已测得启动或传输提升。
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-07-issue-policy-module-ownership.md
|
||||
2026-09-07-issue-policy-module-ownership.md: 696343ddffa3f6a6e411330d038bb4a470ce78d8
|
||||
2026-09-07-issue-policy-module-ownership.zh.md: abe539a5baed35cf2f356194eab8e53d61b67314
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-11-node-office-kit.md
|
||||
2026-09-11-node-office-kit.md: 6cab947d7cb8d4c2c416a83f0b765813c7f98ea9
|
||||
2026-09-11-node-office-kit.zh.md: 8ed1a81922249cf0b9c1a74b06e5ce9d44696e0a
|
||||
@@ -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 are available from the document toolbar’s warning icon; 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.
|
||||
@@ -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` 缓存设置复用页面全局注入通道,因为模块启动图携带包标识而不传递 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 安装包需要独立验收;一个本地架构成功不能证明整个矩阵。
|
||||
+6
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 检查覆盖应用打包。
|
||||
@@ -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: b15aa5421c575451ddb4a7bf91169bcb2fa38d3c
|
||||
2026-09-15-bounded-office-conversion.zh.md: f23c38096764cf32207125cc0c3ebbdf1c04d009
|
||||
@@ -0,0 +1,43 @@
|
||||
# Agent Note: Bounded shared Office conversion
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-15-bounded-office-conversion.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Office preview and explicit document inspection can request the same conversion. A completed-result cache alone leaves source reads, queued payloads, and concurrent readers unbounded. Speculation can also occupy capacity needed by a user, and canceling one consumer must not destroy another consumer's conversion.
|
||||
|
||||
## Decision
|
||||
|
||||
The `office-to-pdf` service returns complete PDF bytes. Its Remote file entry authorizes Session files through `workspaceFiles`, while its in-process conversion accepts authorized deferred reads without requiring that service. The `api/remotes` assembly owns Client namespace mounting. Page rasterization and user presentation remain separate consumers, so conversion naming does not imply image rendering or preview UI.
|
||||
|
||||
The [Host provider](../../../../packages/document/office-to-pdf/README.md) owns a shared conversion queue and transient content cache. Authorized source metadata enters admission before source bytes are loaded. The source callback receives reserved byte capacity and returns its read version; changed sources fail without publishing aliases. Exact source bytes and Office extension determine the digest. Each converter lifetime adds a generation so engine/font/configuration replacement invalidates reuse.
|
||||
|
||||
A bounded source-version index avoids repeated reads after authorization; the digest remains the identity for sharing conversion across distinct paths. Ready PDFs use an entry/byte-bounded LRU. Queued jobs contain metadata and deferred callbacks. Reader, queue, source-byte, and conversion limits also apply before work completes. Each active source locator belongs to live readers, so cancellation cannot grow retained source metadata independently of reader admission. Unknown source sizes reserve the input cap; cancellation retains active capacity until actual read/conversion cleanup settles.
|
||||
|
||||
Foreground preview and explicit QA requests precede background work. Disabling background conversions rejects speculative readers before they can join queued or running jobs, while completed alias hits remain available. Removing a queued foreground blocker immediately admits eligible background work. Queued priority follows live readers: a foreground join promotes a prewarm, and the final foreground cancellation demotes it and admits eligible work. Full queues evict queued speculation for foreground admission. Background concurrency reserves a foreground slot when total concurrency permits it. A speculative admission remains occupied through settlement, so promotion cannot admit additional speculation into capacity reserved for user work. Shared readers cancel independently, including readers joined after content hashing. The cache owns private output bytes and returns a copy to each caller.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Cache only completed PDFs.** This cannot bound pending source buffers, engine work, or response fanout, and separate consumers still duplicate conversion.
|
||||
|
||||
**Use source metadata as the final cache identity.** Metadata is useful before reading, but distinct authorized paths can contain identical bytes. Content hashing provides cross-path reuse without treating a path as document content.
|
||||
|
||||
**Cancel the entire conversion when one reader leaves.** An open preview can share work with speculation or explicit QA. Only the final reader owns cancellation of shared work.
|
||||
|
||||
**Build a second prewarm or QA converter.** Independent queues duplicate resource ownership and cannot prioritize shared foreground work.
|
||||
|
||||
**Separate service-definition and provider packages for the sole LibreOffice implementation.** They evolve together and have no independent alternative implementation. One `office-to-pdf` package supplies the mountable service without duplicated package, dependency, and release configuration. Native and WASM engine selection remains inside the kit; a second independent implementation can justify extracting an interface from actual consumer needs.
|
||||
|
||||
## Consequences
|
||||
|
||||
The cache is transient and cannot bypass source authorization. Oversized PDFs can be returned without retention, and failed or canceled conversions are retried on a later explicit request. Source reservations measure binary bytes; Remote base64 expansion, engine RSS, caller-retained output, and PDF.js page memory remain outside those limits. With one configured conversion slot, foreground work waits for an already-running background conversion to finish.
|
||||
|
||||
Office preview checks source authorization and versions through Workspace Files, then reads raw input with `fs.readBytes` within its conversion reservation. Office input limits govern that read. The Host conversion path avoids base64 source allocation; PDF responses encode only the converted output.
|
||||
|
||||
Controlled source and engine completions verify pre-read admission, content joining, priority, cancellation isolation, delayed resource release, LRU/alias limits, stale versions, and converter replacement. Loader composition and native conversion checks exercise the shared provider independently of presentation consumers.
|
||||
|
||||
## Related
|
||||
|
||||
The [background scheduling proposal](../../proposed/simplification/2026-09-17-defer-office-background-scheduling.md) questions the need for speculative priority without a production caller; this note remains the authority for the implemented behavior.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Agent Note: 有界的共享 Office 转换
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-15-bounded-office-conversion.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Office 预览和显式文档检查可能请求相同转换。仅缓存已完成结果无法限制源读取、排队载荷和并发读取方。推测工作也可能占用用户需要的容量,而取消一个消费者不能破坏另一个消费者的转换。
|
||||
|
||||
## 决策
|
||||
|
||||
`office-to-pdf` 服务返回完整 PDF 字节。其 Remote 文件入口通过 `workspaceFiles` 授权 Session 文件,进程内转换则接受已授权的延迟读取,不要求该服务。`api/remotes` 装配负责 Client 命名空间挂载。页面栅格化和用户展示由独立消费方负责,因此转换命名不隐含图片渲染或预览 UI。
|
||||
|
||||
[宿主提供方](../../../../packages/document/office-to-pdf/README.zh.md)拥有共享转换队列和临时内容缓存。已授权的源文件元数据在加载字节之前进入准入流程。源回调接收预留的字节容量并返回读取版本;源文件变化会导致失败,不发布别名。确切的源字节和 Office 扩展名决定摘要。每个转换器生命周期附加代次,因此引擎、字体或配置替换会使复用失效。
|
||||
|
||||
有界的源版本索引在授权后避免重复读取;摘要仍是不同路径间共享转换的身份。已就绪 PDF 使用按条目与字节限制的 LRU。排队任务包含元数据和延迟回调。读取方、队列、源字节与转换限制在工作完成前也适用。每个在途源定位信息归属于活跃读取方,因此取消操作不能让保留的源元数据脱离读取方准入限制增长。未知源大小预留输入上限;取消后仍保留活动容量,直至实际读取、转换与清理结束。
|
||||
|
||||
前台预览和显式 QA 请求优先于后台工作。禁用后台转换时,推测读取方在加入排队或运行任务前即被拒绝,但仍可命中已完成的别名缓存。移除排队的前台阻塞任务后,符合条件的后台工作立即准入。排队优先级取决于活跃读取方:前台加入会提升预热,最后一个前台读取方取消后则恢复后台优先级,并准入符合条件的工作。队列满时为前台准入移除排队推测工作。总并发允许时,后台并发为前台预留一个槽位。推测工作的准入名额保留到工作结束,因此提权不会把为用户预留的容量用于更多推测工作。共享读取方独立取消,包括内容哈希后加入的读取方。缓存拥有私有输出字节,并为各调用方返回副本。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**仅缓存已完成 PDF。** 无法限制待完成源缓冲区、引擎工作和响应扇出,不同消费者仍会重复转换。
|
||||
|
||||
**以源元数据作为最终缓存身份。** 元数据在读取前有用,但不同的已授权路径可能包含相同字节。内容哈希提供跨路径复用,而不把路径当作文档内容。
|
||||
|
||||
**一个读取方离开就取消整个转换。** 打开的预览可能与推测工作或显式 QA 共享工作。只有最后一个读取方拥有共享工作取消权。
|
||||
|
||||
**另建预热或 QA 转换器。** 独立队列重复拥有资源,无法优先调度共享前台工作。
|
||||
|
||||
**为唯一的 LibreOffice 实现拆分服务定义和提供方包。** 两者共同演化,没有独立的替代实现。单个 `office-to-pdf` 包直接提供可挂载服务,避免重复维护包、依赖和发布配置。原生与 WASM 引擎选择仍由 kit 负责;若出现独立的第二种实现,再根据实际消费者提取接口。
|
||||
|
||||
## 后果
|
||||
|
||||
缓存为临时数据,不能绕过源授权。超出缓存上限的 PDF 可返回而不保留;失败或取消的转换在后续显式请求时重试。源预留按二进制字节计量;Remote base64 膨胀、引擎 RSS、调用方保留的输出和 PDF.js 页面内存不计入这些限制。仅配置一个转换槽位时,前台工作等待已运行的后台转换结束。
|
||||
|
||||
Office 预览通过 Workspace Files 检查源文件授权与版本,再使用 `fs.readBytes` 在转换预留容量内读取原始输入。该读取受 Office 输入上限约束。Host 转换路径不分配 base64 源数据;PDF 响应只编码转换后的输出。
|
||||
|
||||
受控的源读取和引擎完成验证读取前准入、内容合并、优先级、取消隔离、延迟资源释放、LRU 与别名限额、过期版本及转换器替换。Loader 组合与原生转换检查独立于展示消费者验证共享提供方。
|
||||
|
||||
## 相关记录
|
||||
|
||||
[后台调度提案](../../proposed/simplification/2026-09-17-defer-office-background-scheduling.zh.md) 质疑没有生产调用方时保留推测性优先级的必要性;本记录仍定义已实现的行为。
|
||||
@@ -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-client-session-references.md
|
||||
2026-09-15-client-session-references.md: 2d727809b237ca919296dac82a3b201ba2dd8fbf
|
||||
2026-09-15-client-session-references.zh.md: 34fa6526a57f43fd7636c68c7ed72bb982a129f2
|
||||
@@ -0,0 +1,226 @@
|
||||
# Agent Note: Client Session references, reference sources, and UI status
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-15-client-session-references.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Session catalog, live Client objects, views, and asynchronous operations have different lifetimes. Catalog membership does not establish ongoing use. A borrowed binding cannot protect asynchronous work or distinguish successive Client generations with the same Session id. A global current Session makes independently bound components act on another view's Session.
|
||||
|
||||
A reference count identifies ongoing use but not its consumers. Main-area highlighting, Sidebar ownership, and background operations need consumer-source information. Pending interactions and completion reminders also need one UI read interface so Workspace and Conversation do not independently combine the same status.
|
||||
|
||||
Client history access and Host Agent execution have independent lifetimes. History opening can fail; local Context acquisition need not read history. Explicit ownership must preserve navigation, loading, error handling, and recovery behavior without adding unrelated UI policy.
|
||||
|
||||
## Decision
|
||||
|
||||
### Scope and ownership
|
||||
|
||||
Client Session objects, Agent-scoped Client Contexts, references, consumer-source metadata, UI status, and explicit Provider composition use the ownership rules below. Host Session and Agent lifetimes, SlotFactory, activity dashboards, durable Session formats, and both SDKs' Host protocols remain independent.
|
||||
|
||||
| Owner | Responsibility |
|
||||
| --- | --- |
|
||||
| Client Session Controller | Catalog, live generations, references, source counts, bindings, history windows, and existing Session control state |
|
||||
| `ui-session` | Explicit Session Provider integration and the unified UI status source |
|
||||
| View or operation | Its own reference, target, source identifier, and release point |
|
||||
| Workspace UI | Main-area target and reference, navigation, persisted target, and creation flow |
|
||||
| UI composition boundary | Supplies an owner-provided reference to one explicit `SessionProvider` |
|
||||
| Conversation and Sidebar subtrees | Consume only their Provider's Session, never a main-area reference or global selection |
|
||||
| Client Gateway | Invocation-lifetime Context ownership while handling a Host event |
|
||||
|
||||
The [Client layering design](../../implemented/architecture/2026-08-20-client-session-conversation-ownership.md) defines one-way data, adapter, renderer, and presentation dependencies. Reference-source bookkeeping does not give the Controller a dependency on UI packages.
|
||||
|
||||
This decision partially supersedes the list-selected scope lifecycle in the [Web Client Session scope and provide-channel decision](2026-07-25-web-client-session-scope-and-provide-channel.md); that note retains the blank-Session and adoption rationale under explicit Provider ownership.
|
||||
|
||||
### Addresses, bindings, and references
|
||||
|
||||
`SessionTarget` is a known `SessionId` or a durable direct-parent `SubagentAddress`. It identifies what to acquire; it owns nothing. The Controller resolves an explicit address without requiring a preloaded parent catalog, while the Host validates its parent, child, and mode when history opens. Child discovery and an already known child address remain distinct from retaining the child. Navigation uses the same target representation rather than adding a separate navigation address.
|
||||
|
||||
`SessionBinding` is the shared Client generation: `sessionId`, Session face, event source, and scoped Context. Multiple references to the same live generation share this binding. A new generation with the same id has a different binding and Context.
|
||||
|
||||
`SessionReference` owns one use of one exact generation. It exposes read-only `sessionId` and `binding`, a `ready` Promise for the shared initial history opening, plus idempotent `release()` and `Symbol.dispose`. `ready` resolves to the exact binding when the corresponding `Session.open()` attempt resolves, including its stateful Remote-failure result. Reading `binding` after release or generation disposal fails. Retaining, releasing, or inspecting a reference does not create durable Sessions or start, stop, or retain a Host Agent.
|
||||
|
||||
`SessionBinding` is a borrowed value and owns no lifetime. Only unreleased `SessionReference` objects contribute to source and total reference counts. Synchronous code may borrow a binding within its owner's reference lifetime; work that outlives that owner must acquire its own reference.
|
||||
|
||||
| API | Result and caller obligation |
|
||||
| --- | --- |
|
||||
| `sessions.retain(target, options)` | `SessionReference`; returns immediately, and the caller owns the reference until release |
|
||||
| `sessions.using<T>(target, options, operation)` | `Promise<T>`; awaits `reference.ready`, awaits `operation(reference)`, releases its reference, and returns the operation's result |
|
||||
| `sessions.retainInfo(id)` | Stable read-only observable of local reference counts; no acquisition, scope creation, or history I/O |
|
||||
| `sessions.scope(id)` / `sessions.binding(id)` | Borrow an existing live generation or return `undefined`; neither opens nor extends its lifetime |
|
||||
| `sessions.sessionOf(ctx)` | Returns the matching live Session face or `undefined`; an ended Context cannot resolve to a same-id replacement |
|
||||
| `sessions.create(...)` / `sessions.fork(...)` | Existing Host operations returning a Session identity; retaining and displaying it remain explicit |
|
||||
|
||||
`SessionRetainOptions` contains required `source: SessionReferenceSource` and optional `signal: AbortSignal`. Both acquisition methods accept `target: SessionTarget` and these options. Source keys are consumer-defined and declaration-merge extensible; there is no runtime source-registration protocol or default main-view source. A source is a usage label, not another Session address or a permission to act on it.
|
||||
|
||||
### Acquisition, failure, and release
|
||||
|
||||
Acquisition synchronously resolves the target, creates or retains the local Session, Context, fiber, and binding, records one reference and its source contribution, starts the generation's shared initial history opening, and returns the reference. Concurrent acquisitions share that opening but receive independent references and readiness waits. `reference.ready` resolves to the still-live exact binding when the shared `Session.open()` attempt resolves.
|
||||
|
||||
Unknown Session-id addressing fails before a reference is returned; an explicit subagent address is validated by the Host during opening. A thrown opening failure, caller cancellation, reference release, or generation retirement rejects that reference's `ready`; one cancelled waiter does not cancel another owner's shared opening. A Remote failure represented by `openState: 'error'` follows `Session.open()` and resolves readiness, leaving the error renderable through the binding. The caller still owns a returned reference until release, while `sessions.using` releases its reference when readiness or its operation fails.
|
||||
|
||||
`using reference = sessions.retain(target, options)` releases on exit from the enclosing scope; code that requires settlement of the initial history attempt awaits `reference.ready`. `sessions.using` is the callback helper: it waits for that settlement before invoking an operation, waits for its returned value or Promise, and then releases. Returned values must not rely on the helper's released reference remaining usable; a longer-lived consumer acquires its own reference. The helper propagates rejected readiness and operation failures without a fallback result, retry, or error presentation. Model-selection generation checks and error-state updates remain in ModelSelection.
|
||||
|
||||
A fulfilled `ready` Promise means the initial `Session.open()` attempt settled; it does not assert `openState: 'open'` or promise an uninterrupted connection. Stateful opening failures and later stream failures remain observable through existing Session state. Callers retain their existing handling and presentation of those errors.
|
||||
|
||||
Release removes only that reference and its source contribution. Final release withdraws the generation's admission and id mappings before disposing the Session and fiber. A later retain can create a fresh generation immediately; cleanup of the old generation cannot remove the replacement. `release()` initiates local cleanup synchronously, while root disposal awaits outstanding asynchronous teardown.
|
||||
|
||||
The owning Client root invalidates all references during shutdown. It refuses new acquisitions, withdraws live mappings, and joins scoped cleanup and Session stream teardown. References cannot keep a disposed Client root alive. Catalog removal alone does not dispose a referenced generation.
|
||||
|
||||
### Reference sources and Session list records
|
||||
|
||||
The Controller owns source counts alongside each live generation's references. For every source, the count equals that generation's unreleased references bearing the source. A Session can have several sources, and a source can hold several references. Sources have no special lifecycle behavior in the allocator.
|
||||
|
||||
Each available Session list row exposes a read-only `retainedBy` record from source key to positive reference count. An unretained row has an empty record; zero-count keys are absent. Acquisition failure and release update that projection, including removal of the final key. The source counts remain local facts when Host metadata is refreshed; Host responses cannot overwrite them.
|
||||
|
||||
The generation owns the counts even if its Session has not reached the Host catalog or its catalog metadata has been removed. `byId` synthesizes a local fallback row for every live generation so Provider and ownership consumers can resolve it; `ids` remains the Host-list membership and ordering. A fallback row is not Host metadata. Every row's `retainedBy` projection uses the live generation's counts. Neither the reference objects nor the counts are persisted or sent to the Host.
|
||||
|
||||
For example, `retainedBy = { mainView: 1, gateway: 2 }` means that the main view and two Host invocations own three references. Releasing the main-view reference removes only `mainView`; the Gateway invocations continue to own the generation. Source names in this example are consumer keys, not a closed Controller enum.
|
||||
|
||||
`current` means main-view occupancy: a row has that marker exactly when its `mainView` source count is positive. It is one use of the general source record, not a separate `sessions.current` value or Session-selection service. Other consumers can derive their own markers from their source keys. The reference system does not impose a globally unique consumer or select one Session from multiple sources.
|
||||
|
||||
The main-area owner manages its own target and reference transitions. The list does not infer selection from how many Providers happen to be mounted. Source records report actual ownership, including temporary overlap during acquisition; UI navigation remains responsible for its target, without assigning selection authority to the allocator.
|
||||
|
||||
Window-level consumers may inspect source markers. Session business operations still use their supplied scope or explicit reference; they cannot find `mainView` in the catalog to recover a missing operation target. A background reference is ownership evidence, not evidence that a user viewed the Session.
|
||||
|
||||
`SessionRetainInfo` contains `referenceCount` and the read-only `retainedBy` record. `sessions.retainInfo(id)` observes that local ownership independently of catalog membership and remains stable across same-id generation replacement. An identity without a live generation has zero references and an empty source record; reading this value does not assert that the identity exists on the Host.
|
||||
|
||||
`ui-session` exposes `useSessionRetainInfo(sessionId, selector)` for an explicit identity and `useSessionRetainInfo(selector)` for the Session bound by the surrounding Provider. An unbound scope supplies absence to the latter form, never the main area's Session. The renderer constructs both forms from the same bare retain-info source. Consumers test the `mainView` source count to recognize the former current-Session role; other source keys remain equally queryable. Reading or subscribing does not retain the Session.
|
||||
|
||||
### Unified UI Session status
|
||||
|
||||
`ui-session` owns a React-free `sessionStatus` source and exposes it through the standard `useSessionStatus` hook. The snapshot is indexed by Session identity and supplies one UI status record per known Session. It combines these independent facts rather than reducing them to one mutually exclusive phase:
|
||||
|
||||
| Field | Value | Meaning and owner |
|
||||
| --- | --- | --- |
|
||||
| `running` | `boolean` or `undefined` | Latest known Session running fact; absence of a baseline is not confirmed idle |
|
||||
| `pendingInteraction` | `SessionPendingInteraction` or `undefined` | Effective domain-owned request, or absence when no request is pending |
|
||||
| `completionUnread` | `boolean` | UI reminder for an observed stop that has not been acknowledged |
|
||||
|
||||
Source counts remain authoritative in `SessionListState.byId[id].retainedBy`; UI status reads them for acknowledgement policy without owning another reference registry. Titles, Workspace associations, history, queues, and projection data remain with their existing owners.
|
||||
|
||||
Pending domains retain `SessionPendingInteractionMap`, request identities, precedence, publication disposers, and teardown delegation. The unified status includes the same effective request object; it does not copy requests or create a second pending registry. Workspace status indicators and Conversation composer selection read `useSessionStatus` instead of separate `useSessionPendingInteraction` and `useCompletedSessionIds` hooks.
|
||||
|
||||
Completion tracking subscribes to the existing `api-session/status` events so a running-to-idle transition is not lost in batched catalog snapshots. Catalog snapshots establish initial and reconnect baselines. A pending empty catalog is not evidence that Sessions disappeared. The update rules are:
|
||||
|
||||
- An initial idle baseline does not create a completion reminder.
|
||||
- Observing running clears an earlier reminder and records the running baseline.
|
||||
- A known running-to-idle transition sets `completionUnread` only when the Session lacks main-view ownership.
|
||||
- Acquiring main-view ownership clears the reminder; retaining from an unrelated source does not.
|
||||
- Releasing main-view ownership does not manufacture a reminder for an earlier stop.
|
||||
- Removing a Session clears its completion reminder and running baseline; pending-request teardown stays with the request's domain.
|
||||
|
||||
The reminder denotes an observed stop, not successful task completion or completion of a particular queued message. Main-view ownership preserves selection-based acknowledgement even while a global panel temporarily hides the Conversation. The design has no `ui-session/view-presence` event, mounted-Provider index, or implicit acknowledgement from Provider mount.
|
||||
|
||||
The Session Controller publishes running and reference-source facts but owns no completion-reminder set, `consumeCompletion` method, or pending-interaction presentation. Reference acquisition does not execute completion-reminder business logic.
|
||||
|
||||
### Explicit Providers and consumer lifetimes
|
||||
|
||||
The single `SessionProvider` can inherit an outer binding or override its subtree with explicit `session={reference | undefined}`. It neither acquires nor releases ownership. The root Provider resolves its main binding from the `mainView` ownership marker without maintaining another current value; sibling and nested Providers affect only their own subtrees. Explicit absence stays absent instead of falling back to the main area.
|
||||
|
||||
The Provider injects the selected binding without keying its whole body. Strict `session` entries remount when the binding Context changes. A blank `session-maybe` entry adopts its first binding without remounting; after adoption, another binding Context or a return to absence starts a new component incarnation.
|
||||
|
||||
`uiWorkspace` owns the main-area reference with source `mainView`, and `ui-session` derives the root Provider from that reference's ownership marker on the Session record. Conversation, right Sidebar, preset, command, input, and model components beneath a Provider consume only its standard bound data. They do not read the main-area reference or a global selection, and they do not reinterpret an ID as a binding from another Provider.
|
||||
|
||||
Each Provider occurrence establishes an independent rendering scope from its supplied `SessionReference`. Two references may identify different Sessions or share one `SessionBinding`; different bindings isolate business and view data, while Providers for the same binding share Session, Conversation, input, and other Session-level data but retain separate component-local state. The main area and Sidebar can mount two Conversations concurrently without either Provider replacement or teardown redirecting the other subtree.
|
||||
|
||||
`ui-session` reuses one stable business observable per active `SessionBinding`. Generation caches for Conversation assembly, input shells, command popups, input-trigger controllers, and model directories use binding identity instead of Session IDs. Caches that must enumerate live values use `WeakMapWithValues<SessionBinding, Value>`, whose weak key table and strong value set provide identity lookup and value iteration. The container performs no cleanup; subscriptions, controllers, URLs, and other resources still release deterministically through `binding.ctx.effect()`. Provider-occurrence view state ends with that rendering scope, while final generation cleanup belongs to the binding Context.
|
||||
|
||||
Descriptor changes assemble replacement sources before publication without recreating Session generations. A Provider validates its reference while reading it; a released reference, a reference from another Controller, or a reference that no longer matches an active binding cannot establish a scope.
|
||||
|
||||
| Consumer | Acquisition and release |
|
||||
| --- | --- |
|
||||
| Main Conversation | `uiWorkspace` acquires the navigation target; `ui-session` establishes the root Provider from the `mainView` marker, and the Conversation subtree consumes only its Provider binding |
|
||||
| Associated right Sidebar | `RightbarRoot` inherits the root Provider without reading the main-area reference |
|
||||
| Independent Session view | Its owner retains the target and establishes a Provider from its own reference alongside the main area |
|
||||
| Host event handler in the Client | Holds a local Context reference through handler and reply settlement |
|
||||
|
||||
Conversation references belong to their view owners, not Chat, Trajectory, or an individual action. Chat, Trajectory, commands, input, uploads, image reads, and model selection borrow the same binding during the Provider lifetime rather than acquiring a reference per action. Switching or closing that Conversation can end its in-flight local work. Sidebar-tab layout and resource ownership remain separate; only a Sidebar view that hosts a Conversation needs its own Session reference. Empty layouts and guide placeholders retain nothing.
|
||||
|
||||
Scoped business objects capture their binding before awaiting work. They do not reinterpret an old Context or directory as a new generation with the same id. ModelSelection borrows its Provider binding while retaining its own selection-generation and error rules. Editor-detach callbacks may overlap scope teardown; optional trigger and popup resolution returns absence for that retired Context rather than resolving another generation.
|
||||
|
||||
### Main-area navigation and presentation
|
||||
|
||||
`uiWorkspace.openSession`, `openWorkspace`, `forkSession`, and `startSession` remain the navigation entry points. They accept or resolve explicit targets, change the main view, and return the main area to Conversation according to existing navigation policy. `retain` itself never navigates. There is no `registerNavigation` receiver protocol or second navigation service introduced by reference ownership.
|
||||
|
||||
The existing `uiWorkspace` implementation directly owns the main-area reference and target. Its navigation methods update that owner rather than calling a receiver registered by Conversation. `ui-session` derives the root Provider binding from the source marker; the main Conversation and associated right Sidebar only inherit the Provider and neither depend on `uiWorkspace` nor see the main reference. An independent Sidebar Conversation establishes a nested Provider from its own reference and overrides only that subtree's binding. The main reference is not a global standard prop, subtree Hook, or default value for ID-based lookup.
|
||||
|
||||
The main view privately persists its target identity and subagent address under `dsh.sessions.current`, never a reference. Startup restoration, initial Workspace selection, and clearing an archived main target remain UI responsibilities. Archiving or removing catalog metadata does not revoke independent references held by other consumers.
|
||||
|
||||
| UI behavior | Final rule |
|
||||
| --- | --- |
|
||||
| Session-list highlight and blank-row treatment | Derive main-area occupancy from `retainedBy.mainView`, not the number of mounted Session Providers |
|
||||
| New Session Workspace | Explicit Workspace first, then the main Session's Workspace under the existing lookup rule, then the existing recent-Workspace policy |
|
||||
| Onboarding | Evaluate absence or blankness of the main-area Session, not all historical Sessions |
|
||||
| Browser document title | Conversation shows Session title plus product title; a global panel shows product title |
|
||||
| Chat/Trajectory restoration | Restore the view for the explicitly selected main target; independently bound views keep their own state |
|
||||
| Cordis inventory panel | One list without current/other grouping; no public runner getter for main-area selection |
|
||||
|
||||
Source metadata does not change when DOM focus moves or when a global panel hides a retained view. The [global main-panel design](../../implemented/architecture/2026-09-08-global-main-panels.md) owns panel selection and layout; Session reference ownership does not replace it.
|
||||
|
||||
Conversation retains its `hero`, `settling`, and `active` composition and existing history-loading and `openError` handling. Acquisition adds no outer loading/error phase presentation, extra composer-hiding condition, Retry button, or replacement Sidebar recovery panel. Existing error handlers continue to handle their errors; call sites without error presentation gain none. Promise rejection and correct reference release do not imply an additional UI handler.
|
||||
|
||||
Workspace connection and fork preserve their existing navigation guards and panel-switch invalidation. Direct Session opening gains no additional global-navigation cancellation policy. Agent Team refresh preserves its originating-selection validity condition instead of starting a global navigation token before refresh. Reference acquisition does not broaden cancellation to unrelated navigation or running operations, and local cancellation does not roll back Host effects.
|
||||
|
||||
### Presets and creation flows
|
||||
|
||||
Preset directories and deployment defaults may be shared. A bound Session's preset is read or changed through its Provider binding; preset controllers are cached by `SessionBinding` rather than managed by one root current-Session follower. The hero preset seat uses the `session-maybe` Provider: it shows the creation-flow choice without a Session and operates on the exact bound blank Session after one arrives. Header labels read the same Provider-bound Session projection.
|
||||
|
||||
A preset chosen before Session creation remains in the main Conversation's `session-maybe` preset surface. After Workspace creation or reuse establishes the main Provider over a blank Session, that surface applies the choice to its Provider-bound Session. When Settings changes the default preset or picker setting, the preset service selects the blank Session whose established Provider binding carries `mainView` ownership and updates that Session. Non-blank main Sessions, independent Sidebar Providers, and other background references remain unchanged; preset subtrees do not read the main reference or use a global current follower to find their target.
|
||||
|
||||
### Host-event Context ownership and Typert
|
||||
|
||||
A validated Host waterfall identity can arrive before catalog discovery. The Client Context resolver must remain synchronous. It acquires a local generation reference with the Gateway's source and returns `TypertOwnedValue<Context>` without opening history or refreshing child catalogs. A handler that needs history separately acquires a public reference and awaits its `ready` Promise, or uses `sessions.using`.
|
||||
|
||||
Gateway owns the local reference until both handler use and reply settlement end. Context acquisition failures retain the existing report-and-delegate behavior, while handler failures produce rejected replies. Cancellation reaches the handler through its existing signal and suppresses late replies without releasing a Context still in use. Plugin shutdown joins the active connection generation's outstanding handlers; Connection starts no replacement generation until that source settles.
|
||||
|
||||
`TypertOwnedValue` carries a value and its disposer through the generic Gateway. It has no additional reference count and does not teach Gateway about Session-specific ownership. Independently bundled Client modules share the owned-value marker. Client outgoing `identity(ctx)` remains synchronous; Host Context resolution and Host lifecycle are unchanged.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Catalog membership as ownership.** Discovery data cannot establish ongoing view or operation use, and some Context identities arrive before catalog membership.
|
||||
|
||||
**Implicit main Session plus an explicit alternative.** Two target-resolution rules make reusable components depend on where they render. Explicit Providers supply the target, while generic source records serve window-level observation.
|
||||
|
||||
**Provider subtrees reading `mainSession`.** A component would then have both a Provider target and a window-level target, so an independent Sidebar Conversation could be redirected by a main-area change. The main reference participates only in Provider assembly; subtrees consume their Provider.
|
||||
|
||||
**Session ID as a generation-cache key.** Final release allows a replacement generation with the same ID to appear before old cleanup ends. An ID key can reuse the old object or let old cleanup remove the replacement. Business caches use weak binding identity, and the binding Context still owns resource cleanup.
|
||||
|
||||
**Asynchronous `retain` that resolves only after history opens.** It delays Provider installation and main-view navigation until history arrives, so the existing loading state cannot render immediately. A synchronous reference separates ownership from its explicit `ready` result.
|
||||
|
||||
**History I/O in synchronous Context resolution.** Host-event dispatch needs a scoped lifetime, not necessarily a history window; coupling them delays or blocks handlers before catalog discovery.
|
||||
|
||||
**A dedicated current flag.** One consumer-specific flag cannot describe Sidebar and background ownership. A main-view marker is derivable from the general per-source counts.
|
||||
|
||||
**Completion state in Session Controller, or separate pending and completion hooks.** Completion acknowledgement is UI policy. One UI status source composes independent facts while retaining domain-owned pending objects and Controller-owned running facts.
|
||||
|
||||
**Provider presence as acknowledgement or release.** Mounting is neither an owner's lifetime nor selection-based acknowledgement. It cannot decide which background or temporarily hidden uses remain alive or count as viewed.
|
||||
|
||||
**Bare ids in Providers or a global binding revision.** An id does not express generation ownership, and global refresh invalidates unrelated Session consumers.
|
||||
|
||||
**A reference for every action.** The Provider already defines the Conversation's usage lifetime. Reacquiring for every click fragments view ownership into fine-grained sources. Only work that must outlive the Provider acquires another reference.
|
||||
|
||||
**Navigation registration, new cancellation policies, and acquisition-specific recovery UI.** Reference ownership requires explicit targets, release, and failure propagation, not additional navigation or recovery behavior.
|
||||
|
||||
**Client references retaining Host Agents.** History access and Host execution are independent uses; a long-lived Client view cannot define Host business-operation lifetime.
|
||||
|
||||
## Verification
|
||||
|
||||
- Acquisition tests cover shared initial opening, ordinary and unexpected open failures, independent waiter cancellation, exact-generation replacement, and root teardown reaching quiescence.
|
||||
- Source tests cover multiple sources, several references from one source, failed acquisition rollback, idempotent release, catalog refresh/removal, and late release of an ended generation without changing its replacement.
|
||||
- Retain-info hook tests cover explicit identities, Provider-bound defaults, unbound and nested scopes, source updates across generations, and reads that create neither references nor history requests.
|
||||
- Scope and Gateway tests cover synchronous Context acquisition before catalog discovery without history I/O, reported acquisition delegation, rejected handler replies, ownership through cancellation and settlement, and shared owned-value markers across bundles.
|
||||
- UI status tests cover pending precedence and teardown, event transitions lost by snapshot batching, initial and reconnect baselines, main-source acknowledgement, unrelated-source retention, and no Provider-presence event.
|
||||
- View tests cover explicit Providers for two different Sessions and for repeated uses of one Session, scoped presets, descriptor updates, and Sidebar occurrence/adoption lifetimes without new recovery controls.
|
||||
- Assembled browser scenarios preserve main-area highlighting, blank rows, onboarding, Workspace defaults, titles, and the defined navigation/error behavior. Existing recorded model turns remain the behavioral input when only Client ownership changes.
|
||||
- The public `retain` and `using` consumers, generated API catalogs, package contracts, and type checks agree on options and ownership. The helper keeps references through callback settlement and propagates both acquisition and operation failures.
|
||||
|
||||
## Consequences
|
||||
|
||||
Consumers must keep references until their real completion point; an unreleased reference still leaks within a live Client root. Retaining after final release creates a new generation and can reopen history. Source counts describe ownership, not visibility, task success, or authority; using the main marker as a business fallback would recreate implicit current-Session coupling.
|
||||
|
||||
Final release discards non-persisted binding-owned state, including loaded history pages, Chat scroll anchors, preview wrapping, Files expansion, composer attachments, and undo history. Only persisted Session-keyed Store values or a generation kept alive by another reference survive a view switch; the runtime does not clear those persisted values.
|
||||
|
||||
Every public `retain`, including the temporary references used by Session rename and fork-title assignment, starts the generation's shared initial history opening. These metadata operations await `reference.ready` before using the Session and therefore pay that history I/O for a cold generation.
|
||||
|
||||
The catalog, UI status, and view target have separate owners and can publish independently. Their consumers must not infer a lifecycle transition solely from notification order. The main view remains an ordinary reference owner, while its navigation and presentation rules stay in UI rather than the reference allocator.
|
||||
@@ -0,0 +1,226 @@
|
||||
# Agent Note: Client Session 引用、引用来源与 UI 状态
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-15-client-session-references.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Session 目录、活跃 Client 对象、视图与异步操作具有不同生命周期。目录成员关系不能证明仍在使用。借用的 binding 无法保护异步工作,也无法区分同一 Session ID 的不同 Client 代。全局 current Session 会使独立绑定的组件操作另一个视图的 Session。
|
||||
|
||||
引用计数能表明仍在使用,但不能识别使用方。主区域高亮、Sidebar 所有权与后台操作需要使用方来源信息。待处理交互与完成提醒也需要统一的 UI 读取接口,避免 Workspace 和 Conversation 各自组合相同状态。
|
||||
|
||||
Client 历史访问与 Host Agent(智能体)执行具有独立生命周期。历史打开可能失败;获取本地 Context 不一定需要读取历史。显式所有权必须保持导航、加载、错误处理与恢复行为,不添加无关 UI 策略。
|
||||
|
||||
## 决策
|
||||
|
||||
### 范围与所有权
|
||||
|
||||
Client Session 对象、Agent 作用域的 Client Context、引用、使用方来源元数据、UI 状态与显式 Provider 组合遵循下述所有权规则。Host Session 和 Agent 生命周期、SlotFactory、activity 列表视图、持久 Session 格式及两个 SDK 的 Host 协议保持独立。
|
||||
|
||||
| 所有者 | 职责 |
|
||||
| --- | --- |
|
||||
| Client Session Controller | 目录、活跃代、引用、来源计数、binding、历史窗口与既有 Session 控制状态 |
|
||||
| `ui-session` | 显式 Session Provider 集成与统一 UI 状态来源 |
|
||||
| 视图或操作 | 自己的引用、目标、来源标识与释放时机 |
|
||||
| Workspace UI | 主区域目标与引用、导航、持久目标及创建流程 |
|
||||
| UI 组合边界 | 将所有者提供的引用交给一个明确的 `SessionProvider` |
|
||||
| Conversation 与 Sidebar 子树 | 只消费所在 Provider 的 Session,不读取主区域引用或全局选择 |
|
||||
| Client Gateway | 处理 Host 事件时调用期的 Context 所有权 |
|
||||
|
||||
[Client 分层设计](../../implemented/architecture/2026-08-20-client-session-conversation-ownership.zh.md)定义数据、适配器、渲染器与展示层的单向依赖。引用来源统计不会使 Controller 依赖 UI 包。
|
||||
|
||||
本决策部分取代 [Web Client Session scope 与 provide channel 决策](2026-07-25-web-client-session-scope-and-provide-channel.zh.md)中由 list 选择驱动的 scope 生命周期;后者保留显式 Provider 所有权下的 blank Session 与收养语义理由。
|
||||
|
||||
### 地址、binding 与引用
|
||||
|
||||
`SessionTarget` 是已知 `SessionId` 或持久的直接父子 `SubagentAddress`。它标识要获取的目标,不持有任何对象。Controller 解析显式地址时不要求预先加载 parent catalog,Host 则在打开历史时校验其 parent、child 与 mode。子会话发现与已知子会话地址均不等同于持有该子会话。导航使用同一种目标表示,不增加另一套导航地址。
|
||||
|
||||
`SessionBinding` 是共享的 Client 代,包含 `sessionId`、Session 接口、事件来源与作用域 Context。同一活跃代的多个引用共享这个 binding。同 ID 的新一代具有不同的 binding 和 Context。
|
||||
|
||||
`SessionReference` 持有某个确切代的一次使用。它公开只读 `sessionId` 和 `binding`、表示共享首次历史打开的 `ready` Promise,以及幂等 `release()` 与 `Symbol.dispose`。对应 `Session.open()` 尝试解析时,`ready` 解析为该确切 binding,包括以状态表示的 Remote failure 结果。引用释放或其代清理后,读取 `binding` 会失败。获取、释放或查看引用均不创建持久 Session,也不启动、停止或持有 Host Agent。
|
||||
|
||||
`SessionBinding` 是借用值,不构成持有。只有未释放的 `SessionReference` 计入来源与总引用数。同步代码可以在拥有者引用的生命周期内借用 binding;需要越过该生命周期的工作必须拥有自己的引用。
|
||||
|
||||
| API | 返回结果与调用方义务 |
|
||||
| --- | --- |
|
||||
| `sessions.retain(target, options)` | `SessionReference`;立即返回,调用方持有该引用直到释放 |
|
||||
| `sessions.using<T>(target, options, operation)` | `Promise<T>`;依次等待 `reference.ready` 与 `operation(reference)`,释放自己的引用,并返回操作结果 |
|
||||
| `sessions.retainInfo(id)` | 本地引用计数的稳定只读 observable,不获取引用、不创建作用域,也不执行历史 I/O |
|
||||
| `sessions.scope(id)` / `sessions.binding(id)` | 借用已存在的活跃代或返回 `undefined`,不打开它,也不延长其生命周期 |
|
||||
| `sessions.sessionOf(ctx)` | 返回匹配的活跃 Session 接口或 `undefined`;已结束的 Context 不能解析为同 ID 替代代 |
|
||||
| `sessions.create(...)` / `sessions.fork(...)` | 既有 Host 操作,返回 Session 身份;持有与展示仍需显式执行 |
|
||||
|
||||
`SessionRetainOptions` 包含必填 `source: SessionReferenceSource` 与可选 `signal: AbortSignal`。两个获取方法均接受 `target: SessionTarget` 和这些 options。来源键由使用方定义,通过声明合并扩展;不提供运行时来源注册协议,也不默认使用主视图来源。来源是使用标签,不是另一种 Session 地址,也不授予操作权限。
|
||||
|
||||
### 获取、失败与释放
|
||||
|
||||
获取操作同步解析目标,创建或持有本地 Session、Context、fiber 和 binding,记录一份引用及其来源计数,启动该代共享的首次历史打开,然后返回引用。并发获取共享这次打开,但分别获得独立引用与就绪等待。共享的 `Session.open()` 尝试解析时,`reference.ready` 解析为仍然有效的确切 binding。
|
||||
|
||||
未知 Session id 的寻址在返回引用前失败;显式 subagent 地址由 Host 在打开期间校验。抛出的打开失败、调用方取消、引用释放或该代结束会拒绝该引用的 `ready`;取消一个等待方不会取消其他所有者共享的打开操作。以 `openState: 'error'` 表示的 Remote failure 跟随 `Session.open()` 语义并解析就绪,错误仍可通过 binding 渲染。调用方持有已返回的引用直到释放,而 `sessions.using` 会在就绪或操作失败时释放自己的引用。
|
||||
|
||||
`using reference = sessions.retain(target, options)` 在所在作用域退出时释放;需要等待首次历史尝试结算的代码等待 `reference.ready`。`sessions.using` 是面向回调调用方的辅助方法:它先等待该次结算,再调用操作并等待操作返回的普通值或 Promise,最后释放。返回值不能依赖辅助方法已经释放的引用仍可使用;需要更长生命周期的使用方自行获取引用。辅助方法传播被拒绝的就绪与操作失败,不提供兜底结果、重试或错误展示。模型选择的代际检查与错误状态更新保留在 ModelSelection。
|
||||
|
||||
`ready` Promise 成功表示首次 `Session.open()` 尝试已经结算;它不保证 `openState: 'open'`,也不保证连接永不中断。以状态表示的打开失败与之后的流失败继续通过既有 Session 状态公开。调用方保留对这些错误的既有处理与展示。
|
||||
|
||||
释放只移除该引用及其来源计数。最后释放在清理 Session 与 fiber 前撤回该代的准入和 ID 映射。后续 retain 可以立即创建新一代;旧代清理不能移除替代代。`release()` 同步发起本地清理,根清理等待尚未结束的异步释放工作。
|
||||
|
||||
所属 Client 根在关闭时使全部引用失效。它拒绝新的获取,撤回活跃映射,并汇合作用域清理与 Session 流释放。引用不能使已清理的 Client 根继续存活。仅移除目录记录不会清理仍被引用持有的代。
|
||||
|
||||
### 引用来源与 Session 列表记录
|
||||
|
||||
Controller 将来源计数与每个活跃代的引用共同管理。每个来源的计数等于该代尚未释放且带有该来源的引用数。同一 Session 可以有多个来源,同一来源可以持有多份引用。分配器不赋予任何来源特殊生命周期行为。
|
||||
|
||||
每条已有 Session 列表记录公开只读 `retainedBy`,由来源键映射到正数引用计数。未被持有的记录使用空对象;零计数键不存在。获取失败与释放更新该投影,包括删除最后一个来源键。Host 元数据刷新时,来源计数仍是本地事实,Host 响应不能覆盖它。
|
||||
|
||||
即使 Session 尚未进入 Host 目录或其目录元数据已被移除,计数仍由对应代持有。`byId` 为每个活跃代合成本地兜底记录,以便 Provider 与所有权使用方解析它;`ids` 仍表示 Host 列表成员关系与顺序。兜底记录不是 Host 元数据。每条记录的 `retainedBy` 投影均使用活跃代的计数。引用对象与计数均不持久化,也不发送给 Host。
|
||||
|
||||
例如,`retainedBy = { mainView: 1, gateway: 2 }` 表示主视图与两个 Host 调用持有三份引用。释放主视图引用只移除 `mainView`;Gateway 调用继续持有该代。示例中的来源名是使用方键,不是 Controller 内封闭的枚举。
|
||||
|
||||
`current` 表示主视图占用:记录的 `mainView` 来源计数为正数时,就具有这个标记。这只是通用来源记录的一种用途,不是独立的 `sessions.current` 值或 Session 选择服务。其他使用方可以从自己的来源键派生标记。引用系统不要求全局唯一使用方,也不从多个来源中选择一个 Session。
|
||||
|
||||
主区域所有者管理自己的目标与引用切换。列表不根据恰好挂载了多少 Provider 推断选择。来源记录反映实际所有权,包括获取期间的短暂重叠;UI 导航仍负责自己的目标,不把选择权交给分配器。
|
||||
|
||||
窗口级使用方可以查看来源标记。Session 业务操作仍使用传入的作用域或显式引用,不得从目录中查找 `mainView` 来补齐缺失的操作目标。后台引用只能证明所有权,不能证明用户查看过 Session。
|
||||
|
||||
`SessionRetainInfo` 包含 `referenceCount` 与只读 `retainedBy` 记录。`sessions.retainInfo(id)` 独立于目录成员关系观察本地所有权,并在同 ID 换代时保持稳定。没有活跃代的身份具有零引用和空来源记录;读取该值不代表该身份在 Host 上存在。
|
||||
|
||||
`ui-session` 通过 `useSessionRetainInfo(sessionId, selector)` 读取明确身份,通过 `useSessionRetainInfo(selector)` 读取外围 Provider 绑定的 Session。未绑定作用域向后一种形式提供缺失值,不回退到主区域 Session。两种形式均由渲染器从相同的裸 retain-info 来源构造。使用方检查 `mainView` 来源计数以识别原 current-Session 角色,其他来源键也可同等查询。读取或订阅不会持有 Session。
|
||||
|
||||
### 统一的 UI Session 状态
|
||||
|
||||
`ui-session` 拥有不依赖 React 的 `sessionStatus` 来源,通过标准 `useSessionStatus` 钩子公开。快照按 Session 身份索引,为每个已知 Session 提供一条 UI 状态记录。它组合下列独立事实,不将它们压缩成互斥的单一阶段:
|
||||
|
||||
| 字段 | 取值 | 含义与所有者 |
|
||||
| --- | --- | --- |
|
||||
| `running` | `boolean` 或 `undefined` | 最新已知的 Session 运行事实;缺少基线不表示已确认 idle |
|
||||
| `pendingInteraction` | `SessionPendingInteraction` 或 `undefined` | 领域拥有的有效请求;没有待处理请求时缺失 |
|
||||
| `completionUnread` | `boolean` | 已观察到停止、但尚未确认的 UI 提醒 |
|
||||
|
||||
来源计数以 `SessionListState.byId[id].retainedBy` 为准;UI 状态读取它以执行确认策略,不维护另一套引用注册表。标题、Workspace 关联、历史、队列和投影数据保留在各自的既有所有者中。
|
||||
|
||||
待处理领域保留 `SessionPendingInteractionMap`、请求身份、优先级、发布清理函数与卸载委托。统一状态包含同一个有效请求对象,不复制请求,也不创建第二套待处理注册表。Workspace 状态指示器与 Conversation composer 选择读取 `useSessionStatus`,不再分别读取 `useSessionPendingInteraction` 和 `useCompletedSessionIds` 钩子。
|
||||
|
||||
完成跟踪订阅已有 `api-session/status` 事件,避免 running 到 idle 的变化在合批目录快照中丢失。目录快照建立初始与重连基线。pending 状态下的空目录不能证明 Session 已消失。更新规则如下:
|
||||
|
||||
- 初始 idle 基线不产生完成提醒。
|
||||
- 观察到 running 时清除旧提醒,并记录运行基线。
|
||||
- 从已知 running 变为 idle 时,仅在 Session 没有主视图持有的情况下设置 `completionUnread`。
|
||||
- 获得主视图持有时清除提醒;无关来源的 retain 不清除提醒。
|
||||
- 释放主视图持有不会为更早的一次停止补造提醒。
|
||||
- 移除 Session 时清除其完成提醒与运行基线;待处理请求的清理仍属于请求领域。
|
||||
|
||||
提醒表示观察到的一次停止,不代表任务成功,也不代表某条排队消息完成。即使全局面板暂时隐藏 Conversation,主视图所有权仍保持基于选择的确认语义。本设计不提供 `ui-session/view-presence` 事件、已挂载 Provider 索引或 Provider 挂载时的隐式确认。
|
||||
|
||||
Session Controller 发布运行与引用来源事实,但不拥有完成提醒集合、`consumeCompletion` 方法或待处理交互展示。引用获取不执行完成提醒业务逻辑。
|
||||
|
||||
### 显式 Provider、并行 Conversation 与缓存身份
|
||||
|
||||
唯一的 `SessionProvider` 可以继承外层 binding,也可以用显式 `session={reference | undefined}` 覆盖本子树。它不获取或释放所有权。根 Provider 从 `mainView` 所有权标记解析主 binding,不维护另一份 current;并列或嵌套 Provider 只影响各自子树。显式缺失保持缺失,不回退到主区域。
|
||||
|
||||
Provider 注入选定 binding,但不为整个 body 设置 key。严格 `session` entry 在 binding Context 改变时重新挂载。空白 `session-maybe` entry 接受首个 binding 时不重新挂载;接受后,切换到另一个 binding Context 或回到缺失状态会创建新的组件 incarnation。
|
||||
|
||||
`uiWorkspace` 持有来源为 `mainView` 的主区域引用,`ui-session` 从该引用在 Session 记录中的所有权标记建立根 Provider。Provider 下的 Conversation、右 Sidebar、preset、命令、输入与模型组件只能读取 Provider 绑定的标准数据。它们不得读取主区域引用或全局选择,也不得按 Session ID 重新寻找一个可能属于其他 Provider 的 binding。
|
||||
|
||||
每个 Provider occurrence 以传入的 `SessionReference` 建立独立渲染作用域。两个引用可以指向不同 Session,也可以共享同一 `SessionBinding`;不同 binding 的业务与观看数据完全分离,同一 binding 的 Provider 共享 Session、Conversation、输入等 Session 级数据,但保留各自的组件局部状态。主区域与 Sidebar 可以同时挂载两个 Conversation,任一 Provider 的替换或卸载不改变另一棵子树的目标。
|
||||
|
||||
`ui-session` 为每个活跃 `SessionBinding` 复用稳定的业务 observable。Conversation assembly、input shell、command popup、input-trigger controller 与 model directory 等代际缓存以 binding 为弱键,不再以 Session ID 为键;需要枚举活跃值的缓存使用 `util-values` 的 `WeakMapWithValues<SessionBinding, Value>`,由弱键表和强值集合共同维护身份查询与值遍历。该容器不执行清理;订阅、控制器、URL 和其他资源仍通过 `binding.ctx.effect()` 确定性释放。Provider occurrence 的观看状态随该 Provider 的渲染作用域释放,最终 generation 清理由 binding Context 负责。
|
||||
|
||||
Descriptor 变化先组装替代来源再发布,不重建 Session 代。Provider 在读取 reference 时校验它仍有效;已经释放、来自其他 Controller 或不再对应活跃 binding 的 reference 不能创建作用域。
|
||||
|
||||
| 使用方 | 获取与释放 |
|
||||
| --- | --- |
|
||||
| 主 Conversation | `uiWorkspace` 获取导航目标;`ui-session` 从 `mainView` 标记建立根 Provider,Conversation 子树只消费 Provider 绑定 |
|
||||
| 关联右 Sidebar | `RightbarRoot` 继承根 Provider,不读取主区域引用 |
|
||||
| 独立 Session 视图 | 自己的所有者持有目标,并以自己的引用建立 Provider;与主区域同时运行 |
|
||||
| Client 中的 Host 事件 handler | 持有本地 Context 引用直到 handler 与回复结算 |
|
||||
|
||||
Conversation 的引用属于其视图所有者,不属于 Chat、Trajectory 或某次点击。Chat、Trajectory、命令、输入、上传、图片读取与模型选择在 Provider 生命周期内借用同一 binding;它们不按行动重复获取引用。切换或关闭该 Conversation 可以结束仍在途的本地工作。Sidebar tab 的布局与资源所有权保持独立;只有承载 Conversation 的 Sidebar 视图所有者需要自己的 Session 引用。空布局与 guide 占位页不持有引用。
|
||||
|
||||
作用域业务对象从 Provider 捕获自己的 binding,不把旧 Context 或目录重新解释成同 ID 的新一代。ModelSelection 借用 Provider binding,并保留自己的选择代际与错误规则。编辑器脱离回调可能与作用域清理重叠;可选 trigger 和 popup 对已结束的 Context 返回空值,不解析其他 generation。
|
||||
|
||||
### 主区域导航与展示
|
||||
|
||||
`uiWorkspace.openSession`、`openWorkspace`、`forkSession` 和 `startSession` 仍是导航入口。它们接受或解析明确目标,改变主视图,并按既有导航策略将主区域返回 Conversation。`retain` 本身从不导航。引用所有权不引入 `registerNavigation` 接收者协议,也不引入第二个导航服务。
|
||||
|
||||
现有 `uiWorkspace` 实现直接持有来源为 `mainView` 的主区域引用与目标。导航方法直接更新该所有者,不调用 Conversation 注册的接收者。`ui-session` 根据来源标记把该引用对应的 binding 注入根 Provider;主 Conversation 和关联右栏只继承 Provider,既不依赖 `uiWorkspace`,也看不到主引用。独立 Sidebar Conversation 以自己的 reference 建立嵌套 Provider,并覆盖本子树的 binding。主引用不是全局标准 prop、子树 Hook 或按 ID 查询的默认值。
|
||||
|
||||
主视图在 `dsh.sessions.current` 下私下持久化目标身份与子会话地址,不保存引用。启动恢复、初始 Workspace 选择与归档主目标后的清空仍属于 UI。归档或移除目录元数据不撤销其他使用方持有的独立引用。
|
||||
|
||||
| UI 行为 | 最终规则 |
|
||||
| --- | --- |
|
||||
| Session 列表高亮与空白记录处理 | 从 `retainedBy.mainView` 派生主区域占用,不按已挂载 Session Provider 的数量判断 |
|
||||
| New Session 的 Workspace | 优先使用显式 Workspace,其次按既有查找规则使用主 Session 的 Workspace,最后使用既有最近 Workspace 策略 |
|
||||
| Onboarding | 判断主区域 Session 是否缺失或为空白,不判断所有历史 Session |
|
||||
| 浏览器文档标题 | Conversation 显示 Session 标题与产品标题;全局面板显示产品标题 |
|
||||
| Chat/Trajectory 恢复 | 为显式选中的主目标恢复视图;独立绑定的视图保留自己的状态 |
|
||||
| Cordis inventory 面板 | 使用不区分 current/other 的单一列表;runner 不提供主区域选择的公开 getter |
|
||||
|
||||
DOM 焦点移动或全局面板隐藏仍被持有的视图时,来源元数据不变。[全局主面板设计](../../implemented/architecture/2026-09-08-global-main-panels.zh.md)拥有面板选择与布局;Session 引用所有权不替代它。
|
||||
|
||||
Conversation 保留 `hero`、`settling`、`active` 组合与既有历史加载和 `openError` 处理。获取引用不增加外层 loading/error 阶段展示、额外隐藏 composer 的条件、Retry 按钮或替换 Sidebar 内容的恢复面板。已有错误处理方继续处理自己的错误;没有错误展示的调用点不增加展示。Promise 拒绝与正确释放引用不意味着额外增加 UI 处理方。
|
||||
|
||||
Workspace 连接和 fork 保持既有导航检查与面板切换失效规则。直接打开 Session 不增加全局导航取消策略。Agent Team 刷新保留发起时选择仍然有效的条件,不在刷新前启动全局导航 token。引用获取不扩大取消范围,不影响无关导航或正在执行的操作;本地取消不回滚 Host 效果。
|
||||
|
||||
### 预设与创建流程
|
||||
|
||||
预设目录与部署默认值可以共享。已绑定 Session 的预设通过 Provider 的 binding 读取或修改;预设控制器按 `SessionBinding` 缓存,不由根级 current-Session 跟随器管理。Hero 的 preset seat 使用 `session-maybe` Provider:没有 Session 时显示创建流程选择,绑定空白 Session 后操作该确切 Session。标题标签读取同一 Provider 绑定 Session 的投影。
|
||||
|
||||
Session 创建前选择的 preset 保留在主 Conversation 的 `session-maybe` preset surface 中。Workspace 创建或复用空白 Session 并建立主 Provider 后,该 surface 将选择应用到 Provider 绑定的 Session。设置页修改默认 preset 或 picker 设置时,preset 服务从 Provider 已建立的 binding 缓存中选择带 `mainView` 所有权标记的空白 Session,并更新该 Session。非空白主 Session、Sidebar 的独立 Provider 与其他后台引用均不受该设置动作影响;preset 子树不读取主引用,也不通过全局 current follower 寻找目标。
|
||||
|
||||
### Host 事件 Context 所有权与 Typert
|
||||
|
||||
经校验的 Host waterfall(瀑布式事件)身份可以先于目录发现到达。Client Context 解析器必须保持同步。它带上 Gateway 的来源获取本地代引用,返回 `TypertOwnedValue<Context>`,不打开历史,也不刷新子目录。需要历史的 handler 另行获取公开引用并等待其 `ready` Promise,或使用 `sessions.using`。
|
||||
|
||||
Gateway 持有本地引用,直到 handler 使用与回复结算均结束。Context 获取失败保留既有的记录错误并委托语义,handler 失败产生拒绝回复。取消通过已有 signal 到达 handler,并抑制晚回复,但不会在 Context 仍被使用时提前释放。插件关闭汇合当前连接代的在途 handler;Connection 只在该来源结算后启动替代代。
|
||||
|
||||
`TypertOwnedValue` 跨通用 Gateway 传递值及其清理操作。它没有额外引用计数,也不要求 Gateway 理解 Session 专用所有权。独立 Client bundle 共享 owned-value 标记。Client 发请求的 `identity(ctx)` 保持同步;Host Context 解析与 Host 生命周期不变。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**以目录成员关系作为所有权。** 发现数据无法证明视图或操作仍在使用,而且部分 Context 身份先于目录成员关系到达。
|
||||
|
||||
**隐式主 Session 加显式替代路径。** 两套目标解析规则会使可复用组件依赖其渲染位置。显式 Provider 提供目标,通用来源记录服务于窗口级观察。
|
||||
|
||||
**Provider 子树继续读取 `mainSession`。** 同一组件会同时拥有 Provider 与窗口级目标,Sidebar 的独立 Conversation 也会被主区域变化重定向。主引用只参与 Provider 装配,子树只读取 Provider。
|
||||
|
||||
**以 Session ID 为代际缓存键。** 最终释放允许同 ID 新代在旧清理结束前出现,ID 键会复用旧对象或让旧清理删除新对象。业务缓存使用弱引用的 binding 身份,资源清理由 binding Context 负责。
|
||||
|
||||
**只在历史打开后解析的异步 `retain`。** 它会把 Provider 安装与主视图导航推迟到历史到达之后,导致既有加载状态无法立即渲染。同步引用把所有权与显式的 `ready` 结果分开。
|
||||
|
||||
**在同步 Context 解析中执行历史 I/O。** Host 事件派发需要作用域生命周期,不一定需要历史窗口;耦合两者会在目录发现之前延迟或阻止 handler。
|
||||
|
||||
**专用 current 标志。** 一种使用方专用标志无法描述 Sidebar 与后台所有权。主视图标记可以从通用的按来源计数中派生。
|
||||
|
||||
**Session Controller 中的完成状态,或独立的待处理与完成提醒钩子。** 完成确认属于 UI 策略。统一 UI 状态来源组合独立事实,同时保留领域拥有的待处理对象与 Controller 拥有的运行事实。
|
||||
|
||||
**以 Provider 呈现决定确认或释放。** 挂载既不等于所有者生命周期,也不等于基于选择的确认。它无法决定哪些后台或暂时隐藏的使用仍应存活,或应当算作已查看。
|
||||
|
||||
**Provider 中的裸 ID 或全局 binding revision。** ID 不表达代际所有权,全局刷新会使无关 Session 使用方失效。
|
||||
|
||||
**每个行动各自获取引用。** Provider 已经定义 Conversation 的使用生命周期;为每次点击重复持有会把视图所有权拆成大量细粒度来源。只有明确需要越过 Provider 生命周期的工作才另行持有引用。
|
||||
|
||||
**导航注册、新的取消策略与获取专用恢复 UI。** 引用所有权要求显式目标、释放与失败传播,不要求额外导航或恢复行为。
|
||||
|
||||
**Client 引用持有 Host Agent。** 历史访问与 Host 执行是独立使用需求,长时间打开的 Client 视图不能定义 Host 业务操作生命周期。
|
||||
|
||||
## 验证
|
||||
|
||||
- 获取测试覆盖共享首次打开、普通与意外打开失败、独立等待方取消、确切代替换,以及根清理达到完全停稳。
|
||||
- 来源测试覆盖多个来源、同来源多份引用、获取失败回滚、幂等释放、目录刷新或移除,以及旧代的晚释放不改变替代代。
|
||||
- Retain-info 钩子测试覆盖明确身份、Provider 绑定默认值、未绑定与嵌套作用域、跨代来源更新,以及读取不创建引用或请求历史。
|
||||
- 作用域与 Gateway 测试覆盖目录发现前不执行历史 I/O 的同步 Context 获取、获取失败的记录与委托、handler 的拒绝回复、取消与结算期间的所有权,以及跨 bundle 共享的 owned-value 标记。
|
||||
- UI 状态测试覆盖待处理优先级与清理、会被快照合批丢失的事件变化、初始与重连基线、主来源确认、无关来源持有,以及不存在 Provider 呈现事件。
|
||||
- 视图验证覆盖两个不同 Session 和同一 Session 多次使用的并行显式 Provider、互不重定向的 Conversation、独立 Slot store、作用域 preset、descriptor 更新,以及没有新恢复控件的 Sidebar 生命周期。
|
||||
- 实际组合浏览器场景保持主区域高亮、空白记录、onboarding、Workspace 默认值、标题与约定的导航或错误行为。只有 Client 所有权改变时,已有模型轮次录制仍作为行为输入。
|
||||
- 公开 `retain`、`using`、Provider、生成 API 目录、包约定与类型检查对 options 和所有权保持一致。Provider 子树不读取主区域引用,独立所有者各自释放自己的引用。
|
||||
|
||||
## 后果
|
||||
|
||||
使用方必须把引用保留到真实结束点;在活跃 Client 根内,未释放引用仍会泄漏。最后释放后再次 retain 会创建新一代,并可能重新打开历史。来源计数描述所有权,不描述可见性、任务成功或操作权限;以主标记作为业务兜底会重新引入隐式 current-Session 耦合。
|
||||
|
||||
最后一份 reference 释放后,未持久化的 binding 自有状态会被丢弃,包括已加载的历史页、Chat 滚动锚点、预览换行、Files 展开状态、composer 附件和 undo 历史。只有持久化的 Session-keyed Store 值或由另一份 reference 保活的 generation 能跨视图切换保留;runtime 不会清除这些持久值。
|
||||
|
||||
每次公开 `retain` 都会启动该 generation 共享的首次历史打开,包括 Session 重命名和 fork 标题设置所用的临时引用。这些元数据操作在使用 Session 前等待 `reference.ready`,因此冷 generation 会承担这次历史 I/O。
|
||||
|
||||
目录、UI 状态与视图目标由不同所有者管理,可以独立发布。使用方不能仅从通知顺序推断生命周期变化。主视图仍是普通引用所有者,其导航与展示规则留在 UI,不进入引用分配器。
|
||||
+2
-2
@@ -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-15-desktop-native-fatal-recovery.md
|
||||
2026-09-15-desktop-native-fatal-recovery.md: b2a0dfff1a630d1eedaa898e4a4c8144ff7ae776
|
||||
2026-09-15-desktop-native-fatal-recovery.zh.md: 0fee729e3be791d10966050d21482c5132b14fe6
|
||||
2026-09-15-desktop-native-fatal-recovery.md: 94ead425f5483ba3e6ed5d9d595edb4483254c27
|
||||
2026-09-15-desktop-native-fatal-recovery.zh.md: 2138fb11405d00d51775350bf60d2702621aaa19
|
||||
@@ -10,18 +10,20 @@ A recovery document depends on the renderer and preload whose failure can preven
|
||||
|
||||
## Decision
|
||||
|
||||
Electron owns one native fatal dialog per application process. Explicit main-window creation, document-load, preload, renderer, Web initialization, and backend failures enter this path. Ordinary requests and package operations retain their local error handling; the Host restarts after failed package writes, and a failed Host restart enters native recovery; expected cancellation and shutdown do not enter recovery. No elapsed-time heuristic classifies a slow startup as fatal.
|
||||
Electron owns one native fatal dialog per application process. Explicit main-window creation, document-load, preload, renderer, Web initialization, and backend failures enter this path. Ordinary requests retain local error handling, and the shared Web plugin manager owns package-operation failures. Expected cancellation and shutdown do not enter recovery. No elapsed-time heuristic classifies a slow startup as fatal.
|
||||
|
||||
The first report claims presentation before awaiting the dialog. Later reports remain in logs. The Electron console retains the complete reported diagnostic. The dialog bounds the first diagnostic to its final eight lines and limits the complete detail to 1,200 UTF-16 code units, including truncation notice and reinstall advice, because native dialogs cannot scroll. The dialog offers exit, restart, or disabling third-party bundles followed by a whole-application restart. Disabling writes activation metadata under the existing profile transaction lock after Host shutdown, without requiring runtime initialization or deleting installed files. An explicit recovery-operation failure is presented separately and does not count as another automatic fatal report.
|
||||
The first report claims presentation before awaiting the dialog. Later reports remain in logs. The Electron console retains the complete reported diagnostic. The dialog bounds the first diagnostic to its final eight lines and limits the complete detail to 1,200 UTF-16 code units, including truncation notice and reinstall advice, because native dialogs cannot scroll. The dialog offers exit, restart, or disabling third-party bundles followed by a whole-application restart. Recovery calls the shared app-boot `sanitizeProfile` function under the existing profile lock after Host shutdown. The function restores caller-supplied bundles and renames the profile patch to a unique backup without parsing it, requiring runtime initialization, or deleting installed files. Callers own profile shutdown and write exclusion; Desktop is the current production caller. The home-level patch remains unchanged. An explicit recovery-operation failure is presented separately and does not count as another automatic fatal report.
|
||||
|
||||
The Web document stays in place. A carrier callback owns startup failure presentation while the shared boot page retains its spinner; ordinary browser boot still renders its own failure report. Only the primary application frame may report a Web boot failure. The plugin window exposes package operations only; backend state remains in the main process, and native recovery directly owns disabling all third-party bundles. Desktop has no profile reset, plugin-window recovery controls, or emergency recovery document. A fatal backend failure requires one of the native recovery actions rather than an in-process retry.
|
||||
The Web document stays in place. A carrier callback owns startup failure presentation while the shared boot page retains its spinner; ordinary browser boot still renders its own failure report. Only the primary application frame may report a Web boot failure. Backend state remains in the main process, and native recovery directly owns disabling all third-party bundles. Desktop has no profile reset or emergency recovery document. A fatal backend failure requires one of the native recovery actions rather than an in-process retry.
|
||||
|
||||
This supersedes recovery-page and reset behavior in the [immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md), whose immediate visibility and direct Host startup rationale remain active. The [in-place profile decision](2026-09-09-desktop-in-place-profile.md) still owns package transactions and partial changes.
|
||||
This supersedes recovery-page and reset behavior in the [immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md), whose immediate visibility and direct Host startup rationale remain active. The [Web wrapper decision](2026-09-10-desktop-web-wrapper.md) owns shared plugin management and native profile preparation.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
A Web modal depends on client initialization, while a second recovery document adds renderer resources and preload recovery paths. Native dialogs remain usable when those components fail. Automatically resetting configuration or restarting on every report can delete user configuration or create restart loops; explicit actions preserve user control.
|
||||
|
||||
**Disable bundles while retaining the active profile patch.** A malformed patch or a patch that inserts a broken plugin can still prevent startup. Renaming preserves the user’s exact bytes for manual repair while removing that layer from startup; unique backup names preserve earlier recovery attempts.
|
||||
|
||||
## Consequences
|
||||
|
||||
Recovery cannot report a killed or crashed Electron main process, and a silent startup hang has no automatic timeout prompt. Invalid profile JSON can prevent disabling plugins; exit and restart remain available after the operation reports its failure. Focused lifecycle tests cover fatal signals, cancellation, first-report deduplication, and shutdown ordering; locale expectations record dialog diagnostics and actions, and boot tests retain ordinary browser failure presentation.
|
||||
Recovery cannot report a killed or crashed Electron main process, and a silent startup hang has no automatic timeout prompt. A malformed home-level patch still blocks startup after profile recovery and requires manual repair. Backups accumulate in the profile directory without automatic pruning; users remove them when no longer needed. Invalid profile JSON can prevent disabling plugins; exit and restart remain available after the operation reports its failure. Focused lifecycle tests cover fatal signals, cancellation, first-report deduplication, and shutdown ordering; locale expectations record dialog diagnostics and actions, and boot tests retain ordinary browser failure presentation.
|
||||
+7
-5
@@ -10,18 +10,20 @@ Status: implemented
|
||||
|
||||
## Decision
|
||||
|
||||
Electron 在每个应用进程中提供一次原生致命错误对话框。明确的主窗口创建、文档加载、preload、渲染器、Web 初始化和后端失败进入此路径。普通请求和包操作保留局部错误处理;包写入失败后会重新启动 Host,Host 重启失败进入原生恢复;预期取消和关闭不进入恢复。不通过耗时推断慢启动为致命故障。
|
||||
Electron 在每个应用进程中提供一次原生致命错误对话框。明确的主窗口创建、文档加载、preload、渲染器、Web 初始化和后端失败进入此路径。普通请求保留局部错误处理,共享 Web 插件管理器负责包操作失败。预期取消和关闭不进入恢复。不通过耗时推断慢启动为致命故障。
|
||||
|
||||
首次报告在等待对话框前取得展示权。后续报告保留在日志中。Electron 控制台保留完整的已报告诊断。原生对话框无法滚动,因此仅显示首次诊断末尾八行,并将包含截断提示和重装建议的完整详情限制为 1,200 个 UTF-16 代码单元。对话框提供退出、重启或禁用第三方 bundle 后重启整个应用。禁用操作等待 Host 关闭后,在已有 profile 事务锁内写入启用元数据,不要求运行时初始化,也不删除安装文件。显式恢复操作失败会单独展示,不计为另一次自动致命报告。
|
||||
首次报告在等待对话框前取得展示权。后续报告保留在日志中。Electron 控制台保留完整的已报告诊断。原生对话框无法滚动,因此仅显示首次诊断末尾八行,并将包含截断提示和重装建议的完整详情限制为 1,200 个 UTF-16 代码单元。对话框提供退出、重启或禁用第三方 bundle 后重启整个应用。恢复操作等待 Host 关闭后,在已有 profile 锁内调用共享 app-boot `sanitizeProfile` 函数。该函数恢复调用方指定的 bundle,并将 profile patch 重命名为唯一备份,无需解析 patch、初始化运行时或删除安装文件。调用方负责 profile 关闭并排除并发写入;当前生产调用方是 Desktop。home 级 patch 保持不变。显式恢复操作失败会单独展示,不计为另一次自动致命报告。
|
||||
|
||||
Web 文档保留在原位。宿主回调负责启动失败展示,共享启动页保留加载动画;普通浏览器启动仍显示自身的失败报告。只有主应用框架可以上报 Web 启动失败。插件窗口只暴露包操作;后端状态保留在主进程中,原生恢复直接负责禁用全部第三方 bundle。Desktop 不提供 profile 重置、插件窗口恢复控件或应急恢复文档。后端致命故障必须通过原生恢复操作处理,不在当前进程中重试。
|
||||
Web 文档保留在原位。宿主回调负责启动失败展示,共享启动页保留加载动画;普通浏览器启动仍显示自身的失败报告。只有主应用框架可以上报 Web 启动失败。后端状态保留在主进程中,原生恢复直接负责禁用全部第三方 bundle。Desktop 不提供 profile 重置或应急恢复文档。后端致命故障必须通过原生恢复操作处理,不在当前进程中重试。
|
||||
|
||||
这取代了[立即显示窗口决策](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)中的恢复页和重置行为;该决策关于立即可见性和直接启动 Host 的理由仍然有效。[原位 profile 决策](2026-09-09-desktop-in-place-profile.zh.md)仍负责包事务和部分变更。
|
||||
本决策取代[立即显示窗口决策](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)中的恢复页面和重置行为;其立即显示与直接启动 Host 的理由仍有效。[Web 薄壳决策](2026-09-10-desktop-web-wrapper.zh.md)负责共享插件管理和原生 profile 准备。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
Web 模态框依赖客户端初始化,而第二份恢复文档会增加渲染器资源和 preload 恢复路径。原生对话框在这些组件失败时仍然可用。自动重置配置或每次报告都重启可能删除用户配置或形成重启循环;显式操作保留用户控制权。
|
||||
|
||||
**禁用 bundle,但保留生效的 profile patch。** 损坏的 patch 或插入故障插件的 patch 仍可阻止启动。重命名保留用户原始内容供手动修复,同时将该层移出启动过程;唯一备份名保留此前恢复操作的备份。
|
||||
|
||||
## Consequences
|
||||
|
||||
恢复功能无法报告 Electron 主进程被终止或崩溃,静默启动挂起也没有自动超时提示。无效的 profile JSON 可能阻止禁用插件;操作报告失败后仍可退出和重启。定向生命周期测试覆盖致命信号、取消、首次报告去重和关闭顺序;语言预期记录对话框诊断和操作,启动测试保留普通浏览器失败展示。
|
||||
恢复功能无法报告 Electron 主进程被终止或崩溃,静默启动挂起也没有自动超时提示。损坏的 home 级 patch 在 profile 恢复后仍会阻止启动,需要手动修复。备份在 profile 目录中累积,不会自动清理;用户在不再需要时删除。无效的 profile JSON 可能阻止禁用插件;操作报告失败后仍可退出和重启。定向生命周期测试覆盖致命信号、取消、首次报告去重和关闭顺序;语言预期记录对话框诊断和操作,启动测试保留普通浏览器失败展示。
|
||||
@@ -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-platform-office-engines.md
|
||||
2026-09-15-platform-office-engines.md: 4272054672c6163f22b910ab64b6472a9dbdfb4c
|
||||
2026-09-15-platform-office-engines.zh.md: d0a317333049734415ab9c0c05a5624b63b27447
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: One Office engine per platform
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-15-platform-office-engines.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Installing WASM beside a usable native engine adds a second LibreOffice payload to application downloads and installed resources. The original fallback policy in [independent kit ownership](2026-09-14-independent-libreoffice-kit.md) requires that extra payload even on fixed-platform Desktop and Python distributions.
|
||||
|
||||
## Decision
|
||||
|
||||
Harness selects one engine from the installed kit API’s `optionalDependencies`. A declared `@deepseek-ai/libreoffice-kit-${platform}-${arch}` requires that native package; other targets require the shared WASM package. The supported native target set belongs to the kit release, not an operating-system branch in Harness. A missing declared native package is an incomplete installation and never selects WASM. The provider has no direct WASM dependency.
|
||||
|
||||
Python sidecar assembly copies only the selected engine and its dependency closure. Wheel packaging and runtime lookup select from the same kit manifest; the relocated conversion smoke checks the selected backend. Desktop filters its npm installation to that engine before signing and integrity sealing. Engine compilation, resolver compatibility, npm platform metadata, qualification, and publication remain in the kit repository.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep WASM beside every native engine.** This tolerates a missing optional native package but increases every distribution with native support. Fixed-platform distributions require their declared native engine to be present and tested instead.
|
||||
|
||||
**Remove WASM on every platform.** Targets without a released native engine still need conversion. The shared WASM package serves those targets without introducing a new native release target.
|
||||
|
||||
**Make the Python Office sidecar optional.** The runtime carries the shared `dsh` CLI and its Web profile as well as the default SDK profile. Requiring the target engine gives the installed wheel a complete shipped profile set and reports an incomplete payload before launch. SDK and headless users also pay the engine download and installed-size cost.
|
||||
|
||||
## Consequences
|
||||
|
||||
Distributions with a declared native target omit WASM assets. Other targets retain WASM resource and font requirements. The pinned kit declares macOS/Windows ARM64 and x64 native packages, so current Linux distributions select WASM; a kit release can add a native Linux target without changing Harness’s selection rule. This does not expand Harness’s supported release platforms. Harness sidecar, wheel, and runtime-resolution tests cover both declared native targets and WASM selection, including missing native packages. New package bytes require kit qualification and matching dependency integrity records before publication.
|
||||
|
||||
A new engine package identity also requires updates to `LIBREOFFICE_PACKAGES` in `scripts/gen-third-party-notices.ts`, any applicable `minimumReleaseAgeExclude` entry in `pnpm-workspace.yaml`, and the package list in the [kit ownership note](2026-09-14-independent-libreoffice-kit.md). The license allowlist remains explicit.
|
||||
|
||||
The [public Python release workflow](../../../../.github/workflows/python-release.yml) rejects any wheel at or above 100,000,000 bytes. Selecting one engine reduces payload size but does not establish that a runtime wheel meets this limit; npm engine publication and local conversion are separate from wheel upload eligibility.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: 每个平台使用一个 Office 引擎
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-15-platform-office-engines.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
在可用的原生引擎之外安装 WASM,会为应用下载和安装资源增加第二份 LibreOffice 载荷。[独立 kit 归属](2026-09-14-independent-libreoffice-kit.zh.md)中最初的回退策略要求固定平台的 Desktop 和 Python 分发物也携带这份额外载荷。
|
||||
|
||||
## Decision
|
||||
|
||||
Harness 从已安装 kit API 的 `optionalDependencies` 中选择一个引擎。声明了 `@deepseek-ai/libreoffice-kit-${platform}-${arch}` 的目标要求该原生包,其余目标要求共享 WASM 包。受支持的原生目标集合由 kit 发布版本决定,不由 Harness 中的操作系统分支决定。已声明的原生包缺失表示安装不完整,不会改选 WASM。provider 不直接依赖 WASM。
|
||||
|
||||
Python sidecar 组装仅复制所选引擎及其依赖闭包。wheel 打包和运行时查找依据同一份 kit 清单选择;迁移目录后的转换冒烟检查所选后端。Desktop 在签名和完整性封装前将 npm 安装结果过滤为该引擎。引擎编译、解析器兼容性、npm 平台元数据、资格验证与发布仍由 kit 仓库负责。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**在每个原生引擎旁保留 WASM。** 这能容忍可选原生包缺失,却会增大每个支持原生引擎的分发物。固定平台分发物要求其已声明的原生引擎存在且经过测试。
|
||||
|
||||
**在所有平台移除 WASM。** 没有已发布原生引擎的目标仍需转换能力。共享 WASM 包为这些目标提供转换,无需引入新的原生发布目标。
|
||||
|
||||
**将 Python Office 伴随目录设为可选。** 运行时承载共享的 `dsh` CLI 及其 Web profile,而不只有默认 SDK profile。强制携带目标引擎使已安装 wheel 具备完整的内置 profile 集合,并在启动前报告载荷不完整。SDK 和 headless 用户也承担引擎的下载与安装体积。
|
||||
|
||||
## Consequences
|
||||
|
||||
声明了原生目标的分发物省去 WASM 资源,其余目标保留 WASM 的资源和字体要求。锁定的 kit 声明了 macOS/Windows ARM64 和 x64 原生包,因此当前 Linux 分发物选择 WASM;kit 发布版本可以新增 Linux 原生目标,无需修改 Harness 的选择规则。这不扩展 Harness 支持的发布平台。Harness 的 sidecar、wheel 和运行时解析测试覆盖已声明原生目标与 WASM 选择,包括原生包缺失。新包字节在发布前需要 kit 资格验证和匹配的依赖完整性记录。
|
||||
|
||||
新增引擎包标识还需要更新 `scripts/gen-third-party-notices.ts` 中的 `LIBREOFFICE_PACKAGES`、`pnpm-workspace.yaml` 中适用的 `minimumReleaseAgeExclude` 条目,以及 [kit 归属记录](2026-09-14-independent-libreoffice-kit.zh.md)中的包列表。许可证允许列表仍使用明确的包标识。
|
||||
|
||||
[公开 Python 发布工作流](../../../../.github/workflows/python-release.yml)拒绝任何大于等于 100,000,000 字节的 wheel。只选择一个引擎会减少载荷,但不能据此认定运行时 wheel 已满足此限制;npm 引擎发布、本地转换与 wheel 上传资格是不同的验证。
|
||||
Loaded 100 of 2234 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user