diff --git a/.agents/notes/README.i18n.yaml b/.agents/notes/README.i18n.yaml index af337114a0..f6289cf96f 100644 --- a/.agents/notes/README.i18n.yaml +++ b/.agents/notes/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/README.md -README.md: 8bf1adf875b54eb4580b4d189dfeed07c9f671c0 -README.zh.md: ff9bdb358d584d4f4ad0aea295c31d8d8fff9376 +README.md: 6872de0a1390fa5a1707b9f464c9e0edeb2da4be +README.zh.md: ced81f77fa904ad0b5cb5219383ef651d87392b8 diff --git a/.agents/notes/README.md b/.agents/notes/README.md index 8bf1adf875..6872de0a13 100644 --- a/.agents/notes/README.md +++ b/.agents/notes/README.md @@ -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. diff --git a/.agents/notes/README.zh.md b/.agents/notes/README.zh.md index ff9bdb358d..ced81f77fa 100644 --- a/.agents/notes/README.zh.md +++ b/.agents/notes/README.zh.md @@ -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) 目录树。 @@ -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 历史成为决策依据的唯一副本。 diff --git a/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.i18n.yaml b/.agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.i18n.yaml similarity index 69% rename from .agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.i18n.yaml rename to .agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.i18n.yaml index 8d8ad118e3..b8b5ab8c63 100644 --- a/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.i18n.yaml +++ b/.agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.md -2026-09-09-desktop-in-place-profile.md: 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 diff --git a/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.md b/.agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.md similarity index 99% rename from .agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.md rename to .agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.md index 6f0e959890..57dabc0d03 100644 --- a/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.md +++ b/.agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.md @@ -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) diff --git a/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.zh.md b/.agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.zh.md similarity index 99% rename from .agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.zh.md rename to .agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.zh.md index bec79b3b9d..2b57c49ce4 100644 --- a/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.zh.md +++ b/.agents/notes/archived/architecture/2026-09-09-desktop-in-place-profile.zh.md @@ -1,6 +1,7 @@ # Agent Note: 直接修改 Desktop profile Status: implemented +Archived: 2026-09-17 [English](2026-09-09-desktop-in-place-profile.md) | 中文 diff --git a/.agents/notes/implemented/feature/2026-09-09-turn-duration-hour-unit.i18n.yaml b/.agents/notes/archived/architecture/2026-09-10-local-office-preview.i18n.yaml similarity index 58% rename from .agents/notes/implemented/feature/2026-09-09-turn-duration-hour-unit.i18n.yaml rename to .agents/notes/archived/architecture/2026-09-10-local-office-preview.i18n.yaml index 209fc7e492..0afd4836d9 100644 --- a/.agents/notes/implemented/feature/2026-09-09-turn-duration-hour-unit.i18n.yaml +++ b/.agents/notes/archived/architecture/2026-09-10-local-office-preview.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/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 diff --git a/.agents/notes/archived/architecture/2026-09-10-local-office-preview.md b/.agents/notes/archived/architecture/2026-09-10-local-office-preview.md new file mode 100644 index 0000000000..ea32e37f0b --- /dev/null +++ b/.agents/notes/archived/architecture/2026-09-10-local-office-preview.md @@ -0,0 +1,52 @@ +# Agent Note: Local Office preview through WASM PDF bytes + +Status: implemented +Archived: 2026-09-11 + +English | [中文](2026-09-10-local-office-preview.zh.md) + +## Problem + +Office Open XML files are ZIP archives, so text fallback cannot provide a useful preview. Document conversion must stay local without taking focus or adding document content to a model conversation. Engine success alone cannot prove that the input has the claimed format or that a PDF was produced. + +## Decision + +A [document-render Service Definition](../../../../packages/document/document-render/README.md), a LibreOffice WASM provider, and Client/API consumers form the conversion capability. [Boot composition](../../../../packages/document/document-render-auto/README.md) mounts the provider and Remote controller only when `wasm.artifactDirectory` is configured. Invalid explicit configuration fails initialization. The Office Client registration always exists so an unconfigured Host receives guidance. Availability follows the generated Remote namespace without executable discovery or a duplicate Client flag. + +The [WASM provider](../../../../packages/document/document-render-libreoffice-wasm/README.md) uses pinned official LibreOffice source, Emscripten, and a LibreOfficeKit adapter. Each Node Worker owns one engine instance, its pthreads, and its memory filesystem. Terminating the Worker cancels synchronous engine calls; every conversion waits for its Worker to stop before returning. OOXML inspection verifies archive membership, content type, and resource bounds before startup. + +A VCL callback requests a font family before substitution or missing Unicode characters during glyph fallback. Host code indexes installed font metadata and copies selected files into engine memory, including complete font collections. Exact installed families precede configured alternatives; original-first fontconfig aliases preserve that ordering inside LibreOffice. Optional initial families use the same resolver. The engine has no Host filesystem mount, and rendering neither installs nor downloads fonts. The [build recipe](../../../../native/libreoffice-wasm/README.md) records source and toolchain revisions, patches, and asset hashes. Engine bundles remain immutable during provider use; a different build uses a new directory. + +The [independent engine release](../process/2026-09-11-independent-libreoffice-package.md) owns precompiled npm packaging and keeps engine compilation outside ordinary DSH builds. + +The provider returns caller-owned PDF bytes and independent `succeeded`, `timedOut`, and `cancelled` facts. PDF bytes are copied out of engine memory before teardown. The API consumer authorizes the source through [Workspace Files](2026-09-09-workspace-file-read-authority.md), requires successful uninterrupted conversion, and encodes the PDF directly. Source path and freshness version survive the PDF transport. Workspace Files limits source reads; the provider's `maxOutputBytes` alone limits generated PDFs. No temporary PDF or file lease is needed. + +The Office Client watches the selected Conversation's existing Deliverables projection and warms recent Office files without opening tabs. Its bounded memory cache checks authorized source metadata on every read and shares pending conversions between background and foreground callers. Only the last departing reader cancels a shared conversion; failures are not cached and connection reset discards cached bytes. [Document Preview](2026-09-08-document-preview-operations.md) owns format selection, loading, cancellation, and the PDF.js Worker. + +PDF.js's official TextLayerBuilder owns selection boundaries and copy normalization over the width-fitted canvas, with shared page cleanup and a component-owned resize observer. Its end-of-content marker and stacking rules constrain selection in blank regions; line-break highlighting is suppressed. Per-page cancellation uses the builder's cleanup rather than aborting the first page's signal, because the official selection listeners are shared across pages. + +Missing-family observations accompany the PDF through the worker and Remote response. The Office plugin renders a keyed notice above the shared PDF scrollport, so dismissing it removes its layout height. The font resolver intersects explicit source and theme references with actual document requests, excluding engine defaults and font-table inventories; installed aliases and glyph fallback do not by themselves mean a family is absent. + +## Alternatives considered + +**Keep an installed native LibreOffice provider.** A second provider adds executable discovery, version queries, subprocess supervision, scratch files, and platform-specific confinement. This design uses WASM for all supported Office previews. A native provider needs a demonstrated rendering or deployment requirement that WASM cannot satisfy. + +**Return a temporary PDF path.** The Worker already supplies independent PDF bytes. Writing them to disk, authorizing another file read, and maintaining leases adds file ownership without a current consumer. Source authorization and output limits apply directly to the memory result. + +**Automate installed Microsoft Office.** Word for Mac requires GUI automation rather than headless conversion, introducing focus changes and per-file operating-system authorization. Native Office automation remains outside this preview capability. + +**Use an online converter or download the engine automatically.** Remote conversion uploads document content. Automatic installation adds runtime distribution and update responsibilities; explicit artifact configuration leaves deployment with the operator. + +**Convert PDF pages to PNG.** PDF.js already owns PDF presentation. Rasterizing adds another image pipeline and loses the existing PDF controls and selectable text. + +**Add a model-facing rendering tool or durable preview events.** Client preview does not supply model input. A model-facing tool requires logged facts and both SDK projections, so it is a separate consumer. + +**Scan every font-table entry for warnings.** Font tables include unused styles and templates. Reporting them would make the notice unrelated to rendered content; collection instead starts after engine initialization and ends after PDF save. + +## Consequences + +The single-provider design assumes WASM covers the required Office rendering behavior; it is not evidence of complete fidelity equivalence with native LibreOffice. Fidelity depends on the engine build and installed fonts. The [real-engine tests](../../../../packages/document/document-render-libreoffice-wasm/tests/libreoffice-wasm.e2e.ts) cover DOCX, XLSX, PPTX, selectable Chinese text, and embedded system fonts; ABI fixtures alone cannot establish rendering fidelity or assembled browser presentation. + +WASM instances have substantial memory and startup costs, so the provider bounds concurrency. The [font reuse and image resolution decision](2026-09-11-wasm-preview-font-and-image-budgets.md) owns shared Host metadata, Worker request memoization, and raster export limits; only selected fonts enter engine memory. Large collections require bounded memory growth, and CFF export requires checked stack space. A fatal abort prevents further calls into C++. A device without a font covering a character cannot render it correctly without additional fonts. Owning the WASM build adds compiler compatibility, patch maintenance, and redistribution obligations. + +Prewarming consumes conversion resources to reduce foreground waits; entry and byte limits bound retained Client results. Source and PDF bytes remain outside Session storage and persistent caches. The [provider tests](../../../../packages/document/document-render-libreoffice-wasm/tests/provider.spec.ts) cover independent output facts, input/output rejection, queued cancellation, and active Worker termination. Existing preview registry, file read authority, and Session ownership decisions remain active. diff --git a/.agents/notes/archived/architecture/2026-09-10-local-office-preview.zh.md b/.agents/notes/archived/architecture/2026-09-10-local-office-preview.zh.md new file mode 100644 index 0000000000..a1ac8337a1 --- /dev/null +++ b/.agents/notes/archived/architecture/2026-09-10-local-office-preview.zh.md @@ -0,0 +1,52 @@ +# Agent Note: 通过 WASM PDF 字节进行本地 Office 预览 + +Status: implemented +Archived: 2026-09-11 + +[English](2026-09-10-local-office-preview.md) | 中文 + +## 问题 + +Office Open XML 文件是 ZIP 归档,因此文本回退无法提供有用的预览。文档转换必须留在本地,不抢占焦点,也不将文档内容放入模型对话。仅凭引擎成功状态无法证明输入具有声明的格式或生成了 PDF。 + +## 决定 + +[document-render Service Definition](../../../../packages/document/document-render/README.zh.md)、LibreOffice WASM 提供方及 Client/API 消费方构成转换能力。[启动组合](../../../../packages/document/document-render-auto/README.zh.md)仅在配置 `wasm.artifactDirectory` 时挂载提供方和 Remote 控制器。无效的显式配置会使初始化失败。Office Client 注册始终存在,为未配置的 Host 提供指引。可用性由生成的 Remote 命名空间决定,不发现可执行文件,也不维护重复的 Client 标记。 + +[WASM 提供方](../../../../packages/document/document-render-libreoffice-wasm/README.zh.md)使用固定的官方 LibreOffice 源码、Emscripten 和 LibreOfficeKit 适配器。每个 Node Worker 拥有一个引擎实例、其 pthread 和内存文件系统。终止 Worker 可取消同步引擎调用;每次转换均在返回前等待 Worker 停止。OOXML 检查在启动前验证归档成员、内容类型和资源限制。 + +VCL 回调在替换前请求字体族,或在字形回退时请求缺失的 Unicode 字符。Host 代码索引已安装字体元数据,将选中的文件复制到引擎内存,包括完整字体集合。精确匹配的已安装字体优先于配置的替代项;原字体优先的 fontconfig 别名在 LibreOffice 内保留此顺序。可选的初始字体族使用同一解析器。引擎不挂载 Host 文件系统,渲染不安装或下载字体。[构建配方](../../../../native/libreoffice-wasm/README.zh.md)记录源码与工具链版本、补丁及产物哈希。提供方使用期间,引擎包保持不可变;不同构建使用新目录。 + +[独立引擎发布](../process/2026-09-11-independent-libreoffice-package.zh.md)负责预编译 npm 打包,并将引擎编译与普通 DSH 构建隔离。 + +提供方返回调用方拥有的 PDF 字节,以及独立的 `succeeded`、`timedOut` 和 `cancelled` 事实。PDF 字节在清理前从引擎内存复制出来。API 消费方通过 [Workspace Files](2026-09-09-workspace-file-read-authority.zh.md)授权源文件,要求转换成功且未被中断,并直接编码 PDF。源路径和新鲜度版本保留在 PDF 传输中。Workspace Files 限制源文件读取;生成 PDF 仅受提供方的 `maxOutputBytes` 限制。无需临时 PDF 或文件租约。 + +Office Client 观察所选 Conversation 现有的 Deliverables 投影,在不打开 tab 的情况下预转换最近的 Office 文件。有上限的内存缓存每次读取都检查已授权的源文件元数据,并在后台和前台调用方之间共享进行中的转换。仅最后一个退出的读取方取消共享转换;失败不缓存,连接重置丢弃缓存字节。[Document Preview](2026-09-08-document-preview-operations.zh.md)负责格式选择、加载、取消和 PDF.js Worker。 + +PDF.js 官方 TextLayerBuilder 在适配宽度的 canvas 上负责选择边界和复制规范化,共享页面清理,并使用由组件拥有的 resize observer。其内容结束标记和堆叠规则限制空白区域中的选择;换行高亮被抑制。逐页取消使用 builder 的清理操作,而不 abort 第一页的信号,因为官方选择监听器跨页面共享。 + +缺失字体族观察值随 PDF 经过 Worker 和 Remote 响应。Office 插件在共享 PDF 滚动区域上方呈现 keyed 提示,关闭时移除其布局高度。字体解析器取源文档及主题显式引用与实际文档请求的交集,排除引擎默认字体和字体表清单;已安装别名和字形回退本身不代表字体族缺失。 + +## 考虑过的替代方案 + +**保留已安装的原生 LibreOffice 提供方。** 第二个提供方增加可执行文件发现、版本查询、子进程监督、临时文件和平台特定隔离。本设计将 WASM 用于全部受支持的 Office 预览。引入原生提供方需要证明存在 WASM 无法满足的渲染或部署需求。 + +**返回临时 PDF 路径。** Worker 已提供独立的 PDF 字节。将其写入磁盘、授权另一处文件读取并维护租约,会增加文件所有权,而没有当前消费方需要这些操作。源文件授权和输出限制直接应用于内存结果。 + +**自动操作已安装的 Microsoft Office。** Word for Mac 需要 GUI 自动化而非 headless 转换,会引入焦点变化和逐文件操作系统授权。原生 Office 自动化不属于本预览能力。 + +**使用在线转换器或自动下载引擎。** 远程转换会上传文档内容。自动安装增加运行时分发和更新责任;显式产物配置将部署留给运维者。 + +**将 PDF 页面转换为 PNG。** PDF.js 已负责 PDF 展示。栅格化增加另一条图像流水线,并失去现有 PDF 控件和可选择文本。 + +**添加面向模型的渲染工具或持久化预览事件。** Client 预览不提供模型输入。面向模型的工具需要记录事实并更新两种 SDK 投影,因此属于独立消费方。 + +**扫描全部字体表条目生成提示。** 字体表包含未使用的样式和模板。报告这些条目会使提示与实际渲染内容无关,因此收集从引擎初始化完成后开始,在 PDF 保存后结束。 + +## 影响 + +单一提供方设计假设 WASM 覆盖所需的 Office 渲染行为;这不是与原生 LibreOffice 完全等价的保真度证据。保真度取决于引擎构建和已安装字体。[真实引擎测试](../../../../packages/document/document-render-libreoffice-wasm/tests/libreoffice-wasm.e2e.ts)覆盖 DOCX、XLSX、PPTX、可选择中文文本和嵌入系统字体;仅有 ABI fixture 无法证明渲染保真度或完整浏览器展示。 + +WASM 实例有较高的内存与启动成本,因此提供方限制并发。[字体复用与图像分辨率决策](2026-09-11-wasm-preview-font-and-image-budgets.zh.md)负责共享 Host 元数据、Worker 请求记忆化和栅格导出限制;仅选中字体进入引擎内存。大型集合需要有界内存增长,CFF 导出需要受检查的栈空间。致命 abort 后不再调用 C++。设备没有覆盖某字符的字体时,无法在不添加字体的前提下正确渲染该字符。自行构建 WASM 会增加编译器兼容、补丁维护和再分发义务。 + +预转换消耗转换资源以减少前台等待;条目数和字节限制约束 Client 保留的结果。源文件和 PDF 字节不进入 Session 存储或持久缓存。[提供方测试](../../../../packages/document/document-render-libreoffice-wasm/tests/provider.spec.ts)覆盖独立输出事实、输入/输出拒绝、排队取消和活跃 Worker 终止。现有预览注册表、文件读权限和 Session 所有权决策保持有效。 diff --git a/.agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.i18n.yaml b/.agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.i18n.yaml new file mode 100644 index 0000000000..5e3c3d687d --- /dev/null +++ b/.agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-11-wasm-preview-font-and-image-budgets.md +2026-09-11-wasm-preview-font-and-image-budgets.md: ab33b0bb6e28e5749ac38f36c08bc7539a368515 +2026-09-11-wasm-preview-font-and-image-budgets.zh.md: e6ff458e8b68ce08ff5d81ce6767a08a82974a74 diff --git a/.agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.md b/.agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.md new file mode 100644 index 0000000000..ab33b0bb6e --- /dev/null +++ b/.agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.md @@ -0,0 +1,36 @@ +# Agent Note: Font reuse and image resolution in WASM previews + +Status: implemented +Archived: 2026-09-11 + +English | [中文](2026-09-11-wasm-preview-font-and-image-budgets.zh.md) + +## Problem + +Office conversion repeats font metadata parsing across documents and font matching within one document. Image-heavy documents also spend substantial PDF export time resampling images above preview resolution. These costs need separate controls because font reuse does not reduce image decoding or resampling. + +## Decision + +The [WASM provider](../../../../packages/document/document-render-libreoffice-wasm/README.md) builds a font metadata snapshot once during initialization. Each conversion Worker receives a structured clone. The Host retains names, face attributes, paths, sizes, and modification times; glyph coverage, request caches, and imported bytes belong to the Worker. Reloading the provider refreshes the snapshot. Workers validate indexed files when reading them, and an already imported font remains usable within that conversion. + +Each Worker memoizes complete requests: family, style, weight, italic, width, pitch, language, and ordered code points. The cache stores MEMFS paths and missing-family observations. Hits replay those observations because an initialization request can recur after document font collection starts. Worker termination releases both the cache and engine memory. + +Runtime PDF filter options reduce raster images to a configurable `maxImageResolution`, defaulting to 192 DPI. The [shared PDF canvas](../../../../packages/client/ui-sidebar-documentpreview/src/client/pdf/document.ts) renders at 96 CSS DPI times the device pixel ratio; the default covers ratio 2. Text and vector graphics remain scalable. Explicit bookmark export preserves LibreOfficeKit's default when JSON filter options replace its implicit filter data. + +The [local Office preview decision](2026-09-10-local-office-preview.md) continues to own conversion lifetime, authorization, missing-font presentation, and PDF transport. + +## Alternatives considered + +**Index fonts inside every conversion Worker.** This keeps discovery off the Host event loop and sees newly installed fonts immediately, but repeats full-file metadata parsing across previews. A provider snapshot removes that repeated work at the cost of synchronous initialization and explicit reload after font changes. + +**Cache only the family name.** Style, language, pitch, and missing characters can select different files. A complete request key retains those distinctions and still captures repeated layout requests. + +**Keep 300 DPI for every preview.** Higher raster resolution retains detail for high pixel ratios and magnification but increases image export work. The configurable 192-DPI default follows the common display target without rasterizing text or rebuilding the engine. + +## Consequences + +Cold provider startup includes indexing and remains separate from conversion deadlines. Shared metadata retains no original font buffers, while each Worker still reads selected fonts and lazily decodes coverage. Installing or replacing fonts requires provider reload; a file changed since indexing can fail a later conversion. + +Reducing image resolution trades raster detail at high magnification for less export work. It is not an engine memory cap: LibreOffice can still decode full-resolution source images, load large font collections, or exhaust its build-time WASM memory maximum. No persistent font cache, filesystem watcher, or cross-document engine instance is introduced. + +[Font tests](../../../../packages/document/document-render-libreoffice-wasm/tests/fonts.spec.ts) cover complete request keys, repeated missing-family observations, snapshot isolation, and failed imports. [Provider tests](../../../../packages/document/document-render-libreoffice-wasm/tests/provider.spec.ts) cover snapshot reuse and reload. The [real-engine tests](../../../../packages/document/document-render-libreoffice-wasm/tests/libreoffice-wasm.e2e.ts) inspect exported image dimensions with default and overridden DPI settings alongside selectable text and page count; ABI forwarding alone cannot establish filter behavior. diff --git a/.agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.zh.md b/.agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.zh.md new file mode 100644 index 0000000000..e6ff458e8b --- /dev/null +++ b/.agents/notes/archived/architecture/2026-09-11-wasm-preview-font-and-image-budgets.zh.md @@ -0,0 +1,36 @@ +# Agent Note: WASM 预览的字体复用与图像分辨率 + +Status: implemented +Archived: 2026-09-11 + +[English](2026-09-11-wasm-preview-font-and-image-budgets.md) | 中文 + +## 问题 + +Office 转换在不同文档间重复解析字体元数据,并在同一文档内重复匹配字体。图片较多的文档还会在 PDF 导出时花费大量时间,重采样高于预览显示分辨率的图像。这些成本需要分别控制,因为字体复用不会减少图像解码或重采样。 + +## 决策 + +[WASM 提供方](../../../../packages/document/document-render-libreoffice-wasm/README.zh.md)在初始化时建立一次字体元数据快照。每个转换 Worker 接收结构化克隆。Host 保留名称、字体面属性、路径、大小和修改时间;字形覆盖、请求缓存及导入字节属于 Worker。重新加载提供方会刷新快照。Worker 读取已索引文件时校验文件,已经导入的字体在该次转换内仍可使用。 + +每个 Worker 记忆化完整请求:字体族、样式、字重、斜体、字宽、字距类型、语言及有序码点。缓存保存 MEMFS 路径和缺失字体族观察值。命中时重放这些观察值,因为初始化请求可能在文档字体收集开始后再次出现。Worker 终止时释放缓存及引擎内存。 + +运行时 PDF 过滤器选项将栅格图像降低到可配置的 `maxImageResolution`,默认 192 DPI。[共享 PDF 画布](../../../../packages/client/ui-sidebar-documentpreview/src/client/pdf/document.ts)按 96 CSS DPI 乘以设备像素比渲染;默认值覆盖像素比 2。文本和矢量图形仍可缩放。JSON 过滤器选项替换隐含过滤器数据时,显式导出书签保留 LibreOfficeKit 的默认行为。 + +[本地 Office 预览决策](2026-09-10-local-office-preview.zh.md)继续负责转换生命周期、授权、缺失字体展示和 PDF 传输。 + +## 考虑过的替代方案 + +**在每个转换 Worker 内索引字体。** 这会将发现工作移出 Host 事件循环,并立即识别新安装的字体,但会在不同预览间重复读取完整字体文件并解析元数据。提供方快照消除重复工作,代价是同步初始化,以及字体变化后显式重新加载。 + +**只缓存字体族名称。** 样式、语言、字距类型和缺失字符可能选择不同文件。完整请求键保留这些区别,同时仍能命中重复排版请求。 + +**所有预览保持 300 DPI。** 更高栅格分辨率保留高像素比及放大时的细节,但增加图像导出工作。可配置的 192 DPI 默认值符合常见显示目标,无需栅格化文本或重新构建引擎。 + +## 影响 + +提供方冷启动包含索引成本,不计入转换时限。共享元数据不保留原始字体缓冲区,各 Worker 仍会读取选中字体并按需解码字形覆盖。安装或替换字体后需要重新加载提供方;索引后发生变化的文件可能使后续转换失败。 + +降低图像分辨率以高倍缩放下的栅格细节换取更少的导出工作。它不是引擎内存上限:LibreOffice 仍可能解码全分辨率原图、加载大型字体集合,或耗尽构建时设定的 WASM 内存上限。此实现不引入持久化字体缓存、文件系统监听器或跨文档引擎实例。 + +[字体测试](../../../../packages/document/document-render-libreoffice-wasm/tests/fonts.spec.ts)覆盖完整请求键、重复缺失字体族观察值、快照隔离和失败导入。[提供方测试](../../../../packages/document/document-render-libreoffice-wasm/tests/provider.spec.ts)覆盖快照复用和重新加载。[真实引擎测试](../../../../packages/document/document-render-libreoffice-wasm/tests/libreoffice-wasm.e2e.ts)检查默认及覆盖 DPI 设置时的导出图像尺寸、可选择文本和页数;仅验证 ABI 参数转发无法证明过滤器行为。 diff --git a/.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.i18n.yaml b/.agents/notes/archived/bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml similarity index 56% rename from .agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.i18n.yaml rename to .agents/notes/archived/bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml index 9ae953b44a..4fac2496cb 100644 --- a/.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.i18n.yaml +++ b/.agents/notes/archived/bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.md -2026-08-03-web-sticky-collapsible-headers.md: 5bbe91681e656a8278b9d65f12e20a4d9dc779d0 -2026-08-03-web-sticky-collapsible-headers.zh.md: c75846fa7f5ca1088dab530b63243553e50f9579 +# 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 diff --git a/.agents/notes/archived/bug-fix/2026-09-09-content-sized-diagram-previews.md b/.agents/notes/archived/bug-fix/2026-09-09-content-sized-diagram-previews.md new file mode 100644 index 0000000000..fec86111a6 --- /dev/null +++ b/.agents/notes/archived/bug-fix/2026-09-09-content-sized-diagram-previews.md @@ -0,0 +1,26 @@ +# Agent Note: Content-sized diagram previews + +Status: implemented +Archived: 2026-09-10 + +English | [中文](2026-09-09-content-sized-diagram-previews.zh.md) + +## Problem + +Short Graphviz and SVG images leave large empty regions when displayed inside 400px frames. A 59px Graphviz diagram occupies a 400px preview even though its rendered dimensions are already available to the browser. + +## Decision + +Mermaid, Graphviz, and SVG share the inert image canvas in `SourcePreview`. The browser derives its height from the image, preserves aspect ratio when shrinking to the available width, and adds 16px padding. Source switching retains the image and copying reads the original code. Theme changes regenerate Graphviz colors without introducing a separate sizing observer. + +This replaces the diagram-frame choice in [Static Markdown fence previews](../feature/2026-09-09-markdown-static-previews.md); that note continues to own HTML isolation, rendering lifecycle, and license obligations. HTML retains its opaque, script-free, 400px frame. SVG image mode disables scripts, link interaction, and external resource loading without inserting source-controlled markup into the application document. + +## Alternatives considered + +**Set iframe height to auto.** An iframe does not derive its outer height from its inner document. This still reserves an unrelated viewport. + +**Add a frame measurement script or same-origin access.** A diagram already has image dimensions. Extra execution or origin permissions and resize messaging are unnecessary for that content. + +## Consequences + +Short diagrams occupy only their rendered height and canvas padding. The browser geometry regression compares image and container height in light and dark modes, then verifies proportional scaling in a narrow viewport. Browser security checks include SVG scripts, event handlers, nested HTML, and external images; copy and source toggling remain covered. HTML automatic height remains outside this change because arbitrary document layout has different measurement and isolation requirements. diff --git a/.agents/notes/archived/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md b/.agents/notes/archived/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md new file mode 100644 index 0000000000..8880f12853 --- /dev/null +++ b/.agents/notes/archived/bug-fix/2026-09-09-content-sized-diagram-previews.zh.md @@ -0,0 +1,26 @@ +# Agent Note: 按内容定高的图表预览 + +Status: implemented +Archived: 2026-09-10 + +[English](2026-09-09-content-sized-diagram-previews.md) | 中文 + +## Problem + +较矮的 Graphviz 与 SVG 图片显示在 400px iframe 中时,会留下大块空白。浏览器已经知道图片的渲染尺寸,但一张 59px 高的 Graphviz 图表仍占据 400px 的预览区域。 + +## Decision + +Mermaid、Graphviz 与 SVG 共享 `SourcePreview` 的不可执行图片画布。浏览器根据图片决定高度,在可用宽度不足时按比例缩小,并添加 16px 内边距。源码切换保留图片,复制读取原始代码。主题变化重新生成 Graphviz 配色,无需单独的尺寸观察器。 + +这替代了[静态 Markdown fence 预览](../feature/2026-09-09-markdown-static-previews.zh.md)中的图表 iframe 选择;该记录继续维护 HTML 隔离、渲染生命周期与许可证义务。HTML 保留不透明来源、禁用脚本的 400px iframe。SVG 图片模式禁用脚本、链接交互与外部资源加载,无需把源码控制的标记插入应用文档。 + +## Alternatives considered + +**把 iframe 高度设为 auto。** iframe 不会根据内部文档决定外部高度,仍然会保留与内容无关的视口。 + +**添加 iframe 测量脚本或同源访问。** 图表已经具备图片尺寸,不需要额外执行权限、来源权限或尺寸消息协议。 + +## Consequences + +较矮图表只占据渲染高度与画布内边距。浏览器几何回归用例在浅色和深色下比较图片与容器高度,再验证窄视口中的等比例缩放。浏览器安全检查包含 SVG 脚本、事件处理器、嵌套 HTML 与外部图片,并保留复制和源码切换覆盖。HTML 自动高度不属于本次改动,因为任意文档布局具有不同的测量与隔离要求。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.i18n.yaml b/.agents/notes/archived/bug-fix/2026-09-11-session-controller-fork-turn-cut.i18n.yaml similarity index 67% rename from .agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.i18n.yaml rename to .agents/notes/archived/bug-fix/2026-09-11-session-controller-fork-turn-cut.i18n.yaml index 79ba95b677..212dd5d306 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.i18n.yaml +++ b/.agents/notes/archived/bug-fix/2026-09-11-session-controller-fork-turn-cut.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.md -2026-09-11-session-controller-fork-turn-cut.md: 0c8d45c861df964a62586827e80c613704ed17f0 -2026-09-11-session-controller-fork-turn-cut.zh.md: 729a42a596264ac618608d26cd52a1ad836b58db +2026-09-11-session-controller-fork-turn-cut.md: 9223acd4c0c7bbcc84a0bfeb72f3b57d4bf828e2 +2026-09-11-session-controller-fork-turn-cut.zh.md: cae022ae97896c0b43633edb310afb305d690b4a diff --git a/.agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.md b/.agents/notes/archived/bug-fix/2026-09-11-session-controller-fork-turn-cut.md similarity index 99% rename from .agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.md rename to .agents/notes/archived/bug-fix/2026-09-11-session-controller-fork-turn-cut.md index 0c8d45c861..9223acd4c0 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.md +++ b/.agents/notes/archived/bug-fix/2026-09-11-session-controller-fork-turn-cut.md @@ -1,6 +1,7 @@ # Agent Note: Session Controller forks stop at the selected turn end Status: implemented +Archived: 2026-09-16 English | [中文](2026-09-11-session-controller-fork-turn-cut.zh.md) diff --git a/.agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.zh.md b/.agents/notes/archived/bug-fix/2026-09-11-session-controller-fork-turn-cut.zh.md similarity index 96% rename from .agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.zh.md rename to .agents/notes/archived/bug-fix/2026-09-11-session-controller-fork-turn-cut.zh.md index 729a42a596..cae022ae97 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-11-session-controller-fork-turn-cut.zh.md +++ b/.agents/notes/archived/bug-fix/2026-09-11-session-controller-fork-turn-cut.zh.md @@ -1,8 +1,9 @@ # Agent Note: Session Controller 分叉截到选中轮次的结束事件 Status: implemented +Archived: 2026-09-16 -English | [中文](2026-09-11-session-controller-fork-turn-cut.md) +[English](2026-09-11-session-controller-fork-turn-cut.md) | 中文 ## Problem diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json index 0d758eec58..88573f7db2 100644 --- a/.agents/notes/archived/manifest.json +++ b/.agents/notes/archived/manifest.json @@ -364,6 +364,15 @@ "architecture/2026-09-02-protocol-specific-model-listing-discovery.i18n.yaml": "sha256:ba8a62a9fa3263de4162709cb1aaf81f6ad4feee5011d7a5d8b24eb92ad81d30", "architecture/2026-09-02-protocol-specific-model-listing-discovery.md": "sha256:9db9c47559c17f0e2931b9e646f1d2d17b590df4d5699d4e779ed6d7f65fc371", "architecture/2026-09-02-protocol-specific-model-listing-discovery.zh.md": "sha256:9a266590d49093a097bcdb5d09865757cdd07625cfaaddce1303046d580551f1", + "architecture/2026-09-09-desktop-in-place-profile.i18n.yaml": "sha256:08543070a32503373d38ca98b7ce96ee60c972302534030c5ba380f19e04fa00", + "architecture/2026-09-09-desktop-in-place-profile.md": "sha256:82bdcad48207c5aecbc9695717aaaa4c8cca90c82ca58884bfa653b60f192290", + "architecture/2026-09-09-desktop-in-place-profile.zh.md": "sha256:f105636c9160c64b12bd1822c8ec81ee744ba27c0b0ab6eeed870561ccd81ce7", + "architecture/2026-09-10-local-office-preview.i18n.yaml": "sha256:a43a2370c7434293ef98d1901a29b7b4b03f400a108c119f3ec5f2f7ca8ca3f8", + "architecture/2026-09-10-local-office-preview.md": "sha256:754cfb4dde9f1ee70e495095f12fc9e3c43af4a959250bb3a59ae44fd32fd763", + "architecture/2026-09-10-local-office-preview.zh.md": "sha256:d8fb9a65f437b585f2dfa2b4aa091634eeabf571e98e881cf7344b7574704e53", + "architecture/2026-09-11-wasm-preview-font-and-image-budgets.i18n.yaml": "sha256:af027c7dae53a181e4d73fd2309f69e9ae3c48df0d3b1dfc53cae274aa60d69e", + "architecture/2026-09-11-wasm-preview-font-and-image-budgets.md": "sha256:44dd52839cdb67644c0dbcbc4beb6f36802628c4a8d17a0aad11db1f9be649ed", + "architecture/2026-09-11-wasm-preview-font-and-image-budgets.zh.md": "sha256:8285432ee1676be8b79a05953c4b6ac4a7ca5d8e701173b3a2b533608b584d0a", "bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.i18n.yaml": "sha256:8be5b0afd8820c593e2ec254d35fb8c384267814104f6234bb9af824c5f974ba", "bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.md": "sha256:861e6130e893489271478def5f54f212a106a1580b744e6e40f56356f73c7e10", "bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.zh.md": "sha256:3549257e06f074591de580ebfaa71284c7c384fb0ffb1963c30258d7a7c26db7", @@ -739,6 +748,12 @@ "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", + "bug-fix/2026-09-11-session-controller-fork-turn-cut.i18n.yaml": "sha256:4442529ced1c8e48d30f7b50c65ff03eb92dce9815beb438cedbf29f234bce47", + "bug-fix/2026-09-11-session-controller-fork-turn-cut.md": "sha256:a996f24a896c7fc5d81e015db3eae27d8a46c853287572e815623ce35eae1b12", + "bug-fix/2026-09-11-session-controller-fork-turn-cut.zh.md": "sha256:6486f0eacf5067ef9850dc32f1c27b2109a7d4e9e3c498366f5c38742bb53ac5", "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 +1624,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", diff --git a/.agents/notes/implemented/bug-fix/2026-09-09-composer-placeholder-whitespace.i18n.yaml b/.agents/notes/archived/process/2026-09-11-independent-libreoffice-package.i18n.yaml similarity index 55% rename from .agents/notes/implemented/bug-fix/2026-09-09-composer-placeholder-whitespace.i18n.yaml rename to .agents/notes/archived/process/2026-09-11-independent-libreoffice-package.i18n.yaml index c971fb1bfd..7f17afb0a9 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-09-composer-placeholder-whitespace.i18n.yaml +++ b/.agents/notes/archived/process/2026-09-11-independent-libreoffice-package.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-09-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 diff --git a/.agents/notes/archived/process/2026-09-11-independent-libreoffice-package.md b/.agents/notes/archived/process/2026-09-11-independent-libreoffice-package.md new file mode 100644 index 0000000000..05fd8eb86f --- /dev/null +++ b/.agents/notes/archived/process/2026-09-11-independent-libreoffice-package.md @@ -0,0 +1,32 @@ +# Agent Note: Independent precompiled LibreOffice package + +Status: implemented +Archived: 2026-09-11 + +English | [中文](2026-09-11-independent-libreoffice-package.zh.md) + +## Problem + +Compiling LibreOffice inside ordinary DSH builds would require every contributor and CI job to acquire its toolchain and repeat a large native build. Desktop also needs immutable engine resources that can travel with its offline installation material. + +## Decision + +The [engine package](../../../../native/libreoffice-wasm/package.json) has its own version and an asset-only manifest export. Its source directory stays outside the main pnpm workspace. Installation has no lifecycle hook, and packaging verifies an existing successful build without compiling. The [release workflow](../../../../.github/workflows/libreoffice-wasm-release.yml) compiles only on explicit dispatch; publication requires a matching engine version tag and the protected npm environment. + +Each tarball contains the engine manifest and assets, corresponding source pins, patches, full source diff, and the built distribution's license and notices. Packaging rejects mismatched build receipts, modified assets, unlisted engine files, missing notices, and bundled fonts. A failure never triggers compilation automatically. + +The [local preview decision](../architecture/2026-09-10-local-office-preview.md) continues to own conversion and explicit artifact configuration. This change establishes release machinery; it does not add an unpublished engine dependency to DSH. The existing [Desktop package set](../../../../apps/desktop/scripts/prepare-package-set.ts) remains the intended carrier for a future fixed-version dependency and its offline seed. + +## Alternatives considered + +**Compile during ordinary builds or package installation.** This puts engine toolchain availability and compilation cost on every consumer, even when the engine version has not changed. + +**Extract a general Node conversion library.** Font handling, Worker ownership, and the conversion API can remain with the provider. Moving them is unnecessary to distribute precompiled assets or isolate compilation from DSH CI. + +**Link the engine source as a workspace dependency.** Workspace linking would replace a published precompiled package with a source directory that lacks its engine files. + +## Consequences + +The engine can be compiled and versioned independently of DSH. The package exposes files rather than a conversion API; the provider retains runtime ownership. Ordinary builds remain free of LibreOffice compilation, as checked by the [packaging tests](../../../../scripts/libreoffice-package.spec.ts). + +Initial publication and fixed-version consumer integration remain incomplete. A source tree that differs from its recorded recipe cannot produce a release tarball. Real-engine conversion, installation from the packed package, and Desktop offline seed qualification must accompany the first consumed engine release. diff --git a/.agents/notes/archived/process/2026-09-11-independent-libreoffice-package.zh.md b/.agents/notes/archived/process/2026-09-11-independent-libreoffice-package.zh.md new file mode 100644 index 0000000000..b2abf61f8a --- /dev/null +++ b/.agents/notes/archived/process/2026-09-11-independent-libreoffice-package.zh.md @@ -0,0 +1,32 @@ +# Agent Note: 独立的预编译 LibreOffice 包 + +Status: implemented +Archived: 2026-09-11 + +[English](2026-09-11-independent-libreoffice-package.md) | 中文 + +## 问题 + +在普通 DSH 构建中编译 LibreOffice,会要求每个贡献者和 CI job 获取工具链并重复执行大型本机构建。Desktop 也需要能随离线安装材料分发的不可变引擎资源。 + +## 决策 + +[引擎包](../../../../native/libreoffice-wasm/package.json)采用独立版本,只导出产物清单。源码目录不属于主 pnpm workspace。安装没有生命周期 hook,打包只校验已有的成功构建,不执行编译。[发布工作流](../../../../.github/workflows/libreoffice-wasm-release.yml)仅在明确调度时编译;发布要求匹配的引擎版本 tag 和受保护的 npm 环境。 + +每个 tarball 包含引擎清单和产物、对应的源码版本、补丁、完整源码差异,以及构建发行物的许可证和声明。打包拒绝不匹配的构建记录、被修改的产物、未列入清单的引擎文件、缺失的声明和内置字体。失败不会自动触发编译。 + +[本地预览决策](../architecture/2026-09-10-local-office-preview.zh.md)继续负责转换与显式产物配置。此改动建立发布机制,不向 DSH 添加尚未发布的引擎依赖。现有 [Desktop 包集合](../../../../apps/desktop/scripts/prepare-package-set.ts)仍是未来固定版本依赖及其离线 seed 的预期载体。 + +## 考虑过的替代方案 + +**在普通构建或包安装期间编译。** 即使引擎版本没有变化,每个消费方也要承担工具链可用性要求和编译开销。 + +**抽出通用 Node 转换库。** 字体处理、Worker 所有权和转换 API 可以继续由提供方负责。分发预编译产物及将编译与 DSH CI 隔离不需要迁移这些逻辑。 + +**将引擎源码链接为 workspace 依赖。** 工作区链接会把已发布的预编译包替换为缺少引擎文件的源码目录。 + +## 影响 + +引擎可以独立于 DSH 编译和发布版本。包暴露文件而非转换 API;提供方保留运行时所有权。[打包测试](../../../../scripts/libreoffice-package.spec.ts)检查普通构建不编译 LibreOffice。 + +首次发布和固定版本消费方接入尚未完成。与记录的配方不一致的源码树不能产生发布 tarball。首个被消费的引擎发行版必须同时完成真实引擎转换、打包安装,以及 Desktop 离线 seed 验证。 diff --git a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml index 98850aed4c..b62c09f26c 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md -2026-06-11-content-block-vocabulary.md: d7d3f6b43a3f65d1421f026e5b6c2cc1ba1eadd2 -2026-06-11-content-block-vocabulary.zh.md: ed4f915dff6dcb6dbc91400f9bfa5384253aea7b +2026-06-11-content-block-vocabulary.md: 917dd5225fdc4ab391e9e4629769abbb68095731 +2026-06-11-content-block-vocabulary.zh.md: 9d228eb34dafbdebab44b45dc3b290c9327d3b2b diff --git a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md index d7d3f6b43a..917dd5225f 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md @@ -10,7 +10,7 @@ The harness needs one internal language for messages that the loop, session log, ## Decision -Own the vocabulary: messages are arrays of typed content blocks (`text`, `reasoning`, `tool-call`, `tool-result`), with the union derived from the merge-extensible `ContentBlockMap` so plugins add block types via declaration merging. The same merge-extensible-map pattern types every "stringly" field (`MessageSource`, `FinishReason`, `TurnTrigger`, `TurnEndReason`). Streaming is a raw chunk protocol; `BlockAssembler` is the single shared assembly implementation. Adapters translate to provider wire formats — mapping cost lives in adapters, where it belongs. +Own the vocabulary: user, assistant, and system messages are arrays of typed content blocks (`text`, `reasoning`, `tool-call`), with the union derived from the merge-extensible `ContentBlockMap` so plugins add block types via declaration merging. Tool results are first-class tool-role messages whose own content holds result blocks; released Session format v3 retains the historical `tool-result` content block inside a user-role wrapper, and the adjacent V3-to-V4 edge lifts that wrapper into the first-class role. The same merge-extensible-map pattern types every "stringly" field (`MessageSource`, `FinishReason`, `TurnTrigger`, `TurnEndReason`). Streaming is a raw chunk protocol; `BlockAssembler` is the single shared assembly implementation. Adapters translate to provider wire formats — mapping cost lives in adapters, where it belongs. In-session context injection (`context/message`) and mid-turn steering originally rendered as tagged user-role envelopes (the system-reminder pattern) rather than a new role, so adapters carry zero burden. Both now project as plain user content with no wrapper; see [the injected-content-envelope Agent Note](../simplification/2026-07-20-unwrap-injected-content-envelopes.md). Live-adapter validation confirms this rendering for current DeepSeek behavior; a future provider-specific mismatch belongs in that adapter rather than a new canonical role. diff --git a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md index ed4f915dff..9d228eb34d 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md @@ -10,7 +10,7 @@ harness 需要一套统一的内部消息语言,供 agent loop(智能体循 ## 决策 -自主拥有词汇:消息是类型化内容块的数组(`text`、`reasoning`、`tool-call`、`tool-result`),其联合类型派生自可合并扩展的 `ContentBlockMap`,插件通过声明合并添加新的块类型。同一可合并扩展映射模式为所有「字符串化」字段提供类型(`MessageSource`、`FinishReason`、`TurnTrigger`、`TurnEndReason`)。流式输出采用原始分片协议;`BlockAssembler` 是唯一的共享组装实现。适配器负责转换为提供方的协议格式(wire format)——映射成本留在适配器中,正是它该在的地方。 +自主拥有词汇:user、assistant 与 system 消息是类型化内容块的数组(`text`、`reasoning`、`tool-call`),其联合类型派生自可合并扩展的 `ContentBlockMap`,插件通过声明合并添加新的块类型。Tool result 是一等 tool-role message,结果块属于该 message 自己的 content;已发布的 Session format v3 仍将历史 `tool-result` content block 保存在 user-role 包装中,邻接的 V3 到 V4 边会将该包装提升为一等角色。同一可合并扩展映射模式为所有「字符串化」字段提供类型(`MessageSource`、`FinishReason`、`TurnTrigger`、`TurnEndReason`)。流式输出采用原始分片协议;`BlockAssembler` 是唯一的共享组装实现。适配器负责转换为提供方的协议格式(wire format)——映射成本留在适配器中,正是它该在的地方。 会话内上下文注入(`context/message`)和轮次中途 steering(中途引导)最初渲染为带标签的 user-role 信封(system-reminder 模式),而非引入新角色,因此适配器无需承担额外负担。如今两者都投影为无包装的普通用户内容;见[注入内容信封 Agent Note](../simplification/2026-07-20-unwrap-injected-content-envelopes.zh.md)。实际适配器验证已确认此渲染方式符合当前 DeepSeek 的行为;如果未来某提供方出现不兼容,应在该适配器内处理,而非引入新的规范角色。 diff --git a/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.i18n.yaml index 98f54d5d26..081f618b1a 100644 --- a/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md -2026-07-20-routed-model-context-and-compaction-policy.md: 26d8a4a85c284e50b9635017f0dd0bf905a4427f -2026-07-20-routed-model-context-and-compaction-policy.zh.md: 8656200a4b25c2ca70160e8f5949a7518e390168 +2026-07-20-routed-model-context-and-compaction-policy.md: d276354cb21f45eaec1c4e3202c7be1770745efb +2026-07-20-routed-model-context-and-compaction-policy.zh.md: b6e2a0b914f78e19675d80b86d8d962e8ba8c80f diff --git a/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md b/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md index 26d8a4a85c..d276354cb2 100644 --- a/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md +++ b/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md @@ -16,7 +16,7 @@ Neither obvious configuration owner is sufficient. Compact-basic is optional and `LlmAdapter.resolveModel(provider, model, signal?)` returns aggregate metadata for one exact route, with optional `LlmModelContext` under its `context` field. `LlmRuntime.resolveModelInfo()` selects the registered route owner, validates a positive integer `contextWindow`, and returns detached metadata. The query is independent of `listModels()`: an unlisted dynamic model may have capacity metadata, and an absent `context` means only that the adapter cannot describe capacity. -The hand-rolled DeepSeek adapter accepts optional `contextWindow` on each configured model plus an adapter-wide `defaultContextWindow`. Exact model capacity wins; an entry without capacity and an unlisted pass-through id inherit the adapter default, or omit `context` when it is absent. The two built-in model entries each publish an exact 256,000-token capacity. The pi-ai adapter resolves capacity from the same catalog descriptor that authoritatively resolves the request model. +The hand-rolled DeepSeek adapter accepts optional `contextWindow` on each configured model plus an adapter-wide `defaultContextWindow`. Exact model capacity wins; an entry without capacity and an unlisted pass-through id inherit the adapter default, or omit `context` when it is absent. The built-in model entries each declare a 1,000,000-token context capacity. The pi-ai adapter resolves capacity from the same catalog descriptor that authoritatively resolves the request model. ### Token measurement remains model-agnostic @@ -26,17 +26,17 @@ The hand-rolled DeepSeek adapter accepts optional `contextWindow` on each config Compact-basic owns consumer policy. Top-level fields define defaults; `modelPolicies` contains partial overrides keyed by the exact `{ provider, model }` pair. Duplicate targets and unknown or invalid fields fail plugin load. `thresholdRatio` defaults to `0.8`, and retention defaults to `retainRatio: 0.16`; callers may use an absolute `retainTokens` instead, but the two retention forms are mutually exclusive. After inheritance, a ratio retention that is not below its threshold ratio also fails plugin load because no model capacity can make that policy valid. -For proactive pressure, compaction-basic reads the latest durable request route, resolves its adapter capacity and exact-target policy, and scales ratios into a `ResolvedCompactSpec`. It performs this resolution on every check, so a provider or model switch in one session changes capacity and policy immediately. An absolute retained budget that is not below the scaled threshold fails when the target capacity first makes that comparison possible. +For proactive pressure, compaction-basic reads the latest durable route and resolves its adapter capacity `W`, request output reservation `O`, and exact-target policy. The trigger is `floor(min(W × thresholdRatio, W − O − headroomTokens))`, with default headroom of 65,536 tokens. The window fraction limits history growth while the second cap leaves extra room after a full output for compaction input and generation; it does not bound arbitrary tool-result growth. Retention scales `W − O` independently. `O` is the effective envelope’s `maxTokens`, else the adapter’s per-request default, else zero. Resolution runs on every check, so route or output-cap changes immediately affect policy. A nonpositive pressure budget or retention at least as large as the trigger fails when target capacity makes that comparison possible. The same exact-target override can select summarization provider/model, summarization output cap, convergence retries, and overflow retry cap. These are compaction concerns and never enter an LLM provider. ### Target-specific pressure failures preserve optional composition -An adapter that lacks capacity metadata remains a valid LLM route. Manual proactive pressure fails with a target-specific configuration error; the automatic listener warns once per exact route and continues with full history. The same per-route suppression applies when resolved capacity exposes an invalid absolute retention budget, while unrelated operational failures remain independently visible. Canonical provider-confirmed overflow does not need capacity metadata: it bypasses the proactive threshold and normal retention budget, attempts one maximal balanced reduction, and preserves the original provider error unless replacement proves progress. +An adapter that lacks capacity metadata remains a valid LLM route. Manual proactive pressure fails with a target-specific configuration error; the automatic listener warns once per exact route and continues with full history. The same suppression applies when output plus headroom exhausts capacity or retention reaches the trigger, while unrelated operational failures remain independently visible. Canonical provider-confirmed overflow does not need capacity metadata: it bypasses the proactive threshold and normal retention budget, attempts one maximal balanced reduction, and preserves the original provider error unless replacement proves progress. ## Testing -Service tests cover detached context metadata, invalid adapter output, catalog independence, and default absence. Adapter tests cover DeepSeek exact/default/unlisted resolution, invalid capacities, and pi-ai exact descriptor resolution. Compact tests cover ratio scaling, exact provider/model overrides, load-time rejection of invalid merged ratios, runtime absolute-budget validation, same-model-id provider switches, target-specific warning suppression, and capacity-independent overflow recovery. Loader fixtures reject the removed token-meter capacity setting, and examples configure capacity on adapters. +Service tests cover detached context metadata, invalid adapter output, catalog independence, and default absence. Adapter tests cover DeepSeek exact/default/unlisted resolution, invalid capacities, and pi-ai exact descriptor resolution. Compact tests cover both trigger caps, default and overridden headroom, envelope output caps and adapter fallback, independent retention scaling, invalid budgets, exact-target overrides, provider switches, warning suppression, and capacity-independent overflow recovery. The keyless `compaction-output-reserve` headless scenario records a nonzero request cap, proactive compaction before the next step, and replacement history. Loader fixtures reject the removed token-meter capacity setting, and examples configure capacity on adapters. ## Alternatives considered @@ -52,6 +52,6 @@ Service tests cover detached context metadata, invalid adapter output, catalog i - The same compaction-basic instance safely handles different windows, provider switches, and identical model ids under different providers without consulting discovery metadata. - LLM-only and meter-only compositions remain valid; loading compaction-basic adds no reverse dependency from adapters. - DeepSeek deployments may set exact per-model capacities, or use `defaultContextWindow` for entries without capacity and unlisted pass-through ids. -- Ratio defaults scale naturally across models, while exact-target absolute retention remains available for deployment-specific behavior. +- Ratio defaults scale naturally across models and exclude the output tokens each request reserves, while exact-target absolute retention remains available for deployment-specific behavior. This note supersedes the global-capacity and no-model-policy parts of the [replay token meter service Agent Note](../../archived/architecture/2026-07-15-replay-token-meter-service.md). Its single-fold measurement decision remains unchanged. diff --git a/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.zh.md b/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.zh.md index 8656200a4b..b6e2a0b914 100644 --- a/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.zh.md @@ -16,7 +16,7 @@ Status: implemented `LlmAdapter.resolveModel(provider, model, signal?)` 返回一条精确路由的聚合元数据,其中可选的 `LlmModelContext` 位于 `context` 字段下。`LlmRuntime.resolveModelInfo()` 选择已注册的路由所属方,验证 `contextWindow` 为正整数,并返回与适配器内部状态分离的元数据。该查询独立于 `listModels()`:不在目录中的动态模型也可以拥有容量元数据,而缺少 `context` 只表示适配器无法描述容量。 -手写 DeepSeek 适配器允许每个已配置模型提供可选 `contextWindow`,并支持适配器级 `defaultContextWindow`。精确模型容量优先;未提供容量的模型项与未列出的透传 id 会继承适配器默认值,若默认值也不存在则省略 `context`。两个内置模型项都公开精确的 256,000 token 容量。pi-ai 适配器从同一个目录描述符解析容量,该描述符也用于权威解析请求模型。 +手写 DeepSeek 适配器允许每个已配置模型提供可选 `contextWindow`,并支持适配器级 `defaultContextWindow`。精确模型容量优先;未提供容量的模型项与未列出的透传 id 会继承适配器默认值,若默认值也不存在则省略 `context`。内置模型项各自声明 1,000,000 token 的上下文容量。pi-ai 适配器从同一个目录描述符解析容量,该描述符也用于权威解析请求模型。 ### Token 计量保持模型无关 @@ -26,17 +26,17 @@ Status: implemented Compact-basic 拥有消费方策略。顶层字段定义默认值;`modelPolicies` 包含以精确 `{ provider, model }` 组合为键的部分覆盖。重复目标、未知字段或无效字段都会让插件加载失败。`thresholdRatio` 默认为 `0.8`,保留策略默认为 `retainRatio: 0.16`;调用方也可以改用绝对 `retainTokens`,但两种保留形式互斥。完成继承后,如果保留比例不小于阈值比例,插件也会加载失败,因为任何模型容量都无法让该策略有效。 -对于主动压力检查,compaction-basic 读取最新持久请求路由,解析其适配器容量与精确目标策略,再把比例缩放为 `ResolvedCompactSpec`。每次检查都会重新解析,因此同一会话切换提供方或模型后,容量与策略会立即变化。若绝对保留预算不小于缩放后的阈值,系统会在目标容量首次允许比较两者时失败。 +对于主动压力检查,compaction-basic 读取最新持久路由,解析适配器容量 `W`、请求输出预留 `O` 和精确目标策略。触发阈值为 `floor(min(W × thresholdRatio, W − O − headroomTokens))`,默认余量为 65,536 tokens。窗口比例限制历史增长,第二项则在一次完整输出后为压缩输入与生成留出额外空间;它无法限制任意工具结果的增长。保留预算独立按 `W − O` 缩放。`O` 取自生效信封的 `maxTokens`,否则取适配器按请求的默认值,再回退到零。每次检查都会重新解析,因此路由或输出上限变化会立即影响策略。若压力预算非正,或保留预算不低于触发阈值,会在目标容量允许比较时失败。 同一精确目标覆盖还可以选择摘要提供方/模型、摘要输出上限、收敛重试次数与溢出重试上限。这些都属于压缩问题,不会进入任何 LLM 提供方。 ### 目标专用压力错误仍保留可选组合 -缺少容量元数据的适配器仍是有效 LLM 路由。手动主动压力检查会返回目标专用配置错误;自动监听器按精确路由只警告一次,并继续保留完整历史。当已解析容量暴露出无效的绝对保留预算时,系统也按路由抑制重复警告;其他运行故障仍会各自对外可见。提供方已经确认的规范化溢出不需要容量元数据:它绕过主动阈值与普通保留预算,尝试一次最大且平衡的缩减,并在替换无法证明进展时保留原始提供方错误。 +缺少容量元数据的适配器仍是有效 LLM 路由。手动主动压力检查会返回目标专用配置错误;自动监听器按精确路由只警告一次,并继续保留完整历史。当输出预留与余量耗尽容量,或保留预算达到触发阈值时,也会抑制重复警告;其他运行故障仍会各自对外可见。提供方已经确认的规范化溢出不需要容量元数据:它绕过主动阈值与普通保留预算,尝试一次最大且平衡的缩减,并在替换无法证明进展时保留原始提供方错误。 ## 测试 -服务测试覆盖与适配器内部状态分离的上下文元数据、无效适配器输出、目录独立性与默认缺失行为。适配器测试覆盖 DeepSeek 的精确容量、默认容量、未列出模型解析及无效容量,以及 pi-ai 的精确描述符解析。压缩测试覆盖比例缩放、精确提供方/模型覆盖、加载期拒绝无效合并比例、运行时校验绝对预算、相同模型 id 的提供方切换、目标专用警告抑制与不依赖容量的溢出恢复。Loader fixture(测试前置数据)会拒绝已经移除的 token-meter 容量设置,示例则在适配器上配置容量。 +服务测试覆盖与适配器内部状态分离的上下文元数据、无效适配器输出、目录独立性与默认缺失行为。适配器测试覆盖 DeepSeek 的精确容量、默认容量、未列出模型解析及无效容量,以及 pi-ai 的精确描述符解析。压缩测试覆盖两项阈值上限、默认和覆盖余量、信封输出上限与适配器回退、独立保留预算缩放、无效预算、精确目标覆盖、提供方切换、警告抑制和不依赖容量的溢出恢复。无需密钥的 `compaction-output-reserve` headless 场景记录非零请求上限、下一步之前的主动压缩和替换后的历史。Loader fixture(测试前置数据)会拒绝已经移除的 token-meter 容量设置,示例则在适配器上配置容量。 ## 考虑过的替代方案 @@ -52,6 +52,6 @@ Compact-basic 拥有消费方策略。顶层字段定义默认值;`modelPolici - 同一个 compaction-basic 实例无需查询发现元数据,就能安全处理不同窗口、提供方切换,以及不同提供方下的相同模型 id。 - 仅 LLM 与仅 meter 的组合仍然有效;加载 compaction-basic 不会让适配器产生反向依赖。 - DeepSeek 部署可以设置精确的逐模型容量,也可以让未提供容量的模型项与未列出的透传 id 使用 `defaultContextWindow`。 -- 比例默认值会随模型自然缩放,同时仍可按精确目标使用绝对保留值,以满足部署专用行为。 +- 比例默认值会随模型自然缩放,并扣除每次请求预留的输出 token;同时仍可按精确目标使用绝对保留值,以满足部署专用行为。 本记录取代[回放式 token 计量服务 Agent Note](../../archived/architecture/2026-07-15-replay-token-meter-service.md) 中的全局容量与无模型策略部分,单折叠计量决策保持不变。 diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml index a7b0ac458f..e891710dea 100644 --- a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md -2026-07-25-web-client-session-scope-and-provide-channel.md: 6126d06e48111a551a0dc57247659a3086986c66 -2026-07-25-web-client-session-scope-and-provide-channel.zh.md: e7bf4aa6952b4fd7b187c91e2ac5d64aac32cabb +2026-07-25-web-client-session-scope-and-provide-channel.md: 990a9b6eef862a7ac4a549358beae7f9081eaa0b +2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 5c90611fab36611766bb407138c4f0c8a6b3a01b diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md index 6126d06e48..990a9b6eef 100644 --- a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md +++ b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md @@ -63,21 +63,21 @@ Session instances share the scope's lifecycle, while the catalog reports discove A session "materialized but with no first prompt" is governed by the summary-derived bit `blank` (a derived column, not a header field; SessionHeader stays immutable): -- The host criterion: `session.seq === 0` (zero log events = no user message yet). A live session reads `summarize()` straight from memory; a cold session is always `false` — the JSONL provider's lazy-create contract guarantees a never-appended session never enters `persistence.list()`, so blank never touches disk. +- The Host criterion is absence of `turn/start`; slash-command and configuration events do not clear `blank`. Live summaries use the Session projection. Cold summaries use cached metadata, leaving cache misses visible as unknown. Reuse acquires the writer under [blank Session acquisition](2026-09-17-process-local-blank-sessions.md). - The wire carries it in two places: the required `SessionSummary.blank` column, and the required `blank` field on the `host/session-added` frame (always true at creation, letting other tabs enter the same blank-session state into their mirrors). - The client mirror only lowers, never raises (monotonic), flipped from three sources, all reusing existing wire signals: - 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 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. +- Blank Sessions can persist through creation checkpoints and command events. Browser reloads and Host restarts reuse eligible blanks after acquiring their writer; occupied blanks are skipped. Old logs remain on disk without automatic cleanup. ### connectWorkspace: the sole entry point of New Session `workspaces.connectWorkspace(workspaceId): Promise` (owned by WorkspaceRuntime — it holds both the workspace canonical path and the sessions reference): -- 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. +- 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 is adopted with explicit-id `session.create`, which acquires and retains the writer before returning. Only writer contention skips a candidate. +- The create arm: when no candidate can be acquired, `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. 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. @@ -135,6 +135,6 @@ Blank Sessions retain the header's leading and corner slots so navigation contro - Plugins gain session context isomorphic to the host's: per-session state hangs on the actx and mounts/tears down in one piece with the scope fiber, making leaks structurally impossible; two-session isolation is structurally guaranteed by the scope filter. - The client object layer converges to a wire mirror: session identity, lifecycle, and capability adjudication all defer to the host entity — the input system (the next layer) always faces a session with a real Agent, and providers like slash/skill uniformly address by sessionId directly. -- Blank-session governance takes zero dedicated mechanisms: state rides one derived bit, visibility rides the unified list projection (only the current blank shows, as `New Session`), reclamation rides lazy persistence's existing contract (evaporation on restart), and the ordinary ceiling rides same-Workspace reuse. +- Blank-session governance takes zero dedicated mechanisms: state rides one derived bit, visibility rides the unified list projection (only the current blank shows, as `New Session`), reuse acquires the persisted writer, and the ordinary ceiling rides same-Workspace reuse. - The cost: the id→ctx handoff discipline and provide's Concurrent discipline are conventions rather than type-enforced, pinned by review and tests. The single state axis still withholds machine faces until a Session exists; the [resident conversation shell](../../../../packages/client/ui-conversation/README.md) routes activation to the Workspace picker during that interval. - Known gaps: approval/question recovery across prune (TODO); model selection returns in live-mutation shape (the host `selectModel` trio is ready-made, its client consumer not yet built). diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md index e7bf4aa695..5c90611fab 100644 --- a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md @@ -63,21 +63,21 @@ Session 实例与 scope 同生命周期;catalog 只报告可发现性,不持 「实体化但无首条提示词」的会话经 summary 派生位 `blank` 治理(派生列而非 header 字段,SessionHeader 保持不可变): -- host 判据:`session.seq === 0`(零日志事件 = 尚无用户消息)。live 会话 `summarize()` 内存直读;cold 会话恒 `false`——JSONL provider 的 lazy-create 约定保证 never-appended 会话不进入 `persistence.list()`,所以 blank 从不落盘。 +- Host 判据是没有 `turn/start`;slash 命令与配置事件不会清除 `blank`。Live summary 使用 Session 投影。Cold summary 使用缓存元数据,缓存缺失时以未知状态保留在列表中。复用按[空白 Session 获取](2026-09-17-process-local-blank-sessions.zh.md)规则取得写锁。 - wire 承载两处:`SessionSummary.blank` 必填列;`host/session-added` 帧必填 `blank` 字段(创建时恒 true,供别的 tab 按同一空会话状态入镜像)。 - client 镜像只降不升(单调),三来源翻转,全部复用既有 wire 信号: - 发送方本地:首次 `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 会话只显示由 `mainView` 来源持有的一行,并强制标题为 `New Session`。切换 Workspace 后,旧 blank 实体仍在镜像中但从列表隐藏,目标 Workspace 的主 blank 显示;因此用户可见面全局至多一条 blank 行。 -- 残留账零 GC:刷新后 blank 会话带位回来,下次同 workspace 且仍为成员时复用,普通单端路径使每个 workspace 至多保留一个;host 重启后 blank 无盘痕自然蒸发;多 tab 竞态多出的空壳只会成为非 current 隐藏行,后续复用消化,不做协调。 +- 空白 Session 可以通过创建检查点与命令事件持久化。浏览器刷新与 Host 重启会在取得写锁后复用符合条件的空白会话,并跳过被占用的候选。旧日志保留在磁盘上,不自动清理。 ### connectWorkspace:New Session 的唯一入口 `workspaces.connectWorkspace(workspaceId): Promise`(归属 WorkspaceRuntime——它同时持有 workspace 规范 path 与 sessions 引用): -- 复用臂: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。 +- 复用臂: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` 收养,在返回前取得并持续持有写锁。仅写锁争用会跳过候选。 +- 新建臂:没有可获取的候选时,`session.create({workspaceId})`,返回新 id。 - 未知 workspaceId fail loud(不静默创建到别处)。 - 解析保证(两臂同约定):promise resolve 时返回的 id 已在 list store。视图 owner 随后同步 retain,因此 draft 搬运方可以在历史就绪前通过该 binding 写入文本,无需等待 notifier flush。 - 调用方拿 id 安装一份 `mainView` reference;首条提示词发送就是普通 `session.prompt`——Session 本来就在,失败即普通提示词失败,draft 文本还在 machine 里,重试即再次发送。 @@ -135,6 +135,6 @@ blank Session 保留 header 的 leading 与 corner slot,让右侧栏展开入 - 插件获得与 host 同构的会话上下文:逐会话状态挂 actx、随 scope fiber 一次拆装,泄漏结构性不可能;双会话隔离由 scope filter 结构性保证。 - client 对象层收敛为 wire 镜像:会话身份、生命周期、能力判别全部以 host 实体为准——输入体系(下一层)面对的永远是「有真 Agent 的会话」,slash/skill 等提供方一律以 sessionId 直接寻址。 -- 空会话治理零专用机制:状态靠一个派生位,可见性靠统一列表投影(仅 current blank 以 `New Session` 展示),回收靠 lazy persistence 的既有约定(重启蒸发),常规上限靠同 Workspace 复用。 +- 空会话治理零专用机制:状态靠一个派生位,可见性靠统一列表投影(仅 current blank 以 `New Session` 展示),复用时获取持久化写锁,常规上限靠同 Workspace 复用。 - 代价:id→ctx 换乘纪律、provide 的 Concurrent 纪律都是约定而非类型强制,靠 review 与测试钉住。单一状态轴仍会在 Session 存在前隐藏 machine face;这段时间内,[常驻会话壳](../../../../packages/client/ui-conversation/README.zh.md)会把激活操作转到 Workspace picker。 - 已知欠账:approval/question 跨 prune 恢复(TODO);模型选择以 live-mutation 形状回归(host `selectModel` 三件套现成,其 client 消费方尚未构建)。 diff --git a/.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.i18n.yaml index 1b91468214..56d5cea939 100644 --- a/.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.md -2026-07-28-identified-immutable-message-values.md: 9d2b22a5a1fc87ea7b0ff8eb24b2e8c4b86543e1 -2026-07-28-identified-immutable-message-values.zh.md: 6b36cfec0a24196ca526b0ffb21180967aa640eb +2026-07-28-identified-immutable-message-values.md: 73c9c14479117cf654d10b3da23d155cca77054d +2026-07-28-identified-immutable-message-values.zh.md: 7a5b9d6524beff01ac8a9e2020f75093271b7ab3 diff --git a/.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.md b/.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.md index 9d2b22a5a1..73c9c14479 100644 --- a/.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.md +++ b/.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.md @@ -16,7 +16,7 @@ This made identity a routing side effect rather than a message invariant. Produc `createMessage(input)` is the canonical role-generic creation boundary. It mints a `MessageId`, detaches the supplied role, content, and source, and deep-freezes the complete value before returning it. `createUserMessage({ content, source })` fixes the user role for prompt and context producers. `createAssistantMessage({ content, source })` fixes both the assistant role and the model source kind, so model-output producers supply only content plus provider, model, and optional replay state. All creation helpers exclude an input id so callers cannot accidentally present creation as import. `freezeMessage(message)` is the separate import or transformation boundary: it detaches and deep-freezes a message whose identity already exists, without minting a replacement. -The message helpers live in `dsh-llm` beside the base message vocabulary because their complete contracts depend only on that vocabulary. They use `dsh-brand`'s stateless `brandString()` constructor for `MessageId` and `dsh-util-values`'s shared `deepFreeze()` implementation after detaching input with `structuredClone()`. `createToolResultMessage()` belongs with the other creation helpers: it couples a tool call id to the exact user-role tool-result block and source without depending on session state or events. `dsh-session` consumes complete messages rather than owning their construction. +The message helpers live in `dsh-llm` beside the base message vocabulary because their complete contracts depend only on that vocabulary. They use `dsh-brand`'s stateless `brandString()` constructor for `MessageId` and `dsh-util-values`'s shared `deepFreeze()` implementation after detaching input with `structuredClone()`. `createToolResultMessage()` belongs with the other creation helpers: it couples a tool call id to a tool-role message carrying the result content, error state, and source without depending on session state or events. `dsh-session` consumes complete messages rather than owning their construction. The `Agent` interface accepts a complete `UserMessage` through `followup`, `steer`, and `inject`. These operations never allocate or return identity; they freeze an imported value whose id the caller already holds. Inbox claims and `agent/pre-step` receive that message directly. A content rewrite creates a frozen replacement with the same id, while an additional context is a separately created `UserMessage` with its own id. diff --git a/.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.zh.md b/.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.zh.md index 6b36cfec0a..7a5b9d6524 100644 --- a/.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.zh.md @@ -16,7 +16,7 @@ harness 曾存在多种形似消息的表示,各自采用不同的标识规则 `createMessage(input)` 是角色通用的规范创建边界。它会生成 `MessageId`,将传入的角色、内容和来源与调用方对象解除引用关系,并在返回完整值前将其深度冻结。`createUserMessage({ content, source })` 为提示词和上下文生产方固定 user 角色。`createAssistantMessage({ content, source })` 同时固定 assistant 角色与模型来源类别,因此模型输出生产方只需提供内容,以及提供方、模型和可选的回放状态。所有创建辅助函数的输入都不包含 id,因此调用方不会意外地把新消息的创建伪装成已有消息的导入。`freezeMessage(message)` 是独立的导入或转换边界:它会将已有标识的消息与调用方对象解除引用关系并深度冻结,不会生成替代标识。 -消息辅助函数位于基础消息词汇旁的 `dsh-llm` 中,因为它们的完整约定只依赖该词汇。它们使用 `dsh-brand` 的无状态 `brandString()` 构造函数生成 `MessageId`,并在通过 `structuredClone()` 分离输入后使用 `dsh-util-values` 的共享 `deepFreeze()` 实现。`createToolResultMessage()` 与其他创建辅助函数同属此处:它将工具调用 id 与确切的 user-role 工具结果块及来源耦合起来,不依赖会话状态或事件。`dsh-session` 只消费完整消息,不负责构造它们。 +消息辅助函数位于基础消息词汇旁的 `dsh-llm` 中,因为它们的完整约定只依赖该词汇。它们使用 `dsh-brand` 的无状态 `brandString()` 构造函数生成 `MessageId`,并在通过 `structuredClone()` 分离输入后使用 `dsh-util-values` 的共享 `deepFreeze()` 实现。`createToolResultMessage()` 与其他创建辅助函数同属此处:它将工具调用 id 与携带结果内容、错误状态及来源的 tool-role 消息耦合起来,不依赖会话状态或事件。`dsh-session` 只消费完整消息,不负责构造它们。 `Agent` 接口通过 `followup`、`steer` 和 `inject` 接收完整的 `UserMessage`。这些操作绝不会分配或返回标识;它们会冻结导入的值,而调用方已经持有该值的 id。inbox 领取和 `agent/pre-step` 会直接接收该消息。改写内容时会创建具有相同 id 的冻结替代值,而每个附加上下文都是单独创建的 `UserMessage`,拥有自己的 id。 diff --git a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml index f5d261079d..cabffaebf6 100644 --- a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md -2026-07-29-dsh-source-launch-tsx-esm.md: 5401b0257faa69eaf76b11e7b51d06afd7ec2b8c -2026-07-29-dsh-source-launch-tsx-esm.zh.md: f32fa3ece91bf7b9829d7012ceb090cabaa833a9 +2026-07-29-dsh-source-launch-tsx-esm.md: 700577cb9b137d67f20baeb639f99f4c0877011d +2026-07-29-dsh-source-launch-tsx-esm.zh.md: 4d322d3dcf49442ee62f82203f17bd2ab16467fe diff --git a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md index 5401b0257f..700577cb9b 100644 --- a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md +++ b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md @@ -14,11 +14,13 @@ Startup latency also mattered: the off-thread `module.register()` hooks worker s ## Decision -The `dsh` TUI, Web, and headless source launches run `node --import tsx/esm`: tsx's ESM-only hook owns both TypeScript transformation and tsconfig `paths` projection. The root `dsh` script uses that vector directly from the repository root; artifact generation is a separate operation under the [source-launch/build separation decision](../../archived/simplification/2026-08-12-separate-source-launch-from-build.md). The CJS hook stays off because the CLI source graph is ESM-only; measured runtime launch to the TUI banner is ~0.7s versus ~1.1s under the full tsx default and ~0.75s under the removed native chain. +The `dsh` CLI source launches run `node --import tsx/esm`: tsx's ESM-only hook owns both TypeScript transformation and tsconfig `paths` projection. The root `dsh` script uses that vector directly from the repository root; artifact generation is a separate operation under the [source-launch/build separation decision](../../archived/simplification/2026-08-12-separate-source-launch-from-build.md). The CJS hook stays off because the CLI source graph is ESM-only; the implementation-time measurement to the TUI banner was ~0.7s versus ~1.1s under the full tsx default and ~0.75s under the removed native chain. -`scripts/tspath-loader.ts` and `apps/cli/src/tsconfig-paths-loader.ts` are deleted. With them went the loader's runtime rule of mapping a workspace import only for declared runtime dependencies — tsx applies the `paths` map unconditionally. Declaration completeness now rests on the static gates alone: `verify-cordis-config` for configured bare plugins, and workspace constraints for manifests. (That runtime rule found real bugs: `dsh-plan-mode` and `dsh-tool-jobs` imported `@deepseek-ai/dsh-llm` while declaring it only in devDependencies; since fixed.) +tsx owns workspace `paths` mapping without checking whether the importer declares each package as a runtime dependency. Declaration completeness rests on the static gates: `verify-cordis-config` for configured bare plugins, and workspace constraints for manifests. The removed repo-owned paths loaders enforced those declarations at runtime and caught imports of `@deepseek-ai/dsh-llm` declared only in devDependencies; tsx does not provide that check. -The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`apps/cli/tests/source-launch.compat.spec.ts`): a keyless piped-stdio launch of the exact production runtime vector asserting the non-zero-exit TTY refusal. Any future Node change to module hooks or TypeScript handling turns this gate red instead of breaking developers' `pnpm dsh`. +tsx skips the paths map when the importer URL contains `/node_modules/`. The [runtime resolution](2026-09-09-profile-resolution-generations.md) therefore records real declaring-package anchors, including recursive dependencies. This lets workspace fallback imports resolve to `src/` consistently, rather than mixing built `lib/` providers with source callers whose module-local Symbols differ. Packages without a workspace mapping retain ordinary export resolution. + +The node-compat CI matrix runs `dsh-source-launch-smoke` (`apps/cli/tests/source-launch.compat.spec.ts`) without a build: keyless launches assert the required-profile diagnostic and profile dependency resolution without mixed Tools/AgentLoop `src/` and `lib/` instances. The source and built-bin suites share ordinary-directory and npm-link profile cases; the latter launches plain Node against built exports. The required build-backed smoke also runs `apps/cli/tests/profiles/headless/tests/source-tool.built.e2e.ts`: it checks real headless tool dispatch through tsx after the native addon and generated typert contributors are available, while still requiring core workspace modules to load from source. Future changes to module hooks or TypeScript handling are checked through these real entry paths. ## Alternatives considered @@ -35,4 +37,4 @@ The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (` - One launch vector across the whole engines range, including future Node lines that change native TypeScript support; the smoke gate enforces it per matrix line. - TypeScript transformation is delegated to tsx/esbuild again, reversing the prior note's goal of proving Node-native transformation; that goal is unreachable while vendored sources use non-erasable syntax and Node ships no transform mode. - The runtime declared-dependency enforcement in source launches is gone; undeclared workspace imports now surface only through static gates or built-mode resolution failures. -- Runtime launch improves ~0.4s over the full tsx default; ACP keeps `--import tsx` because its graph was not audited for CJS-hook dependence and its launch latency is not on the interactive path. +- Every CLI source profile uses the same ESM-only hook, including ACP. Workspace runtime entries reached by this launch must expose ESM exports; the launcher does not install a CJS TypeScript transform hook. diff --git a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md index f32fa3ece9..4d322d3dcf 100644 --- a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md @@ -14,11 +14,13 @@ Status: implemented ## 决策 -`dsh` 的 TUI、Web 与无头源码启动运行 `node --import tsx/esm`:由 tsx 的 ESM-only 钩子同时负责 TypeScript 转换与 tsconfig `paths` 投影。根目录的 `dsh` 脚本直接从仓库根目录使用同一启动方式;产物生成是独立操作,由[源码启动与构建分离决策](../../archived/simplification/2026-08-12-separate-source-launch-from-build.md)规定。CJS 钩子保持关闭,因为 CLI(命令行界面)源码图是纯 ESM;实测运行时启动至 TUI banner 耗时约 0.7s,对比完整 tsx 默认形态约 1.1s、已移除的原生链约 0.75s。 +`dsh` CLI(命令行界面)源码启动运行 `node --import tsx/esm`:由 tsx 的 ESM-only 钩子同时负责 TypeScript 转换与 tsconfig `paths` 投影。根目录的 `dsh` 脚本直接从仓库根目录使用同一启动方式;产物生成是独立操作,由[源码启动与构建分离决策](../../archived/simplification/2026-08-12-separate-source-launch-from-build.md)规定。CJS 钩子保持关闭,因为 CLI 源码图是纯 ESM;实现时测得启动至 TUI banner 耗时约 0.7s,对比完整 tsx 默认形态约 1.1s、已移除的原生链约 0.75s。 -`scripts/tspath-loader.ts` 与 `apps/cli/src/tsconfig-paths-loader.ts` 已删除。随之消失的还有该 loader「仅为已声明运行时依赖映射 workspace import」的运行时规则——tsx 无条件应用 `paths` 映射。声明完整性现在仅由静态门禁保障:配置的裸插件走 `verify-cordis-config`,manifest(元数据清单)走 workspace constraints。(该运行时规则确实发现过真实缺陷:`dsh-plan-mode` 与 `dsh-tool-jobs` 导入 `@deepseek-ai/dsh-llm` 却只声明在 devDependencies;后已修复。) +tsx 负责 workspace `paths` 映射,不检查导入方是否把每个包声明为运行时依赖。声明完整性由静态门禁保障:配置的裸插件走 `verify-cordis-config`,manifest(元数据清单)走 workspace constraints。已移除的仓库自有 paths loader 曾在运行时强制这些声明,并发现过将 `@deepseek-ai/dsh-llm` 仅声明在 devDependencies 中的 import;tsx 不提供这项检查。 -node-compat CI 矩阵(Node 22.19 与 26)新增 `dsh-source-launch-smoke`(`apps/cli/tests/source-launch.compat.spec.ts`):以精确的生产运行时启动向量做 keyless 管道 stdio 启动,断言进程会因 TTY 拒绝而以非零状态退出。未来 Node 对模块钩子或 TypeScript 处理的任何改动都会让该门禁变红,而不是破坏开发者的 `pnpm dsh`。 +导入方 URL 含有 `/node_modules/` 时,tsx 会跳过 paths 映射。因此,[runtime resolution](2026-09-09-profile-resolution-generations.zh.md)记录声明包的真实锚点,也覆盖递归依赖。这让 workspace fallback import 一致解析到 `src/`,避免混用构建后的 `lib/` 提供方与模块本地 Symbol 不同的源码调用方。没有 workspace 映射的包仍使用普通导出解析。 + +node-compat CI 矩阵在不执行 build 的情况下运行 `dsh-source-launch-smoke`(`apps/cli/tests/source-launch.compat.spec.ts`):无密钥启动断言必需 profile 的诊断,以及不混用 Tools/AgentLoop `src/` 与 `lib/` 实例的 profile 依赖解析。源码与构建入口测试共享普通目录和 npm-link profile 用例;后者通过普通 Node 加载构建后的导出。必跑的 build-backed smoke 还运行 `apps/cli/tests/profiles/headless/tests/source-tool.built.e2e.ts`:在 native addon 和生成的 typert 贡献文件就绪后,通过 tsx 检查真实 headless 工具派发,同时仍要求核心 workspace 模块从源码加载。模块钩子或 TypeScript 处理的后续变化由这些真实入口检查。 ## 备选方案 @@ -35,4 +37,4 @@ node-compat CI 矩阵(Node 22.19 与 26)新增 `dsh-source-launch-smoke`(` - 整个 engines 范围(包括未来改变原生 TypeScript 支持的 Node 版本线)只有一个启动向量;冒烟门禁按矩阵行强制执行。 - TypeScript 转换重新委托给 tsx/esbuild,逆转了前一篇 Agent Note「证明 Node 原生转换可用」的目标;在 vendor 源码使用不可擦除语法且 Node 不再提供 transform 模式的情况下,该目标不可达。 - 源码启动中的运行时依赖声明强制不复存在;未声明的 workspace import 现在只能通过静态门禁或构建模式的解析失败暴露。 -- 运行时启动相比完整 tsx 默认形态快约 0.4s;ACP(Agent Client Protocol)保留 `--import tsx`,因为它的依赖图尚未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上。 +- 每个 CLI 源码 profile 都使用相同的 ESM-only 钩子,包括 ACP(Agent Client Protocol)。该启动方式访问的 workspace 运行时入口必须提供 ESM 导出;启动器不安装 CJS TypeScript 转换钩子。 diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml index 4ebcbc33d9..c58f37c2b3 100644 --- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md -2026-08-05-profile-plugin-bundles.md: 48786a9c1ccaceb5f16c9eefed01e556cf759e6d -2026-08-05-profile-plugin-bundles.zh.md: c88cbf98e4276549a6fae6a5b1253d3d838a64ca +2026-08-05-profile-plugin-bundles.md: 0607f1c9a2414cc16515967b4520da8a325853fd +2026-08-05-profile-plugin-bundles.zh.md: 9d25d3bea6337c472d161c721e44a66dca8470c9 diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md index 48786a9c1c..0607f1c9a2 100644 --- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md +++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md @@ -14,14 +14,14 @@ Everything becomes a **profile**: a directory `$DSH_HOME/profiles/` with a The default Profile templates use `@deepseek-ai/dsh-base` as the shared core for `web`, `headless`, `sdk`, and `acp`, with one mode bundle above it. The [standalone `sdk-minimal` profile](../../../../packages/bundle/sdk-minimal/README.md) instead lists one bundle that owns its complete explicit tree. Generic `dsh --profile ` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, headless owns its task positional, and the protocol profiles accept no app options. Patch overlays use launcher-owned `--patch`. A new, non-shipped target can use `--from-default-profile