diff --git a/.agents/notes/AGENTS.md b/.agents/notes/AGENTS.md
index 958aff54fc..e8e1a0ef66 100644
--- a/.agents/notes/AGENTS.md
+++ b/.agents/notes/AGENTS.md
@@ -1,3 +1,7 @@
# AGENTS.md — Agent Notes
Agent Notes are effectively RFCs written by agents: durable proposals and decision records that preserve rationale, alternatives, consequences, and verification contracts. Follow the [documentation standard](../../docs/AGENTS.md) and the [Agent Note contract](README.md).
+
+**Every new Agent Note triggers a supersession check.** Search the active tree for older notes covering the same decision or mechanism, classify any full or partial supersession with [`dsh-archive-agent-notes`](../skills/dsh-archive-agent-notes/SKILL.md), and archive every qualifying implemented triplet in the same PR. Keep partial supersessions active and cross-linked.
+
+Files under [`archived/`](archived/AGENTS.md) are frozen historical snapshots: never edit them or treat them as current authority.
diff --git a/.agents/notes/README.i18n.yaml b/.agents/notes/README.i18n.yaml
new file mode 100644
index 0000000000..6c7c2c635b
--- /dev/null
+++ b/.agents/notes/README.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
+README.md: 3cfbb5154713046846a3bfcb2ccea62c0e4cb6c0
+README.zh.md: ddecac79519219c4a76cf9ba19edea312eea9d0d
diff --git a/.agents/notes/README.md b/.agents/notes/README.md
index e62dc18954..3cfbb51547 100644
--- a/.agents/notes/README.md
+++ b/.agents/notes/README.md
@@ -1,5 +1,7 @@
# Agent Notes
+English | [中文](README.zh.md)
+
One kind of design doc lives here. An **Agent Note** records a decision or proposal that shapes this codebase — the *why* and *what we gave up*, the parts code and docs can't carry. This file is the front door and contract: where Agent Notes live, when to write one, and [the in-file format](#the-file-format).
## Layout and naming
@@ -9,12 +11,12 @@ Every Agent Note has two axes, both encoded in its **path** — `{lifecycle}/{cl
- **Lifecycle** (the top-level folder) is the Agent Note's status, and an Agent Note moves between folders as that status changes:
- **`proposed/`** — proposals reviewed before implementation; not yet built (or only partly).
- **`implemented/`** — the decision shipped. The file records what was decided and what was rejected, and is **kept current with what actually shipped**: when the code later moves a file, renames a package, or changes a key/default, the Agent Note is updated in the same change to match (facts only — paths, names, structure — not the decision itself). See [implemented/AGENTS.md](implemented/AGENTS.md).
- - **`rejected/`** — the proposal was considered and declined. Kept for the record so the rejection isn't re-litigated.
+ - **`rejected/`** — the proposal was considered and declined. Keep it only while its rationale prevents a tempting, meaningful mistake; otherwise delete the complete triplet.
- **Class** (the nested folder) is the *kind* of decision — see [Classification](#classification) below.
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 tree is the 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.
+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.
## Classification
@@ -31,15 +33,27 @@ Each Agent Note belongs to one path-encoded class from the closed set in `script
The `architecture` / `process` line: **architecture** is about the source we ship; **process** is the surrounding tooling and workflow. (`refactor` is deliberately absent — it overlaps `simplification`, whose discriminator, "does observable behavior change?", already covers it.)
+## 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.
+
+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.
+
+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 cross-file or cross-package contract, 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)).
-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 behavioral, contractual, structural, process, or rationale change is exempt. An Agent Note is never edited into a *different decision*: supersede it with a new one and cross-link. 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. Only a purely mechanical or local edit with no behavioral, contractual, structural, process, or rationale change 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).
+
+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, verification contract, 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.
+
+A feature-addition note may be consolidated into the later removal note only when the feature is absent from production code, configuration, schemas, durable or wire formats, migration, and compatibility behavior; no current documentation presents it as available; and no test exercises it as supported behavior. Removal rationale and tests that verify absence may remain. The removal owner preserves the original motivation, why it no longer justified the feature, alternatives to full removal, the capability given up, conditions for reintroduction, and verification of complete absence. Obsolete implementation inventories and tests that only verified the deleted behavior are not current verification contracts. Removing one transport, default, implementation, or presentation is partial supersession, as is any surviving durable data or compatibility handling.
## The file format
-Every Agent Note follows one in-file format, enforced by `pnpm run verify-agent-note-format` ([scripts/verify-agent-note-format.ts](../../scripts/verify-agent-note-format.ts), part of `doc-sync`); the rationale for the format — and the alternatives it rejected — is [the uniform-format Agent Note](implemented/process/2026-07-05-uniform-agent-note-format.md).
+Every active Agent Note follows one in-file format, enforced by `pnpm run verify-agent-note-format` ([scripts/verify-agent-note-format.ts](../../scripts/verify-agent-note-format.ts), part of `doc-sync`); the rationale for the format — and the alternatives it rejected — is [the uniform-format Agent Note](implemented/process/2026-07-05-uniform-agent-note-format.md). Archived notes retain the format they had when sealed plus the archive-date line above.
### The header block
diff --git a/.agents/notes/README.zh.md b/.agents/notes/README.zh.md
new file mode 100644
index 0000000000..ddecac7951
--- /dev/null
+++ b/.agents/notes/README.zh.md
@@ -0,0 +1,129 @@
+# Agent Notes
+
+[English](README.md) | 中文
+
+这里存放一类设计文档。**Agent Note(agent 决策记录)** 记录塑造本代码库的决策或提案:代码和文档无法承载的*为什么*以及*放弃了什么*。本文件是入口和契约:Agent Note 存放在哪里、何时需要写一份,以及[文件内格式](#the-file-format)。
+
+## 布局与命名
+
+每份 Agent Note 有两个维度,都编码在其**路径**中:`{lifecycle}/{class}/yyyy-mm-dd-topic-title.md`。
+
+- **生命周期**(顶层文件夹)是 Agent Note 的状态,Agent Note 随状态变化在文件夹之间移动:
+ - **`proposed/`**:实施前评审的提案;尚未构建(或仅部分构建)。
+ - **`implemented/`**:决策已交付。文件记录做了什么决定、否决了什么,并**与实际交付的内容保持同步**:当代码后续移动文件、重命名包(package)或更改键名/默认值时,Agent Note 在同一个变更中同步更新(仅限事实——路径、名称、结构——而非决策本身)。见 [implemented/AGENTS.md](implemented/AGENTS.md)。
+ - **`rejected/`**:提案经过讨论后被否决。仅当其决策依据仍能避免一种诱人且影响重大的错误时保留;否则删除完整的三个配对文件。
+- **类别**(嵌套文件夹)是决策的*种类*——见下方[分类](#classification)。
+
+文件名中的日期是该主题**首次提出**的时间(以 git 历史为准)。Agent Note 之间的交叉引用使用相对 Markdown 链接(`[topic](../../implemented/architecture/2026-…-….md)`),从不使用纯文字或编号,这样既可机械检查,也能在文件夹间移动时保持有效。
+
+活跃生命周期目录树就是工作清单:浏览其生命周期/类别文件夹,或搜索仓库即可。请勿添加集中式 `INDEX.md`;设计理由见[不设索引的 Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.md)。未来指导价值较低的已实施记录会移至下文所述、单独冻结的 [`archived/`](archived/AGENTS.md) 目录树。
+
+
+
+## 分类
+
+每份 Agent Note 属于 `scripts/agent-note-tree.ts` 中封闭集合里的一个路径编码类别;分类门禁拒绝其他文件夹。新增类别需要同时更新规范集合与本节。见[分类 Agent Note](implemented/process/2026-06-20-agent-note-classification.md)。
+
+| 类别 | 覆盖范围 |
+|---|---|
+| `feature` | 面向用户或模型的新功能。 |
+| `bug-fix` | 修正缺陷或弥补事故复盘(postmortem)发现的缺口。 |
+| `simplification` | 在不增加功能的前提下移除代码、行为或对外表面积。 |
+| `architecture` | 关于**交付源码**的结构性决策:包之间的关系、运行时词汇。 |
+| `process` | 代码**周边**的工具、策略或工作流——门禁、包管理器、vendor 化——不涉及运行时行为。 |
+| `testing` | 测试基础设施与策略。 |
+
+`architecture` 与 `process` 的界线:**architecture** 关乎我们交付的源码;**process** 关乎围绕源码的工具与工作流。(`refactor` 被有意排除:它与 `simplification` 重叠,而后者的判别标准「可观察行为是否改变」已经覆盖了它。)
+
+## 归档与删除
+
+当一份 implemented Agent Note 记录的交付决策已经完整落地,且其决策依据不太可能再指导未来工作时,将其归档。如果其中的备选方案、归属边界、否定性保证、持久化语义或协议语义、安全规则,或者重新引入条件仍有价值,则继续作为活跃记录保留。绝不归档 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` 行,重新记录伴随文件,并修复或删除入站链接。归档时只允许对内容做这些更改。
+
+封存后,每组归档文件都永久冻结。禁止编辑、翻译、重新格式化、更新、移动或删除,也不得将其视为当前行为的权威依据。文档门禁会跳过归档源文件,包括其中的出站链接;当活跃文档有意引用历史时,仍可链接到归档 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.md) 记录了设计依据。
+
+## 何时需要写一份
+
+每个非平凡变更都必须在同一 PR(Pull Request)中新增或更新至少一份 Agent Note。如果变更修改了行为、架构、跨文件或跨包契约、流程或工具、测试策略、磁盘、协议或配置格式,或者其他维护者可能合理重新审视的决策,就属于非平凡变更。对未来重大工作的提案从 `proposed/` 开始;已经做出的决策从 `implemented/` 开始。选择与决策匹配的类别文件夹(见[分类](#classification))。
+
+更新已经拥有该决策的 Agent Note 即可满足规则;不要创建重复记录。只有不涉及行为、契约、结构、流程或理由变化的纯机械性或局部编辑才可豁免。Agent Note 永远不会被编辑为一个*不同的决策*:用新 Agent Note 取代旧记录,并让两个记录保持互相链接,除非后续依据下方规则完全合并旧记录。编辑 `implemented/` Agent Note 以跟踪其现有决策的所在位置是必需的,而非禁止的;见 [implemented/AGENTS.md](implemented/AGENTS.md)。
+
+被完全取代的 implemented Agent Note 可以合并到当前持有该决策的记录中,并删除原文件。删除前,当前记录必须保存所有独有的决策依据、备选方案、影响、验证契约和明确指出的覆盖缺口;修复所有入站链接;并在同一变更中删除中文对侧文件和一致性记录。仅部分被取代的记录不符合此条件:保留两个记录并让它们互相链接,同时更新所有仍然适用的事实。合并不得将旧文件改写成与其相反的决策,也不得让 git 历史成为决策依据的唯一副本。
+
+只有当一项功能已从生产代码、配置、schema、持久化格式或协议格式、迁移和兼容行为中完全消失,当前文档不再将其描述为可用,且没有测试把它作为受支持行为来执行时,新增该功能的 Agent Note 才可合并进后续的移除记录。移除决策的依据和验证该功能已不存在的测试可以保留。移除决策的持有记录必须保留最初动机、为什么该动机已不足以证明保留该功能的合理性、完全移除之外的备选方案、放弃的能力、重新引入的条件,以及证明已彻底移除的验证。过时的实现清单和只验证已删除行为的测试不属于当前验证契约。仅移除一种传输、默认值、实现或展示属于部分取代;仍有任何持久数据或兼容处理也同样如此。
+
+
+
+## 文件格式
+
+每份活跃 Agent Note 遵循统一的文件内格式,由 `pnpm run verify-agent-note-format`([scripts/verify-agent-note-format.ts](../../scripts/verify-agent-note-format.ts),`doc-sync`(文档同步门禁)的一环)强制执行;该格式的设计动机及其否决的替代方案见[统一格式 Agent Note](implemented/process/2026-07-05-uniform-agent-note-format.md)。归档记录保留封存时的格式,并增加上述归档日期行。
+
+### 头部块
+
+每份 Agent Note 的前三行严格为:
+
+```markdown
+# Agent Note:
+
+Status:
+```
+
+后跟一个空行。`Status:` 的值有三种形式,且必须与文件所在的生命周期文件夹一致——门禁会交叉检查:
+
+- `Status: proposed`
+- `Status: implemented`
+- `Status: rejected — `
+
+状态行不带日期、不带括号补充说明:文件名记录首次提出日期,git 记录其余一切;「以修订形式接受」之类的说明属于正文内容(在陈述决策的地方说明修订)。拒绝原因是唯一带内容的状态,因为读者查阅被否决的 Agent Note 时,结论正是他们要找的。
+
+### 正文骨架
+
+每份 Agent Note 的正文以 `## Problem` 开头:动机,写法上不依赖解决方案即可独立成文。后续内容取决于生命周期;固定章节使用以下规范名称且仅限这些名称,而真正独特的技术章节(包拓扑、协议契约、schema 等)在必需章节之间可自由组织。
+
+#### `proposed/`
+
+```markdown
+## Problem
+## Proposal
+…bespoke sections…
+## Alternatives considered
+## Acceptance criteria
+## Risks
+```
+
+`## Proposal` 描述拟议的变更,可以合理地使用将来时态——计划、迁移步骤和待解决问题在工作尚未完成时属于此处。`## Acceptance criteria` 说明什么可观察状态意味着完成。`## Risks` 涵盖可能出错的事项以及该变更有意放弃的东西。
+
+#### `implemented/`
+
+```markdown
+## Problem
+## Decision
+…bespoke sections…
+## Alternatives considered
+## Consequences
+```
+
+`## Decision` 以现在时态描述已交付的现实,整个文件按 [implemented/AGENTS.md](implemented/AGENTS.md) 的要求与之保持同步。`## Consequences` 记录权衡的代价**与**收益。提案阶段的标题在此属于规格用语,门禁会拒绝它们:`## Proposal`、`## Plan`、`## Migration plan` 和 `## Acceptance criteria` 不得出现在 implemented Agent Note 中(原因见 [slop 检查清单](../../docs/AGENTS.md))。`## Testing`、`## Deferred` 或 `## Related` 章节在陈述现在时态的事实时是允许的。
+
+#### `rejected/`
+
+被否决的 Agent Note 是冻结的提案:保留提案时的所有章节(包括 `## Acceptance criteria` 或 `## Plan`),结论写在 `Status:` 行上。仅头部块、`## Problem` 开头、`## Proposal` 章节以及下方的「曾考虑的替代方案」强制要求适用。
+
+### 曾考虑的替代方案——必需
+
+每份 Agent Note 都必须包含 `## Alternatives considered` 章节:每个真实的替代方案及其落选原因,每个替代方案用一个加粗引导的段落,或对争议较大的替代方案用 `### Why not ?` 子节。记录决策时不记录它击败了什么,就是在邀请反复争论——正是这些 Agent Note 存在的意义所要防止的。
+
+替代方案是记录下来的,不是凭空编造的。日期早于 2026-07-05 且替代方案无法从记录中重建的 Agent Note,在该章节位置放置以下精确注释,门禁仅对格式规范之前的文件接受此注释:
+
+```markdown
+
+```
+
+### 在生命周期之间移动
+
+将文件在生命周期文件夹之间移动意味着在同一个变更中更新 `Status:` 行并满足目标文件夹的骨架要求——否则门禁会失败。具体而言,`proposed/` → `implemented/` 将 `## Proposal` 改写为现在时态的 `## Decision`,将 `## Acceptance criteria` 和 `## Risks` 折入 `## Consequences`(或折入一个现在时态的 `## Testing`/`## Verification` 章节,用于描述现在锁定该行为的内容),并用实际交付的内容替换计划——即 [implemented/AGENTS.md](implemented/AGENTS.md) 所要求的改写,使之机械化。`proposed/` → `rejected/` 仅在 `Status:` 行添加原因并冻结文件。
+
+### 中文对侧文件
+
+`.zh.md` 对侧文件按 [i18n 契约](../../docs/i18n/README.md)逐章节镜像其英文兄弟文件的结构;机器检查的头部标记(`# Agent Note: ` 和 `Status:` 行)保持英文原样不翻译。格式门禁跳过 `.zh.md` 文件——配对门禁负责它们的一致性。
diff --git a/.agents/notes/archived/AGENTS.md b/.agents/notes/archived/AGENTS.md
new file mode 100644
index 0000000000..2ac4518a6a
--- /dev/null
+++ b/.agents/notes/archived/AGENTS.md
@@ -0,0 +1,7 @@
+# AGENTS.md — Archived Agent Notes
+
+Archived Agent Note triplets under the kind directories are frozen historical snapshots, not current authority. Never edit, reformat, translate, repair, delete, or move a sealed artifact; use an active Agent Note or current documentation for new decisions and facts.
+
+The archival change may only relocate a complete English/Chinese/sidecar triplet, insert the identical `Archived: YYYY-MM-DD` line below both `Status: implemented` lines, re-record the sidecar, and repair or delete inbound links. Do not inspect, verify, or repair links out of archived notes.
+
+Run the [`dsh-archive-agent-notes`](../../skills/dsh-archive-agent-notes/SKILL.md) workflow and append new artifact hashes with `pnpm run verify-archived-agent-notes --write`. The normal verifier rejects changed or missing sealed artifacts, incomplete triplets, unknown kind folders, and invalid archive metadata.
diff --git a/.agents/notes/archived/architecture/2026-06-11-custom-schema-dsl.i18n.yaml b/.agents/notes/archived/architecture/2026-06-11-custom-schema-dsl.i18n.yaml
new file mode 100644
index 0000000000..ae1e7c0afe
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-06-11-custom-schema-dsl.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
+2026-06-11-custom-schema-dsl.md: e09fea6c4bb80e287b1b64471eee4c87f24fba4a
+2026-06-11-custom-schema-dsl.zh.md: 2bacbde02838ef61b05f38796bbeeea262fc2d23
diff --git a/.agents/notes/implemented/architecture/2026-06-11-custom-schema-dsl.md b/.agents/notes/archived/architecture/2026-06-11-custom-schema-dsl.md
similarity index 95%
rename from .agents/notes/implemented/architecture/2026-06-11-custom-schema-dsl.md
rename to .agents/notes/archived/architecture/2026-06-11-custom-schema-dsl.md
index 41b72f1551..e09fea6c4b 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-custom-schema-dsl.md
+++ b/.agents/notes/archived/architecture/2026-06-11-custom-schema-dsl.md
@@ -1,6 +1,9 @@
# Agent Note: Custom typed tool-schema DSL instead of schemastery
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-06-11-custom-schema-dsl.zh.md)
## Problem
diff --git a/.agents/notes/archived/architecture/2026-06-11-custom-schema-dsl.zh.md b/.agents/notes/archived/architecture/2026-06-11-custom-schema-dsl.zh.md
new file mode 100644
index 0000000000..2bacbde028
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-06-11-custom-schema-dsl.zh.md
@@ -0,0 +1,24 @@
+# Agent Note: 使用自定义类型化工具 schema DSL 替代 schemastery
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-06-11-custom-schema-dsl.md) | 中文
+
+## 问题
+
+工具参数必须以标准 JSON Schema 形式到达模型,同时让工具作者在 `execute(args)` 中获得类型化的参数而无需类型断言。Schemastery 已用于插件配置,但工具作者 API 需要逐属性的 `required: true` 布尔值,而非 JSON Schema 的独立 `required` 数组。
+
+## 决策
+
+该决策已由[统一 JSON 值 schema DSL](2026-07-20-unified-json-value-schema-dsl.md)取代;新设计保留小型编写接口,同时让参数与类型化值共享一套词汇。`ParameterSchemaSpec` 保留逐属性的 `required: true`;`InferArgs` 将必需键映射为非可选属性;`parameterSchemaSpecToJsonSchema()` 编译隐式开放的对象根;`defineTool()` 则将类型推导、编译与校验串联起来。原始 JSON Schema 的 `ToolDefinition` 仍是 `ToolRegistry.register()` 接受的输入,供 MCP 和其他外部工具使用。
+
+## 曾考虑的替代方案
+
+**Schemastery**(已作为 vendor 引入,用于插件 Config)经评估后被否决:它面向的是基于 StandardSchema 的校验/转换,而非 JSON Schema *生成*,因此会增加间接层却无法干净地产出协议格式(wire format)。
+
+## 后果
+
+- 第一方工具作者获得零类型断言的类型化参数;类型体操的成本留在核心包内部(符合 AGENTS.md 的类型安全策略)。
+- 当前节点、字面量约束、联合类型、JSON 值边界与对象开放性规则均由上述统一说明定义。
+- `InferArgs` 映射在类型层面有回归测试,源于早期一个可选性 bug。
diff --git a/.agents/notes/archived/architecture/2026-06-11-tool-schemas-in-prompt-assembly.i18n.yaml b/.agents/notes/archived/architecture/2026-06-11-tool-schemas-in-prompt-assembly.i18n.yaml
new file mode 100644
index 0000000000..a1e3f9c72c
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-06-11-tool-schemas-in-prompt-assembly.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
+2026-06-11-tool-schemas-in-prompt-assembly.md: 4d5251163969127ad9fd8ed0c64416057713dfa8
+2026-06-11-tool-schemas-in-prompt-assembly.zh.md: c04246baa196f50ec1c0dc6f91e9b7b12f67c5e0
diff --git a/.agents/notes/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md b/.agents/notes/archived/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md
similarity index 94%
rename from .agents/notes/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md
rename to .agents/notes/archived/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md
index 59ea9117dc..4d52511639 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md
+++ b/.agents/notes/archived/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md
@@ -1,6 +1,9 @@
# Agent Note: Tool schemas are part of the system-prompt assembly
Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-06-11-tool-schemas-in-prompt-assembly.zh.md)
## Problem
diff --git a/.agents/notes/archived/architecture/2026-06-11-tool-schemas-in-prompt-assembly.zh.md b/.agents/notes/archived/architecture/2026-06-11-tool-schemas-in-prompt-assembly.zh.md
new file mode 100644
index 0000000000..c04246baa1
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-06-11-tool-schemas-in-prompt-assembly.zh.md
@@ -0,0 +1,24 @@
+# Agent Note: 工具 schema 是系统提示词组装的一部分
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-06-11-tool-schemas-in-prompt-assembly.md) | 中文
+
+## 问题
+
+在协议格式(wire format)层面,工具 schema 通过模型请求中专用的 `tools` 字段传输,而非嵌入提示词文本。然而从架构角度看,「模型被告知它能做什么」是一个统一的关注点:提示词段落与工具列表由相同的插件贡献组装,并在同一时刻被消费。
+
+## 决策
+
+`PromptAssembly { sections, tools }`:系统提示词服务同时收集有序的文本段落和工具 schema(工具注册表自动贡献一个提供方)。agent loop(智能体循环)每个步骤消费一份 assembly;适配器将 `sections` 映射到提供方的 system 槽位,将 `tools` 映射到协议格式的 `tools` 字段。因此 `system-prompt/assemble` waterfall(瀑布式事件)是模型预先获知的所有信息的唯一拦截点:工具过滤(ToolSearch / 渐进式披露)是一次 assembly 重写,与提示词编辑无异。
+
+## 曾考虑的替代方案
+
+**循环从工具注册表和提示词服务分别查询**:将一个统一的关注点拆到两个 seam 上;任何想影响「模型被告知什么」的拦截(工具过滤、plan 模式)都需要在两个接口上各挂一个监听器,而非一次 assembly 重写即可完成。
+
+## 后果
+
+- 一条 waterfall 统管模型的常驻上下文;plan 模式等插件可以在一个监听器中同时替换提示词文本和可见工具。
+- assembly 接口通过声明合并实现可扩展(没有无类型的 `extras` 包——扩展即声明合并),为未来的槽位预留空间。
+- 将 schema 放在「提示词」服务中略有概念上的意外感,已在本文及包 README 中加以说明。
diff --git a/.agents/notes/archived/architecture/2026-06-15-turn-enclosure-invariant.i18n.yaml b/.agents/notes/archived/architecture/2026-06-15-turn-enclosure-invariant.i18n.yaml
new file mode 100644
index 0000000000..9f712421c3
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-06-15-turn-enclosure-invariant.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
+2026-06-15-turn-enclosure-invariant.md: f38f5b2600b57ad8f5fc4c8975579e281b5f3a49
+2026-06-15-turn-enclosure-invariant.zh.md: b74ddf3f6b65a7990329a8a903aafe2e4ca41c90
diff --git a/.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md b/.agents/notes/archived/architecture/2026-06-15-turn-enclosure-invariant.md
similarity index 98%
rename from .agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md
rename to .agents/notes/archived/architecture/2026-06-15-turn-enclosure-invariant.md
index 00352b01c4..f38f5b2600 100644
--- a/.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md
+++ b/.agents/notes/archived/architecture/2026-06-15-turn-enclosure-invariant.md
@@ -1,6 +1,9 @@
# Agent Note: Every session event is enclosed in a turn
Status: implemented
+Archived: 2026-07-28
+
+English | [中文](2026-06-15-turn-enclosure-invariant.zh.md)
## Problem
diff --git a/.agents/notes/archived/architecture/2026-06-15-turn-enclosure-invariant.zh.md b/.agents/notes/archived/architecture/2026-06-15-turn-enclosure-invariant.zh.md
new file mode 100644
index 0000000000..b74ddf3f6b
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-06-15-turn-enclosure-invariant.zh.md
@@ -0,0 +1,43 @@
+# Agent Note: 每个会话事件都封闭在一个轮次内
+
+Status: implemented
+Archived: 2026-07-28
+
+[English](2026-06-15-turn-enclosure-invariant.md) | 中文
+
+## 问题
+
+持久化的会话持久化后端(在配套变更中引入)以**轮次**作为崩溃恢复边界:崩溃可能留下一个未关闭的最终轮次,`load` 会用一个合成的 `turn/end {kind:'interrupted'}` 将其关闭,同时保留该轮次的真实事件(见[会话持久化](2026-06-14-session-persistence.md))。这种恢复只有在没有任何*合法的*持久事件位于轮次之外(即上一个 `turn/end` 与下一个 `turn/start` 之间的间隙)时才是良定义的,否则这类事件会被卷入下一个轮次的中断关闭中。
+
+这一假设并不成立。有两条路径在任何轮次之外记录了事件:
+
+1. **排队的用户消息。** agent loop(智能体循环)排空排队消息并在 `turn/start` *之前*追加 `user/message`——于是一个轮次自身的提示词落在了前一个 `turn/end` 与下一个 `turn/start` 之间的间隙中。
+2. **空闲时的上下文注入。** `agent.inject()` 直接追加一条 `context/message`。它在生产环境中的真实调用方是 `dsh-tool-bash`,后者从 `ctx.bash.onTaskDone` 注入后台任务完成通知——该回调在后台 bash 任务完成时触发,而这经常发生在 agent **空闲**(轮次之间)时。
+
+在情况 2 中,如果注入的 `context/message` 是 flush/dispose 之前的最后一个事件(之后没有轮次追加 `turn/end`),`scanLog` 会将其视为崩溃残留并在**恢复时丢弃**——注入的上下文已持久写入磁盘,但重新加载后被静默丢失。情况 1 本身无害(`user/message` 之后总会跟着它触发的轮次),但使「什么可以出现在轮次之外」这条规则变得模糊。
+
+## 决策
+
+**每个会话事件都位于一个轮次内部**:在 `turn/start` 与其匹配的 `turn/end` 之间。具体而言:
+
+- agent loop 在 `turn/start` **之后**(轮次内部)追加排队的 `user/message` 事件,而非之前。因此,一旦这些消息被记录,就欠下一个 `turn/end`,既有的 finalizer 保证它被写入。
+- agent **运行中**调用 `agent.inject()` 时,它会加入已打开的轮次。当前步骤执行 assistant 工具调用期间,已接受的上下文按到达顺序等待该批次结算,随后在每个已记录结果之后追加;即使执行中断,也会在轮次关闭前写入。
+- agent **空闲时**调用 `agent.inject()`,则将 `context/message` 包裹在一个一次性轮次中:`turn/start{trigger:{kind:'injection'}}` → `context/message` → `turn/end{completed}`。一个新的 `injection` 变体加入可合并扩展的 `TurnTriggerMap`。
+- agent loop 每次迭代从日志推导下一个轮次编号(`lastTurnNumber(session) + 1`),而不是维护一个私有计数器,这样空闲注入的一次性轮次不会与下一个真实轮次的编号冲突。
+- `dsh-session/invariant` companion 将该检查注册到 `ctx.invariants`:选中后,在没有打开轮次的情况下追加 `user/message` / `context/message` / `steering/message` 会抛出归因于 `@deepseek-ai/dsh-session` 的 `InvariantError`。
+
+可序列化性不变式在同一源码边界处强制执行(`Session.append` 对不可 JSON 序列化的数据抛出异常),因此「什么可以进入日志」现在由一个位置统一管控,而非由下游碰巧在监听的某个后端各自发现。
+
+## 曾考虑的替代方案
+
+**放宽读取端而非约束生产端**——让 `scanLog` 提交位于已打开轮次之外的事件。否决:一条单一、可检查的生产端规则优于一个更宽松的边界扫描(后者需要同时推理部分轮次*和*轮次间的散落事件)。
+
+## 后果
+
+轮次现在是*唯一的*持久性/回放边界,因此[会话持久化](2026-06-14-session-persistence.md)的崩溃恢复规则是完备的,而不仅仅是充分的:被中断的最终轮次被关闭(用合成的 `turn/end {interrupted}`),其真实事件得以保留,且零风险将轮次间上下文混入其中,因为不存在轮次间上下文。`scanLog` 保持简洁(最多一个可能未关闭的最终轮次,绝无散落的轮次间事件),空闲时的后台任务通知在持久化 + 恢复后依然存活。
+
+代价:空闲时调用 `agent.inject()` 现在写入三行日志而非一行;派生的历史中多出一个仅包含注入上下文(无 assistant 输出)的轮次——`deriveMessages()` 已经纯粹按事件类型派生,因此渲染结果完全相同。`injection` 触发器是一个新的磁盘词汇值;与每次 `SessionEventMap`/`TurnTriggerMap` 的新增一样,它属于冻结格式的一部分。轮次内的事件顺序发生了变化(`turn/start` 现在先于 `user/message`),这对任何断言旧顺序的代码可观测——agent loop 自身的测试是唯一的此类消费方。
+
+该规则有意采用生产端强制、开发环境检查的方式,而非读取端容忍的方式:未来的后端(SQLite/WAL)无需额外工作即可继承同样干净的边界,而在轮次外记录事件的插件会在开发环境中大声失败,而非在下次重新加载时静默丢失数据。
+
+轮次内检测到的失败在 `turn/end` 之前记录。后续的 flush 失败没有有效的轮次内位置,因此通过 `agent/error` 和日志报告,而非作为会话事件追加。这保持了回放日志的平衡;持久化的运维诊断需要一个独立的遥测通道。
diff --git a/.agents/notes/archived/architecture/2026-06-20-extract-example-app-packages.i18n.yaml b/.agents/notes/archived/architecture/2026-06-20-extract-example-app-packages.i18n.yaml
new file mode 100644
index 0000000000..92053c2212
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-06-20-extract-example-app-packages.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
+2026-06-20-extract-example-app-packages.md: 06466aa575a535afe0ba614fb2c2c5b3e857aeab
+2026-06-20-extract-example-app-packages.zh.md: ccd8eae1524210d75770248566810423280df3b0
diff --git a/.agents/notes/implemented/architecture/2026-06-20-extract-example-app-packages.md b/.agents/notes/archived/architecture/2026-06-20-extract-example-app-packages.md
similarity index 96%
rename from .agents/notes/implemented/architecture/2026-06-20-extract-example-app-packages.md
rename to .agents/notes/archived/architecture/2026-06-20-extract-example-app-packages.md
index 953a977c19..06466aa575 100644
--- a/.agents/notes/implemented/architecture/2026-06-20-extract-example-app-packages.md
+++ b/.agents/notes/archived/architecture/2026-06-20-extract-example-app-packages.md
@@ -1,6 +1,9 @@
# Agent Note: Extract example apps into packages
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-06-20-extract-example-app-packages.zh.md)
## Problem
@@ -40,7 +43,7 @@ The old `base*.yml`/`acp-tail.yml` includes already deduped the *config*, but a
- Example directories contain only their config, README, and tests: `start.ts`, the infrastructure preamble, and the shared YAML includes are gone.
- `demo:tui`, `demo:headless`, and `demo:acp` invoke the app-package bins.
- Each new package has a README and per-file 100% coverage; each app package also has a keyless real-Loader-path bin smoke that catches export-shape failures described in [postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md).
-- The ACP replay transcript remains unchanged because the plugin set and load order did not change.
+- The ACP replay suite boots through the app-package bin, so protocol wiring and assembled backend behavior cross the real Loader boundary.
## Consequences
diff --git a/.agents/notes/archived/architecture/2026-06-20-extract-example-app-packages.zh.md b/.agents/notes/archived/architecture/2026-06-20-extract-example-app-packages.zh.md
new file mode 100644
index 0000000000..ccd8eae152
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-06-20-extract-example-app-packages.zh.md
@@ -0,0 +1,58 @@
+# Agent Note: 将示例应用提取为独立包
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-06-20-extract-example-app-packages.md) | 中文
+
+## 问题
+
+示例目录本应是*精简的*——只包含演示的可变接线,而非演示的基础设施。在此次变更之前,它是臃肿的。每个示例都携带一份手写的 `start.ts` 启动引导、一段基础设施前导(`timer`,以及 stdio 演示所需的 `logger` + `hmr`(热模块替换))、三个共享 YAML 片段的嵌套引用(`base.yml` / `base-core.yml` / `acp-agent/acp-tail.yml`),还有各示例自身的 `agent-loop`/持久化/系统提示词配置。真正的应用——每个 agent(智能体)都需要的服务主干——散落在叶子配置和那些 include 中。
+
+叶子配置还拥有耦合的前门。ACP(Agent Client Protocol)要求 stdout 纯净,并通过 `session/new` 创建 agent;终端应用和 Headless 应用则预创建 `main`,但进程 I/O 契约不同。防止错误组合的唯一屏障是文档中的文字警告,而三个 `start.ts` 文件重复着 Loader 引导和生命周期代码。
+
+## 决策
+
+每个示例现在**主要是对一个应用包(package)的调用**,沿着既有的[接口 / 实现 / 消费方 seam](2026-06-13-capability-seams.md) 拆分接线:**应用包拥有组合**,叶子 `cordis.yml` 只拥有**可替换的选择**(哪个 LLM(大语言模型)适配器、哪个 bash 执行器、模型、提示词、持久化根目录)。
+
+- **`@deepseek-ai/dsh-agent-spine-demo`**([packages/examples/agent-spine-demo](../../../../packages/examples/agent-spine-demo))组合了不含提供方、不含执行器、不含 UI 的主干,并转发 agent loop(智能体循环)的 agent 列表配置。它对具体 loop 的依赖是有意为之,因为该包组合的是主干而非扩展主干;替换 loop 意味着提供另一个 bundle。
+- **`@deepseek-ai/dsh-tui-demo`**、**`@deepseek-ai/dsh-cli-demo`** 和 **`@deepseek-ai/dsh-acp-demo`** 各自内置其进程角色。TUI 包含全屏 UI 和预创建的 `main`;Headless 包含 one-shot driver 和预创建的 `main`;ACP 包含 bridge 且不预创建 agent。三者都包含 JSONL 持久化,并省略 stdout logger。
+- **`start.ts` 已移除。** 每个应用包都暴露一个 bin;`demo:*` 脚本调用它。Loader 引导、`.env` 加载和快速失败守卫位于共享的 [`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot) 包(在逐文件覆盖率门禁下有单元测试——见[共享应用 bin 的启动胶水](../simplification/2026-07-04-share-app-bin-boot-glue.md));精简的自执行入口由 keyless 的 Loader 路径测试驱动。
+- **每个叶子 `cordis.yml` 精简为**后端、可选产品工具,以及一个承载应用配置的 app 条目。TUI 和 Headless 把模型/会话选择路由到预创建的 agent;ACP 把初始提供方/模型路由到 bridge。
+- **`base.yml`、`base-core.yml` 和 `acp-agent/acp-tail.yml` 已退役**——它们共享的主干现在位于 `dsh-agent-spine-demo` 中。
+
+`bash-local` 和 LLM 适配器仍然是**叶子选择**:bundle 提供 `tool-bash`(消费方 schema),叶子选择执行器实现,因此沙箱执行器或回放适配器无需触碰应用即可替换。
+
+### 实现修正:`hmr` 保留为叶子条目
+
+提案最初将 `hmr` 列入交互式应用内置的前门集群。对照代码验证后发现,将 `hmr` 内置到应用包中会在两个方面与 Cordis 冲突,因此改为作为**叶子 `cordis.yml` 条目**交付:
+
+1. `@cordisjs/plugin-hmr` 是一个仅限 Loader、仅限子进程的开发插件——它需要活跃的 `loader` 服务及其内部模块访问权限,因此只能在真实的 `demo:*`/bin 子进程中运行,不能在进程内的单元/覆盖率测试层运行。
+2. 进程内测试层(vitest)甚至无法*导入* vendor 的 `hmr` 模块(其 class-decorator `@Inject` 形式在 Vite 的 transform 下会失败),因此一个 `apply` 静态导入了它的包永远无法满足其主函数的逐文件 100% 覆盖率门禁。
+
+关键在于,`hmr` 不是 stdout 纯净隐患:ACP 配置中误加该条目不会破坏 JSON-RPC 帧。所有已交付应用都省略 stdout 控制台 logger;stdout 只归应用或协议 driver 所有。
+
+## 曾考虑的替代方案
+
+### 为什么不继续用共享 YAML include 来管理接线?
+
+旧的 `base*.yml`/`acp-tail.yml` include 已经去重了*配置*,但 YAML include 无法**封装**前门耦合——它只能在注释中描述,并信任每个叶子遵守。它也无法拥有 `bin`,因此启动胶水一直在三个 `start.ts` 文件中重复。包将「ACP 应用绝不向 stdout 输出日志」从文字警告变成了产物的属性:叶子中不存在可以写错的 logger 条目。
+
+## 验证
+
+- 示例目录只包含配置、README 和测试:`start.ts`、基础设施前导和共享 YAML include 已移除。
+- `demo:tui`、`demo:headless` 和 `demo:acp` 调用应用包的 bin。
+- 每个新包都有 README 和逐文件 100% 覆盖率;每个应用包还有一个 keyless 的真实 Loader 路径 bin 冒烟测试,用于捕获[事后分析 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md) 中描述的导出形状故障。
+- ACP 回放套件通过应用包的 bin 启动,因此协议接线与组装后的后端行为都跨越真实的 Loader 边界。
+
+## 后果
+
+- **裸插件树的教学性。** 主干现在隐藏在 bundle 之后,查看完整树意味着打开 `dsh-agent-spine-demo`。应用包的 README 承担了这份教学职责。
+- **多了一层间接。**「这个演示加载了什么?」从扫描单个 YAML 变成了阅读一个包。
+
+## 相关
+
+- 取代[使共享示例基础配置与提供方无关](../../rejected/architecture/2026-06-20-providerless-example-base.md):一旦主干移入 `dsh-agent-spine-demo` 且 `base*.yml` 文件被删除,将 `base.yml` 重命名为无提供方核心便不再有意义。
+- 基于[能力 seam](2026-06-13-capability-seams.md)的接口/实现/消费方拆分——后端和展示层保持为叶子选择;主干是共享 bundle。
+- 与[将包重组为模块化层级结构](2026-06-20-package-hierarchy.md)互补:新的 app/core 包按该层级结构归入既有分组(`core` 放可复用的主干 bundle,`ui` 放应用特有的前门)。
+- 后续的[冗余 agent 移除](../simplification/2026-07-20-remove-stdio-and-echo-agents.md)拥有最终的 TUI/Headless 拆分,并移除行式与仅 mock 的叶子。
diff --git a/.agents/notes/archived/architecture/2026-06-20-package-hierarchy.i18n.yaml b/.agents/notes/archived/architecture/2026-06-20-package-hierarchy.i18n.yaml
new file mode 100644
index 0000000000..4608b8452b
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-06-20-package-hierarchy.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
+2026-06-20-package-hierarchy.md: 43435279cdf4eec544638a5b36c53b4833b4cbbd
+2026-06-20-package-hierarchy.zh.md: a44304d071d165cfe8f112e987160dd37626a58a
diff --git a/.agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md b/.agents/notes/archived/architecture/2026-06-20-package-hierarchy.md
similarity index 85%
rename from .agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md
rename to .agents/notes/archived/architecture/2026-06-20-package-hierarchy.md
index 1ba3b94e8b..43435279cd 100644
--- a/.agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md
+++ b/.agents/notes/archived/architecture/2026-06-20-package-hierarchy.md
@@ -1,8 +1,11 @@
# Agent Note: Reorganize packages into a modular hierarchy
Status: implemented
+Archived: 2026-07-27
-The later [fold-stdio-helper](../simplification/2026-07-04-fold-stdio-ui-helper.md) decision superseded the original `support/ui-stdio` placement, and the [redundant-agent removal](../simplification/2026-07-20-remove-stdio-and-echo-agents.md) subsequently removed that surface entirely. The uniform depth-two hierarchy remains the decision owned here.
+English | [中文](2026-06-20-package-hierarchy.zh.md)
+
+The [redundant-agent removal](../simplification/2026-07-20-remove-stdio-and-echo-agents.md) deletes the original `support/ui-stdio` surface instead of relocating it, and the [automation-only ACP decision](../simplification/2026-07-23-acp-automation-only-protocol.md) places ACP under `packages/acp/acp` instead of the human-UI group. The uniform depth-two hierarchy remains the decision owned here.
## Problem
@@ -34,8 +37,9 @@ packages/
session-persistence/
session-persistence-jsonl/
session-persistence-sqlite/
- ui/ (product integration)
+ acp/ (product automation integration)
acp/
+ ui/ (human interaction and presentation)
support/ (dev/test/example infrastructure)
invariants/
ui-stdio/
@@ -47,7 +51,7 @@ packages/
- **Same-name nesting for capability families.** A family's interface package sits at `packages///` (`llm/llm`, `bash/bash`, `session-persistence/session-persistence`), with implementations and consumers as flat siblings. There is no extra `adapters/`/`impls/` sub-tier — every package is exactly depth 2, which keeps the workspace glob a clean `packages/*/*` and lets one `@deepseek-ai/dsh-*` tsconfig wildcard resolve every package (unique dir names make first-on-disk-wins unambiguous).
- **`session` stays in `core/`; persistence is its own family.** The session log is core product API. Its storage backends form a parallel capability family (`session-persistence/`) mirroring `llm/` and `bash/`, rather than nesting under `core/session/`.
- **`agent-loop` is in `core/`.** It is the one concrete implementation of the `agent` seam, but it ships as the harness's default product loop, so it lives with the core spine. Plugins still depend on the `agent` vocabulary, never on `agent-loop`, so the loop stays swappable.
-- **`invariants` and `ui-stdio` are `support/`, not product.** `invariants` is dev-mode contract checking. `ui-stdio` was extracted from the examples for reuse and the coverage gate — it is example-coupled, so it sits in `support/` alongside `llm-replay` (the snapshot-test replay adapter). `acp` is the only `ui/` member because it is a real product surface (the ACP bridge an editor drives), structurally distinct from the readline demo helper.
+- **Product automation and human UI are separate groups.** `acp` is a product transport under `acp/`, while commands, approvals, interaction, and presentation adapters live under `ui/`. Dev-only invariants and replay infrastructure remain under `support/`.
### Deduplicating the package lists
@@ -68,7 +72,7 @@ Two doc-sync/hygiene gates keep the structure and its references honest, so the
- **A third tier (`adapters/` / `impls/` under each family)** — rejected: uniform depth 2 keeps the workspace glob a clean `packages/*/*` and lets one `@deepseek-ai/dsh-*` tsconfig wildcard resolve every package.
- **Nesting persistence under `core/session/`** — rejected: the storage backends form a parallel capability family mirroring `llm/` and `bash/`, while the session log itself stays core product API.
-- **`ui-stdio` under `ui/`** — rejected: it is example-coupled dev support, not a product surface; `acp` is the only `ui/` member because an editor actually drives it.
+- **`ui-stdio` under `ui/`** — rejected: it was example-coupled dev support, not a product surface.
## Consequences
diff --git a/.agents/notes/archived/architecture/2026-06-20-package-hierarchy.zh.md b/.agents/notes/archived/architecture/2026-06-20-package-hierarchy.zh.md
new file mode 100644
index 0000000000..a44304d071
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-06-20-package-hierarchy.zh.md
@@ -0,0 +1,79 @@
+# Agent Note: 将包重组为模块化层级结构
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-06-20-package-hierarchy.md) | 中文
+
+[冗余 agent 移除](../simplification/2026-07-20-remove-stdio-and-echo-agents.md)直接删除最初的 `support/ui-stdio` 接口,而不是将其迁移;[仅面向自动化的 ACP 决策](../simplification/2026-07-23-acp-automation-only-protocol.md)把 ACP 放在 `packages/acp/acp` 下,而不是面向人类的 UI 组。这里拥有的决策仍是统一的二层目录深度。
+
+## 问题
+
+`packages/` 原先是扁平的:18 个包(package)全部位于 `packages//`,从路径上完全看不出一个包属于核心产品 API、可替换的能力 seam、提供方适配器、产品集成,还是示例/测试支撑。包的 README 带着 `FIXME(package-hierarchy)`,`scripts/publint-all.ts` 带着 `TODO(package-inventory)`,标记的正是这个问题。核心包、提供方集成、能力 seam、示例 UI 支撑和仅用于快照的回放支撑看起来同样基础。
+
+这不仅仅是外观问题。由于每个顶层包看起来都属于同一个公开接口,未来移除更加困难,而 publish/lint/doc 脚本不得不通过注释或手工维护的静态列表来编码意图,而不是从布局中直接读取。
+
+## 决策
+
+按模块角色将包分组,统一放在 `packages///` 深度。分组目录是纯容器(没有 `package.json`);每个包保留其 `@deepseek-ai/dsh-` 名称——这是仓库结构与维护策略的调整,不是包的重命名。
+
+```text
+packages/
+ core/ (product API spine)
+ session/
+ system-prompt/
+ tools/
+ agent/
+ agent-loop/
+ llm/ (product — capability family)
+ llm/
+ llm-deepseek/
+ llm-pi-ai/
+ bash/ (product — capability family)
+ bash/
+ bash-local/
+ tool-bash/
+ session-persistence/ (product — capability family)
+ session-persistence/
+ session-persistence-jsonl/
+ session-persistence-sqlite/
+ acp/ (product automation integration)
+ acp/
+ ui/ (human interaction and presentation)
+ support/ (dev/test/example infrastructure)
+ invariants/
+ ui-stdio/
+ llm-replay/
+```
+
+### 放置决策
+
+- **能力族使用同名嵌套。** 一个族的接口包位于 `packages///`(`llm/llm`、`bash/bash`、`session-persistence/session-persistence`),实现和消费方作为扁平兄弟并列。不设额外的 `adapters/`/`impls/` 子层——每个包恰好在深度 2,这使 workspace glob 保持简洁的 `packages/*/*`,并让一条 `@deepseek-ai/dsh-*` tsconfig 通配符即可解析所有包(唯一的目录名使 first-on-disk-wins 无歧义)。
+- **`session` 留在 `core/`;持久化独立成族。** 会话日志是核心产品 API。其存储后端构成一个平行的能力族(`session-persistence/`),与 `llm/` 和 `bash/` 对称,而非嵌套在 `core/session/` 下。
+- **`agent-loop` 在 `core/` 中。** 它是 `agent` seam 唯一的具体实现,但作为 harness 的默认产品循环交付,因此与核心主干同处。插件仍然依赖 `agent` 的词汇,从不依赖 `agent-loop`,所以循环仍可替换。
+- **产品自动化与面向人类的 UI 是两个独立分组。** `acp` 是位于 `acp/` 下的产品传输层,而命令、审批、交互和展示适配器位于 `ui/` 下。仅开发用的 invariants 与回放基础设施仍留在 `support/` 中。
+
+### 去重包列表
+
+包列表此前在五个地方重复枚举。统一的深度 2 布局使大部分可以被推导:
+
+- `tsconfig.base.json` 通过一条 `@deepseek-ai/dsh-*` `paths` 通配符(每个分组列一个候选)映射所有包,取代了逐包条目。聚合配置(`tsconfig.host.json`、`tsconfig.client.json`)复用该源映射,并携带显式 project references 以保持包/vendor 类型检查边界完整。(这里引入了一个细节:路径候选中包含 `/*/`,朴素的正则注释剥离器会将其误认为块注释——`scripts/doc-typecheck.ts` 正是因此通过 TypeScript 解析器读取 JSONC 配置,而非手动剥离注释。)
+- `scripts/publint-all.ts` 通过读取层级结构(`packages//`)推导列表,解决了 `TODO(package-inventory)`。
+- 聚合配置的 project `references` 仍为显式列表——TypeScript project references 没有通配符形式。从 manifest(元数据清单)生成这些引用留作后续工作(见[通过发现机制获取包清单](../../proposed/process/2026-06-20-discover-package-inventory.md))。
+
+### 新增的护栏
+
+两道 doc-sync/hygiene 门禁确保结构及其引用保持正确,使本次重组所需的手动检查无需日后重复:
+
+- `scripts/verify-package-paths.ts` 标记 Markdown 或 `.ts` 注释/字符串中的 `packages/` 引用,如果该引用无法解析**且**某个路径段命名了一个真实存在的包,即指向已移动包的陈旧路径。如果路径命名的包在任何地方都不存在(前瞻性提案),则不予标记,因此该门禁在 proposed/implemented/rejected 中统一适用。
+- `scripts/check-workspace-constraints.ts` 断言 `packages//` 形状:分组目录不带 `package.json`,且没有包扁平地位于根层或嵌套更深。分组名称保持开放——添加新分组无需修改门禁;只有深度 2 的形状是固定的。
+
+## 曾考虑的替代方案
+
+- **第三层(每个族下设 `adapters/`/`impls/`)**:否决。统一深度 2 使 workspace glob 保持简洁的 `packages/*/*`,并让一条 `@deepseek-ai/dsh-*` tsconfig 通配符即可解析所有包。
+- **将持久化嵌套在 `core/session/` 下**:否决。存储后端构成一个平行的能力族,与 `llm/` 和 `bash/` 对称,而会话日志本身属于核心产品 API。
+- **`ui-stdio` 放在 `ui/` 下**:否决。它曾是与示例耦合的开发支撑,不是产品接口。
+
+## 后果
+
+本次重组在一次协调的变更中搅动了 import、workspace glob、文档链接、构建引用和包路径。这种变动在发布前是可接受的(依据 AGENTS.md 中「基础优先于爆炸半径」的立场),因为它阻止了扁平布局将支撑包固化为产品契约,且这是一次性成本:通配符 `paths`、glob 推导的 publint 列表和形状门禁意味着新增一个包无需额外的结构性编辑。
diff --git a/.agents/notes/archived/architecture/2026-07-02-result-time-applied-hunk-diffs.i18n.yaml b/.agents/notes/archived/architecture/2026-07-02-result-time-applied-hunk-diffs.i18n.yaml
new file mode 100644
index 0000000000..a9516a0625
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-07-02-result-time-applied-hunk-diffs.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
+2026-07-02-result-time-applied-hunk-diffs.md: 19b41cc441f5cab299dbeded0a9eae25ab7e9f97
+2026-07-02-result-time-applied-hunk-diffs.zh.md: b77ec75f0f8af615bde3b95d8fb4620fddedbc9c
diff --git a/.agents/notes/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md b/.agents/notes/archived/architecture/2026-07-02-result-time-applied-hunk-diffs.md
similarity index 80%
rename from .agents/notes/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md
rename to .agents/notes/archived/architecture/2026-07-02-result-time-applied-hunk-diffs.md
index ce0f146b39..19b41cc441 100644
--- a/.agents/notes/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md
+++ b/.agents/notes/archived/architecture/2026-07-02-result-time-applied-hunk-diffs.md
@@ -1,10 +1,13 @@
# Agent Note: Result-time applied-hunk diffs for file mutations
Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-07-02-result-time-applied-hunk-diffs.zh.md)
## Problem
-The [tagged render-intent union](2026-07-02-tool-render-intent-union.md) gave `dsh-tool-fs` write/edit a `card:'diff'` at CALL time, derived purely from the tool's args: write ⇒ `{oldText:null, newText:content}` (the whole new file), edit ⇒ `{oldText:old_string, newText:new_string}` (the bare replaced snippet). An editor renders that as an inline diff, but it is a **context-free** diff — the bare `old_string`→`new_string` with no surrounding lines, and a `replace_all` that touched five scattered sites still renders as one snippet pair.
+The [tagged render-intent union](2026-07-02-tool-render-intent-union.md) gives `dsh-tool-fs` write/edit a `card:'diff'` at call time, derived purely from the tool's args: write ⇒ `{oldText:null, newText:content}` (the whole new file), edit ⇒ `{oldText:old_string, newText:new_string}` (the bare replaced snippet). A UI can render that as an inline diff, but it is a **context-free** diff — the bare `old_string`→`new_string` with no surrounding lines, and a `replace_all` that touched five scattered sites still renders as one snippet pair.
Driving `claude-agent-acp`'s own ACP bridge shows what a full editor diff looks like: after the mutation applies, it emits a SECOND `tool_call_update` whose diff is the **applied hunk with ±3 context lines** (and one hunk per changed site for `replace_all`), reconstructed from the tool's `structuredPatch`. That result-time hunk is what makes Zed show the change *in place* in the file rather than as a floating snippet. Our tools stopped at the call-time snippet; the completed result carried only the plain "updated successfully" text, no diff.
@@ -27,11 +30,11 @@ This remains the general shape ("a tool projects durable result presentation"),
Per the [capability-seam split](2026-06-13-capability-seams.md), the storage backend returns only **storage facts** and the model-facing tool owns **presentation**:
- `dsh-fs` widens `FsEditOutcome` with `{ before: string; after: string }` and `FsWriteOutcome` with `{ before: string | null; after: string }` (`before: null` ⇒ a create, or an existing-but-undiffable binary/non-UTF-8 file). The local backend already holds both texts at write time; it returns them as raw LF-normalized text, with **no diff/UI concept** entering the seam.
-- `dsh-tool-fs` returns canonical before/after mutation facts and projects contextual hunks as `meta: { diffs: FileDiff[] }`. Successful mutations always complete with a diff card because ACP result content replaces the pending card: creates or unchanged overwrites fall back to an args-derived whole-file diff, while edits use applied hunks. Failed mutations carry no diff metadata and render their error normally.
+- `dsh-tool-fs` returns canonical before/after mutation facts and projects contextual hunks as `meta: { diffs: FileDiff[] }`. Successful mutations complete with a diff view: creates or unchanged overwrites fall back to an args-derived whole-file diff, while edits use applied hunks. Failed mutations carry no diff metadata and render their error normally.
-### 3. The bridge renders a `diff` result card
+### 3. UI transports render a `diff` result view
-`ToolResultView` gains a `DiffResultView { card:'diff'; title?; diffs: FileDiff[] }`; the bridge's result-side `switch (view.card)` gets a `diff` arm emitting the `{type:'diff'}` `ToolCallContent` blocks (mirroring the call-side arm). An ACP `tool_call_update.content` REPLACES the call's content in an editor, so the result diff **supersedes** the call-time snippet (and keeps the model-facing result text from clobbering it) — the two-update sequence (call snippet, then result diff) matches `claude-agent-acp` exactly.
+`ToolResultView` includes `DiffResultView { card:'diff'; title?; diffs: FileDiff[] }`. TUI and JSON-RPC/Web consumers switch on the same tagged view and replace the pending call's context-free snippet with the applied result hunk. The [automation-only ACP bridge](../simplification/2026-07-23-acp-automation-only-protocol.md) does not carry tool presentation.
## Alternatives considered
diff --git a/.agents/notes/archived/architecture/2026-07-02-result-time-applied-hunk-diffs.zh.md b/.agents/notes/archived/architecture/2026-07-02-result-time-applied-hunk-diffs.zh.md
new file mode 100644
index 0000000000..b77ec75f0f
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-07-02-result-time-applied-hunk-diffs.zh.md
@@ -0,0 +1,58 @@
+# Agent Note: 结果时刻的 applied-hunk diff 用于文件变更
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-07-02-result-time-applied-hunk-diffs.md) | 中文
+
+## 问题
+
+[带标签的 render-intent 联合类型](2026-07-02-tool-render-intent-union.md)为 `dsh-tool-fs` 的 write/edit 在调用时刻提供 `card:'diff'`,纯粹从工具参数推导:write ⇒ `{oldText:null, newText:content}`(整个新文件),edit ⇒ `{oldText:old_string, newText:new_string}`(裸替换片段)。UI 可以将其渲染为行内 diff,但这是一个**无上下文**的 diff:裸的 `old_string`→`new_string` 没有周围行,而一次触及五个分散位置的 `replace_all` 仍然渲染为一对片段。
+
+在对接 `claude-agent-acp` 自身的 ACP(Agent Client Protocol) bridge 时可以看到完整编辑器 diff 的样子:变更应用后,它发出第二个 `tool_call_update`,其 diff 是**带 ±3 行上下文的 applied hunk**(`replace_all` 的每个变更位置各一个 hunk),由工具的 `structuredPatch` 重建。这个结果时刻的 hunk 正是让 Zed 在文件中*原位*显示变更(而非浮动片段)的关键。我们的工具止步于调用时刻的片段;完成后的结果只携带纯文本「updated successfully」,没有 diff。
+
+障碍在于一个 seam 边界:`presentResult(args, result)` 是 **`args` + 面向模型的 `result`(`{content, isError}`)的纯函数**——它在实时流式输出和会话日志回放中都会运行,因此必须具备回放确定性且不能做 I/O。它看不到文件的前后内容,而 `FsEditOutcome`/`FsWriteOutcome` 只携带替换计数和版本号,没有文本。因此无法计算——甚至无法携带——applied hunk 给 presenter。
+
+## 决策
+
+添加一个**持久化的、工具私有的展示通道**,使工具的 `execute` 能附加一个结果时刻的渲染载荷并在回放中存活,并用它来携带 applied-hunk diff。
+
+### 1. 规范工具输出上的可回放展示投影(core)
+
+原始实现允许 `execute` 返回 `{ content, meta }`。[规范工具输出契约](2026-07-20-canonical-tool-output-contract.md)取代了这种编写形态:每个工具如今返回一个由 schema 声明的 JSON 值,`output.render(args, value)` 从中派生面向模型的内容块,可选的 `output.presentationMeta(args, value)` 则派生可回放的 UI 数据。
+
+`presentationMeta` 是工具自有的 `JsonValue`,core 会持久化它,但不解释其中的字段。`Session.append` 将它与事件的其余部分一并校验,回放再把存储的载荷传回 `presentResult`;因此展示无需 I/O 或重新计算即可复现。规范值本身只存在于执行期间,不会加入会话格式。
+
+这仍是通用形态(「工具投影持久化的结果展示」),而非 fs 特有;任何工具都可以使用。
+
+### 2. 工具计算 hunk;后端返回 before/after(fs)
+
+按照 [capability-seam 拆分](2026-06-13-capability-seams.md),存储后端只返回**存储事实**,面向模型的工具拥有**展示**:
+
+- `dsh-fs` 将 `FsEditOutcome` 扩展为包含 `{ before: string; after: string }`,将 `FsWriteOutcome` 扩展为包含 `{ before: string | null; after: string }`(`before: null` 表示创建,或已存在但不可 diff 的二进制/非 UTF-8 文件)。本地后端在写入时已持有两份文本;它以原始 LF 规范化文本返回,**不让任何 diff/UI 概念进入 seam**。
+- `dsh-tool-fs` 返回规范的变更前/后事实,并将上下文 hunk 投影为 `meta: { diffs: FileDiff[] }`。成功的变更以 diff 视图完成:创建或无变化的覆写回退到由参数推导的整文件 diff,而编辑使用 applied hunk。失败的变更不携带 diff 元数据,正常渲染其错误信息。
+
+### 3. UI 传输层渲染 `diff` 结果视图
+
+`ToolResultView` 包含 `DiffResultView { card:'diff'; title?; diffs: FileDiff[] }`。TUI 与 JSON-RPC/Web 消费方在同一个带标签的视图上做 switch,用 applied 结果 hunk 替换待定调用的无上下文片段。[仅面向自动化的 ACP 桥接层](../simplification/2026-07-23-acp-automation-only-protocol.md)不承载工具展示。
+
+## 曾考虑的替代方案
+
+**手写或 vendor diff 算法。** 上下文 hunk 有已知的边界情况,因此 `dsh-tool-fs` 使用带类型的 [`diff`](https://www.npmjs.com/package/diff) 包,并在一个模块中规范化 `structuredPatch` 输出。仓库的 vendor 策略适用于框架源码,而非每个叶子工具库。
+
+## 后果
+
+`tool/result` 事件携带工具私有的 `meta` 载荷;它属于磁盘格式词汇的一部分,由 `Session.append` 在运行时限制为 JSON。任何工具都可以投影持久化的结果展示,无需再改 core。diff 卡片在会话重载和快照回放时免费复现:它从日志中读回,从不重新计算。代价:覆写操作在内存中同时持有旧文本和新文本以计算仅用于 UI 的 hunk(`TODO(overwrite-diff-bound)`),且 `dsh-tool-fs` 引入了一个小型、知名的运行时依赖。
+
+## 非目标
+
+- **实时增量 diff 流式输出。** hunk 在变更完成后一次性计算;没有逐键 diff。
+- **对二进制/非 UTF-8 覆写做 diff。** 此类文件的 `before` 为 `null`(没有文本 diff 基础);写入仍然成功,结果渲染整文件 diff(`oldText: null`)而非上下文 hunk。
+- **重命名/移动 diff。** 仅限单个已解析路径的内容 diff。
+- **限制覆写 diff 基础的大小。** 覆写操作将整个旧文件读入内存以计算上下文 hunk(加上已持有的新内容),因此非常大的文本覆写会为仅 UI 用途的 diff 分配两份文本。未来的改进可以设定预读上限,超过阈值时回退到整文件/无上下文 diff;在读取位置以 `TODO(overwrite-diff-bound)` 跟踪。
+
+## 相关
+
+- 补全了[带标签的 render-intent 联合类型](2026-07-02-tool-render-intent-union.md)中作为非目标列出的最后一项表示差异——该 Agent Note 的「非目标」一节已更新,记录 applied-hunk diff 在此处交付。
+- 基于[文件系统 capability seam](2026-06-17-filesystem-capability-seam.md)(before/after 是后端返回的存储事实)和[事件溯源会话](2026-06-11-event-sourced-sessions.md)(`meta` 载荷持久化在 `tool/result` 事件上,因此回放可复现卡片)。
+- `meta` 通道有意设计为通用的:未来的工具(结构化搜索、数据表结果)可以附加自己的持久化结果展示而无需再改 core。
diff --git a/.agents/notes/archived/architecture/2026-07-03-filesystem-directory-listing-seam.i18n.yaml b/.agents/notes/archived/architecture/2026-07-03-filesystem-directory-listing-seam.i18n.yaml
new file mode 100644
index 0000000000..860757f042
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-07-03-filesystem-directory-listing-seam.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
+2026-07-03-filesystem-directory-listing-seam.md: eb2650daf567d4bd98ed8553a4b743f5a01d945c
+2026-07-03-filesystem-directory-listing-seam.zh.md: 4bcda9f093f22c06348d4c69c4de3bd62f216f8e
diff --git a/.agents/notes/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md b/.agents/notes/archived/architecture/2026-07-03-filesystem-directory-listing-seam.md
similarity index 97%
rename from .agents/notes/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md
rename to .agents/notes/archived/architecture/2026-07-03-filesystem-directory-listing-seam.md
index d40c50695d..eb2650daf5 100644
--- a/.agents/notes/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md
+++ b/.agents/notes/archived/architecture/2026-07-03-filesystem-directory-listing-seam.md
@@ -1,6 +1,9 @@
# Agent Note: Add direct directory listing to the filesystem seam
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-03-filesystem-directory-listing-seam.zh.md)
## Problem
diff --git a/.agents/notes/archived/architecture/2026-07-03-filesystem-directory-listing-seam.zh.md b/.agents/notes/archived/architecture/2026-07-03-filesystem-directory-listing-seam.zh.md
new file mode 100644
index 0000000000..4bcda9f093
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-07-03-filesystem-directory-listing-seam.zh.md
@@ -0,0 +1,54 @@
+# Agent Note: 为文件系统 seam 添加直接目录列举能力
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-03-filesystem-directory-listing-seam.md) | 中文
+
+## 问题
+
+`@deepseek-ai/dsh-fs` 是文件系统访问的提供方 seam,本地后端与未来的非本地后端共享同一个 `ctx.fs` 契约。在本次变更之前,它能解析路径、stat 目标、读取文本、流式读取文本、写入文本和编辑文本。这对面向模型的文件工具已经足够,但对于需要枚举目录而又不想直接导入 `node:fs` 的非模型侧消费方来说还不够。
+
+直接的压力来自 skill(技能)加载:读取单个 `SKILL.md` 已经可以走 `ctx.get('fs')`,但发现哪些 skill 根目录包含 `/SKILL.md` 或 `.md` 仍需要目录枚举。如果仅在 `dsh-skill` 中添加目录列举,要么保留对 Node 的直接依赖,要么在文件系统提供方栈之外发明一个一次性的本地辅助函数。
+
+本决策只添加提供方能力,不涉及面向模型的 `ls`/`list` 工具或 skill 发现机制的变更。那些消费方需要独立的 UX、提示词与策略决策。
+
+## 决策
+
+在 `@deepseek-ai/dsh-fs` 中添加 `FileSystem.listDir(target, signal?)`。
+
+`listDir` 仅列举一层目录。它以稳定的名称顺序返回直接子项,包含以下字段:
+
+- `name`:子项的 basename;
+- `type`:`file`、`directory` 或 `other`;
+- `target`:已解析的子项 `FsTarget`;
+- `version`:可用时返回的轻量元数据;
+- `size`:可用时返回的常规文件大小。
+
+它从不读取文件内容。递归遍历、glob 匹配、分页、搜索、文件监听和面向模型的渲染均有意不在范围内。
+
+本地后端通过 `readdir({ withFileTypes: true })`、`resolveLocalTarget` 以及元数据 `stat`/`realpath` 探测来实现。结果顺序是确定性的(`name.localeCompare`),以保持未来消费方的提示词/列表输出稳定,并提高前缀缓存复用率。
+
+损坏或已消失的子项可以表示为 `type: 'other'`(不带 `version`/`size`);它们不会中止整个列举。在列举目录或解析/探测子项元数据时遇到权限或后端 I/O 故障,则以结构化的 `FsError` 错误码使整个列举失败:
+
+- `FS_NOT_FOUND`:目标不存在;
+- `FS_NOT_DIRECTORY`:目标存在但不是目录;
+- `FS_PERMISSION_DENIED`:权限不足;
+- `FS_IO_ERROR`:其他后端 I/O 故障;
+- `FS_ABORTED`:调用被中止。
+
+## 曾考虑的替代方案
+
+**在添加 seam 的同时添加面向模型的 list 工具。** 否决。其提示词、schema 和渲染契约与提供方原语相互独立。
+
+**让每个消费方自行枚举目录。** 否决。这会将 `dsh-skill` 等产品包绑定到 Node/本地文件系统行为上,绕过策略/远程/沙箱后端。
+
+**让 `listDir` 支持递归或 glob 形式。** 暂时否决。skill 根发现只需要直接子项,而简单的单层列举是未来消费方可以安全组合的最小后端契约。
+
+**跳过元数据解析失败的子项。** 否决。API 承诺返回已解析的子项 target,因此解析子项时的权限/IO 故障属于契约失败。损坏或已消失的子项是例外,因为它们仍可在不声称拥有一个活跃已解析文件的前提下被表示。
+
+## 后果
+
+每个文件系统后端现在必须多实现一个提供方原语。这是 harness 尚未发布时有意为之的基础工作,但也意味着未来的沙箱/远程后端需要定义等价的直接子项列举行为。
+
+该能力仍停留在提供方层面。在消费方落地之前,ACP(Agent Client Protocol)/模型会话仍需使用 `bash` 等既有工具来列举目录。缺少面向模型的 `listdir` 工具是预期行为,而非接线错误。
diff --git a/.agents/notes/archived/architecture/2026-07-05-windows-fs-permissions.i18n.yaml b/.agents/notes/archived/architecture/2026-07-05-windows-fs-permissions.i18n.yaml
new file mode 100644
index 0000000000..a05e6cec2d
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-07-05-windows-fs-permissions.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
+2026-07-05-windows-fs-permissions.md: da3aabd872156d04e27b8b5521486e1190ac1173
+2026-07-05-windows-fs-permissions.zh.md: 8cb3e90922894f1755e8861411a36463d1ec7367
diff --git a/.agents/notes/implemented/architecture/2026-07-05-windows-fs-permissions.md b/.agents/notes/archived/architecture/2026-07-05-windows-fs-permissions.md
similarity index 97%
rename from .agents/notes/implemented/architecture/2026-07-05-windows-fs-permissions.md
rename to .agents/notes/archived/architecture/2026-07-05-windows-fs-permissions.md
index 932b6ddbf4..da3aabd872 100644
--- a/.agents/notes/implemented/architecture/2026-07-05-windows-fs-permissions.md
+++ b/.agents/notes/archived/architecture/2026-07-05-windows-fs-permissions.md
@@ -1,6 +1,9 @@
# Agent Note: Windows write-permission semantics — inherited DACLs, not mode bits
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-05-windows-fs-permissions.zh.md)
The replacement-file decision in this record is superseded by [Windows DACL preservation](../bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.md).
diff --git a/.agents/notes/archived/architecture/2026-07-05-windows-fs-permissions.zh.md b/.agents/notes/archived/architecture/2026-07-05-windows-fs-permissions.zh.md
new file mode 100644
index 0000000000..8cb3e90922
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-07-05-windows-fs-permissions.zh.md
@@ -0,0 +1,34 @@
+# Agent Note: Windows 写入权限语义:继承 DACL,而非权限模式位
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-05-windows-fs-permissions.md) | 中文
+
+本记录中关于替换文件的决策已由 [Windows DACL 保留机制](../bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.md)取代。
+
+## 问题
+
+`writeFileAtomic` 在 `@deepseek-ai/dsh-fs-local` 中使用 POSIX 权限模式位保护正在写入的内容:以 `0o700` 创建暂存目录,以 `0o600` 打开临时文件,新文件也默认使用 `0o600`。在 POSIX 上,无论父目录的权限如何,这些设置都能保证临时内容仅对所有者可见。
+
+Windows 在同一 API 背后没有可用的对等机制。Node 的 `chmod` 在 Windows 上只会驱动只读属性(此包传入的每种模式都包含所有者写权限,因此这些调用是无害的空操作),`stat().mode` 则报告合成的 `0o666`/`0o444` 权限位。真正的安全状态由文件的 DACL 决定:新建文件或目录会从父目录继承,替换操作则需要由取代本文的 Agent Note 所定义的显式处理。
+
+## 决策
+
+Windows 新建文件使用目录继承,而不使用合成的权限模式位:暂存目录在目标的父目录(`dirname(absolutePath)`)内创建,因此它和临时文件都会继承目标目录的 DACL。替换文件遵循更严格的 [DACL 保留契约](../bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.md)。
+
+测试仅在 POSIX 上断言权限模式位。Windows 原生覆盖率锁定由本包(package)负责的替换行为;新文件继承仍属于操作系统契约,而不是针对特定机器的 ACL 允许清单。
+
+## 备选方案
+
+**为新文件显式设置仅所有者可用的 DACL。** 不予采纳,因为这会破坏继承,也会使特意共享项目目录的用户感到意外。替换写入会复制目标现有的 DACL,而不会自行设计仅所有者可用的策略。
+
+**在测试中验证 ACL。** `Get-Acl` SID 允许清单或 `icacls` 验证的是 Windows 继承机制以及当前机器的 `%TEMP%` ACL,而非包的行为;`icacls` 还会对知名账户名进行本地化,导致解析容易受语言区域影响。
+
+**在 Windows 上跳过 `chmod`。** 为无害的空操作调用增加平台守卫分支,不会改变任何行为。
+
+## 后果
+
+无论父目录的权限如何,POSIX 都会继续将临时内容限制为仅所有者可用。Windows 中的新目标如果位于广泛可访问的目录内,将按设计继承这种可访问性;如果替换目标存在更严格的 DACL,则会保留该 DACL。
+
+在 Windows 上,替换时的模式保留会退化为空操作:可写文件的探测结果为 `0o666`,通过 `chmod` 重放该模式会使只读属性继续保持清除状态。由于发布操作会在合成模式发挥作用前失败,Windows 上无法替换只读目标。
diff --git a/.agents/notes/implemented/architecture/2026-07-23-unified-session-query-service.i18n.yaml b/.agents/notes/archived/architecture/2026-07-23-unified-session-query-service.i18n.yaml
similarity index 62%
rename from .agents/notes/implemented/architecture/2026-07-23-unified-session-query-service.i18n.yaml
rename to .agents/notes/archived/architecture/2026-07-23-unified-session-query-service.i18n.yaml
index 2a27e6432f..0ac9bdaedf 100644
--- a/.agents/notes/implemented/architecture/2026-07-23-unified-session-query-service.i18n.yaml
+++ b/.agents/notes/archived/architecture/2026-07-23-unified-session-query-service.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
-2026-07-23-unified-session-query-service.md: 0a466e1c36ff1796c858666b0eb36bbd0f480bb0
-2026-07-23-unified-session-query-service.zh.md: 448122b8e6951058b9f633cd56112b0391e1912e
+2026-07-23-unified-session-query-service.md: f69836f60dfd73f9d8490687294b8407e53e9b32
+2026-07-23-unified-session-query-service.zh.md: bf25f4337dc8696ae54ffb16a7dd74437d45858b
diff --git a/.agents/notes/implemented/architecture/2026-07-23-unified-session-query-service.md b/.agents/notes/archived/architecture/2026-07-23-unified-session-query-service.md
similarity index 80%
rename from .agents/notes/implemented/architecture/2026-07-23-unified-session-query-service.md
rename to .agents/notes/archived/architecture/2026-07-23-unified-session-query-service.md
index 0a466e1c36..f69836f60d 100644
--- a/.agents/notes/implemented/architecture/2026-07-23-unified-session-query-service.md
+++ b/.agents/notes/archived/architecture/2026-07-23-unified-session-query-service.md
@@ -1,6 +1,7 @@
# Agent Note: Unified session query service
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-23-unified-session-query-service.zh.md)
@@ -16,6 +17,8 @@ The interface package already owns the shared record, filter, trace, search-requ
`SessionQuerySqlite` extends that service and is the sole concrete backend. One mounted instance therefore exposes every operation through `ctx.sessionQuery`; its inherited exact operations use the shared corpus implementation, while its SQLite-owned lifecycle observes sources, reconciles the derived FTS index, ranks matches, and owns cursor generations. The interface package has no standalone concrete plugin, search-provider registry, or second context key.
+SQLite reconciliation is one quiescent serialized state machine. It passes the caller's exact abort signal into durable snapshot listing and inspection, awaits each started backend operation itself, and checks cancellation after every await and before starting the next source or index operation. Cancellation therefore cannot release the serializer while an ignored or cooperative backend call is still cleaning up, and it cannot start a subsequent listing, inspection, reconciliation, or query after the signal is observed.
+
Backend configuration includes the inherited `readWindowMax` setting alongside its own index path, journal mode, page limits, and snippet limit. First-party apps that need session queries mount the SQLite backend and place its disposable index beside their configured persistence root.
This service topology supersedes the separate-key portion of the [exact query decision](../feature/2026-07-10-session-query-service.md) and [SQLite search decision](../feature/2026-07-10-sqlite-session-query-provider.md); their corpus, query, tokenizer, reconciliation, and safety decisions remain in force.
@@ -32,4 +35,6 @@ Consumers inject one service and can combine exact and full-text operations with
The unified object deliberately retains two internal observation strategies: exact operations read authoritative live/persisted sources per call, while full-text operations reconcile a disposable index. Sharing the context key does not make the derived index authoritative or couple exact-read availability to an FTS query.
+Queued cancellation remains prompt. Cancellation during active asynchronous source observation waits for that started operation to settle, which makes rejection a quiescence boundary and preserves single-file execution for a following search. Synchronous SQLite statements remain non-preemptible and are bracketed by signal checks.
+
Unit coverage pins inherited and abstract behavior on one key, SQLite coverage exercises both operation families on the concrete backend, and the real Loader path verifies that one exported plugin registers the combined service.
diff --git a/.agents/notes/implemented/architecture/2026-07-23-unified-session-query-service.zh.md b/.agents/notes/archived/architecture/2026-07-23-unified-session-query-service.zh.md
similarity index 80%
rename from .agents/notes/implemented/architecture/2026-07-23-unified-session-query-service.zh.md
rename to .agents/notes/archived/architecture/2026-07-23-unified-session-query-service.zh.md
index 448122b8e6..bf25f4337d 100644
--- a/.agents/notes/implemented/architecture/2026-07-23-unified-session-query-service.zh.md
+++ b/.agents/notes/archived/architecture/2026-07-23-unified-session-query-service.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 统一会话查询服务
Status: implemented
+Archived: 2026-07-26
[English](2026-07-23-unified-session-query-service.md) | 中文
@@ -16,6 +17,8 @@ Status: implemented
`SessionQuerySqlite` 扩展该服务,并且是唯一的具体后端。因此,一个挂载实例便可通过 `ctx.sessionQuery` 暴露全部操作;其继承的精确操作使用共享的语料库实现,而由 SQLite 管理的生命周期负责观察数据源、对齐派生 FTS 索引、对匹配项排序并管理游标代际。接口包不提供独立的具体插件、搜索提供方注册表或第二个上下文键。
+SQLite 的对齐过程是一个具备静止性保证的串行状态机。它将调用方的原始中止信号传给持久化快照列表与检查操作,直接等待每个已经启动的后端操作,并在每次等待后以及启动下一个数据源或索引操作前检查是否已取消。因此,即使后端忽略取消或正在配合清理,串行器也不会提前释放;观察到中止信号后,也不会再启动后续的列表、检查、对齐或查询操作。
+
后端配置除了自身的索引路径、日志模式、分页限制与文本片段长度上限外,还包含继承的 `readWindowMax` 设置。需要会话查询的第一方应用挂载 SQLite 后端,并将其可丢弃索引放在已配置的持久化根目录旁。
这一服务拓扑取代了[精确查询决策](../feature/2026-07-10-session-query-service.md)和 [SQLite 搜索决策](../feature/2026-07-10-sqlite-session-query-provider.md)中关于分离上下文键的部分;其中关于语料库、查询、分词器、对齐与安全性的决策仍然有效。
@@ -32,4 +35,6 @@ Status: implemented
统一后的对象有意保留两种内部观察策略:精确操作在每次调用时读取权威的实时源或持久化源,全文操作则使可丢弃索引与数据源对齐。共用上下文键不会让派生索引成为权威来源,也不会使精确读取的可用性依赖 FTS 查询。
+排队阶段的取消仍会及时生效。在异步数据源观察已经开始后取消时,调用方会等待该操作完成清理后才收到拒绝;因此拒绝本身构成静止边界,并保证后续搜索仍按单一串行流程执行。同步 SQLite 语句无法在执行中被抢占,服务会在其前后检查中止信号。
+
单元测试在同一个键上同时固定继承实现与抽象方法的契约,SQLite 测试在具体后端上覆盖两类操作,真实 Loader 路径则验证单个导出的插件能够注册组合后的服务。
diff --git a/.agents/notes/archived/architecture/2026-07-24-dsh-commander-argument-adapter.i18n.yaml b/.agents/notes/archived/architecture/2026-07-24-dsh-commander-argument-adapter.i18n.yaml
new file mode 100644
index 0000000000..57b242ea29
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-07-24-dsh-commander-argument-adapter.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
+2026-07-24-dsh-commander-argument-adapter.md: a5f6e580b91c0de1cdb433e1973bdff960384f06
+2026-07-24-dsh-commander-argument-adapter.zh.md: 4321ab154996c9b23ce58175b112234c92013cb8
diff --git a/.agents/notes/archived/architecture/2026-07-24-dsh-commander-argument-adapter.md b/.agents/notes/archived/architecture/2026-07-24-dsh-commander-argument-adapter.md
new file mode 100644
index 0000000000..a5f6e580b9
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-07-24-dsh-commander-argument-adapter.md
@@ -0,0 +1,54 @@
+# Agent Note: Parse `dsh` argv through one Commander adapter
+
+Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-24-dsh-commander-argument-adapter.zh.md)
+
+## Problem
+
+The `dsh` CLI entry (`apps/cli`) parsed argv in three hand-rolled idioms that did not compose and gave no `--help`/`--version`. `bin.ts` dispatched by raw inspection — `argv[0] === 'web'`, then `argv.includes('-p') || argv.includes('--prompt')`, else TUI — which is positional-blind: a prompt flag or a config path in the wrong position could misroute the mode, and `argv.includes('-p')` could not tell a real flag from an incidental token. `headless.ts` and `web.ts` each ran their own `node:util` `parseArgs` with inline host/port validation, and `dsh-app-boot` carried `parseResumeArg`, a ~30-line bespoke scanner reimplementing flag/`=`-form/value/repeat handling for `--resume`. Usage was a single hardcoded `usage: dsh -p "task"` line; there was no version flag and no rendered help.
+
+## Decision
+
+Argv is parsed once, in `apps/cli/src/args.ts`, through a Commander adapter (the same parser the SDK bins — `create-sdk`, `dsh-scripts` — already standardize on). `parseDshArgs(argv, version)` returns a discriminated `DshInvocation` union of the three real modes: `{ mode: 'tui', config?, resume? }`, `{ mode: 'headless', prompt }`, or `{ mode: 'web', host?, port?, dev }`. It does **not** model help/version/errors as data: Commander owns those, printing usage or the diagnostic and exiting at the point of failure. `exitOverride()` turns each into a thrown `CommanderError` carrying the intended code (0 for help/version, 1 for a parse or domain error), which one `try/catch` in `parseDshArgs` turns into `process.exit`.
+
+`bin.ts` calls the adapter once and switches on `mode` (closed union, `satisfies never` default), dynamic-importing only the chosen mode's module; only a valid, non-help invocation reaches the switch, so it has no help/version/error cases. Each mode module consumes already-parsed values: `runTui(config, resume)`, `runHeadless(task)`, `runWeb(host, port, dev, workspaceRoot)` — none re-reads argv. It is **one Commander program**: the default surface (no subcommand) carries option-only flags — `--config `, `-p/--prompt `, `--resume ` — and `web` is a real `program.command('web')` subcommand. The default surface takes no positional argument, which is what lets `web` be a real subcommand without a positional collision, so `dsh --help` lists `web` natively (no hand-pasted command text). The default action and the `web` action set the resolved mode, then bail via `command.error(...)` (print + exit 1) on the domain checks Commander cannot express: `--prompt` selects headless and rejects an empty task or a `--config`/`--resume` alongside it rather than silently dropping a TUI input; an empty `--resume=` id fails loud (agent-loop treats `''` as no-resume). Commander parses the default-surface options on either side of the `web` token into `program.opts()`; since `web` shares none of them, the `web` action rejects a leaked `--config`/`-p`/`--resume` (`dsh web -p x`, `dsh --config c.yml web`) rather than silently serving and dropping it. `dsh web`'s `--host`/`--port` are unvalidated pass-through overrides: the adapter assigns no default and does no validation, only `Number`-coercing the port string (the schema wants a number). The `dsh-host-webserver` schemastery `Config` (`host` a `127.0.0.1`/`0.0.0.0` literal union, `port` a natural ≤ 65535) is the single source of both the default (the shipped `apps/cli/cordis.yml` `webserver` row stands when a flag is absent) and validity — `AppCLIEntry` patches an explicit flag straight into that row, so a bad host/port fails loud at the schema on boot, not at parse. `--dev` mounts the client HMR driver and bundle watch, and `--workspace-root ` is a plain pass-through to `AppCLIEntry` (the parent directory for name-created workspaces). A repeated `--resume`, or a following flag captured as a `--resume`/`--prompt` value, is Commander's standard behavior (last-wins / next-token) and is left alone; a bad id fails loud downstream when the session cannot load. `--version` reads this app's `package.json`.
+
+`dsh` takes no positional argument. `--config ` names an alternate cordis tree to boot instead of the shipped default; it exists only so the demo/test call sites (`demo:cordis`, `demo:code-mode`, the keyless PTY smokes) can point the shipped bin at an example tree. A bare `dsh` boots the shipped tree plus the `~/.dsh/config.yaml` personal overlay; a real user never passes `--config`.
+
+CLI parsing lives entirely in `apps/cli`. `dsh-app-boot` holds the boot/env/config/personal-overlay helpers and no argv scanner.
+
+## Session resume through the boot context
+
+`dsh --resume ` is the one way to resume a persisted session, with no environment variable. `runTui` provides the parsed id on the boot context through `boot`'s `prepare(ctx)` hook — `ctx.provide(RESUME_SESSION_ID_KEY, id)` (a `dsh-app-boot` export, value `'resumeSessionId'`) — and the shipped tui-agent/cordis configs read it as a bare identifier: `resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"`. The expression is quoted because YAML otherwise parses the `?`/`:` as a mapping; the `typeof` guard tolerates a launcher that never provides the slot. The `/resume` in-place handoff (`process.execve`) rebuilds its re-exec argv from the parsed values as `dsh --resume= [--config ]`.
+
+## One terminal front door: `dsh`
+
+`dsh` is the only terminal entry point; the `dsh-tui-demo` package ships the TUI app bundle plugin the shipped config mounts, and no bin of its own. `demo:cordis`, `demo:code-mode`, and both the tui-agent and cordis-agent keyless PTY smokes launch through `apps/cli/src/bin.ts` with `--config `. `dsh`'s TTY guard (refuse piped stdio before booting, pointing at `dsh -p` for automation) is pinned by `apps/cli/tests/built-bin.e2e.ts`, which runs the built `lib/bin.js` under plain Node with piped stdio (`apps/cli/tests` is in the e2e vitest include). `cli-demo`, `acp-demo`, and `jsonrpc-demo` keep their own bins because each is a distinct surface (headless, ACP, JSON-RPC) `dsh` does not provide.
+
+## Package topology
+
+The argument surface stays inside `apps/cli`, the assembly tier, not a `packages/*` library: it is this one app's routing, not a reusable seam. `dsh-app-boot` shrinks to boot glue with no CLI-parsing responsibility. `commander@^15` is added to `apps/cli/package.json`, matching the SDK bins' pin.
+
+## Alternatives considered
+
+**Keep `node:util` `parseArgs` and only unify the dispatch** — rejected: `parseArgs` has no subcommand model, no rendered help, and no version flag, so `web` routing and `--help`/`--version` would stay hand-rolled. The repo already chose Commander for its other CLIs; a second parser idiom for `dsh` alone is the fragmentation this change removes.
+
+**Keep `parseResumeArg` as a shared helper and feed it Commander's residual args** — rejected: the whole point is to retire the bespoke scanner. Commander parses `--resume` (space and `=` forms, missing-value, position-independence) natively; keeping a parallel hand-written path for the one flag would preserve the duplication the change exists to end.
+
+**A bare `dsh ` positional for the alternate tree** — rejected: a root positional and a real `web` subcommand cannot coexist in one Commander program (the subcommand claims the first positional). A positional would force `web` into a reserved-first-token dispatch to a separate parser and a hand-maintained `web` line in `--help`. Only the demo/test sites ever need to name an alternate tree, so a `--config` flag serves them while leaving the default surface positional-free — `web` is then a normal subcommand in one program with native `--help`.
+
+**Make the argument surface a `packages/*` seam** — rejected: nothing outside `dsh` consumes it, and capability seams are not split preemptively. The Commander adapter is `apps/cli`'s own concern.
+
+**Keep `RESUME_SESSION_ID` as the resume bridge** — rejected: with `--resume` parsed into a value the bin already holds, threading it through an environment variable the config re-reads is indirection with no benefit, and it left the demo bin a second, env-only resume path. Providing the id on the boot context is the same channel `boot`'s `prepare` hook already uses for `tuiResumeHost`.
+
+**Keep the `dsh-tui-demo` bin** — rejected: it duplicated `dsh --config ` exactly, and keeping it forced the demo-only `RESUME_SESSION_ID` fallback to stay alive. Its plugin is what the configs actually mount; only the front-door bin was redundant, and `dsh` is the one terminal entry point.
+
+## Testing
+
+`apps/cli/tests/args.spec.ts` (new; `apps/*/tests` added to the vitest include and `apps/cli/tests` to `tsconfig.host.json`) covers the adapter at the level that matters: mode routing by shape (including `web --dev` and the host/port pass-through), the exit-code behavior for the adapter's fail-loud checks (empty resume/prompt, `--prompt` mixed with a config/`--resume`, unknown option, stray positional), and `--help`/`--version`, captured through a `process.exit` spy. Host/port validity is the webserver schema's job, exercised on boot by the web smoke, not the adapter spec. Both PTY smoke groups in `examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` now drive the real `apps/cli/src/bin.ts`: the `tui-agent` group boots an example tree through `--config`, and the `dsh CLI` group covers default boot, personal overlay, invalid config, the `--resume` config intake, the `process.execve` in-place resume handoff, and the source-path prompt. `examples/cordis-agent/tests/keyless-smoke.e2e.ts` likewise launches through `dsh`. `packages/ui/app-boot/tests/app-boot.spec.ts` drops its `parseResumeArg`/`replaceResumeArg` blocks; the TUI unit and snapshot fixtures use the `dsh --resume {session}` resume command.
+
+## Consequences
+
+`dsh` has rendered `--help`/`--version` and consistent fail-loud parse errors, and mode routing does not depend on flag position. Argv parsing lives in one place with one parser idiom shared with the SDK bins, at the cost of a `commander` dependency on `apps/cli` and Commander's parse semantics (its error strings, its `exitOverride` contract) sitting on the CLI's front door. `dsh-app-boot` owns no CLI-parsing surface; a consumer needing `--resume`-style parsing composes Commander. Session resume rides the boot context rather than an environment variable, and `dsh` is the single terminal front door — the `dsh-tui-demo` package is a plugin bundle a config mounts.
diff --git a/.agents/notes/archived/architecture/2026-07-24-dsh-commander-argument-adapter.zh.md b/.agents/notes/archived/architecture/2026-07-24-dsh-commander-argument-adapter.zh.md
new file mode 100644
index 0000000000..4321ab1549
--- /dev/null
+++ b/.agents/notes/archived/architecture/2026-07-24-dsh-commander-argument-adapter.zh.md
@@ -0,0 +1,54 @@
+# Agent Note: 通过单个 Commander 适配器解析 `dsh` 的 argv
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-24-dsh-commander-argument-adapter.md) | 中文
+
+## 问题
+
+`dsh` 的 CLI(命令行界面)入口(`apps/cli`)以三种手写方式解析 argv,这些方式无法组合,也不提供 `--help`/`--version`。`bin.ts` 通过原始检查进行分发:先判断 `argv[0] === 'web'`,再判断 `argv.includes('-p') || argv.includes('--prompt')`,否则走 TUI。这种方式对位置不敏感:位置错误的 prompt 标志或配置路径可能把模式路由错,而 `argv.includes('-p')` 无法区分真正的标志和偶然出现的 token。`headless.ts` 和 `web.ts` 各自运行自己的 `node:util` `parseArgs`,并内联校验 host/port,而 `dsh-app-boot` 携带 `parseResumeArg`——一个约 30 行的定制扫描器,为 `--resume` 重新实现了标志、`=` 形式、取值和重复的处理。用法说明只有一行硬编码的 `usage: dsh -p "task"`;既没有版本标志,也没有渲染出的帮助信息。
+
+## 决策
+
+argv 只在 `apps/cli/src/args.ts` 中解析一次,并使用 Commander 适配器(SDK bin `create-sdk`、`dsh-scripts` 已经统一采用的同一解析器)。`parseDshArgs(argv, version)` 返回仅包含三种实际模式的判别式 `DshInvocation` 联合类型:`{ mode: 'tui', config?, resume? }`、`{ mode: 'headless', prompt }` 或 `{ mode: 'web', host?, port?, dev }`。它**不会**将帮助、版本信息或错误建模为数据:这些情况由 Commander 处理,在触发处打印用法或诊断信息并退出。`exitOverride()` 会将每种情况转为抛出的 `CommanderError`,并携带预期退出码(帮助或版本为 0,解析错误或领域错误为 1);唯一一处 `try/catch` 位于 `parseDshArgs` 中,捕获错误后调用 `process.exit`。
+
+`bin.ts` 只调用适配器一次,并对 `mode` 做分支切换(封闭联合类型,默认分支为 `satisfies never`),仅动态导入所选模式对应的模块;只有合法的非帮助请求才会进入这段分支逻辑,因此其中没有帮助、版本或错误分支。每个模式模块只消费已解析好的值:`runTui(config, resume)`、`runHeadless(task)`、`runWeb(host, port, dev, workspaceRoot)`,都不会再次读取 argv。整个 CLI 由**单个 Commander 程序**实现:默认接口(不使用子命令时)只包含选项标志——`--config `、`-p/--prompt `、`--resume `——而 `web` 是通过 `program.command('web')` 定义的真正子命令。默认接口不接受位置参数,因此 `web` 可以成为真正的子命令且不会发生位置参数冲突,`dsh --help` 也会原生列出 `web`,无需手工拼接命令文本。默认命令和 `web` 子命令的处理函数会设置解析得到的模式,随后对 Commander 无法表达的领域校验调用 `command.error(...)` 立即终止(打印信息并以退出码 1 退出):`--prompt` 选择 headless 模式;如果任务为空,或调用中还包含 `--config` 或 `--resume`,它会拒绝调用,而不会静默丢弃 TUI 输入;空的 `--resume=` id 会显式失败(agent-loop 把 `''` 视为不恢复)。Commander 会将 `web` token 前后的默认接口选项都解析进 `program.opts()`;由于 `web` 不与默认接口共用任何选项,`web` 子命令的处理函数会拒绝误入的 `--config`/`-p`/`--resume`(`dsh web -p x`、`dsh --config c.yml web`),而不是静默启动服务并丢弃这些选项。`dsh web` 的 `--host`/`--port` 是未经校验、直接透传的覆盖值:适配器既不设置默认值,也不执行校验,只使用 `Number` 将端口字符串转换为数字(schema 要求该值为数字)。`dsh-host-webserver` 的 schemastery `Config`(`host` 是 `127.0.0.1`/`0.0.0.0` 字面量联合类型,`port` 是不大于 65535 的自然数)是默认值与有效性的唯一真源:未提供标志时,随产品提供的 `apps/cli/cordis.yml` 中 `webserver` 配置项保持原值;`AppCLIEntry` 将显式标志的值直接写入该配置项,因此无效的 host/port 会在启动时触发 schema 校验并显式失败,而不是在参数解析阶段失败。`--dev` 会挂载客户端 HMR(热模块替换)驱动,并启用构建产物监视,`--workspace-root ` 则是直接透传给 `AppCLIEntry` 的选项(按名称创建 workspace 时使用的父目录)。重复提供 `--resume`,或后续标志被捕获为 `--resume` 或 `--prompt` 的值,都是 Commander 的标准行为(最后一次取值生效/将下一 token 作为值),本适配器不作干预;无效 id 会在下游无法加载会话时显式失败。`--version` 读取本应用的 `package.json`。
+
+`dsh` 不接受位置参数。`--config ` 指定一份替代 Cordis 配置树,系统启动该配置树而不是随产品提供的默认配置树;该标志仅用于让演示和测试调用点(`demo:cordis`、`demo:code-mode`、无密钥 PTY 冒烟测试)通过随产品提供的 bin 启动一份示例树。直接运行 `dsh` 会启动随产品提供的配置树,并叠加 `~/.dsh/config.yaml` 个人覆盖;实际用户从不传入 `--config`。
+
+CLI 解析完全位于 `apps/cli` 中。`dsh-app-boot` 提供启动、环境变量、配置和个人覆盖辅助函数,不包含 argv 扫描器。
+
+## 通过启动上下文恢复会话
+
+`dsh --resume ` 是恢复持久化会话的唯一方式,无需环境变量。`runTui` 通过 `boot` 的 `prepare(ctx)` 钩子,在启动上下文中提供已解析的 id:`ctx.provide(RESUME_SESSION_ID_KEY, id)`(`dsh-app-boot` 的一项导出,值为 `'resumeSessionId'`);随产品提供的 tui-agent/cordis 配置将该值作为裸标识符读取:`resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"`。这个表达式需要加引号,否则 YAML 会把 `?` 和 `:` 解析为映射;`typeof` 守卫使从未提供该槽位的启动器也能正常运行。`/resume` 原地交接(`process.execve`)根据已解析的值将重新执行时的 argv 构造成 `dsh --resume= [--config ]`。
+
+## 唯一的终端入口:`dsh`
+
+`dsh` 是唯一的终端入口;`dsh-tui-demo` 包(package)提供 TUI 应用组合插件,随产品提供的配置会挂载该插件,而该包不提供自己的 bin。`demo:cordis`、`demo:code-mode` 以及 tui-agent 和 cordis-agent 的无密钥 PTY 冒烟测试都通过 `apps/cli/src/bin.ts` 启动,并传入 `--config `。`dsh` 的 TTY 守卫会在启动前拒绝标准输入输出接入管道的调用,并提示自动化场景使用 `dsh -p`;`apps/cli/tests/built-bin.e2e.ts` 锁定了这一行为:该测试将标准输入输出接入管道,并通过普通 Node 运行构建后的 `lib/bin.js`(e2e Vitest 的 include 包含 `apps/cli/tests`)。`cli-demo`、`acp-demo` 和 `jsonrpc-demo` 保留各自的 bin,因为它们分别提供 `dsh` 所没有的独立接口(headless、ACP(Agent Client Protocol)、JSON-RPC)。
+
+## 包拓扑
+
+参数解析留在 `apps/cli`(组装层)内,而不是 `packages/*` 库中:它是这一个应用自身的路由,而非可复用的 seam。`dsh-app-boot` 收缩为纯粹的 boot 胶水代码,不再承担 CLI 解析职责。`commander@^15` 被加入 `apps/cli/package.json`,与 SDK bin 锁定的版本一致。
+
+## 考虑过的替代方案
+
+**保留 `node:util` `parseArgs`,只统一分发。** 已否决:`parseArgs` 没有子命令模型、没有渲染出的帮助、也没有版本标志,因此 `web` 路由和 `--help`/`--version` 仍将保持手写。本仓库其他 CLI 已经选择了 Commander;单独为 `dsh` 引入第二套解析器方式,正是这次变更要消除的碎片化。
+
+**保留 `parseResumeArg` 作为共享辅助函数,并向它喂入 Commander 的残余参数。** 已否决:整件事的核心就是要退役这个定制扫描器。Commander 原生解析 `--resume`(空格和 `=` 形式、缺值、位置无关性);为这一个标志保留一条平行的手写路径,只会保留这次变更要终结的重复。
+
+**使用裸 `dsh ` 位置参数指定替代配置树。** 已否决:根级位置参数与真正的 `web` 子命令无法在同一个 Commander 程序中共存(子命令会占用第一个位置参数)。位置参数会迫使系统把位于首位的 `web` 作为保留 token 分发给另一个解析器,并手工维护一行 `web` 文本,供 `--help` 显示。只有演示和测试调用点需要指定替代配置树,因此 `--config` 标志既能满足这些调用点,又能让默认接口不包含位置参数;这样,`web` 就能在单个程序中成为普通子命令,并由原生 `--help` 展示。
+
+**把参数解析做成 `packages/*` 的 seam。** 已否决:`dsh` 之外没有任何消费方使用它,而能力 seam 不应被提前拆分。这个 Commander 适配器是 `apps/cli` 自身的事务。
+
+**保留 `RESUME_SESSION_ID` 作为恢复通道**:不予采纳。`--resume` 已被解析成 bin 当前持有的值;若再通过环境变量传递并由配置重新读取,只会引入无益的间接层,还会使演示 bin 保留第二条仅依赖环境变量的恢复路径。在启动上下文中提供 id,与 `boot` 的 `prepare` 钩子为 `tuiResumeHost` 提供值所采用的是同一通道。
+
+**保留 `dsh-tui-demo` bin**:不予采纳。它与 `dsh --config ` 的功能完全重复;保留它还会迫使演示专用的 `RESUME_SESSION_ID` 回退路径继续存在。配置实际挂载的是该包的插件;冗余的只有作为终端入口的 bin,而 `dsh` 是唯一的终端入口。
+
+## 测试
+
+`apps/cli/tests/args.spec.ts`(新增;`apps/*/tests` 加入 vitest include,`apps/cli/tests` 加入 `tsconfig.host.json`)覆盖适配器的关键行为:根据参数形态进行模式路由(包括 `web --dev` 和 host/port 透传),并通过 `process.exit` spy 捕获适配器的显式报错检查(恢复 id 或提示词为空、`--prompt` 与配置或 `--resume` 混用、未知选项、多余的位置参数)以及 `--help`/`--version` 的退出码。host/port 的有效性由 webserver schema 负责,并由 web 冒烟测试在启动时验证,不属于适配器测试的覆盖范围。`examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` 中的两组 PTY 冒烟测试现在都驱动真实的 `apps/cli/src/bin.ts`:`tui-agent` 组通过 `--config` 启动示例树,`dsh CLI` 组覆盖默认启动、个人覆盖、无效配置、配置对 `--resume` 的接收、通过 `process.execve` 原地恢复交接,以及包含源码路径的系统提示词。`examples/cordis-agent/tests/keyless-smoke.e2e.ts` 同样通过 `dsh` 启动。`packages/ui/app-boot/tests/app-boot.spec.ts` 移除其 `parseResumeArg` 和 `replaceResumeArg` 测试块;TUI 单元测试和快照 fixture(测试前置数据)使用 `dsh --resume {session}` 恢复命令。
+
+## 影响
+
+`dsh` 会渲染 `--help`/`--version`,并以一致方式显式报告解析错误;模式路由不依赖标志位置。argv 解析集中在一处,并与 SDK bin 共用一套解析器方式,代价是 `apps/cli` 依赖 `commander`,且 Commander 的解析语义(错误字符串和 `exitOverride` 契约)成为 CLI 入口的一部分。`dsh-app-boot` 不提供任何 CLI 解析接口;需要 `--resume` 式解析的消费方通过组合 Commander 来实现。会话恢复通过启动上下文完成,而不使用环境变量;`dsh` 是唯一的终端入口;`dsh-tui-demo` 包是由配置挂载的插件组合包。
diff --git a/.agents/notes/implemented/bug-fix/2026-07-20-code-mode-result-card-completeness.i18n.yaml b/.agents/notes/archived/bug-fix/2026-07-20-code-mode-result-card-completeness.i18n.yaml
similarity index 61%
rename from .agents/notes/implemented/bug-fix/2026-07-20-code-mode-result-card-completeness.i18n.yaml
rename to .agents/notes/archived/bug-fix/2026-07-20-code-mode-result-card-completeness.i18n.yaml
index 8453907b5c..dc1bbe851e 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-20-code-mode-result-card-completeness.i18n.yaml
+++ b/.agents/notes/archived/bug-fix/2026-07-20-code-mode-result-card-completeness.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
-2026-07-20-code-mode-result-card-completeness.md: 03c14cd780832fa03977dade2c7d14feb0399369
-2026-07-20-code-mode-result-card-completeness.zh.md: 45047cc5bcb8b74668702302077ff91fd3ff6bdc
+2026-07-20-code-mode-result-card-completeness.md: aff755e40b238e7ee448013fe0063bb26450fffe
+2026-07-20-code-mode-result-card-completeness.zh.md: 275e870c4be73e1adaa485f4e9fb979054fbf7c2
diff --git a/.agents/notes/implemented/bug-fix/2026-07-20-code-mode-result-card-completeness.md b/.agents/notes/archived/bug-fix/2026-07-20-code-mode-result-card-completeness.md
similarity index 60%
rename from .agents/notes/implemented/bug-fix/2026-07-20-code-mode-result-card-completeness.md
rename to .agents/notes/archived/bug-fix/2026-07-20-code-mode-result-card-completeness.md
index 03c14cd780..aff755e40b 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-20-code-mode-result-card-completeness.md
+++ b/.agents/notes/archived/bug-fix/2026-07-20-code-mode-result-card-completeness.md
@@ -1,12 +1,13 @@
# Agent Note: Keep the Code Mode result card complete
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-20-code-mode-result-card-completeness.zh.md)
## Problem
-The outer `run_code` tool persisted complete rendered content, but its editor presenter ignored that content and rebuilt the card body from a logs-only `presentationMeta` projection. A result-only run appeared correct because an empty presenter body let ACP and TUI fall back to `tool/result.content`. Once the program emitted a log, the presenter supplied non-empty content, that fallback stopped, and the returned value disappeared from the completed card. A spill policy's final head/tail preview was vulnerable to the same split ownership whenever captured logs made the stale projection non-empty.
+The outer `run_code` tool persisted complete rendered content, but its UI presenter ignored that content and rebuilt the card body from a logs-only `presentationMeta` projection. A result-only run appeared correct because an empty presenter body let consumers fall back to `tool/result.content`. Once the program emitted a log, the presenter supplied non-empty content, that fallback stopped, and the returned value disappeared from the completed card. A spill policy's final head/tail preview was vulnerable to the same split ownership whenever captured logs made the stale projection non-empty.
Nested Code calls never owned cards, so producing metadata for the outer call solely to reconstruct one incomplete card also obscured the intended one-card boundary.
@@ -16,13 +17,13 @@ The canonical tool registry pipeline owns the final model-facing outer content.
`run_code` omits `presentResult`. The established generic result fallback keeps the pending program title and renders the raw final `tool/result.content`; that durable, replayable, post-policy projection is the card's only result-content source. The host API proxy therefore omits a separate result view instead of serializing the same content in both `event.data.content` and `view.view.content`. The redundant logs-only `presentationMeta` projection remains removed.
-Nested dispatch remains unchanged. Calls marked by `exec.parent` emit bounded `tool/code-dispatch` diagnostics but no `tool/call` or `tool/result` surface cards, so one outer `run_code` invocation still produces exactly one card.
+Nested dispatch remains unchanged. Calls marked by `exec.parent` emit `tool/code-dispatch` events (full rendered content) but no `tool/call` or `tool/result` surface cards, so one outer `run_code` invocation still produces exactly one card.
## Testing
Tool unit coverage drives logs-only, result-only, logs-plus-result, no-output, spilled-result, and failure outcomes through the canonical registry, then pins the durable content and absence of a result presenter. A host-mux regression uses a call-only presenter to prove the result frame carries raw content exactly once and no view. These cases prove stale metadata cannot replace final content without making the host duplicate that content.
-The keyless ACP and TUI Code Mode snapshots execute one outer program that performs two nested bash calls, logs `captured output`, and returns `CODE_ONE+CODE_TWO`. Both surfaces show one completed outer card containing both lines and no nested cards.
+The keyless ACP backend and TUI Code Mode snapshots execute one outer program that performs two nested bash calls, logs `captured output`, and returns `CODE_ONE+CODE_TWO`. The persisted ACP log pins the complete result; the TUI surface shows one completed outer card containing both lines and no nested cards.
## Alternatives considered
@@ -30,10 +31,10 @@ The keyless ACP and TUI Code Mode snapshots execute one outer program that perfo
**Merge presenter metadata with `result.content`.** Rejected because the rendered content already contains the logs; merging would duplicate them and require brittle deduplication.
-**Forward `result.content` through a generic result presenter.** Rejected because the durable event already carries that content and ACP/TUI already have a generic raw-content fallback. The host mux serializes a tool-owned result view beside the event, so forwarding would duplicate the rendered content in one frame merely to recreate the fallback; the default worker alone admits a 64 MiB variable-payload budget before rendering.
+**Forward `result.content` through a generic result presenter.** Rejected because the durable event already carries that content and UI consumers already have a generic raw-content fallback. The host mux serializes a tool-owned result view beside the event, so forwarding would duplicate the rendered content in one frame merely to recreate the fallback; the default worker alone admits a 64 MiB variable-payload budget before rendering.
**Create one card per nested dispatch.** Rejected because intermediate values are intentionally execution-local and never model-facing. Multiple cards would expose an implementation trace instead of the single Code Mode operation the model and user invoked.
## Consequences
-ACP and TUI display the same complete content the model receives and replay persists, including post-policy spill previews, through their generic result fallback. The host API retains the pending program title without duplicating the raw result in a separate view payload. New `run_code` results no longer carry the optional logs metadata, but this requires no session-format bump: existing records remain valid because presentation reads their durable rendered content.
+TUI and JSON-RPC/Web display the same complete content the model receives and replay persists, including post-policy spill previews, through their generic result fallback. The host API retains the pending program title without duplicating the raw result in a separate view payload. New `run_code` results no longer carry the optional logs metadata, but this requires no session-format bump: existing records remain valid because presentation reads their durable rendered content.
diff --git a/.agents/notes/implemented/bug-fix/2026-07-20-code-mode-result-card-completeness.zh.md b/.agents/notes/archived/bug-fix/2026-07-20-code-mode-result-card-completeness.zh.md
similarity index 62%
rename from .agents/notes/implemented/bug-fix/2026-07-20-code-mode-result-card-completeness.zh.md
rename to .agents/notes/archived/bug-fix/2026-07-20-code-mode-result-card-completeness.zh.md
index 45047cc5bc..275e870c4b 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-20-code-mode-result-card-completeness.zh.md
+++ b/.agents/notes/archived/bug-fix/2026-07-20-code-mode-result-card-completeness.zh.md
@@ -1,12 +1,13 @@
# Agent Note: 保证 Code Mode 结果卡片内容完整
Status: implemented
+Archived: 2026-07-26
[English](2026-07-20-code-mode-result-card-completeness.md) | 中文
## 问题
-外层 `run_code` 工具会持久化完整的渲染内容,但编辑器的卡片展示逻辑忽略了这些内容,转而根据仅含日志的 `presentationMeta` 投影重新构建卡片正文。仅有结果的运行看似正确,是因为展示逻辑未提供正文时,ACP 和 TUI 会回退到 `tool/result.content`。只要程序输出一条日志,展示逻辑就会提供非空内容,回退随即停止,返回值便会从完成态卡片中消失。当已捕获的日志使陈旧投影变为非空时,输出落盘策略最终生成的头尾预览也会受到同一职责拆分的影响。
+外层 `run_code` 工具会持久化完整的渲染内容,但其 UI 展示器忽略了这些内容,转而根据仅含日志的 `presentationMeta` 投影重新构建卡片正文。仅有结果的运行看似正确,是因为展示器正文为空时,消费方会回退到 `tool/result.content`。只要程序输出一条日志,展示器就会提供非空内容,回退随即停止,返回值便会从完成态卡片中消失。当已捕获的日志使陈旧投影变为非空时,输出落盘策略最终生成的头尾预览也会受到同一职责拆分的影响。
嵌套 Code 调用从不生成自己的卡片。因此,仅仅为了重建这一张不完整卡片而给外层调用生成元数据,还掩盖了每次外层调用只生成一张卡片的预期边界。
@@ -16,13 +17,13 @@ Status: implemented
`run_code` 不提供 `presentResult`。既有的通用结果回退机制会保留待完成的程序标题,并渲染原始的最终 `tool/result.content`;这一持久、可回放且经过 post-policy 处理的投影是卡片中结果内容的唯一来源。宿主 API 代理因此不提供单独的结果视图,而不会在 `event.data.content` 与 `view.view.content` 中重复序列化同一内容。冗余的仅含日志的 `presentationMeta` 投影继续保持移除状态。
-嵌套分发保持不变。带有 `exec.parent` 标记的调用会发出有界的 `tool/code-dispatch` 诊断,但不会生成与 `tool/call` 或 `tool/result` 对应的界面卡片,因此一次外层 `run_code` 调用仍然只会生成一张卡片。
+嵌套分发保持不变。带有 `exec.parent` 标记的调用会发出 `tool/code-dispatch` 事件(携带完整渲染内容),但不会生成与 `tool/call` 或 `tool/result` 对应的界面卡片,因此一次外层 `run_code` 调用仍然只会生成一张卡片。
## 测试
工具单元测试通过规范注册表覆盖仅有日志、仅有结果、日志与结果并存、无输出、结果落盘和失败的结果,然后固定持久内容以及结果展示器不存在这一事实。宿主 mux 回归测试使用仅有调用的展示器,证明结果帧恰好携带一次原始内容,且不含视图。这些案例证明陈旧元数据无法替换最终内容,同时不会让宿主重复该内容。
-无密钥的 ACP 与 TUI Code Mode 快照会执行一个外层程序:程序进行两次嵌套 bash 调用,记录 `captured output`,并返回 `CODE_ONE+CODE_TWO`。两个界面都只显示一张完成态外层卡片,其中包含这两行内容,且没有嵌套卡片。
+无密钥的 ACP(Agent Client Protocol)后端快照与 TUI Code Mode 快照会执行一个外层程序:程序进行两次嵌套 bash 调用,记录 `captured output`,并返回 `CODE_ONE+CODE_TWO`。ACP 持久化日志固定完整结果;TUI 界面只显示一张完成态外层卡片,其中包含这两行内容,且没有嵌套卡片。
## 备选方案
@@ -30,10 +31,10 @@ Status: implemented
**把展示元数据与 `result.content` 合并:**不予采纳。渲染内容已经包含日志;合并会造成重复,还需要依赖脆弱的去重逻辑。
-**通过通用结果展示器转发 `result.content`:**不予采纳。持久事件已经携带该内容,ACP 和 TUI 也已有通用的原始内容回退机制。宿主 mux 会在事件旁序列化工具拥有的结果视图,因此转发仅仅是为了重建该回退机制,却会在一个帧中重复渲染内容;仅默认 worker 在渲染前允许 64 MiB 的可变载荷预算。
+**通过通用结果展示器转发 `result.content`:**不予采纳。持久事件已经携带该内容,UI 消费方也已有通用的原始内容回退机制。宿主 mux 会在事件旁序列化工具拥有的结果视图,因此转发仅仅是为了重建该回退机制,却会在一个帧中重复渲染内容;仅默认 worker 在渲染前允许 64 MiB 的可变载荷预算。
**为每次嵌套分发创建一张卡片:**不予采纳。中间值有意只存在于执行期间,永远不面向模型。多张卡片会暴露实现轨迹,而不是模型与用户调用的单次 Code Mode 操作。
## 影响
-ACP 和 TUI 通过通用结果回退机制显示与模型接收及回放持久化相同的完整内容,其中包括 post-policy 输出落盘预览。宿主 API 保留待完成的程序标题,同时不在单独的视图负载中重复原始结果。新的 `run_code` 结果不再携带可选的日志元数据,但无需提升会话格式版本:现有记录仍然有效,因为展示逻辑会读取其中持久化的渲染内容。
+TUI 与 JSON-RPC/Web 通过通用结果回退机制显示与模型接收及回放持久化相同的完整内容,其中包括 post-policy 输出落盘预览。宿主 API 保留待完成的程序标题,同时不在单独的视图负载中重复原始结果。新的 `run_code` 结果不再携带可选的日志元数据,但无需提升会话格式版本:现有记录仍然有效,因为展示逻辑会读取其中持久化的渲染内容。
diff --git a/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.i18n.yaml b/.agents/notes/archived/bug-fix/2026-07-22-collapsed-sidebar-control-rail.i18n.yaml
similarity index 62%
rename from .agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.i18n.yaml
rename to .agents/notes/archived/bug-fix/2026-07-22-collapsed-sidebar-control-rail.i18n.yaml
index 19e9446f50..5e586b2802 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.i18n.yaml
+++ b/.agents/notes/archived/bug-fix/2026-07-22-collapsed-sidebar-control-rail.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
-2026-07-22-collapsed-sidebar-control-rail.md: 940fcabf126941cc0e411b01c337e45831e442aa
-2026-07-22-collapsed-sidebar-control-rail.zh.md: 70ace36fafcb28aa714000262e31c8555d394854
+2026-07-22-collapsed-sidebar-control-rail.md: 6b61f5c64f3f1db19a2e242e9d9f054f30cb470c
+2026-07-22-collapsed-sidebar-control-rail.zh.md: b487950fc2c261060c44ad5b0ddc5820ca326006
diff --git a/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.md b/.agents/notes/archived/bug-fix/2026-07-22-collapsed-sidebar-control-rail.md
similarity index 99%
rename from .agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.md
rename to .agents/notes/archived/bug-fix/2026-07-22-collapsed-sidebar-control-rail.md
index 940fcabf12..6b61f5c64f 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.md
+++ b/.agents/notes/archived/bug-fix/2026-07-22-collapsed-sidebar-control-rail.md
@@ -1,6 +1,7 @@
# Agent Note: A collapsed sidebar retains its control rail
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-22-collapsed-sidebar-control-rail.zh.md)
diff --git a/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.zh.md b/.agents/notes/archived/bug-fix/2026-07-22-collapsed-sidebar-control-rail.zh.md
similarity index 99%
rename from .agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.zh.md
rename to .agents/notes/archived/bug-fix/2026-07-22-collapsed-sidebar-control-rail.zh.md
index 70ace36faf..b487950fc2 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.zh.md
+++ b/.agents/notes/archived/bug-fix/2026-07-22-collapsed-sidebar-control-rail.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 侧边栏折叠后保留控制栏
Status: implemented
+Archived: 2026-07-26
[English](2026-07-22-collapsed-sidebar-control-rail.md) | 中文
diff --git a/.agents/notes/implemented/bug-fix/2026-07-23-demo-web-builds-client-bundles.i18n.yaml b/.agents/notes/archived/bug-fix/2026-07-23-demo-web-builds-client-bundles.i18n.yaml
similarity index 62%
rename from .agents/notes/implemented/bug-fix/2026-07-23-demo-web-builds-client-bundles.i18n.yaml
rename to .agents/notes/archived/bug-fix/2026-07-23-demo-web-builds-client-bundles.i18n.yaml
index 49482469f1..06fdf67ec7 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-23-demo-web-builds-client-bundles.i18n.yaml
+++ b/.agents/notes/archived/bug-fix/2026-07-23-demo-web-builds-client-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
-2026-07-23-demo-web-builds-client-bundles.md: a7d21987d4544246fd3c53864cedfc86279e9440
-2026-07-23-demo-web-builds-client-bundles.zh.md: f10184642b0c7869378802d3040ebf4dbe67d4e0
+2026-07-23-demo-web-builds-client-bundles.md: abd031c4ee6aeb7ed8c0baa61dfac16e5d64cc33
+2026-07-23-demo-web-builds-client-bundles.zh.md: 70604b344b5607b01815382da316805a9beaf27e
diff --git a/.agents/notes/implemented/bug-fix/2026-07-23-demo-web-builds-client-bundles.md b/.agents/notes/archived/bug-fix/2026-07-23-demo-web-builds-client-bundles.md
similarity index 99%
rename from .agents/notes/implemented/bug-fix/2026-07-23-demo-web-builds-client-bundles.md
rename to .agents/notes/archived/bug-fix/2026-07-23-demo-web-builds-client-bundles.md
index a7d21987d4..abd031c4ee 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-23-demo-web-builds-client-bundles.md
+++ b/.agents/notes/archived/bug-fix/2026-07-23-demo-web-builds-client-bundles.md
@@ -1,6 +1,7 @@
# Agent Note: demo:web builds the client plugin bundles
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-23-demo-web-builds-client-bundles.zh.md)
diff --git a/.agents/notes/implemented/bug-fix/2026-07-23-demo-web-builds-client-bundles.zh.md b/.agents/notes/archived/bug-fix/2026-07-23-demo-web-builds-client-bundles.zh.md
similarity index 99%
rename from .agents/notes/implemented/bug-fix/2026-07-23-demo-web-builds-client-bundles.zh.md
rename to .agents/notes/archived/bug-fix/2026-07-23-demo-web-builds-client-bundles.zh.md
index f10184642b..70604b344b 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-23-demo-web-builds-client-bundles.zh.md
+++ b/.agents/notes/archived/bug-fix/2026-07-23-demo-web-builds-client-bundles.zh.md
@@ -1,6 +1,7 @@
# Agent Note: demo:web 构建客户端插件的打包产物
Status: implemented
+Archived: 2026-07-26
[English](2026-07-23-demo-web-builds-client-bundles.md) | 中文
diff --git a/.agents/notes/implemented/bug-fix/2026-07-23-thinking-row-disclosure-target.i18n.yaml b/.agents/notes/archived/bug-fix/2026-07-23-thinking-row-disclosure-target.i18n.yaml
similarity index 62%
rename from .agents/notes/implemented/bug-fix/2026-07-23-thinking-row-disclosure-target.i18n.yaml
rename to .agents/notes/archived/bug-fix/2026-07-23-thinking-row-disclosure-target.i18n.yaml
index a2fcf167a7..9daab92e7e 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-23-thinking-row-disclosure-target.i18n.yaml
+++ b/.agents/notes/archived/bug-fix/2026-07-23-thinking-row-disclosure-target.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
-2026-07-23-thinking-row-disclosure-target.md: f698c3cb0b73bf5c65b5d4b5b3f29de3080e0af6
-2026-07-23-thinking-row-disclosure-target.zh.md: 0fba5c1d8f7beec7300dcd51e118a08d57d0e74f
+2026-07-23-thinking-row-disclosure-target.md: 9f748b6d76b9ddfe657e56da9f9e7f576b05599e
+2026-07-23-thinking-row-disclosure-target.zh.md: e33c951ca33e032e2eb2d306d90979d1e8471a17
diff --git a/.agents/notes/implemented/bug-fix/2026-07-23-thinking-row-disclosure-target.md b/.agents/notes/archived/bug-fix/2026-07-23-thinking-row-disclosure-target.md
similarity index 98%
rename from .agents/notes/implemented/bug-fix/2026-07-23-thinking-row-disclosure-target.md
rename to .agents/notes/archived/bug-fix/2026-07-23-thinking-row-disclosure-target.md
index f698c3cb0b..9f748b6d76 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-23-thinking-row-disclosure-target.md
+++ b/.agents/notes/archived/bug-fix/2026-07-23-thinking-row-disclosure-target.md
@@ -1,6 +1,7 @@
# Agent Note: Thinking rows use one disclosure target
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-23-thinking-row-disclosure-target.zh.md)
diff --git a/.agents/notes/implemented/bug-fix/2026-07-23-thinking-row-disclosure-target.zh.md b/.agents/notes/archived/bug-fix/2026-07-23-thinking-row-disclosure-target.zh.md
similarity index 98%
rename from .agents/notes/implemented/bug-fix/2026-07-23-thinking-row-disclosure-target.zh.md
rename to .agents/notes/archived/bug-fix/2026-07-23-thinking-row-disclosure-target.zh.md
index 0fba5c1d8f..e33c951ca3 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-23-thinking-row-disclosure-target.zh.md
+++ b/.agents/notes/archived/bug-fix/2026-07-23-thinking-row-disclosure-target.zh.md
@@ -1,6 +1,7 @@
# Agent Note: thinking 行使用单一展开目标
Status: implemented
+Archived: 2026-07-26
[English](2026-07-23-thinking-row-disclosure-target.md) | 中文
diff --git a/.agents/notes/archived/bug-fix/2026-07-26-intent-draft-same-tick-echo.i18n.yaml b/.agents/notes/archived/bug-fix/2026-07-26-intent-draft-same-tick-echo.i18n.yaml
new file mode 100644
index 0000000000..022b9a67f6
--- /dev/null
+++ b/.agents/notes/archived/bug-fix/2026-07-26-intent-draft-same-tick-echo.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
+2026-07-26-intent-draft-same-tick-echo.md: 3ef91b123f9abe0817bf5f7e1ad48e2e6f1e2cb3
+2026-07-26-intent-draft-same-tick-echo.zh.md: d890a68f4b9c83713b7a52c44ecb6bcd16265669
diff --git a/.agents/notes/archived/bug-fix/2026-07-26-intent-draft-same-tick-echo.md b/.agents/notes/archived/bug-fix/2026-07-26-intent-draft-same-tick-echo.md
new file mode 100644
index 0000000000..3ef91b123f
--- /dev/null
+++ b/.agents/notes/archived/bug-fix/2026-07-26-intent-draft-same-tick-echo.md
@@ -0,0 +1,28 @@
+# Agent Note: Intent draft echoes in the same tick
+
+Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-26-intent-draft-same-tick-echo.zh.md)
+
+## Problem
+
+The hero composer ("Let's start building") is a controlled textarea whose value is the frontend Session Intent's retained prompt, read from the sessions **list** snapshot (`EmptyState` binds `intent.prompt` via `useSessions`). Typing routed through `SessionManager.updateIntent → Session.updatePendingPrompt`, which flushes the **Session's own** notifier synchronously — but the list snapshot the composer actually renders from only heard about the change through the intent watch subscription in `startIntent`, which calls `markDirty()`, a microtask-deferred flush.
+
+A deferred echo violates the controlled-input contract documented on the Notifier (see the [web client architecture note](../architecture/2026-07-19-gui-web-client-architecture.md)): React compares the DOM value against the still-stale snapshot during the same tick as `onChange` and rolls the textarea back. With plain typing this shows as caret jumps; with an IME it corrupts input — every composition update gets rolled back and re-applied against a stale value, so typing Pinyin "nihao" commits fragments like "nnini hni hani hao你好". The resident composer (`ConversationRoot`) was not affected: its draft lives in the chat store (sync flush) or comes from `updateSessionPrompt`, which reads the Session snapshot directly rather than the list projection.
+
+## Decision
+
+`SessionManager.updateIntent` calls `this.notifier.notifyNow()` after `updatePendingPrompt`, flushing the list snapshot in the same tick as the change event. This matches the Notifier's channel rule: a direct echo of a user gesture whose controlled input renders from this snapshot uses `notifyNow`; the intent watch keeps `markDirty` for every other (async) intent transition.
+
+## Alternatives considered
+
+**Change the intent watch callback in `startIntent` to `notifyNow`.** Wrong channel for that seam: the watch also fires on frame-driven Session changes (publication, send phases), and the architecture note bans `notifyNow` for frame-driven sources because it collapses batching.
+
+**Have `EmptyState` read the prompt from the Session snapshot instead of the list.** Restructures the slot contract (EmptyState is deliberately bound to the standard `useSessions` feed and has no session scope yet — the frontend Session is page-local) for no gain over flushing the projection it already reads.
+
+**Suppress the rollback in `InputBar` with local uncontrolled state.** Hides the symptom, forfeits the single-source-of-truth draft (the retained prompt must survive workspace retargeting and send/retry), and leaves every other list-snapshot-controlled input exposed.
+
+## Consequences
+
+Typing in the hero composer, IME composition included, echoes synchronously. `updateIntent` on a no-intent state stays a no-op with no notification. The web workspace-flow snapshot's composer helper now asserts the same-tick echo instead of waiting for it, so a regression to a deferred echo fails the keyless snapshot gate; a runtime unit test pins the same contract at the manager seam.
diff --git a/.agents/notes/archived/bug-fix/2026-07-26-intent-draft-same-tick-echo.zh.md b/.agents/notes/archived/bug-fix/2026-07-26-intent-draft-same-tick-echo.zh.md
new file mode 100644
index 0000000000..d890a68f4b
--- /dev/null
+++ b/.agents/notes/archived/bug-fix/2026-07-26-intent-draft-same-tick-echo.zh.md
@@ -0,0 +1,28 @@
+# Agent Note: Intent draft echoes in the same tick
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-26-intent-draft-same-tick-echo.md) | 中文
+
+## Problem
+
+hero composer(「Let's start building」)是一个受控(controlled)的 textarea,它的值取自前端 Session Intent 保留下来的提示词,读自会话**列表**快照(`EmptyState` 通过 `useSessions` 绑定 `intent.prompt`)。输入经由 `SessionManager.updateIntent → Session.updatePendingPrompt`,后者会同步刷新 **Session 自身的** notifier——但 composer 实际渲染所依据的那份列表快照,只能通过 `startIntent` 中的 intent watch 订阅得知这次变更,而该订阅调用的是 `markDirty()`,即一次延迟到微任务的刷新。
+
+延迟的回显违反了 Notifier 上所记录的受控输入契约(见 [web 客户端架构笔记](../architecture/2026-07-19-gui-web-client-architecture.md)):React 在与 `onChange` 相同的 tick 内,把 DOM 值与仍然陈旧的快照相比对,随后把 textarea 回滚。普通输入时,这表现为光标跳动;使用输入法(IME)时,它会损坏输入——每一次 composition 更新都会被回滚,并针对陈旧的值重新应用,因此输入拼音「nihao」会提交出类似「nnini hni hani hao你好」这样的片段。resident composer(`ConversationRoot`)不受影响:它的草稿存放在 chat store 中(同步刷新),或来自 `updateSessionPrompt`,后者直接读取 Session 快照,而不是列表投影。
+
+## Decision
+
+`SessionManager.updateIntent` 在 `updatePendingPrompt` 之后调用 `this.notifier.notifyNow()`,从而在与变更事件相同的 tick 内刷新列表快照。这符合 Notifier 的通道规则:当某个用户手势的受控输入正是从该快照渲染时,对它的直接回显使用 `notifyNow`;而 intent watch 对其余所有(异步的)intent 状态转换仍保留 `markDirty`。
+
+## Alternatives considered
+
+**把 `startIntent` 中的 intent watch 回调改为 `notifyNow`。** 对那个 seam 而言是错误的通道:该 watch 也会在帧驱动的 Session 变更(发布、发送阶段)时触发,而架构笔记禁止对帧驱动的来源使用 `notifyNow`,因为那会瓦解批处理。
+
+**让 `EmptyState` 从 Session 快照而非列表读取提示词。** 这会重构槽位契约(EmptyState 有意绑定到标准的 `useSessions` 数据源,且尚无 session 作用域——前端 Session 是页面本地的),相比刷新它本就读取的那份投影并无收益。
+
+**在 `InputBar` 中用本地的非受控状态抑制回滚。** 这只是掩盖症状,放弃了单一真源的草稿(保留下来的提示词必须在工作区重定向以及发送/重试后依然存在),并让其余每一个由列表快照控制的输入都暴露在同一问题之下。
+
+## Consequences
+
+在 hero composer 中输入(包括输入法 composition 在内)会同步回显。在无 intent 的状态上调用 `updateIntent` 仍是一次空操作,不发出任何通知。web workspace-flow 快照的 composer 辅助函数现在断言的是同一 tick 内的回显,而不是等待它,因此一旦回退成延迟回显,就会让无密钥快照门禁失败;一个运行时单元测试在 manager 这一 seam 处钉住了同一份契约。
diff --git a/.agents/notes/archived/feature/2026-06-14-acp-agent-client-protocol.i18n.yaml b/.agents/notes/archived/feature/2026-06-14-acp-agent-client-protocol.i18n.yaml
new file mode 100644
index 0000000000..56a3c4f3d4
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-06-14-acp-agent-client-protocol.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
+2026-06-14-acp-agent-client-protocol.md: ee616a58cbd0201f14c7000672aec7c2485af0b1
+2026-06-14-acp-agent-client-protocol.zh.md: 8af061a30fed72c60c9e7a1747970a23415d530f
diff --git a/.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md b/.agents/notes/archived/feature/2026-06-14-acp-agent-client-protocol.md
similarity index 90%
rename from .agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md
rename to .agents/notes/archived/feature/2026-06-14-acp-agent-client-protocol.md
index 7c47fc78e9..ee616a58cb 100644
--- a/.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md
+++ b/.agents/notes/archived/feature/2026-06-14-acp-agent-client-protocol.md
@@ -1,6 +1,11 @@
# Agent Note: Agent Client Protocol (ACP) support — drive the coding agent from external editors
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-06-14-acp-agent-client-protocol.zh.md)
+
+> Superseded by [ACP as an automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md). This note records the retired editor-facing bridge design.
## Problem
@@ -10,7 +15,7 @@ The bridge must preserve the harness's existing ownership boundaries. It cannot
## Decision
-`@deepseek-ai/dsh-acp` is a UI/client-driver plugin under `packages/ui/acp`. It uses `@agentclientprotocol/sdk`'s `AgentSideConnection` over stdin/stdout and programs only interface services: the agent create/resume factory, session persistence, tool registry, user interaction, and optional approval/bash capabilities. It does not change the agent loop and is not a capability-seam implementation.
+`@deepseek-ai/dsh-acp` was a UI/client-driver plugin in the `ui` package group (it now lives in `acp`). It used `@agentclientprotocol/sdk`'s `AgentSideConnection` over stdin/stdout and programmed only interface services: the agent create/resume factory, session persistence, tool registry, user interaction, and optional approval/bash capabilities. It did not change the agent loop and was not a capability-seam implementation.
The bridge implements the following stable session path:
@@ -30,7 +35,7 @@ The bridge also provides the ACP-backed `UserInteractionProvider`: `ask_user_que
Lifecycle ownership is explicit. The bridge holds an `AgentHandle` per live session. Disconnect and Cordis disposal cancel pending prompts, dispose every handle in parallel, await loop quiescence and persistence flush, and then remove the records. Stream notification failures are contained so a vanished client cannot corrupt an agent turn. The ACP app composition loads no stdout logger; a test guards stdout as framed JSON-RPC only.
-The precise supported and deferred protocol rows live in [`packages/ui/acp/acp-feature-support.md`](../../../../packages/ui/acp/acp-feature-support.md); the package README is the operational contract.
+The current protocol contract lives in the [`dsh-acp` package README](../../../../packages/acp/acp/README.md).
## Alternatives considered
diff --git a/.agents/notes/archived/feature/2026-06-14-acp-agent-client-protocol.zh.md b/.agents/notes/archived/feature/2026-06-14-acp-agent-client-protocol.zh.md
new file mode 100644
index 0000000000..8af061a30f
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-06-14-acp-agent-client-protocol.zh.md
@@ -0,0 +1,62 @@
+# Agent Note: Agent Client Protocol(ACP)支持——从外部编辑器驱动编码 agent
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-06-14-acp-agent-client-protocol.md) | 中文
+
+> 已被 [ACP 作为仅面向自动化的协议](../simplification/2026-07-23-acp-automation-only-protocol.md)取代。本 Agent Note 记录已退役的面向编辑器的桥接层设计。
+
+## 问题
+
+harness 最初仅通过 readline 循环暴露 agent。该接口能传输文本,但编辑器无法以结构化方式创建或恢复会话、关联提示词完成、流式输出推理(reasoning)与工具活动、渲染工具专属 UI、请求权限,或在不干扰其他对话的前提下取消某个对话。ACP(Agent Client Protocol)将这些交互定义为基于 stdio 的 JSON-RPC,Zed 是用于做出具体兼容性决策的目标客户端。
+
+桥接层必须保持 harness 既有的所有权边界。它不能依赖具体的 agent loop(智能体循环),不能绕过工具注册表,不能在编辑器中执行 shell 命令,也不能发明第二个会话真源。stdout 同时也是协议传输通道,因此任何意外的日志输出都会破坏连接。
+
+## 决策
+
+`@deepseek-ai/dsh-acp` 曾是 `ui` 包组中的 UI/客户端驱动插件(现位于 `acp`)。它使用 `@agentclientprotocol/sdk` 的 `AgentSideConnection`(基于 stdin/stdout),仅编排接口服务:agent 创建/恢复工厂、会话持久化、工具注册表、用户交互,以及可选的审批/bash 能力。它不修改 agent loop,也不是能力 seam 的实现。
+
+桥接层实现以下稳定的会话路径:
+
+- `initialize` 协商协议版本,声明支持 text 与 `resource_link` 类型的提示词,并声明 `loadSession` 能力。
+- `session/new` 校验绝对路径 `cwd`,将其存入 `SessionHeader`,通过 `ctx.agents` 创建 agent,并返回由组合层支持的配置选项。
+- `session/load` 在构造 agent 之前校验请求的 cwd 与持久化元数据是否一致,在异步恢复期间保留 id,将用户/助手/工具事件作为 ACP update 回放,并报告恢复后的 config-option 折叠结果。
+- `session/prompt` 接受文本和 resource link,拒绝不支持的或空的内容,每个会话同时只允许一个 in-flight 提示词,并在该提示词所属的 `turn/end` 时结算。错误轮次拒绝 RPC;其他关闭轮次的原因通过一个全覆盖的 ACP stop-reason 编解码器映射。
+- `session/cancel` 调用队列感知的 agent 取消路径,仅结算被寻址会话的提示词。
+
+工具调用的展示仍由工具自身负责。工具的 `presentCall` 和 `presentResult` 返回 `generic`、`terminal` 或 `diff` 渲染意图变体;桥接层对该联合类型做 switch 并映射到 ACP。没有 presenter 的工具获得通用回退。Bash 终端卡片使用 Zed 的能力门控约定 `_meta.terminal_info`、`_meta.terminal_output` 和 `_meta.terminal_exit`;harness 仍通过 `ctx.bash` 执行命令,保留沙箱、环境清洗、所有权和 cwd。不支持该扩展的客户端收到普通文本内容。文件系统工具提供 diff 卡片和文件位置,桥接层中无需硬编码工具名分支。
+
+权限处理是[用户审批 seam](2026-07-06-approval-seam.md)上的一个 answerer,而非 ACP 中的「每次工具调用都询问」策略。对桥接层所属 agent 且带有 call id 的 `approval/request`,会变为该 agent 编辑器会话上的 `session/request_permission`,提供一次性允许/拒绝选项。外部请求或无 call id 的请求委托给下游;缺失或失败的 answerer 会在故障时保持拒绝。发起询问的插件(如预执行策略或 bash 升级)拥有「是否询问」的决策权。
+
+当 `ctx.permission` 被组合时,桥接层从部署的预设表中暴露一个 `permission` select。已发布的 `workspace-write` 和 `danger-full-access` 预设各自捆绑一个沙箱模式与一条审批策略;无法匹配的有效旋钮组合产生只能切走的 `custom` 状态。`session/set_config_option` 通过 `PermissionService.set()` 校验并写入两个所属旋钮事件。在开放轮次中的切换立即追加;空闲时的切换叠加在响应中,并在下一次 `agent/prompt-submit` 时锚定到开放轮次之前的请求组装阶段。在此之前它仅存于内存,因此崩溃后恢复的是持久化的折叠结果。ACP session mode 不被建模,因为 config option 是面向未来的协议表面;`AcpConfig.model` 保持连接级别。
+
+桥接层还提供基于 ACP 的 `UserInteractionProvider`:`ask_user_question` 请求变为所属会话上的表单引导。select、multi-select、选项描述与自定义回答覆盖语义均被保留。
+
+生命周期所有权是显式的。桥接层为每个活跃会话持有一个 `AgentHandle`。断连和 Cordis dispose(资源释放)会取消待处理的提示词,并行 dispose 所有 handle,等待循环完全停稳与持久化刷写,然后移除记录。流通知失败被隔离,因此消失的客户端不会破坏 agent 轮次。ACP 应用组合不加载 stdout logger;一个测试守卫 stdout 仅包含帧化的 JSON-RPC。
+
+当前的协议契约见 [`dsh-acp` 包 README](../../../../packages/acp/acp/README.md)。
+
+## 曾考虑的替代方案
+
+**在 `tools/execute` 监听器前置一层,对每个 ACP 所属调用都询问权限**:否决。这会将权限策略硬编码到 UI 桥接层,即使没有策略要求也会询问,且无法服务于执行开始后才产生的审批请求。共享的 user-approval seam 将机制、询问策略和 UI answerer 分离。
+
+**注入具体的 `agentLoop`**:否决。agent 的创建、恢复、空闲观察与释放是 `dsh-agent` 上的接口级所有权操作;UI 插件不需要依赖规则例外。
+
+**通过 ACP `terminal/*` 执行 bash**:否决。这会将执行移到 harness 之外,绕过其沙箱、凭证清洗、任务所有权、cwd 解析与会话日志。终端元数据仅用于展示。
+
+**将权限预设表示为 ACP session mode**:否决。部署定义的预设已经是一个 config-option select,而 session mode 是 ACP v2 计划移除的遗留接口。
+
+**防御性劫持 stdout**:否决。进程级 monkey-patching 超出 Cordis 副作用所有权范围,且与协议传输存在竞争。应用组合拥有 stdout 纯净性。
+
+## 后果
+
+编辑器可以通过一条 ACP 连接创建、加载、提交提示词、取消、渲染、询问和重新配置多个 harness 会话,无需依赖特定的循环实现。会话事件日志仍是回放、提示词结算、cwd 与每会话配置的持久真源。工具展示与人工回答通道仍是可扩展的插件契约,而非 ACP 专属行为。
+
+桥接层有意不实现会话列表/删除/恢复/关闭能力、MCP 透传、附加目录、图片/音频/嵌入资源提示词、plan、斜杠命令、用量更新、编辑器文件系统委托或 ACP 终端执行子协议。后续已通过标准会话配置选项加入运行时模型选择,见 [LLM 目录与 ACP 选择 Agent Note](../architecture/2026-07-15-llm-model-catalog-and-acp-selection.md)。
+
+空闲时的配置选择在实时响应中是真实的,但在下一次 `agent/prompt-submit` 将其锚定到开放轮次之前不具持久性。在该边界之前崩溃会丢失待定选择;这是保持会话事件封闭于轮次内且回放安全的代价。
+
+## 验证
+
+ACP 测试套件覆盖内存协议编解码器、创建/加载回放、精确的提示词结算、取消竞争、不支持的内容、工具展示、终端能力回退、权限结果映射、config-option 校验与持久化、多会话隔离、断连/释放后的完全停稳,以及 HMR(热模块替换)清理。快照测试与 built-bin 测试验证应用组合,真实 API 的 e2e 测试在无 key 时自动跳过。
diff --git a/.agents/notes/archived/feature/2026-06-18-acp-terminal-and-tool-rendering.i18n.yaml b/.agents/notes/archived/feature/2026-06-18-acp-terminal-and-tool-rendering.i18n.yaml
new file mode 100644
index 0000000000..fee3d69d57
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-06-18-acp-terminal-and-tool-rendering.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
+2026-06-18-acp-terminal-and-tool-rendering.md: 3ffe9b698d453a5d53ce4cc28bc85d8dd75f37a0
+2026-06-18-acp-terminal-and-tool-rendering.zh.md: 3dd8a110260519d0b6342f8984be98a2d1c53f01
diff --git a/.agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md b/.agents/notes/archived/feature/2026-06-18-acp-terminal-and-tool-rendering.md
similarity index 96%
rename from .agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md
rename to .agents/notes/archived/feature/2026-06-18-acp-terminal-and-tool-rendering.md
index 8d9c0197eb..3ffe9b698d 100644
--- a/.agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md
+++ b/.agents/notes/archived/feature/2026-06-18-acp-terminal-and-tool-rendering.md
@@ -1,6 +1,11 @@
# Agent Note: Rich ACP bash rendering — the terminal card via the `_meta` convention
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-06-18-acp-terminal-and-tool-rendering.zh.md)
+
+> Superseded for ACP by [ACP as an automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md). Tool render intents remain available to UI transports, but ACP no longer projects them into terminal cards.
## Problem
diff --git a/.agents/notes/archived/feature/2026-06-18-acp-terminal-and-tool-rendering.zh.md b/.agents/notes/archived/feature/2026-06-18-acp-terminal-and-tool-rendering.zh.md
new file mode 100644
index 0000000000..3dd8a11026
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-06-18-acp-terminal-and-tool-rendering.zh.md
@@ -0,0 +1,51 @@
+# Agent Note: 富 ACP bash 渲染——通过 `_meta` 约定实现终端卡片
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-06-18-acp-terminal-and-tool-rendering.md) | 中文
+
+> 就 ACP 而言已被 [ACP 作为仅面向自动化的协议](../simplification/2026-07-23-acp-automation-only-protocol.md)取代。工具渲染意图对 UI 传输层仍然可用,但 ACP 不再将其投影为终端卡片。
+
+## 问题
+
+ACP(Agent Client Protocol)桥接层允许每个工具通过 `presentCall`/`presentResult` 自行控制调用渲染(见[工具调用 UI 呈现](2026-06-14-acp-agent-client-protocol.md)与 `packages/core/tools`)。对于 `bash`,我们将确切命令作为 `tool_call` 标题呈现,模型的 `description` 作为一个内容文本块,`kind: 'execute'`,完成后的输出包裹在 ` ```console ` 围栏文本块中。
+
+参考编辑器将终端元数据渲染为一张专用卡片,包含 cwd、命令、实时风格的输出和退出状态;纯文本则丢失了这些结构。命令之所以作为标题,是因为执行卡片隐藏原始输入,而人类可读的描述保留为卡片上方的独立块。
+
+## 关键发现:agent 执行的终端使用 `_meta` 约定,而非 `terminal/create`
+
+ACP 规范有一个*客户端侧*终端子协议:agent(智能体)调用客户端的 `terminal/create`(传入 `{ command, args, cwd, env }`),由**编辑器**执行进程,然后 agent 读取 `terminal/output` / `wait_for_exit`。这个模型不适合我们:我们的 harness 通过 `dsh-bash` 自行执行 bash(沙箱化的环境清理、后台任务所有权、按会话的 cwd)。将执行路由到编辑器会绕过所有这些机制,并将执行分叉到两个后端。
+
+研究两个参考 agent(2026-06-18)发现,二者都没有为自己的 shell 工具使用 `terminal/create`——**两者都保持 agent 侧执行,并发出一套 `_meta` 约定**,由 Zed 特殊处理:
+
+- **`claude-agent-acp`**(`tools.ts`、`acp-agent.ts`):以 `clientCapabilities._meta.terminal_output` 为门控。`tool_call` 携带 `content: [{ type: 'terminal', terminalId }]` 与 `_meta.terminal_info.{ terminal_id, cwd }`;输出和退出通过 `tool_call_update` 的 `_meta.terminal_output.{ terminal_id, data }` 与 `_meta.terminal_exit.{ terminal_id, exit_code, signal }` 到达。
+- **`codex-acp`**(`CodexToolCallMapper.ts`、`TerminalOutputMode.ts`):调用上同样携带 `terminal_info`;输出通过 `_meta.terminal_output`(完整)或 `_meta.terminal_output_delta`(增量),由同一个 `_meta.terminal_output` 能力选择。
+
+Zed 侧(`crates/agent_servers/src/acp.rs`,已验证):收到 `ToolCall` 且其 `_meta.terminal_info.terminal_id` 已设置时,注册一个**仅展示**的终端(header = `terminal_info.cwd`,label = `tool_call.title`);收到 `ToolCallUpdate` 时,`_meta.terminal_output.data` 写入该终端,`_meta.terminal_exit.{exit_code,signal}` 设置状态。客户端通过 `clientCapabilities._meta.terminal_output = true` 声明此能力。`_meta` 本身是 ACP 规范认可的扩展点(在 `ToolCall`/`ToolCallUpdate` 上类型为 `{[k]: unknown} | null`);这里的*具体键*(`terminal_info`/`terminal_output`/`terminal_exit`)是 Zed 约定,不属于 ACP 规范,但它们是 Zed 集成的事实契约,也是在保持 agent 侧执行的前提下获得终端卡片的唯一方式。
+
+## 决策
+
+保持 `dsh-bash` 的 agent 侧执行;通过 `_meta` 约定渲染终端卡片,以能力声明为门控,以 ` ```console ` 文本块作为保底回退。
+
+1. **能力声明。** `initialize` 读取 `clientCapabilities._meta.terminal_output`,桥接层按连接记住它。
+2. **提供方无关的展示词汇。** `dsh-tools` 新增一种终端形态的展示结构,工具可返回它——提供方无关(`cwd`、输出 `data`、`exitCode`/`signal`),不含 ACP 类型。`dsh-tool-bash` 为 `bash` 返回该结构(cwd 来自解析后的工作目录;输出与退出从运行结果解析)。
+3. **桥接映射。** 当客户端声明了该能力时,桥接层将展示结构映射为:在 `tool_call` 上,`content:[…, {type:'terminal', terminalId}]`(工具的任何 `content`,如描述,渲染在终端块之前)+ `_meta.terminal_info.{terminal_id,cwd}`;在 `tool_call_update` 上,`_meta.terminal_output.{terminal_id,data}`(捕获的输出)+ `_meta.terminal_exit.{terminal_id, exit_code|signal}`(解析后的退出),且 update 的文本 `content` 被省略(ACP 的 `tool_call_update.content` 会替换调用的 content 集合,因此重新发送围栏块会覆盖终端内容块)。`terminalId` 由 harness 的 `callId` 派生(稳定、每次调用唯一)。当能力未声明时,桥接层在调用上发送描述内容块,在 update 上发送既有的 ` ```console ` 文本内容——行为不变。
+4. **退出信息从渲染输出中解析;无新执行路径,无实时流式传输。** 输出在完成时附加(来自 agent 自身的 `tool/result`),不逐 token 流式传输。退出状态(`_meta.terminal_exit.{exit_code,signal}`)确实会发出:纯 `presentResult(args, result)` seam 只能看到内容块,因此 `dsh-tool-bash` 通过解析 `renderResult` 追加的状态标记(`[exit code: N]` / `[killed by signal: …]`)来恢复结构化退出信息——解析是标记发出的精确逆操作,二者在同一文件中共同演进,一个往返测试守护这对关系。资源释放不受影响:无需新增拆除逻辑,因为桥接层从未创建客户端侧终端。
+
+## 曾考虑的替代方案
+
+- **ACP 客户端侧终端子协议(`terminal/create`)**:明确否决。编辑器将执行进程,绕过 `dsh-bash` 的环境清理、后台任务所有权和按会话的 cwd,并将执行分叉到两个后端。两个参考 agent 以同样的方式否决了它(见上述关键发现);agent 侧执行加 `_meta` 约定是在保持 harness 执行策略的同时获得终端卡片的唯一形态。
+- **通过事件 schema 传递结构化退出信息**:否决,改用标记往返方案。纯 `presentResult(args, result)` seam 只能看到内容块,而解析是标记发出的精确逆操作,二者在同一文件中共同演进,由往返测试守护。
+
+## 后果
+
+- **Zed 约定的 `_meta` 键。** 终端卡片依赖 Zed 特有的键(`terminal_info`/`terminal_output`/`terminal_exit`),位于 ACP 规范认可的 `_meta` 扩展点内,而非 ACP 终端子协议。不识别这些键的客户端仍然获得文本回退(能力门控确保我们仅在客户端通过 `_meta.terminal_output` 声明支持时才发出这些键),因此非 Zed 客户端不会变差。如果 ACP 日后标准化了 agent 执行的终端,则迁移到该标准并移除约定键。
+- **能力诚实。** 仅在客户端声明了 `_meta.terminal_output` 时才发出终端元数据;文本回退是对其他所有客户端的契约,绝不可退化。由一个无能力测试覆盖,断言 ` ```console ` 路径。
+- **terminalId 冲突。** 从每次调用的 `callId` 派生,保证在会话内唯一且在 call/result 对之间稳定;绝不跨调用复用。
+- **退出信息从渲染文本解析。** 退出信息通过解析 `renderResult` 的状态标记恢复 `exit_code`/`signal`,而非通过事件 schema 传递结构化退出(纯 `presentResult` seam 看不到后者)。解析是标记发出的精确逆操作,且位于同一文件中;往返测试固定了这对关系,标记格式变更若破坏解析则测试套件失败。如果标记格式日后需要与退出信息分道扬镳,则改为在 result 事件上暴露结构化退出。
+- **提供方无关词汇的蔓延。** 终端展示结构扩大了 `dsh-tools` 的接口面;保持其中立性(不让 ACP 类型泄漏到 `dsh-tools`),且只提供第二个 UI 消费方同样需要的丰富度。
+
+## 超出范围 / 非目标
+
+文本块基线仍为无能力声明时的默认行为。以下两项后续工作有意不在此处构建,各自需要单独的 Agent Note:**实时增量流式传输**(在分片到达时发出 `_meta.terminal_output_delta`,需要在 `dsh-bash` 上新增增量输出 seam);**命令分类**(将 `cat`/`sed` 解析为带文件位置的 `read` 卡片,将 `grep` 解析为 `search`,回退到终端卡片——仅展示,绝不改变实际执行内容)。
diff --git a/.agents/notes/archived/feature/2026-06-25-ask-user-question.i18n.yaml b/.agents/notes/archived/feature/2026-06-25-ask-user-question.i18n.yaml
new file mode 100644
index 0000000000..3506271563
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-06-25-ask-user-question.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
+2026-06-25-ask-user-question.md: 0d7b2c4d9e8f986366fc56d1123b322333a48262
+2026-06-25-ask-user-question.zh.md: fd5c0d3f339074cccb3f1b3b10109afc06b14774
diff --git a/.agents/notes/implemented/feature/2026-06-25-ask-user-question.md b/.agents/notes/archived/feature/2026-06-25-ask-user-question.md
similarity index 69%
rename from .agents/notes/implemented/feature/2026-06-25-ask-user-question.md
rename to .agents/notes/archived/feature/2026-06-25-ask-user-question.md
index cd1a423499..0d7b2c4d9e 100644
--- a/.agents/notes/implemented/feature/2026-06-25-ask-user-question.md
+++ b/.agents/notes/archived/feature/2026-06-25-ask-user-question.md
@@ -1,6 +1,9 @@
# Agent Note: Ask-user question capability
Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-06-25-ask-user-question.zh.md)
## Problem
@@ -16,7 +19,7 @@ The model-facing request vocabulary is deliberately aligned with the product-res
Providers return `{ answers: [{ id, selected, custom? }] }`. `selected` is always an array of selected option labels, so single-select and `multi_select` answers share one result shape. `custom` carries a free-text "Other" answer; optionless questions collect `custom` directly. When `custom` is present, it overrides any selected choices and `selected` is empty. A provider that supports partial completion represents a deliberately skipped item with the existing `{ id, selected: [] }` shape, preserving the other answers without extending the tool result vocabulary.
-`UserInteractionError` extends `HarnessError`, so failures such as `NO_PROVIDER`, `ASK_ABORTED`, ACP cancellation, or missing session routing survive `ctx.tools.execute()` as machine-routable `{ name, code }` tool errors. This matches the structured-error taxonomy and lets the model or a wrapping plugin distinguish "user cancelled" from a generic thrown exception.
+`UserInteractionError` extends `HarnessError`, so failures such as `NO_PROVIDER`, `ASK_ABORTED`, or missing request ownership survive `ctx.tools.execute()` as machine-routable `{ name, code }` tool errors. This matches the structured-error taxonomy and lets the model or a wrapping plugin distinguish "user cancelled" from a generic thrown exception.
## UI mappings
@@ -26,9 +29,7 @@ The Web composer shows one question at a time while retaining every request in t
`dsh-tui` renders each question as a keyboard overlay, shows option descriptions, supports single- and multi-select choices plus free-form custom answers, and rejects pending questions on abort, provider disposal, or terminal shutdown. Batched and simultaneous requests are queued so one overlay owns keyboard focus at a time.
-`dsh-acp` provides the same seam for ACP sessions. It resolves the calling `Agent` through `ownedRecord`, requiring the forward session-map record at `agent.session.id` to own that exact agent object, and calls ACP `unstable_createElicitation` with a session-scoped form for each question. Single-select options become a `choice` string enum; `multi_select` options become a `choice` array enum; optionless questions use a required `custom` text field. If the client returns both `choice` and non-empty `custom`, the custom answer wins. ACP `decline`/`cancel`, a missing answer, a missing session, and a client without elicitation support all become structured `UserInteractionError`s.
-
-The ACP mapping deliberately uses elicitation, not `session/request_permission`. `request_permission` is still reserved for the separate permission gate: it is a yes/no-or-policy authorization protocol around tool execution. `ask_user_question` is a general information-gathering tool with optional free-form answers, so ACP form elicitation is the closer protocol fit. The bridge's session routing is shared with the future permission gate, but the user intent is different.
+An ACP elicitation mapping existed while the bridge was an editor UI; [ACP as an automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md) removed that third mapping.
## Alternatives considered
@@ -36,18 +37,16 @@ The ACP mapping deliberately uses elicitation, not `session/request_permission`.
**Core-owned ask-user packages.** The first implementation split the seam and the model-facing tool across `packages/core` and `packages/ui`, but both names describe one UI-backed human-interaction affordance. The seam remains provider-neutral, but it is not providerless core infrastructure like sessions, tools, or the agent registry. Keeping `dsh-user-interaction` and `dsh-tool-ask-user` together under `packages/ui` makes the package map match the product boundary: apps and bridges provide the human-answer provider, and the stdio app opts into the model-facing tool.
-**ACP `session/request_permission`.** Permission requests are authorization around tool execution; `ask_user_question` is information gathering with optional free-form answers. Using permission for general questions would collapse two different product concepts and make the future permission gate harder to reason about.
+**Use a permission request for general questions.** Permission requests authorize tool execution; `ask_user_question` gathers information with optional free-form answers. Reusing the permission channel would collapse two different product concepts.
**A loop-level pause primitive.** The agent loop already knows how to await a tool call and resume from a tool result. Adding a new loop special case would duplicate that async shape and make every loop implementation learn about a UI concern.
## Consequences
-ACP elicitation is currently marked unstable in the SDK. The fallback is still structured: if a client does not implement it, the tool returns `ASK_FAILED` rather than hanging. A later ACP stabilization may rename or reshape the method; that migration should stay inside `dsh-acp` because the core `ctx.userInteraction` vocabulary is provider-neutral.
-
The feature gives the model a powerful pause primitive, so prompt guidance matters. The tool description tells the model to ask concise questions and use options when possible. Product policy can later wrap `tools/execute` to restrict when the tool is allowed, but the loop should not special-case it.
-`dsh-user-interaction` and `dsh-tool-ask-user` both live in `packages/ui` because they form one product-facing human-interaction capability. `agent-core` does not load either the tool or a provider. `dsh-tui-demo` opts into the seam, TUI provider, and model-facing tool. `dsh web` boots the seam/provider in the host runtime and exposes the tool through the selected Web question plugin. `acp-agent` keeps only the `userInteraction` seam/provider by default: ACP elicitation support is still client-dependent, so an ACP leaf must opt into the model-facing tool deliberately once its client can complete elicitation requests.
+`dsh-user-interaction` and `dsh-tool-ask-user` both live in `packages/ui` because they form one product-facing human-interaction capability. `agent-core` does not load either the tool or a provider. `dsh-tui-demo` opts into the seam, TUI provider, and model-facing tool. `dsh web` boots the seam/provider in the host runtime and exposes the tool through the selected Web question plugin. The ACP automation app mounts neither the seam nor the tool.
## Testing
-Unit coverage pins provider registration/disposal, duplicate-provider rejection, abort-before-provider, empty-question rejection, structured tool errors through `ctx.tools.execute()`, batched answers, multi-select answers, custom answers, explicit per-item skips, and the model schema including the removal of `value`, `recommended`, `allow_custom`, and `desc`. TUI tests cover option descriptions, queued requests, shutdown/abort cleanup, optionless free-form input, invalid choices, duplicate multi-select selections, and batched question flows. ACP bridge tests drive a real in-memory ACP connection with the real `ask_user_question` tool and verify selected-option, custom-overrides-choice, multi-select, and optionless free-form elicitation paths continue the agent loop. Web tests pin stable-id replay, response validation, first-wins settlement, duplicate and late responses, whole-request cancellation versus owner abort, single-select advance, IME-safe Enter submission, per-item skip preservation, composer takeover, structured batch submission, and restoration of the normal composer.
+Unit coverage pins provider registration/disposal, duplicate-provider rejection, abort-before-provider, empty-question rejection, structured tool errors through `ctx.tools.execute()`, batched answers, multi-select answers, custom answers, explicit per-item skips, and the model schema including the removal of `value`, `recommended`, `allow_custom`, and `desc`. TUI tests cover option descriptions, queued requests, shutdown/abort cleanup, optionless free-form input, invalid choices, duplicate multi-select selections, and batched question flows. Web tests pin stable-id replay, response validation, first-wins settlement, duplicate and late responses, whole-request cancellation versus owner abort, single-select advance, IME-safe Enter submission, per-item skip preservation, composer takeover, structured batch submission, and restoration of the normal composer.
diff --git a/.agents/notes/archived/feature/2026-06-25-ask-user-question.zh.md b/.agents/notes/archived/feature/2026-06-25-ask-user-question.zh.md
new file mode 100644
index 0000000000..fd5c0d3f33
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-06-25-ask-user-question.zh.md
@@ -0,0 +1,52 @@
+# Agent Note: ask-user 提问能力
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-06-25-ask-user-question.md) | 中文
+
+## 问题
+
+agent(智能体)有时仅凭模型推理(inference)无法安全地继续执行:它需要人类选择路径、确认有风险的或默认的操作,或者提供缺失的信息。在此变更之前,获取答案的唯一方式是模型在 assistant 文本中提问然后停止,这打断了正常的工具调用循环:agent 没有结构化的暂停方式,没有供 UI 使用的选项元数据,没有中止/错误分类体系,也没有让非 stdio 前端一致地呈现问题的途径。
+
+这是一个面向用户的能力,但它也跨越了包(package)边界。面向模型的工具需要一套提供方无关的请求词汇;每个 UI 界面需要决定如何展示和收集答案;agent loop(智能体循环)应保持不变,因为工具调用本身已具备正确的异步形状。
+
+## 决策
+
+引入 `dsh-user-interaction` 作为 `ctx.userInteraction` 的提供方无关接口包,与面向模型的消费方 `dsh-tool-ask-user` 一同放在 `packages/ui` 下。这一分组是有意为之的:向人类提问是一种由 UI 支撑的产品功能,不属于无提供方的核心主干。seam 仍然拥有稳定的请求/应答/错误词汇,而 UI 产品界面提供收集答案的具体提供方。该工具注册 `ask_user_question`,转发 `{ questions, agent, signal }`,并将提供方计算出的结构化答案作为工具结果返回。
+
+面向模型的请求词汇有意与产品调研 schema 对齐:`ask_user_question({ questions: [{ id, question, header?, options?: [{ label, description? }], multi_select? }] })`。`id` 按问题提供并在结果中回传,使批量请求无需依赖问题文本即可路由。`label` 既是面向用户的显示文本,也是返回给模型的选中值;没有单独的 `value`,没有 `recommended`,没有 `allow_custom`,也没有 `desc` 别名。
+
+提供方返回 `{ answers: [{ id, selected, custom? }] }`。`selected` 始终是选中选项 label 的数组,因此单选和 `multi_select` 的答案共享同一种结果形状。`custom` 承载自由文本的「其他」答案;无选项的问题直接收集 `custom`。当 `custom` 存在时,它覆盖任何已选择的选项,`selected` 为空。支持部分完成的提供方使用现有的 `{ id, selected: [] }` 形状表示某项被有意跳过,在不扩展工具结果词汇的前提下保留其他答案。
+
+`UserInteractionError` 继承 `HarnessError`,因此 `NO_PROVIDER`、`ASK_ABORTED` 或请求归属缺失等失败会以机器可路由的 `{ name, code }` 工具错误形式通过 `ctx.tools.execute()` 传出。这与结构化错误分类体系一致,使模型或包装插件能够区分「用户取消」与一般的抛出异常。
+
+## UI 映射
+
+`dsh web` 挂载 `dsh-client-ui-question`:其 host 侧使 Web 产品选择性加载面向模型的工具,浏览器侧则在 conversation 拥有的具名输入区 slot 中注册 `question` 项。`createApiProxy` 使用以 host 生成的 rpcId 为键的进程内 pending 表实现 Web 提供方。它先注册等待项,再广播 `question/requested`;每次 mux 重开时以相同 id 重放;在受理前校验会话和完整答案批次;并在回答、取消、中止或资源释放后广播 `question/resolved`。受理会同步删除该条目,因此首个有效响应胜出,重复或迟到的响应返回 `not-pending`。
+
+Web 输入区一次显示一个问题,同时在会话对象层保留每个请求。它支持单选、多选、无选项问题或显式自定义答案、描述文本与可视化推荐标记,但不会自动选中推荐项。选择单选项后会立即进入下一项;当所有项都已回答或显式跳过时,按 Enter 提交;IME 组字期间按 Enter 只会确认输入候选项。页脚只跳过当前项并保留先前的草稿;关闭控件以 `ASK_CANCELLED` 拒绝整个工具调用。常规输入区只有在 host 的 resolved 帧移除待处理项后才会恢复。
+
+`dsh-tui` 将每个问题渲染为键盘叠层,展示选项描述,支持单选、多选和自由格式自定义答案,并在中止、提供方 dispose(资源释放)或终端关闭时拒绝待处理的问题。批量请求和并发请求都会排队,确保同一时刻只有一个叠层占用键盘焦点。
+
+在桥接层还是编辑器 UI 时曾存在一个 ACP(Agent Client Protocol)elicitation 映射;[ACP 作为仅面向自动化的协议](../simplification/2026-07-23-acp-automation-only-protocol.md)移除了这第三个映射。
+
+## 曾考虑的替代方案
+
+**Assistant 文本后跟一个停止的轮次。** 模型可以在纯 assistant 文本中向用户提问然后停止。这会丢失结构化选项元数据,UI 没有提供方无关的方式来渲染选择,且下一条人类回答只能作为新的 user 提示词到达,而非作为需要答案的那次操作的结果。
+
+**核心拥有的 ask-user 包。** 最初实现将 seam 和面向模型的工具分别放在 `packages/core` 和 `packages/ui`,但两者描述的是同一个由 UI 支撑的人机交互功能。seam 仍然是提供方无关的,但它不是像会话、工具或 agent 注册表那样的无提供方核心基础设施。将 `dsh-user-interaction` 和 `dsh-tool-ask-user` 一起放在 `packages/ui` 下,使包的划分与产品边界一致:应用和 bridge 提供人类答案的提供方,stdio 应用选择性加载面向模型的工具。
+
+**用权限请求处理通用提问。** 权限请求是对工具执行的授权;`ask_user_question` 是带可选自由格式答案的信息收集。复用权限通道会混淆两个不同的产品概念。
+
+**循环级别的暂停原语。** agent loop 已经知道如何等待工具调用并从工具结果恢复。添加新的循环特殊分支会重复这一异步形状,并迫使每个循环实现都了解一个 UI 关注点。
+
+## 后果
+
+该功能赋予模型一个强大的暂停原语,因此提示词引导很重要。工具描述告诉模型:提问要简洁,尽可能使用选项。产品策略后续可以包装 `tools/execute` 来限制工具何时可用,但循环不应对其做特殊处理。
+
+`dsh-user-interaction` 和 `dsh-tool-ask-user` 都位于 `packages/ui`,因为它们共同构成一个面向产品的人机交互能力。`agent-core` 不加载工具或提供方。`dsh-tui-demo` 选择性加载 seam、TUI 提供方和面向模型的工具。`dsh web` 在 host 运行时启动 seam/提供方,并通过选定的 Web question 插件暴露该工具。ACP 自动化应用既不挂载 seam 也不挂载该工具。
+
+## 测试
+
+单元覆盖率固定了以下场景:提供方注册/释放、重复提供方拒绝、提供方就绪前中止、空问题拒绝、通过 `ctx.tools.execute()` 传出的结构化工具错误、批量答案、多选答案、自定义答案、显式按项跳过,以及模型 schema(包括移除 `value`、`recommended`、`allow_custom` 和 `desc`)。TUI 测试覆盖选项描述、排队请求、关闭/中止清理、无选项自由格式输入、无效选择、重复多选和批量问题流。Web 测试固定稳定 id 重放、响应校验、首个响应胜出的结算、重复和迟到响应、整个请求的取消与拥有方中止的区别、单选后前进、IME 安全的 Enter 提交、按项跳过保留、输入区接管、结构化批量提交,以及常规输入区的恢复。
diff --git a/.agents/notes/archived/feature/2026-06-30-subagent-observe-enrich.i18n.yaml b/.agents/notes/archived/feature/2026-06-30-subagent-observe-enrich.i18n.yaml
new file mode 100644
index 0000000000..e3de092b15
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-06-30-subagent-observe-enrich.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
+2026-06-30-subagent-observe-enrich.md: 7140616ac4a9725652ed779cba3d4232b6b5127b
+2026-06-30-subagent-observe-enrich.zh.md: 5cf81e9ace48650b834f2cf5e8ec7cc81b8b0e4d
diff --git a/.agents/notes/implemented/feature/2026-06-30-subagent-observe-enrich.md b/.agents/notes/archived/feature/2026-06-30-subagent-observe-enrich.md
similarity index 97%
rename from .agents/notes/implemented/feature/2026-06-30-subagent-observe-enrich.md
rename to .agents/notes/archived/feature/2026-06-30-subagent-observe-enrich.md
index 861779fc50..7140616ac4 100644
--- a/.agents/notes/implemented/feature/2026-06-30-subagent-observe-enrich.md
+++ b/.agents/notes/archived/feature/2026-06-30-subagent-observe-enrich.md
@@ -1,6 +1,9 @@
# Agent Note: Subagent lifecycle enrichment — lastAssistantMessage (observe-only)
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-06-30-subagent-observe-enrich.zh.md)
## Problem
diff --git a/.agents/notes/archived/feature/2026-06-30-subagent-observe-enrich.zh.md b/.agents/notes/archived/feature/2026-06-30-subagent-observe-enrich.zh.md
new file mode 100644
index 0000000000..5cf81e9ace
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-06-30-subagent-observe-enrich.zh.md
@@ -0,0 +1,32 @@
+# Agent Note: Subagent 生命周期丰富化——lastAssistantMessage(仅观察)
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-06-30-subagent-observe-enrich.md) | 中文
+
+## 问题
+
+钩子子系统([拦截 seam Agent Note](2026-06-30-interception-seams.md))允许插件在生命周期节点观察和拦截 agent(智能体)。Claude Code 和 Codex 都暴露了 **SubagentStart / SubagentStop** 钩子,且 CC 的钩子携带 subagent 的最终消息。harness 已经发出 `subagent/start` 和 `subagent/end` 生命周期事件([subagent 能力 seam](2026-06-21-subagent-capability-seam.md)),但其载荷极为精简(`provider`、`id`,以及 end 时的 `stopReason`),不足以让钩子桥接层在不单独访问活跃 run 的情况下报告 subagent 产出了什么。
+
+本 Agent Note 丰富 end 载荷。它刻意限定为**仅观察**:不改变控制流,不引入 waterfall(瀑布式事件)。影响 run 的 subagent-stop 决策(续行、改变 run 的注入)属于另一个更大的重设计,不在本 Agent Note 范围内。
+
+## 决策
+
+**在 `SubagentRunEndInfo` 中添加 `lastAssistantMessage`——子 agent 的最终输出。** 在正常结束路径上,它是只读的类型化 `SubagentResult.output`,观察者无需持有 run 即可看到子 agent 产出了什么。在基础设施拒绝(不存在 `SubagentResult`)的情况下,该字段缺失,事件报告 `stopReason: 'error'`。提供方与监听方是受信任的同进程协作者,遵守借用不可变载荷的契约。
+
+两个事件仍为普通 **`emit`**。异步的 `SubagentService.start()` 将结果观察附加到就绪的提供方 run 上,发出 `subagent/start`,然后返回该 run;进程内监听方因此可以通过 `ctx.agents.get(info.id)` 访问已发布的子 agent,而远程提供方无需在本地注册表中有对应条目。提供方启动被拒绝时不发出任何事件。回调保持仅观察,且逐监听方隔离确保一个异常订阅者不会阻塞活跃 run 或饿死后续监听方。
+
+## 曾考虑的替代方案
+
+**`agentType` subagent 类别标签**(CC 的 `subagent_type` 在 harness 中的对应物),放在请求与两个生命周期载荷上。早期草案曾包含它;评审中移除,因为它是 Claude Code 的概念,不适合我们自己的 seam(此处没有任何逻辑解释它,唯一消费方是 CC 方言桥接层)。CC 桥接层改为直接为其 SubagentStart/Stop 的 `agent_type` matcher 填入 Claude Code 自身的默认值 `"general-purpose"`,因此本 Agent Note 只交付**一项**丰富化:`lastAssistantMessage`。
+
+**控制流式 `subagent/end`**:推迟;见下文。
+
+## 为何仅观察,以及推迟了什么
+
+控制流式 `subagent/end`(一个被 await 的 waterfall,返回停止/继续决策,与其他拦截 seam 一致)需要:将 `subagent/end` 从 emit 改为 waterfall、重构 `SubagentService.start` 使其在结算前 await 监听方、在进程内提供方中实现 `resume` 能力以便「继续」能真正重新运行子 agent。这属于[能力 seam Agent Note](2026-06-21-subagent-capability-seam.md) 已推迟的后台/steering(中途引导)subagent 重设计(同一个重设计还将统一 subagent 与 bash 之间的长时间运行工具处理)。本 Agent Note 交付钩子桥接层当前所需的仅观察丰富化;`FIXME(subagent-continuation)` / `TODO` 锚点标记了控制流版本在重设计发生时的落点。
+
+## 后果
+
+钩子桥接层(或原生插件)现在可以通过订阅既有 emit 将子 agent 的 `lastAssistantMessage` 转发给 SubagentStop 处理器,无需新的控制流接口。词汇新增记录在 [docs/core-data-structures/subagent.md](../../../../docs/core-data-structures/subagent.md)(事件行文部分)与两个 subagent README 中;catalog 已重新生成。生产行为无变化——事件触发方式与之前完全一致,end 载荷上多了一个可选字段——因此无需更新快照或 e2e 测试。
diff --git a/.agents/notes/archived/feature/2026-07-07-plan-mode.i18n.yaml b/.agents/notes/archived/feature/2026-07-07-plan-mode.i18n.yaml
new file mode 100644
index 0000000000..aaae551f3e
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-07-plan-mode.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
+2026-07-07-plan-mode.md: dfc81c04baeb924ae04fbb51a27d282c5050f217
+2026-07-07-plan-mode.zh.md: 20662e208a2211a3c6add266845b746b31af20d9
diff --git a/.agents/notes/implemented/feature/2026-07-07-plan-mode.md b/.agents/notes/archived/feature/2026-07-07-plan-mode.md
similarity index 94%
rename from .agents/notes/implemented/feature/2026-07-07-plan-mode.md
rename to .agents/notes/archived/feature/2026-07-07-plan-mode.md
index 06fd50e1f0..dfc81c04ba 100644
--- a/.agents/notes/implemented/feature/2026-07-07-plan-mode.md
+++ b/.agents/notes/archived/feature/2026-07-07-plan-mode.md
@@ -1,9 +1,14 @@
# Agent Note: Plan mode — a logged per-agent session mode
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-07-plan-mode.zh.md)
> **Superseded vocabulary (2026-07-22):** [Collapse named session modes into plan mode](../simplification/2026-07-22-plan-specific-collaboration-state.md) replaces this note's generic `dsh-mode`, `mode/set`, definition map, and `ctx.modes` design with the current plan-specific `dsh-plan-mode`, `plan/mode`, `{ section }`, and `ctx.planMode` contract. The review, boundary, reconstructability, and sandbox-orthogonality decisions below remain in force; generic API examples are retained as the historical design this simplification removed.
+> **Superseded ACP mapping:** [ACP as an automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md) removes the picker, config-option, and elicitation mappings described below. Plan mode remains available to human-facing interfaces.
+
## Problem
Before this change, the harness had no durable way to put one agent into a distinct working stance. Plan mode needs the agent to explore and design under planning guidance, produce a reviewable artifact, cross an explicit approval boundary, and restore that state across resume and fork without making the model-visible request diverge from the session log.
@@ -118,7 +123,7 @@ No new cordis event is declared (`mode/set` rides `session/event`; the listeners
Each behind its own decision: subagent mode inheritance via a forwarded creation-time mode option (removed as unconsumed; it returns with its first consumer), preset modes beyond `plan` (read-only, accept-edits), the idle-record primitive if pending-intent loss proves real, and — the big one — **effects self-declaration on tool definitions**: a per-tool read-only/mutating classification (the MCP `ToolAnnotations` vocabulary — `readOnlyHint`/`destructiveHint` — is the natural template, with its untrusted-hint caveat implying trust tiers). That item is what a general per-mode tool policy waits on: this Agent Note first shipped an interim per-mode name allowlist and removed it before release — a hand-maintained list mislabels the effects question, must track every tool a deployment composes, and rots silently as tools arrive — so mode-scoped tool availability (and per-tool `ask` policies) returns as a CONSUMER of declared effects, which is its restart trigger.
-The canonical [`examples/acp-agent`](../../../../examples/acp-agent/) composition mounts the mode and question-tool plugins on the full ACP coding server; plan mode is an additive session feature, not a second server profile. Its snapshot suite pins the plan-shaped initial header, a real read, scripted approval, stable tool schemas across the pure-removal header delta, a subsequent edit, rejection feedback, and the keyless mode wire. A self-skipping real-API smoke boots that same leaf, verifies the file before approving the review, and verifies the approved implementation afterward.
+The ACP automation composition does not mount plan mode or the question tool. Human-facing compositions own plan selection and review; focused plan-mode tests and interactive-interface snapshots pin its logged state, guidance, review, and stable tool schemas.
## FAQ
@@ -138,13 +143,13 @@ Behavioral clarifications of the chosen design; rejected designs live in [Altern
**How does plan mode relate to the sandbox's read-only mode?** They are separate axes that never touch: the mode is the collaboration stance (a `mode/set` fold), the sandbox mode is an enforcement knob (a `bash/sandbox-mode` fold, [the sandbox Agent Note](2026-07-06-sandbox.md)) — plan mode neither reads nor caps it, exactly as Codex keeps its Plan/Default presets separate from its sandbox and approval settings. A user who wants kernel-enforced read-only while planning sets both: flip the mode picker AND the sandbox-mode option, in either order; each switch changes only its own fold, so there is no interference and no restore step to crash out of. The log attributes each axis to its own event — the stance to `mode/set`, the confinement to `bash/sandbox-mode`.
-**Why aren't sandbox mode, approval policy, or the model themselves modes?** They are individual environment knobs and belong to ACP's `session/set_config_option`; the division this proposal pins is picker-to-modes / knobs-to-config-options, recorded in [the feature matrix](../../../../packages/ui/acp/acp-feature-support.md) now that both this stack's picker and the sandbox stack's config options are landed. A mode definition may later bundle env facts (applied through `ctx.envState` where mounted) so a Codex-style preset stays a single mode; fusing approval policy into the mode CONCEPT itself is rejected in [Alternatives considered](#alternatives-considered).
+**Why aren't sandbox mode, approval policy, or the model themselves modes?** They are individual environment knobs independent of collaboration state. The retired ACP mapping is recorded by the [automation-only protocol decision](../simplification/2026-07-23-acp-automation-only-protocol.md). A mode definition may later bundle env facts (applied through `ctx.envState` where mounted) so a Codex-style preset stays a single mode; fusing approval policy into the mode CONCEPT itself is rejected in [Alternatives considered](#alternatives-considered).
## Prior art
A survey of shipped plan modes (Claude Code, Cursor, Copilot, OpenCode, Gemini CLI, Cline, Windsurf, Codex) shows the same five parts everywhere — the low-authority tool policy, plan artifact, approval moment, execution-state switch, and durable state that [Problem](#problem) builds on.
-The mode surface is a LIST everywhere it is advertised, never a boolean: Claude Code's picker offers `plan` beside `acceptEdits` (plus an auto-mode entry into plan), and Codex exposes `Plan` beside `Default` as collaboration-mode presets while keeping approval and sandbox settings separate. This is the surface [the ACP feature matrix](../../../../packages/ui/acp/acp-feature-support.md) records as the gap, and what sizes the vocabulary as named modes rather than a flag.
+The mode surface is a LIST everywhere it is advertised, never a boolean: Claude Code's picker offers `plan` beside `acceptEdits` (plus an auto-mode entry into plan), and Codex exposes `Plan` beside `Default` as collaboration-mode presets while keeping approval and sandbox settings separate. The ACP transport does not advertise this human-facing control.
The deployment-owned example prompt borrows the instrumental behavior, not product-specific mechanics. From Codex: remain in plan mode despite imperative implementation language, explore before asking, distinguish repository facts from user-owned choices, and make the plan decision-complete across APIs, data flow, failures, tests, and assumptions. From Claude Code: prohibit mutations and commits, prefer existing patterns, use questions only for requirements or approach choices, and finish through the exit tool rather than a prose approval request. It deliberately omits Codex protocol tags and Claude's plan-file or phased-subagent machinery because those belong to their runtimes, not this plugin contract.
@@ -186,7 +191,7 @@ What holds now, pinned by the unit, protocol, snapshot, and real-API tiers:
- Native tool schemas and Code Mode's SDK stay byte-identical across default, plan, and custom-mode transitions; only the configured guidance section changes.
- Plan mode changes nothing on the enforcement axes: the toolset, the sandbox mode, escalation, and the approval policy behave identically in plan and default — pairing the mode with the independent sandbox/approval knobs is how a deployment hardens planning.
- Mode definitions are changeable from `cordis.yml` with no code edit; the complete plan instructions are required there, while missing plan config, malformed definitions, and unknown keys fail at load and unknown mode names fail at `set()`.
-- `exit_plan_mode` is always advertised, rejects outside plan, drops only plan guidance after approval, and carries keep-planning feedback in a corrective `isError`; ACP mode updates and each surface's user-interaction provider carry the human side.
+- `exit_plan_mode` is always advertised, rejects outside plan, drops only plan guidance after approval, and carries keep-planning feedback in a corrective `isError`; each human-facing surface's user-interaction provider carries the review.
- The docs tail shipped with the landing: READMEs, regenerated catalogs (persistence log, config, cordis services, tools), the packages map and architecture rows, and the cookbook row.
-The accepted costs: a pending user flip set while idle is lost if the process dies before the next turn (the UI re-applies; the idle-record primitive is the escape hatch if this bites in practice). A mode transition changes the system prompt at order 50, so the cache path from that point onward changes, but the tool schemas and Code Mode SDK no longer churn. **A mode restrains by guidance alone**: a model that ignores the section CAN mutate during plan — the review moment, the session log, and independent sandbox, approval, and filesystem policies are the containment surface. Hardening planning means setting those knobs, not widening the mode; the removed enforcement shapes and their effects-declaration restart trigger remain in [Alternatives considered](#alternatives-considered) and [Deferred](#deferred). The ACP mode surface carries the picker while sandbox, approval, and model selectors remain config options under the division pinned in the [FAQ](#faq) and [feature matrix](../../../../packages/ui/acp/acp-feature-support.md). If ACP removes session modes in favor of config options, the picker mapping can migrate without changing the logged mode state or model surface.
+The accepted costs: a pending user flip set while idle is lost if the process dies before the next turn (the UI re-applies; the idle-record primitive is the escape hatch if this bites in practice). A mode transition changes the system prompt at order 50, so the cache path from that point onward changes, but the tool schemas and Code Mode SDK no longer churn. **A mode restrains by guidance alone**: a model that ignores the section CAN mutate during plan — the review moment, the session log, and independent sandbox, approval, and filesystem policies are the containment surface. Hardening planning means setting those knobs, not widening the mode; the removed enforcement shapes and their effects-declaration restart trigger remain in [Alternatives considered](#alternatives-considered) and [Deferred](#deferred). Human-facing interfaces own the plan picker and review interaction; the ACP automation transport carries neither.
diff --git a/.agents/notes/archived/feature/2026-07-07-plan-mode.zh.md b/.agents/notes/archived/feature/2026-07-07-plan-mode.zh.md
new file mode 100644
index 0000000000..20662e208a
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-07-plan-mode.zh.md
@@ -0,0 +1,197 @@
+# Agent Note: plan mode——记录到日志的逐 agent 会话模式
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-07-plan-mode.md) | 中文
+
+> **已取代的词汇(2026-07-22):**[将具名会话模式收敛为 plan mode](../simplification/2026-07-22-plan-specific-collaboration-state.md) 已将本笔记中通用的 `dsh-mode`、`mode/set`、定义 map 与 `ctx.modes` 设计,替换为当前 plan 专用的 `dsh-plan-mode`、`plan/mode`、`{ section }` 和 `ctx.planMode` 契约。下文的评审、边界、可重建性与沙箱正交性决策仍然有效;通用 API 示例则作为此次简化所移除的历史设计保留下来。
+
+> **已取代的 ACP(Agent Client Protocol)映射:**[ACP 作为仅面向自动化的协议](../simplification/2026-07-23-acp-automation-only-protocol.md)移除了下文所述的选择器、配置选项和 elicitation 映射。面向人类的接口仍可使用 plan mode。
+
+## 问题
+
+此次变更之前,harness 无法持久地让某个 agent(智能体)采用独特的工作姿态。Plan mode 要求 agent 在规划指引下探索和设计,产出可供评审的产物,跨过明确的审批边界,并在恢复与 fork 后还原该状态,同时不能让模型可见请求偏离会话日志。
+
+既有扩展 seam 已经提供了周边机制:[`system-prompt/assemble`](../../../../packages/core/system-prompt/README.md) 为每个步骤塑造指引,已发送的请求则记录在 `request/header*` 事件中(参见[可重建性](../../implemented/architecture/2026-07-05-reconstructable-requests.md));[`ctx.userInteraction`](../../../../packages/ui/user-interaction/README.md) 承载审批问题与纠正反馈(参见 [ask-user 先例](../../implemented/feature/2026-06-25-ask-user-question.md));`SessionEventMap` 承载逐 agent 的持久事实(参见 [`todo/write` 先例](../../implemented/feature/2026-06-29-todo-write-tool.md))。缺少的是将这些 seam 连在一起的具名会话状态,同时仍让独立的沙箱轴与审批轴负责执行约束。
+
+## 决策
+
+交付项是 **plan mode**。它作为首个**会话模式**发布,即一个具名、记录到日志且逐 agent 生效的协作状态:模式定义是由部署配置、供模型查看的指引;对某个 agent 生效的模式则是从其日志折叠出的会话状态。模式构成一条轴,强制约束旋钮——沙箱模式与审批策略——构成其他轴;它们从不互相读写,这与 Codex 将 Plan/Default 协作预设同沙箱及审批设置分开的做法一致。新的产品包(package)`@deepseek-ai/dsh-mode` 位于 `packages/mode/mode/`,拥有事件词汇、精简的 `ctx.modes` 服务和全部监听器;循环无需改动。`plan` 是唯一的必需定义;采用模式形状的词汇,是为了以后增加第二种模式时无需重命名持久事件类型,而不是因为当前还会发布其他模式。
+
+该状态是 `SessionEventMap` 的一个成员:**`mode/set`** 是只记录日志、不进入 surface 的事件,携带具有整值替换语义的 `{ mode: string }`;另有纯函数 `foldMode(events)` 返回生效模式,即最后一个 `mode/set`,没有该事件时则返回默认模式。由于[日志是事实通道](../../implemented/architecture/2026-06-30-event-domain-semantics.md),恢复、fork 和压缩无需额外机制即可还原模式,UI 则从 `session/event` 读取模式切换。默认模式表示不存在模式指引,即没有段落、过滤或门禁。加载 `dsh-mode` 后,每种模式仍会贡献同一个稳定的 `exit_plan_mode` schema;这项固定成本避免了模式边界处的工具目录抖动。
+
+模式的所有外显行为都是软约束:`mode:policy` 提示词段落渲染当前定义的指引,而 `exit_plan_mode` 在每种模式下都留在已注册的工具目录中,仅当折叠模式不是 `plan` 时才在执行阶段拒绝。因此,转换只会在下一步骤改变可归因 `request/header` 中的系统提示词部分,从而在不改变 Native schema 或 Code Mode SDK 的情况下继续满足[可重建性](../../implemented/architecture/2026-07-05-reconstructable-requests.md)。模式有意不强制执行任何约束:没有执行门禁,不过滤工具,也不触及沙箱或审批旋钮。若用户希望规划期间存在硬性的只读下限,可以在模式选择器旁切换沙箱模式选项;二者先后顺序任意,任何一条轴都不会扰动另一条轴。同样也不存在逐模式的工具允许/拒绝清单;模式允许哪些工具属于副作用问题,在工具定义能够声明自身副作用前暂缓处理(见[延期工作](#deferred))。模式只依靠其段落指引与退出评审来约束行为。
+
+模型通过 **`exit_plan_mode`** 工具离开 plan mode。其唯一参数是 plan 文本,因此可以从日志重建 plan;该工具自行通过用户交互 seam 完成评审:问题的辅助详情携带确切 plan,并提供选项与自由文本通道,而不是只有一项裸权限。审批通过后,记录到日志的模式切回默认模式;拒绝则成为携带用户逐字反馈的纠正错误,让模型能沿明确方向继续规划。用户可从任意接口通过 `ctx.modes.set()` 切换模式;切换会在下一个轮次边界应用(会话事件都封闭在轮次内),且只有模型可见状态确实变化时才向模型讲述一次。
+
+## 高层 API
+
+### 一次端到端的 plan-mode 会话
+
+用户通过 ACP 模式选择器或终端入口中的 `/plan [message]` 将会话切换到 plan mode;从下一步骤开始,每个请求都携带已配置的 plan 指引段落。如果给出可选消息,同一命令还会把它提交到受影响的步骤中。`exit_plan_mode` schema 在默认模式下已经存在,并会保持逐字节不变。
+
+模型进行探索与设计;段落中的指引会让它把变更推迟到 plan 中。沙箱与审批旋钮保持用户设置的值不变;希望规划期间由内核强制只读的部署方(或用户),可以把 plan mode 与独立的沙箱模式选项配合使用。
+
+准备就绪后,模型调用 `exit_plan_mode`,并把 plan markdown 作为参数;评审问题将这段确切 markdown 作为辅助详情,用户可以批准,也可以要求继续规划并自由填写反馈。Native 调用还会渲染 plan 卡片;Code Mode 嵌套分发没有 Native 卡片,因此评审详情是共用的呈现接口。
+
+批准后,工具把记录到日志的模式切回默认模式:下一步骤会移除 plan 段落,但保留同一个工具目录(变化后的 header 已记录到日志),此后的执行跟踪本就由 `todo_write` 负责。要求继续规划时,模型会收到携带用户反馈文本的纠正错误,随后修改并再次呈现。
+
+### 部署配置
+
+模式定义是经过校验的插件 Config;依照仓库约定,它可以通过 `cordis.yml` 修改,无需编辑代码。部署必须提供完整的 `plan` 段落;该包不内置任何模型指令。其他模式使用同一份配置 map:
+
+```yaml
+- id: mode
+ name: '@deepseek-ai/dsh-mode'
+ config:
+ modes:
+ plan:
+ section: |
+ You are in plan mode: explore and design, then present the
+ plan for approval through exit_plan_mode.
+```
+
+定义的精确形状是 `{ section }`;其中有意不提供逐模式工具清单或强制约束字段(见[常见问题](#faq))。定义名称使用小写斜杠命令子集 `/^[a-z][a-z0-9_-]*$/u`;`default` 是保留项(表示没有策略),不能用作键。名称无效或存在未知定义键——包括 `tools` 清单或 `access` 上限——会在加载时校验失败;未知模式名称则会在调用 `set()` 时大声失败。
+
+### 在终端中
+
+终端入口通过插件自有的命令注册表(`@deepseek-ai/dsh-commands`),为每个已配置定义获得一条进入命令:`dsh-mode` 为必需定义注册 `/plan [message]`,例如还会注册 `/review [message]`(当配置 `review` 时)。每条命令都记录其具名切换;非空的可选消息会去除首尾空白并传给 `agent.steer()`,后者会把消息放入运行中 agent 的下一步骤,或委托给 `send()` 以开启新的空闲轮次。命令名称与结果不会进入模型历史;这条显式消息则会作为所选模式下的普通用户消息记录到日志。合成的 `default` 条目不贡献命令。退出评审无需新机制即可直接在终端中提示:它是普通的用户交互问题,因此会进入组合后的用户交互提供方提示队列,与 `ask_user_question` 使用的队列相同。
+
+### 通过 ACP
+
+模式选择器是该包的对外接口:`session/new`/`session/load` 会通告 `availableModes`/`currentModeId`,其值来自 `ctx.modes`(通过 `ctx.get` 机会式消费,沿用 `tool-bash` 模式);`session/set_mode` 调用 `set()` 并乐观通知 `current_mode_update`(待生效模式就是用户的选择,记录到日志的 `mode/set` 会在边界处跟进);`session/event` 监听器则会在每次已记录切换不同于最近一次已发送值时再次通知。退出工具复用用户交互 ACP 提供方的 elicitation 流程;其 ACP 映射会携带评审 `detail`,因为 Code Mode 嵌套分发没有 Native plan 卡片,而 Native 调用还可以额外流式传输该卡片。沙箱模式、审批策略和模型等单项环境旋钮不是模式,应归入 `session/set_config_option`(见[常见问题](#faq))。
+
+### 面向 agent 创建方
+
+`ctx.modes` 是完整的程序化接口:`list()` 返回已配置定义和供选择器使用的合成 `default` 条目;`get(agent)` 返回折叠模式与可能存在的待生效意图;`set(agent, mode)` 则根据 `list()` 的词汇校验名称,并记录将在边界应用的意图。`default` 始终是有效目标,因此退出模式与进入模式使用同一次调用。创建时没有模式选项;调用方在首个轮次前通过 `set()` 选择模式,随后以相同方式刷写。系统也不提供可订阅的实时 `agent/*` 镜像:UI 依照[事件领域语义](../../implemented/architecture/2026-06-30-event-domain-semantics.md)读取 `mode/set`,该事件来自 `session/event`。
+
+## 详细设计
+
+### 词汇
+
+```text
+'mode/set': { mode: string } // SessionEventMap merge in dsh-mode: log-only, non-surface,
+ // whole-value replace — the last one in the log wins
+DEFAULT_MODE = 'default' // the fold of a log with no mode/set; reserved, not definable
+```
+
+载荷不携带原因/溯源字段:工具驱动的切换在日志中紧邻其 `tool/call`,用户切换则位于轮次边界,因此原因就在日志相邻位置。这与[可重建性 Agent Note](../architecture/2026-07-05-reconstructable-requests.md)针对请求头事实所作的「叙述字段可以派生」决策相同(进行中的 `env/state` 事件之所以携带 `source`,正是因为其漂移变体在日志相邻位置没有原因;二者形成对照,并不冲突)。模式名称是配置声明的词汇,不是不透明的跨边界 id,因此仍使用裸字符串(不使用 `Branded`)。
+
+### 配置与解析步骤
+
+```text
+interface ModeDefinition { section: string } // prompt text — a mode's whole vocabulary
+interface ModeConfig { modes: Record } // plan is required and owns its complete prompt
+resolveConfig(config): ResolvedModes // explicit resolve (the dsh-bash template), fail-loud:
+ // missing plan, 'default', blank sections, and unknown keys rejected
+```
+
+单字段形状是有意采用的最简设计,并非最终词汇:逐工具策略维度会以工具定义中的副作用元数据形式回归(见[延期工作](#deferred)),在此处读取,而不是由每种模式重新声明;该维度到来时,配置形状不应需要迁移。
+
+### 折叠、服务与刷写
+
+`foldMode(events)` 是纯函数(导出供重建方与测试使用),直接折叠仅追加的会话日志;`mode/set` 不是 surface 节点,因此压缩无法遮蔽它。`set(agent, mode)` 根据 `list()` 的词汇校验名称,即已配置定义加上保留的 `default`;后者不能用作配置键,却始终可以作为 `set()` 目标。目标与待生效模式相同(没有待生效模式时则与当前模式相同)时,该方法丢弃无操作;其余情况会把 `{ mode, narrate }` 记录到 `WeakMap` 的待生效意图槽中。它不能立即追加,因为[每个会话事件都封闭在轮次内](../../implemented/architecture/2026-06-15-turn-enclosure-invariant.md),而空闲 agent 没有打开的轮次。
+
+循环拦截 seam 上经过故障隔离的监听器(参见[防御模式](../../../../docs/defensive-patterns.md):策略插件不得阻塞提示词或轮次)会把待生效意图刷写为一条 `mode/set` 追加:`agent/prompt-submit` 在刚打开的轮次中、首次组装前触发;`agent/turn-continuation` 则在普通步骤关闭后、后续步骤开始前触发。自动请求恢复会绕过 continuation,因此,前置的 `agent/request-error` 包装器会先委托给组合后的策略和异步退避,只在 waterfall 返回循环前刷写 `retry` 决策;effect 作用域的生命周期守卫会抑制在插件资源释放后才恢复的已捕获包装器。三条路径都位于工具执行与日志发布之外(提交后的 `session/event` 观察器只负责观察),因此每个步骤都在其组装所折叠出的模式下运行。刷写模式与最后一个 `request/header` 处的折叠结果不同时,刷写会在同一帧中追加一条合并后的 `context/message` 通知(「用户已将此会话切换到 plan mode。」);面向用户的叙述情形列在[常见问题](#faq)中。
+
+### 软层:计算得出的段落与稳定的退出 schema
+
+已注册的提示词段落从 `AssembleContext.agent` 读取调用 agent 的模式,并解析为当前定义的指引或 `''`。循环逐步骤渲染,并在渲染后的 header 发生变化时记录完整的 `request/header`,因此进入或离开模式均可归因。该段落在每种模式内保持静态,plan 本身则以消息和工具参数留在对话中;无需为了跨压缩保留状态,而在每个请求中重新注入独立的 plan 状态([既有方案](#prior-art)采用的办法),徒增提示词抖动。
+
+指引贡献为 `{ name: 'mode:policy', order: 50, text: context => … }`:排在人设(0)之后、工具指引(100–199)之前,并在默认模式或没有 agent 的组装中为空。`exit_plan_mode` 只通过 `ctx.tools` 注册一次且从不过滤,因此模式切换期间 Native schema 与 Code Mode 生成的 SDK 保持逐字节相同;未部署 `dsh-mode` 的环境则没有这项绑定。系统不注册 `tools/pre-execute` 监听器:模式不设置任何门禁,退出工具自身的折叠模式检查会拒绝 plan 之外的调用。退出评审是一个带选项和反馈的问题,不是权限,因此位于工具通过用户交互 seam 执行的过程内。
+
+### `exit_plan_mode`
+
+`defineTool` 有一个必填的 `plan: string` 参数。Native 执行会把它记录在普通 `tool/call` 中;Code Mode 在执行前记录外层 `run_code` 源码,并在分发结算后把规范化的嵌套参数追加到 `tool/code-dispatch`。`execute` 会拒绝没有 agent 的调用(沿用 [`todo_write` 先例](../../implemented/feature/2026-06-29-todo-write-tool.md))和折叠模式不是 `plan` 的调用,并在询问评审人前拒绝空 plan 或不含标题的 plan;随后,它通过 `ctx.userInteraction.ask()` 发起一次单选评审,其 `detail` 是确切 plan,并开放自由文本反馈,供用户批准或要求继续规划。只有恰好选择一个 `Approve` 才表示同意,其他任何形状都按失败关闭处理。批准会记录一项将在边界生效且不叙述的意图,用于切换到 `default`,并返回简短确认。部署指引要求模型把这次调用作为回复中唯一且最后一次工具调用;如果模型违反该规则,运行时仍会让该批次剩余部分保留 plan 指引,下一步骤才记录变化后的 header,其中移除指引而工具 schema 保持不变。所有未获批准的结果都会返回纠正性的 `isError`,并让模式留在 `plan`。
+
+其[渲染意图](../../implemented/architecture/2026-07-02-tool-render-intent-union.md)在设计之初就已确定:`presentCall` 是 `generic` 卡片,以 plan 的首个标题命名、以 plan markdown 作为内容,另配一张 `generic` 结果卡片。Native 入口会在问题之前显示该卡片;Code Mode 嵌套分发不会产生 Native 调用卡片事件,因此用户交互 `detail` 会在每个提供方上独立携带同一份 plan。系统机会式消费该 seam(`ctx.get('userInteraction')`),所以 `dsh-mode` 在没有它时仍可组合,并降级为[常见问题](#faq)中确定的手动退出方式。
+
+### 依赖与接口
+
+`dsh-mode` 是一个产品包,而不是由三个包组成的能力 seam(见[考虑过的替代方案](#alternatives-considered)):它对等依赖 `cordis`、`dsh-session`、`dsh-agent`、`dsh-tools` 与 `dsh-system-prompt`,注入 `['tools', 'systemPrompt']`,并在执行时机会式读取 `ctx.userInteraction`(指向 `dsh-user-interaction` 的仅类型对等依赖边);其仅有的 UI 侧边也是可选的仅类型对等依赖(逐定义进入命令使用 `dsh-commands`)。除 `ctx.modes` 调用接口外,所有内容都通过监听器参与,因此移除该包会平稳移除模式,而不会破坏消费方。终端入口无需模式专用代码:组合命令注册表后,`dsh-mode` 会自行注册每个定义的命令(指向 `dsh-commands` 的可选仅类型对等依赖边),退出评审则使用组合后的用户交互提供方提示队列。[高层 API](#over-acp) 已确定 ACP 协议映射;在包关系上,桥接层对 `dsh-mode` 采用仅类型对等依赖边并机会式读取服务,所以不含该插件的桥接层行为与当前完全相同。
+
+### 已记录场景与 harness 操作
+
+`input.json` 新增一种步骤操作 `{ "op": "setMode", "modeId": "plan" }`,通过真实的 `session/set_mode` RPC 驱动,并配有脚本化的 `elicitationAnswers` 队列。`plan-mode` 场景在第 1 个轮次前进入 plan,在独立配置的沙箱下运行真实的 `cat`,通过 `exit_plan_mode` 呈现 plan,接收脚本化审批,然后在下一步骤编辑。首个 `request/header` 包含完整、稳定的工具集和已配置模式段落;批准后变化的 header 会保留逐字节相同的工具 schema,只移除该段落。`plan-mode-reject` 固定纠正性的自由文本反馈和未变化的 plan 状态。两份记录都在 Seatbelt 或 bwrap 下回放宿主命令;后端特有的沙箱拒绝仍留在 bash 工具单元层。
+
+### 机械收尾
+
+系统不声明新的 Cordis 事件(`mode/set` 通过 `session/event` 传递,监听器附着到现有 waterfall),因此事件目录不变。同一变更重新生成以下内容:持久化日志目录(`mode/set`)、服务目录(`ctx.modes`,JSDoc 完整)、配置目录(`ModeConfig`)、工具目录(`exit_plan_mode`)、生产方/消费方 map 与文档图,以及模块图。仓库接线包括:根 tsconfig 的 `paths` 条目、新包组 README 和[包索引](../../../../packages/README.md)中的一行(新增顶层包组正是该表所命名的有意操作)、`architecture.md` 中经过预算检查的 `ctx.modes` 能力服务行,以及实操手册对应行的升级。
+
+## 延期工作
+
+以下各项都需要独立决策:通过转发的创建时模式选项实现 subagent 模式继承(由于没有消费方而移除,将随首个消费方回归);`plan` 之外的预设模式(只读、接受编辑);若待生效意图丢失被证明是真实问题,则引入空闲记录原语;以及最重要的**在工具定义上自行声明副作用**,即逐工具的只读/变更分类(MCP `ToolAnnotations` 词汇——`readOnlyHint`/`destructiveHint`——是自然模板,其中对不可信提示的警告意味着还需区分信任层级)。通用的逐模式工具策略正在等待这一项:本 Agent Note 最初发布过临时的逐模式名称允许清单,并在发布前移除;手工维护的清单错误地表达了副作用问题,必须跟踪部署所组合的每个工具,且会随工具增加而无声腐化。因此,按模式限制的工具可用性(以及逐工具 `ask` 策略)会作为已声明副作用的消费方回归,而首项消费需求就是其重启触发条件。
+
+ACP 自动化组合不挂载 plan mode 或问题工具。面向人类的组合拥有 plan 选择与评审;聚焦的 plan-mode 测试和交互接口快照会固定其已记录状态、指引、评审和稳定工具 schema。
+
+## 常见问题
+
+以下内容澄清选定设计的行为;遭否决的设计见[考虑过的替代方案](#alternatives-considered),已接受的代价见[后果](#consequences)。
+
+**用户切换模式后何时生效?** 在下一个组装前边界生效:`agent/prompt-submit` 覆盖首个步骤,`agent/turn-continuation` 覆盖普通后续步骤,组合策略之后的 `agent/request-error` 重试决策覆盖自动恢复。因此,在请求或重试退避进行期间选择的模式会塑造下一次模型请求。这就是[既有方案](#prior-art)中每项产品都采用的「应用于后续请求」语义。
+
+**何时向模型讲述模式变化?** 仅当模型可见状态确实变化时:刷写会把刚刷写的模式与最后一个 `request/header` 处的折叠结果进行比较,并合并讲述一次。净变化为零的切换序列(先进入 plan,再在边界前切回)不会产生叙述;工具驱动的退出只通过自身工具结果叙述;首个轮次前设置的模式也不叙述,因为该段落本身就是状态说明。该原则来自进行中 env-state 提案的边界叙述:如果提示词表层悄然切换,transcript(文本记录)仍会依据 header 已不再具备的状态进行论述。
+
+**恢复时,配置已不再定义折叠出的模式会怎样?** 当前配置不再定义的折叠模式名称会在不通知的情况下表现为默认模式,因此会话既不会获得替代约束,也不会变得不可用。`set()` 的大声校验只覆盖写入路径;恢复后的日志以当时找到的配置为准。
+
+**如果部署没有组合用户交互提供方,会怎样?** Plan mode 仍然安全,但只能手动退出:`ctx.userInteraction.ask()` 会抛出 `NO_PROVIDER`(seam 不存在时甚至无法解析到该服务),工具返回纠正性的 `isError`,退出方式降级为由用户切换模式,绝不会在未经评审时退出。模式段落会要求模型通过 `exit_plan_mode` 呈现 plan,并在失败时改用普通文本询问用户,因此模型会继续呈现,而不会停滞。
+
+**为何没有逐模式工具允许清单?** 因为「哪些工具在规划模式下安全」是每个工具自身的属性(即副作用),不是模式的属性。逐模式名称清单会在错误的归属位置重新声明该事实,必须枚举部署所组合的每个工具(包括 MCP 服务器),且会随工具到来而无声腐化。在工具定义声明其副作用前(见[延期工作](#deferred),其中归档了被移除的临时允许清单及其重启触发条件),模式只通过自身段落和退出评审约束行为;由此产生的暴露面属于已接受代价(见[后果](#consequences))。
+
+**subagent 是否继承父级模式?** fork 子级可以直接继承,因为父级的 `mode/set` 位于种子前缀中。spawn 子级从默认模式开始;创建时模式选项与 subagent 提供方的自动转发一并延期(见[延期工作](#deferred))。
+
+**plan mode 与沙箱只读模式有何关系?** 二者是互不接触的独立轴:模式是协作姿态(`mode/set` 折叠),沙箱模式是强制约束旋钮(`bash/sandbox-mode` 折叠,参见[沙箱 Agent Note](2026-07-06-sandbox.md))。Plan mode 既不读取也不限制沙箱模式,与 Codex 将 Plan/Default 预设同沙箱及审批设置分开的做法完全一致。希望规划期间由内核强制只读的用户需要同时设置两者:以任意顺序切换模式选择器与沙箱模式选项;每次切换只改变自身折叠结果,因此二者互不干扰,也不存在可能崩溃的还原步骤。日志会把每条轴归因到各自事件:协作姿态对应 `mode/set`,隔离约束对应 `bash/sandbox-mode`。
+
+**为何沙箱模式、审批策略或模型本身不属于模式?** 它们是独立于协作状态的单项环境旋钮。已退役的 ACP 映射记录在[仅面向自动化的协议决策](../simplification/2026-07-23-acp-automation-only-protocol.md)中。未来模式定义可以捆绑环境事实(在挂载处通过 `ctx.envState` 应用),让 Codex 风格的预设仍是一种模式;但把审批策略融合进模式概念本身的方案已在[考虑过的替代方案](#alternatives-considered)中遭否决。
+
+## 既有方案
+
+对已发布 plan mode(Claude Code、Cursor、Copilot、OpenCode、Gemini CLI、Cline、Windsurf、Codex)的调研表明,各产品都包含同样五个部分:低权限工具策略、plan 产物、审批时刻、执行状态切换,以及[问题](#problem)所依赖的持久状态。
+
+只要产品公开模式接口,该接口就一定是清单,绝不是布尔值:Claude Code 的选择器提供 `plan`,旁边是 `acceptEdits`(另有自动进入 plan 的模式);Codex 则把 `Plan` 与 `Default` 并列公开为协作模式预设,同时让审批和沙箱设置保持独立。ACP 传输层不公开这项面向人类的控制。
+
+由部署拥有的示例提示词借鉴工具性行为,而非产品特有机制。它借鉴 Codex 的以下做法:即使收到祈使式实现语言也留在 plan mode;提问前先探索;区分仓库事实与由用户决定的选择;让 plan 完整覆盖 API、数据流、失败、测试和假设,从而足以作出决策。它还借鉴 Claude Code 的以下做法:禁止变更与提交;优先沿用现有模式;只针对需求或方案选择提问;通过退出工具完成规划,而不在普通文本中请求审批。它有意省略 Codex 协议标签,以及 Claude 的 plan 文件或分阶段 subagent 机制,因为这些属于各自运行时,而非本插件契约。
+
+把模式留给约定的生态展示了应避免的失败形态。Pi 风格的模式扩展会争抢一个后写覆盖的全局活跃工具清单,只靠提示词文本强制「只读」(模型幻觉调用一个仍已注册的工具时,该调用会实际执行),并在每个请求中重新注入 plan 状态以跨过压缩。这里通过逐 agent 的折叠状态,以及压缩无法遮蔽的只记录日志、非 surface 事件,从结构上消除了有争议的全局清单和重复注入补丁。相较之下,仅靠提示词的形态被有意保留:Codex 的 Plan 正是如此实现,这也是模式轴可以与强制约束轴自由组合的原因。需要硬性下限的部署会把模式与独立的沙箱旋钮配对,而不是让模式携带自身强制约束(见[常见问题](#faq))。
+
+## 考虑过的替代方案
+
+**以权限模式作为核心概念(Claude Code 的形态)。** 用一个 `permissionMode` 融合审批策略和工具策略。本设计中,它们是归不同所有者负责的两条轴:审批 seam 拥有「谁回答这个问题」,模式拥有「模型获得什么接口」。ACP 将二者建模为相关但有区别的概念(模式以后可以选择审批策略;届时是模式定义增加字段,而不是合并两者)。
+
+**由三个包组成的能力 seam。** 接口/实现/消费方适合可替换后端;模式的可变部分是配置值,而不是实现。拆分会制造一个空实现包,与审批 seam 及 [`todo/`](../../implemented/feature/2026-06-29-todo-write-tool.md) 作出的「不要过早拆分」决策相同。
+
+**由循环拥有模式状态。** 依据既有规则(用插件,而不修改循环)予以否决:该功能所需的每个钩子——组装、执行前处理、轮次边界、会话事件——都已经是有文档记录的 seam,修改循环只会增加耦合,别无收益。
+
+**带默认拒绝门禁的逐模式工具允许清单(首个发布形态)。** 已在发布前移除。手工维护的名称清单会逐模式重新声明一项逐工具事实(即其副作用):它必须枚举部署所组合的每个工具,包括 MCP 服务器和未来注册项,并会随工具增加而无声腐化(新增只读工具时,在有人编辑每种模式前都会被阻止;编写负担落在了解模式的人身上,而不是了解工具的人身上)。它还过度承诺:该清单看似安全边界,但 shell 之外的任何能力其实都不存在这种真实边界。通用维度已停放到副作用自声明(见[延期工作](#deferred));由此产生的后果——plan mode 只提供指引,也就是该门禁一度弥补的 Pi 缺口——是有意接受的,并计入[后果](#consequences)。
+
+**模式上的 `access` 沙箱上限(第二个发布形态)。** 同样已在发布前移除。`ModeDefinition.access` 会把 bash seam 的逐调用沙箱解析限制在模式声明的上限内(一个 `bash/resolve-mode` waterfall 加上取阶梯最小值的监听器;守卫会在执行器无法施加约束时隐藏 bash,并在模式中途拒绝提升权限)。状态仍保持正交,因为上限从不写入沙箱旋钮;但两条轴并不正交:进入 plan 会改变沙箱实际强制执行的内容,把协作姿态与强制约束级别融合起来,违背评审最终达成的 Codex 形态分离方式(Plan/Default 预设从不触及沙箱或审批设置)。这种融合有一项用户可见症状:规划期间把沙箱选项切换为 `workspace-write` 不会产生任何效果。上限、waterfall 和 mode→bash 依赖边随后一并移除;部署可以把模式与独立沙箱模式选项配合使用,从而在规划期间由内核强制只读。以后也可以重新引入模式触发的预设(模式定义捆绑建议的旋钮值,并作为普通旋钮切换加以应用),而无需再次融合两条轴。
+
+**仅存在于运行时的模式(只在 UI 或桥接层本地存在,不记录日志)。** 恢复与 fork 会悄然丢失模式,模式引起的 header 增量在日志中也没有可归因原因。记录到日志的状态让模式无需额外机制即可审计和还原。
+
+**把模式切换作为 `context/message` 并通过 `agent.inject()` 写入。** 这样可以复用现有的轮次封闭路径,却会把策略状态放入模型 transcript;模型无需被告知两次(段落已经告诉它),而只记录日志的事实不应占用 surface。
+
+**plan 文件存储(`.plans/` 目录)。** 这会为日志已经以可回放方式承载的内容创建第二个持久归属位置;需要文件的部署可以添加一个写入文件的工具。同一事实只应有一个归属位置。
+
+**用布尔值 `planMode` 取代具名模式。** 对仓库已经跟踪的接口而言过于狭窄:ACP 通告的是模式清单,已发布的选择器也不只填入 plan(见[既有方案](#prior-art));以后再泛化会重命名持久事件词汇。字符串形状的机制现在不产生额外成本;只有 `plan` 作为定义发布。
+
+**工具策略栈服务(对 Pi 批评的补救方案)。** 现在就为工具策略建立专用组合服务为时过早:本实现不执行按模式限制的工具过滤,未来的副作用策略可以通过现有带守卫的执行 seam 组合。只有已声明的工具副作用产生具体组合需求后,才应正式建立该服务。
+
+**通过审批 seam 执行退出审批(一个 `{ kind: 'ask' }` 门禁决策)。** 最初草案提出该方案;当时审批 seam 是唯一正在落地的询问机制,所以显得自然,但它把评审放进了权限的位置。该 seam 的结果词汇有意封闭且仅供单次使用(`allowed-once`/`rejected`),因此拒绝无法携带反馈,批准也永远无法增加选项(例如「批准并接受编辑」)。退出时刻是一个问题,而非权限;用户交互 seam 为其提供选项与自由文本通道,拒绝反馈也会逐字传给模型。审批 seam 仍适合真正的权限门禁(沙箱提升权限),注册表的 `ask` 词汇也继续供希望在该处设置询问的部署使用。
+
+**使用普通文本或 steering(中途引导)退出,而不使用工具。** 这样既没有产物,也没有审批时刻。工具参数本身就是可供评审的 plan,而评审问题会把结构化的是/否选择附着到确切转换上并交给人类。
+
+## 后果
+
+以下保证现在已经由单元、协议、快照和真实 API 测试层固定:
+
+- 生效模式是会话日志的纯函数:恢复与 fork 无需额外机制即可还原它;下一变化步骤中,一条 `mode/set` 后会出现匹配的完整 `request/header`。
+- 用户驱动的切换会在下一边界恰好叙述一次,净变化为零的切换序列不会产生叙述;工具驱动的退出只通过自身工具结果叙述。
+- 默认模式下,插件不贡献模式段落,但会贡献稳定的 `exit_plan_mode` schema;未部署 `dsh-mode` 的环境没有这项绑定。
+- 默认、plan 与自定义模式之间转换时,Native 工具 schema 与 Code Mode SDK 保持逐字节相同;只有已配置的指引段落发生变化。
+- Plan mode 不改变强制约束轴上的任何内容:工具集、沙箱模式、权限提升和审批策略在 plan 与默认模式下表现完全相同;部署通过把模式与独立的沙箱/审批旋钮配合使用来强化规划。
+- 模式定义可通过 `cordis.yml` 修改,无需编辑代码;其中必须提供完整 plan 指令。缺失 plan 配置、定义畸形和未知键会在加载时失败,未知模式名称会在 `set()` 时失败。
+- `exit_plan_mode` 始终通告,在 plan 之外会拒绝;批准后只移除 plan 指引,并通过纠正性的 `isError` 携带继续规划反馈;每个面向人类的接口都由其用户交互提供方承载评审。
+- 随功能落地一并交付的文档收尾包括:README、重新生成的目录(持久化日志、配置、Cordis 服务、工具)、包索引与架构行,以及实操手册中的对应行。
+
+已接受的代价如下:在 agent 空闲时设置的待生效用户切换,如果进程在下一轮次前退出,就会丢失(UI 会重新应用;若实践中出现问题,空闲记录原语就是逃生口)。模式转换会改变顺序 50 处的系统提示词,因此从该处开始的缓存路径也会变化,但工具 schema 与 Code Mode SDK 不再抖动。**模式只依靠指引约束行为**:忽略该段落的模型可以在 plan 期间执行变更;评审时刻、会话日志以及独立的沙箱、审批和文件系统策略共同构成约束边界。强化规划意味着设置这些旋钮,而不是扩大模式职责;被移除的强制约束形态及其副作用声明重启触发条件仍记录在[考虑过的替代方案](#alternatives-considered)和[延期工作](#deferred)中。面向人类的接口拥有 plan 选择器与评审交互;ACP 自动化传输层两者都不承载。
diff --git a/.agents/notes/archived/feature/2026-07-07-session-prefix.i18n.yaml b/.agents/notes/archived/feature/2026-07-07-session-prefix.i18n.yaml
new file mode 100644
index 0000000000..e8a135a973
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-07-session-prefix.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
+2026-07-07-session-prefix.md: 8b74e956db42631914a64fe81c1581ff7748a955
+2026-07-07-session-prefix.zh.md: c33c4971984f0486ea2302ed739f006ec95a2e5b
diff --git a/.agents/notes/implemented/feature/2026-07-07-session-prefix.md b/.agents/notes/archived/feature/2026-07-07-session-prefix.md
similarity index 85%
rename from .agents/notes/implemented/feature/2026-07-07-session-prefix.md
rename to .agents/notes/archived/feature/2026-07-07-session-prefix.md
index 7faf80cf80..8b74e956db 100644
--- a/.agents/notes/implemented/feature/2026-07-07-session-prefix.md
+++ b/.agents/notes/archived/feature/2026-07-07-session-prefix.md
@@ -1,6 +1,11 @@
# Agent Note: The session prefix — request-only messages in front of the derived history
Status: implemented
+Archived: 2026-07-28
+
+English | [中文](2026-07-07-session-prefix.zh.md)
+
+The request-only prefix seam described below was later removed by the [unified sourced-message decision](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md). Current producers inject durable sourced `user/message` context at `agent/step`; this record preserves the earlier design and its trade-offs.
## Problem
@@ -10,7 +15,7 @@ The obvious third option — let a plugin edit the request's `messages` on the w
## Decision
-`agent/session-prefix` is a waterfall on the agent event map ([`packages/core/agent/src/types.ts`](../../../../packages/core/agent/src/types.ts)): listeners receive a frozen empty seed and return an extension (the canonical contribution is a prepend, `[mine, ...await next()]`, which yields registration order on the wire). The loop ([`packages/core/agent-loop/src/loop.ts`](../../../../packages/core/agent-loop/src/loop.ts)) fires it once per loop instance, lazily before the instance's first `agent/pre-step`; the composed list is deep-cloned, deep-frozen, cached on the instance, and placed in front of the ENTIRE derived history — directly after the provider's system slot — on every request the instance sends ([wire order](../../../../docs/core-data-structures/core.md#the-request-envelope-llmcallconfig-and-the-logged-header)).
+`agent/session-prefix` is a waterfall on the agent event map ([`packages/core/agent/src/types.ts`](../../../../packages/core/agent/src/types.ts)): listeners receive a frozen empty seed and return an extension (the canonical contribution is a prepend, `[mine, ...await next()]`, which yields registration order on the wire). The loop ([agent-loop source](../../../../packages/core/agent-loop/src/)) fires it once per loop instance, lazily before the instance's first `agent/pre-step`; the composed list is deep-cloned, deep-frozen, cached on the instance, and placed in front of the ENTIRE derived history — directly after the provider's system slot — on every request the instance sends ([wire order](../../../../docs/core-data-structures/core.md#the-request-envelope-llmcallconfig-and-the-logged-header)).
Three properties carry the design:
@@ -22,7 +27,7 @@ Because composition runs before the boundary snapshot, a composing listener's se
## Testing
-[Interception tests](../../../../packages/core/agent-loop/tests/interception.spec.ts) pin compose-once reuse without changed headers, prepend order, empty-prefix omission, immutability, composition before pre-step, and the prefix on the routed header; [cancellation tests](../../../../packages/core/agent-loop/tests/cancel.spec.ts) pin discard and recomposition. Session, invariant, token-meter, and compaction tests cover header round trips, request reconstruction, and durable prefix-aware pressure accounting. Snapshot normalization preserves prefix counts, while the [pinned-header scenario](../testing/2026-07-06-pin-request-header-content-in-one-scenario.md) owns content and the default example remains prefix-free. The provider-independent seam needs no dedicated e2e; the with-key [request-cache e2e](../../../../packages/core/agent-loop/tests/request-cache.e2e.ts) covers its cache economics.
+[Interception tests](../../../../packages/core/agent-loop/tests/interception.spec.ts) pin compose-once reuse without changed headers, prepend order, empty-prefix omission, immutability, composition before pre-step, and the prefix on the routed header; [cancellation tests](../../../../packages/core/agent-loop/tests/cancel.spec.ts) pin discard and recomposition. Session, invariant, token-meter, and compaction tests cover header round trips, request reconstruction, and durable prefix-aware pressure accounting. Snapshot normalization preserves prefix counts, while the [pinned-header scenario](../../archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md) owns content and the default example remains prefix-free. The provider-independent seam needs no dedicated e2e; the with-key [request-cache e2e](../../../../packages/core/agent-loop/tests/request-cache.e2e.ts) covers its cache economics.
## Alternatives considered
diff --git a/.agents/notes/archived/feature/2026-07-07-session-prefix.zh.md b/.agents/notes/archived/feature/2026-07-07-session-prefix.zh.md
new file mode 100644
index 0000000000..c33c497198
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-07-session-prefix.zh.md
@@ -0,0 +1,46 @@
+# Agent Note: 会话前缀——派生历史之前的仅请求消息
+
+Status: implemented
+Archived: 2026-07-28
+
+[English](2026-07-07-session-prefix.md) | 中文
+
+下文所述的仅请求前缀 seam 后来已被[统一带来源消息的决策](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md)移除。当前的生产方在 `agent/step` 时注入持久的带来源 `user/message` 上下文;本记录保留了早先的设计及其权衡。
+
+## 问题
+
+插件经常拥有一段会话级别稳定的开场内容,模型必须始终看到它:技能目录、AGENTS.md 摘要、工作区基线。在引入本 seam 之前,harness 为这类内容提供了两个归属位置,但两者都不合适。系统提示词是一个渲染后的单一字符串:消息形态的内容(user 角色的 `` 信封、多消息引导序列)放不进去,而且提供方对会话消息和系统文本的权重处理不同。持久化历史(`agent.inject()`、会话启动时的 `context/message`)使开场内容变为永久:每个 `deriveMessages()` 消费方都会回放它,压缩(compaction)的保留遍历拥有它,fork 会将其以陈旧状态固化,恢复也无法刷新它——会话诞生时捕获的目录会比它所描述的世界活得更久。
+
+显而易见的第三种选项——让插件在请求发出途中编辑 `messages`——被[可重建请求 Agent Note](../architecture/2026-07-05-reconstructable-requests.md)禁止:每个由循环构建的请求都是会话日志的纯函数,因此无论哪个通道承载开场内容,都必须精确记录它所发送的内容。缺失的是一个带有持久记录的仅请求消息通道。
+
+## 决策
+
+`agent/session-prefix` 是 agent 事件映射上的一个 waterfall(瀑布式事件)([`packages/core/agent/src/types.ts`](../../../../packages/core/agent/src/types.ts)):监听器接收一个冻结的空种子并返回扩展(规范的贡献方式是前置插入 `[mine, ...await next()]`,在协议格式上产生注册顺序)。agent loop(智能体循环)([agent-loop 源码](../../../../packages/core/agent-loop/src/))在每个循环实例中触发一次,惰性地在实例首次 `agent/pre-step` 之前执行;组合后的列表被深拷贝、深冻结、缓存在实例上,并在该实例发出的每个请求中置于整个派生历史之前——紧接在提供方的 system 槽位之后([协议格式顺序](../../../../docs/core-data-structures/core.md#the-request-envelope-llmcallconfig-and-the-logged-header))。
+
+三个属性承载了这一设计:
+
+- **仅请求,记录在 header 中。** `deriveMessages()` 从不返回前缀;它唯一的持久记录是实例锚定的 `request/header` 快照上的 `EpochHeader.messagePrefix`——可重建请求 Agent Note 已为请求的非历史部分拥有的通道,因此不引入新的会话事件。配套的 [`dsh-agent-loop/invariant`](../../../../packages/core/agent-loop/src/invariant.ts)对每个循环构建的请求重新计算 `messagePrefix + boundary derivation`;启用该贡献时,未记录的前缀无法到达协议格式。
+- **按实例冻结。** 复用是结构性的,而非靠纪律保证:缓存的产物在会话中途不可变,因此提供方的提示词缓存从构造上成立,前缀以每步零边际成本扩展了可缓存区域。进程重启或 `ctx.agents.resume()` 产生新实例:它重新组合,任何漂移都可追溯地落在 `'resume'` header 快照上。这就是本 seam 创建的路由规则:会话冻结的开场内容走前缀;会话中途变化的内容走仅追加历史通道(`agent.inject()` 或工具/prompt-submit 的 `additionalContexts`——[拦截 seam Agent Note](2026-06-30-interception-seams.md)),每条都是一次性支付的持久 `context/message`,之后被前缀缓存覆盖。
+- **在持久请求信封中保持精确。** 组合先于实例的首次 `agent/pre-step` 和请求边界。第一个已路由请求会把当前前缀记录在其 header 上,因此步骤后的 token 压力会将精确前缀与实际提示词、工具和已路由模型一起读取;通用的步骤前检查点 seam 不携带压缩专属参数。被取消/dispose 中断的组合会被丢弃,永不缓存:感知中止的监听器的降级回退不会泄漏到后续请求中,下一轮次在活信号下重新组合。
+
+由于组合在边界快照之前运行,组合监听器的会话追加会加入当前请求的派生历史。压缩在结构上不可能触及前缀(或系统提示词):它重写的是表面节点,而 header 状态从不进入表面。
+
+## 测试
+
+[拦截测试](../../../../packages/core/agent-loop/tests/interception.spec.ts)固定了以下行为:没有变更 header 时的组合一次复用、前置插入顺序、空前缀省略、不可变性、组合在步骤前检查点之前完成,以及已路由 header 上的前缀;[取消测试](../../../../packages/core/agent-loop/tests/cancel.spec.ts)固定了丢弃与重新组合。Session、不变式、token-meter 和压缩测试覆盖 header 往返、请求重建与持久前缀感知的压力核算。快照归一化保留前缀计数,[固定 header 场景](../../archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)拥有内容,默认示例保持无前缀。与提供方无关的 seam 无需专门 e2e;带密钥的 [request-cache e2e](../../../../packages/core/agent-loop/tests/request-cache.e2e.ts) 覆盖了其缓存经济性。
+
+## 曾考虑的替代方案
+
+- **每请求 `before`/`after` 槽位,每步重新计算**(最初提出的形态:一个每请求触发的 waterfall,贡献冻结的 `before` 消息置于历史之前、新鲜的 `after` 消息置于历史之后):否决。每步重新组合 `before` 会引入漂移,必须记录为完整的变更 header;`after` 槽位位于不断增长的历史之后,其 token 在每个请求中重复支付,且其后的所有内容不可缓存。对照各替代方案衡量,当前所有更新模式都能通过持久追加更廉价地满足(支付一次,此后缓存读取),而唯一没有归属的内容是会话稳定的开场——它需要的是冻结,而非重新计算。
+- **系统提示词分段**(`system-prompt/assemble`):对此类内容否决。assembly 渲染为单一 `system` 字符串,消息形态的开场放不进去;且系统提示词被设计为每步重新组装(变化时带完整的变更 header),而开场内容需要按实例冻结的语义。
+- **持久化历史开场**(会话启动时 `inject()`):否决。永久历史正是问题陈述中的失败模式——到处被回放、可被压缩、在恢复后仍保持陈旧状态。
+- **按轮次组合而非按实例组合**:否决。轮次边界的重新组合要么与日志静默失同步,要么强制产生变更 header;且它每次触发都会破坏提供方缓存。合理的刷新点是实例边界,`'resume'` 快照已在那里可追溯地记录漂移。
+- **通过 `agent/pre-step` 携带提示词/前缀,用于临时压力估算**:否决,因为它把通用生命周期 seam 耦合到一个消费方,而且仍会遗漏更晚的请求路由和工具;步骤后的回放会从持久的已路由 header 读取请求信封的每个字段。
+- **专用会话事件承载前缀**:否决。header 事件按设计就是请求的非历史记录;第二个事件会为同一事实提供第二个归属,并多出一个需要保持完整的编解码器。
+
+## 后果
+
+- `agent/pre-step` 保持通用的 `(agent, turn, step, signal)` 检查点。压缩不接收 prefix 参数;`ctx.tokenMeter` 在步骤后从规范的已路由 header 折叠前缀。
+- 贡献者的内容在会话中途变化时,直到下一个实例才会被重新读取——这是设计意图。需要会话中途目录更新的部署,应将变更通知路由到仅追加历史通道,支付一条持久 `context/message`。
+- 被放弃的 `after` 槽位意味着请求尾部附近没有仅请求通道;仓库中没有任何功能需要它,且恢复它会重新引入本设计旨在避免的每步重复支付成本。
+- 空组合即为规范缺失:无贡献者的部署不记录额外的 header 字节,其请求就是裸派生。
diff --git a/.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.i18n.yaml b/.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.i18n.yaml
new file mode 100644
index 0000000000..3b3dbe1b07
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.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
+2026-07-08-repeat-tool-guard.md: 218ed381c67a62e3af65ebb375d8800f908414cd
+2026-07-08-repeat-tool-guard.zh.md: e522908fb08fe0294cdcac034e301febf0f8683f
diff --git a/.agents/notes/implemented/feature/2026-07-08-repeat-tool-guard.md b/.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-08-repeat-tool-guard.md
rename to .agents/notes/archived/feature/2026-07-08-repeat-tool-guard.md
index 08f5bb01cf..218ed381c6 100644
--- a/.agents/notes/implemented/feature/2026-07-08-repeat-tool-guard.md
+++ b/.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.md
@@ -1,6 +1,9 @@
# Agent Note: Repeat-tool-call guard plugin
Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-07-08-repeat-tool-guard.zh.md)
## Problem
diff --git a/.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.zh.md b/.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.zh.md
new file mode 100644
index 0000000000..e522908fb0
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.zh.md
@@ -0,0 +1,77 @@
+# Agent Note: 重复工具调用守卫插件
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-07-08-repeat-tool-guard.md) | 中文
+
+## 问题
+
+模型陷入循环时,会以字节级相同的参数反复发起同一个工具调用——重新运行一条失败的 grep、重新读取一个未变化的文件、轮询一条已经给出答案的命令——每一轮往返都消耗 token、挂钟时间以及(对付费 API 而言)金钱,却不带来新信息。harness 目前没有任何机制能察觉这一点:循环没有步骤预算,没有插件追踪调用重复,模型只有在碰巧改变自身行为时才能跳出。这种失败模式真实存在且检测成本极低——[pi-repeat-tool-guard](https://github.com/Kingwl/pi-repeat-tool-guard) 正是以 pi coding-agent 扩展的形式提供了这一功能:统计连续相同调用次数,超过阈值后追加一条 `` 告诉模型停止重复并换个方向。
+
+harness 已经具备 pi 扩展所使用的全部 seam,而且更好:[拦截 seam Agent Note](2026-06-30-interception-seams.md)赋予 `tools/post-execute` 一种经过认可的方式,将面向模型的上下文附加到已完成的调用上;循环缓冲并注入该上下文,同时保持调用/结果的邻接关系;注入的上下文是一条已记录的 `context/message`——因此原生守卫无需新增会话事件即可满足「模型可见 ⟺ 已记录」规则。缺少的只是插件本身。
+
+## 决策
+
+该守卫是一个循环卫生插件,而非面向模型的工具。它统计对同一工具以相同规范化参数发起的连续调用次数,并在配置的阈值处注入建议性提醒。它从不延迟、阻止或改写调用;模型自行决定是换种方式重试还是结束。
+
+插件为 `@deepseek-ai/dsh-repeat-tool-guard`,位于 `packages/guard/repeat-tool-guard/`,开辟 `guard/` 分组用于循环卫生插件(单包(package)分组有先例:[todo-write Agent Note](2026-06-29-todo-write-tool.md)发布了 `todo/tool-todo`)。它注册两个监听器,将状态保存在以存活 `Agent` 对象为键的 `WeakMap` 中——工具注册表是上下文级别的单例,其 waterfall(瀑布式事件)交错所有 agent(智能体)的调用(subagent 运行在同一个上下文上),因此按 agent 分键是正确性要求,而非锦上添花;弱对象键还使得纯清理用途的 disposal 监听器不再必要。
+
+- **`tools/post-execute`(waterfall)**——唯一的检测点。监听器同时接收 `(exec, result)`,因此计数和提醒投递无需跨事件的 pending map(pi 扩展需要它,仅因为其 `tool_call`/`tool_result` 钩子是分开的事件)。它始终通过 `next()` 委托,当命中阈值时,将提醒前置到下游决策的 `additionalContexts`——这正是[钩子桥接](2026-06-30-hook-bridges.md)已采用的「观察并丰富」姿态,遵守 waterfall 契约。计数放在此处而非 `tools/pre-execute`,因为 post-execute 也会为被拒绝的调用触发(`ToolRegistry.execute` 将 deny 路由到同一条流水线),而模型反复敲击一个被拒绝的调用恰恰是值得打破的循环。
+- **`agent/prompt-submit`(waterfall)**——纯重置钩子:通过 `next()` 委托,清除提交 agent 的链。用户介入改变了上下文;跨越介入的重复不是循环。
+
+### 检测语义
+
+链的键是 `(tool name, canonical arguments)`;与前一个被追踪调用相同的调用递增该 agent 的连续计数器,不同的被追踪调用将其重置为 1。规范化方式为深度键排序加 `JSON.stringify`:`ToolExecution.arguments` 按构造就是循环中 `JSON.parse` 的输出(或格式错误的参数 JSON 的原始字符串回退,其本身也是可比较的值),因此 pi 原版对 bigint/循环引用/`undefined` 的处理在此没有输入,被有意去除。
+
+两条刻意的规则,均记录在[包 README](../../../../packages/guard/repeat-tool-guard/README.md) 中,因为它们是读者否则只能猜测的行为:
+
+- **未追踪的调用对链透明。** 被 `include`/`exclude` 排除的调用既不递增也不重置计数器,因此 `grep X → todo_write → grep X` 在 `todo_write` 被排除时仍计为两次连续的 `grep X`。这正是排除功能有用的原因——穿插在循环中的簿记工具不得为循环洗白——也是 pi 扩展的(未文档化的)语义,有意保留并明确写下。
+- **没有 agent 的调用被忽略。** 直接调用 `ctx.tools.execute()` 的调用方(测试、非循环消费方)没有可提醒的模型,也没有可作键的存活 agent 对象。
+
+### 提醒投递
+
+提醒作为独立条目搭载在 `additionalContexts` 上(source 为 `{kind: 'plugin', plugin: 'repeat-tool-guard'}`——依照 `HookContext`,该标签承载语义),绝不替换 `content`:`tool/result` 事件仍是工具自身的审计输出,循环则在步骤结果之后把缓冲的上下文追加为 `context/message`,会话将其渲染为带标签的合成 user 信封,并由派生历史回放。阈值逐级升级:第一个配置阈值获得简短的「你正在重复自己,请分析先前结果」提示;后续各阈值获得详细形式,包含工具、重复计数和规范参数(在头部截断到 `argumentsPreviewChars`,默认 500——循环中的 `write` 级 payload 不得无界地进入下一次请求;链键始终比较完整规范字符串),并说明这些调用没有取得进展。pi 原版把温和文本硬编码为字面计数 3;本守卫以 `thresholds[0]` 为键,修复了移植中的这一 bug。下游钩子桥贡献仍是独立数组条目,因此两个插件都保留各自的 source、信封与元数据。
+
+### 配置
+
+```yaml
+- id: repeat-tool-guard
+ name: '@deepseek-ai/dsh-repeat-tool-guard'
+ config:
+ thresholds: [3, 5, 8] # default; consecutive counts that trigger a reminder
+ include: [] # tool-name patterns to track; empty ⇒ all tools
+ exclude: [todo_write] # tool-name patterns transparent to the chain
+ argumentsPreviewChars: 500 # default; cap on arguments quoted in the detailed reminder
+```
+
+`thresholds` 在加载时校验,遇到空列表、非整数、小于 2 的值或重复项时抛出异常——配置错误快速失败,取代 pi 原版的静默回退到默认值。`include`/`exclude` 条目支持 `*` 通配符。模式是对调用时实际存在的工具的谓词,而非对注册表条目的引用,因此匹配不到当前已注册工具的条目不是错误——与 `toolOrder` 的引用检查不同,`exclude: [mcp_*]` 在未加载 MCP 工具的部署中也必须保持有效。
+
+## 测试
+
+- **单元测试:** 使用脚本化适配器的真实循环,覆盖计数与重置规则、未追踪透明性、dispose(资源释放)清理、按 agent 隔离、规范化参数键序、升级、被拒绝的调用、无 agent 执行、通配符转义、无效配置,以及下游阻止或 replacement 决策,达到逐文件 100% 覆盖率。
+- **快照测试:** keyless 的 `repeat-tool-guard` 场景发起五次相同的 `todo_write` 调用,在 ACP 输出和会话日志中固定第三次调用的温和提醒与第五次调用的详细提醒。该插件在实时示例中加载,但在其他场景中保持静默。
+- **E2e 测试:** 无。该插件是确定性的且与提供方无关,其 seam 契约由各自的所有者覆盖。
+
+## 曾考虑的替代方案
+
+- **将提醒追加到工具结果中**(以替换 `content` 的方式 `accept`——pi 扩展的机制,它修补结果内容是因为那是其 API 提供的唯一通道):否决。这会让已记录的 `tool/result` 对工具实际返回的内容撒谎,而 `additionalContexts` 是 post-execute 评注的独立认可通道,循环级缓冲保持了调用/结果的邻接关系。
+- **在 `tools/pre-execute` 中计数并使用 pending-reminder map**(pi 的两阶段形态):否决。post-execute 单独就能同时看到 `(exec, result)` 且也为被拒绝的调用触发,因此一个监听器、无跨事件状态即可以更少的机制覆盖严格更多的尝试。
+- **在最高阈值升级为 `block`**:在初始范围内否决。阻止调用会惩罚合法的相同重复(轮询长时间运行的终端、重新检查 agent 预期会变化的文件),而建议性提醒让模型保持控制权。待有证据后重新审视;决策形状(`PostToolDecision`)已支持此选项。
+- **通过 CC/Codex 桥接的逐部署外部钩子**(一个 `PostToolUse` 脚本):否决作为最终答案。它对单个部署有效,但一个已发布、有单元测试、可通过 `cordis.yml` 配置的插件才是 harness 原生的形式,且没有逐调用的子进程开销。
+- **在 `agent-loop` 中设置循环级步骤或重复预算**:否决。「用插件,不改循环」;硬性步骤预算是一种更粗粒度的正交控制,需要自己的提案。
+- **模糊/近似相同检测**(路径归一化、相似但不完全相同的参数):否决。规范化后的精确匹配成本低、确定性强、且可向模型解释;相似度阈值引入误报风险,需要证据才能换取复杂度。
+- **将包放在 `core/`**:否决。core 是产品主干;行为守卫是可选的叶子插件,`todo/` 的先例是每个插件族一个小型专属分组。
+
+## 后果
+
+- 提醒在设计上是建议性的:有意重复相同调用的幂等轮询模式仍会在超过阈值后收到提示,减压阀是配置(`thresholds`、`exclude`)加上明确允许「在已收集足够证据时结束」的提醒文本。每次触发在下一次请求中增加提醒 token 的开销;阈值限制了触发频率。
+- 链状态仅存于内存:从持久化恢复的会话以全新的链开始,因此跨越恢复的循环比实时循环更晚收到提醒——可以接受,守卫是启发式提示而非已记录的不变式,持久化计数器状态带来的收益不值得其复杂度。
+- 当多个 post-execute 生产者在同一次调用上附加上下文时,每项贡献保持为独立的 `HookContext`;顺序遵循 waterfall 嵌套关系,每个条目保留自己的溯源信息。
+- 实现快照层时暴露了 suite kit 的一项隐藏假设:fixture guard 把「撰写的模型场景」等同于「由 override 驱动」。`Scenario` 表现在携带显式的 `overridden` 标志,并且 sidecar 是否存在会以双向方式与其核对(未注册的游离 sidecar 会静默替换派生脚本)——suite kit 比本插件出现前更严格。
+
+## 延后事项
+
+- 压缩(compaction)不重置链:压缩后的历史改变了模型所见的内容,但重复风险通常在压缩后仍然存在。
+- 在高阈值升级为 `block` 未实现;`PostToolDecision` 已支持此选项,待证据到来时启用。
+- subagent 的链按 agent 隔离;在出现具体用例之前不提供共享机制。
diff --git a/.agents/notes/archived/feature/2026-07-09-bash-backed-grep-glob-discovery.i18n.yaml b/.agents/notes/archived/feature/2026-07-09-bash-backed-grep-glob-discovery.i18n.yaml
new file mode 100644
index 0000000000..edf7a382a1
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-09-bash-backed-grep-glob-discovery.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
+2026-07-09-bash-backed-grep-glob-discovery.md: 63dacd97443e0dfa44108fec42b5f1cf1640c96f
+2026-07-09-bash-backed-grep-glob-discovery.zh.md: 325e27c54ed975522819053863cc57705991fbdb
diff --git a/.agents/notes/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md b/.agents/notes/archived/feature/2026-07-09-bash-backed-grep-glob-discovery.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md
rename to .agents/notes/archived/feature/2026-07-09-bash-backed-grep-glob-discovery.md
index 64fa232831..63dacd9744 100644
--- a/.agents/notes/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md
+++ b/.agents/notes/archived/feature/2026-07-09-bash-backed-grep-glob-discovery.md
@@ -1,6 +1,9 @@
# Agent Note: Bash-backed grep and glob discovery tools
Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-07-09-bash-backed-grep-glob-discovery.zh.md)
## Problem
diff --git a/.agents/notes/archived/feature/2026-07-09-bash-backed-grep-glob-discovery.zh.md b/.agents/notes/archived/feature/2026-07-09-bash-backed-grep-glob-discovery.zh.md
new file mode 100644
index 0000000000..325e27c54e
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-09-bash-backed-grep-glob-discovery.zh.md
@@ -0,0 +1,171 @@
+# Agent Note: 由 Bash 支持的 grep 与 glob 发现工具
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-07-09-bash-backed-grep-glob-discovery.md) | 中文
+
+## 问题
+
+harness 需要面向模型的 `glob` 和 `grep` 工具,但如果将它们实现为 `ctx.fs` 提供方的方法,就会把本地产品便利功能变成所有文件系统后端都必须实现的契约。本地工作区发现天然适合由进程支持的 `rg` 工作流;远程或虚拟文件系统后端可能公开自己的搜索 API,可能无法共享本地 `ripgrep` 视图,也可能完全不支持发现。文件读取/写入/编辑 seam 尚未证明此需求前,v1 不应要求每个文件系统后端都实现搜索。
+
+搜索输出还受到两层不同的预算约束。工具需要足够多的原始 `rg` 输出,才能计算稳定的逻辑结果;模型则只能收到有界预览,并在格式化结果超出内联预算时获得恢复路径。通用落盘策略只能看到最终工具结果,因此无法恢复搜索工具已经省略的匹配项。搜索工具必须自行负责保留,并尽力落盘格式化结果。
+
+## 决策
+
+`glob` 和 `grep` 是 `@deepseek-ai/dsh-tool-fs-search` 中的条件式面向模型工具,由 bash seam 支持,不会成为新的 `ctx.fs` 提供方方法。加载插件时,该包(package)执行 `command -v rg >/dev/null 2>&1`:先通过 `ctx.bash.resolve(request)` 解析请求,再通过 `ctx.bash.run(spec)` 运行;如果命令以非零状态退出,该包会记录警告,并且既不注册工具,也不注册提示词章节。如果探针无法启动、超时、中止、被终止,或没有产生退出码,插件加载会明确失败,因为这意味着 bash 执行器损坏,而不是可选二进制文件缺失。注册后,执行流程同样依次调用 `ctx.bash.resolve(request)` 与 `ctx.bash.run(spec)`,并使用工具组装的固定 `rg` 命令模板。工具层负责 schema、参数验证、shell 引用、结果解析、结果格式化、保留、格式化结果落盘交接,以及超时声明。bash 执行器负责请求默认值解析与上限控制、子进程执行、进程组终止、环境清理、原始输出捕获,以及在本地、沙箱或远程 bash 实现之间替换后端。
+
+这些工具不使用 `ctx.bash.start()`,也不创建模型可见的后台任务。从 agent loop(智能体循环)的视角看,它们是普通前台工具:只有当 `rg` 命令退出、超时、中止或失败后,工具调用才返回。`defineTool({ timeoutMs })` 声明协作式工具调用预算,`@deepseek-ai/dsh-timeout-policy` 通过 `exec.signal` 强制执行;工具会在 `resolve()`/`run()` 前将该信号转发给 bash 请求。bash 后端自身的超时仍作为第二层安全上限;先触发的中止生效。
+
+这些工具使 `path` 与 Claude Code 的搜索工具保持一致,但将解析绑定到 bash workdir,而不是 `ctx.fs`。工具从 `exec.agent?.session.header.cwd` 派生 bash 请求 workdir,与 `dsh-tool-bash` 和 `dsh-tool-fs` 一致;如果会话没有 cwd,它会省略 `request.workdir`,由 bash 实现通过 `resolve()` 应用其配置的 cwd 或进程 cwd。对于 `grep`,`path` 是可选的 ripgrep 目标,可以是文件或目录;省略时使用已解析的 bash workdir。对于 `glob`,`path` 是可选的目录搜索根;省略时同样使用已解析的 bash workdir。相对 `path` 值基于该 workdir 解析。只要可行,返回路径就会显示为相对于已解析 bash workdir 的形式;只有在共置部署中,bash workdir 与文件系统 `read` 根指向同一个工作区时,这些路径才保证可以继续读取。v1 会记录这项部署要求,但不执行跨服务运行时验证。在形成共享工作区/根契约或提供方专用搜索后端之前,远程或虚拟文件系统搜索保持暂缓。
+
+该包不注入 `fs`,而是注入 `tools`、`systemPrompt` 和 `bash`;它有意读取 `spillStore` 时使用 `ctx.get('spillStore')`,而不使用静态注入,因为格式化结果落盘是可选功能。现有 `@deepseek-ai/dsh-tool-fs` 部署若只需要 `read`/`write`/`edit`,则无需加载 bash。加载搜索功能的部署则必须让 bash 执行器环境可以使用 `rg`,这些工具才会进入模型可见 schema。
+
+### 包结构
+
+v1 包保持精简。`@deepseek-ai/dsh-tool-fs-search` 内部的源代码布局如下:
+
+```text
+src/index.ts
+src/glob.ts
+src/grep.ts
+src/search-core.ts
+src/shell-quote.ts
+```
+
+`glob.ts` 和 `grep.ts` 各自负责参数验证、命令构造、结果解析、格式化和注册。`shell-quote.ts` 是一个共享辅助模块,因为 shell 引用是两个工具都必须经过的安全边界;`search-core.ts` 是另一个共享模块(实现时对原四文件方案所作的修订):`SEARCH_*` 错误词汇、bash 运行与原始输出获取、格式化结果落盘交接,以及 workdir 相对显示,在两个工具中完全相同。若在每个工具中重复这套精细管道,正是对称性约定所指出的漏提取问题。命令构造器禁止自行拼凑引用,也不能把未经引用、由模型控制的值直接连接到 shell 命令中。
+
+### Schema 与配置
+
+`glob` 公开精简的发现形状:
+
+```ts
+interface GlobArgs {
+ pattern: string
+ path?: string
+}
+```
+
+`grep` 公开 OpenCode 风格的最小形状:
+
+```ts
+interface GrepArgs {
+ pattern: string
+ path?: string
+ include?: string
+}
+```
+
+常规预算不会进入面向模型的 schema。`@deepseek-ai/dsh-tool-fs-search` 拥有以下带默认值并经过验证的配置字段:
+
+| 字段 | 默认值 | 作用 |
+|---|---:|---|
+| `globMaxResults` | `100` | 内联保留的最大路径数;与 Claude Code 的默认 `GlobTool` 结果上限一致。 |
+| `grepMaxMatches` | `250` | 内联保留的最大扁平匹配数;与 Claude Code 的默认 `GrepTool` `head_limit` 一致。 |
+| `grepMaxLineBytes` | `2000` | 每条匹配行预览保留的最大字节数,通过 `TextRetainer({ kind: 'head', maxBytes: grepMaxLineBytes })` 应用。 |
+| `rawOutputMaxBytes` | `20000000` | 工具会解析的完整原始 `rg` stdout 最大字节数;与 Claude Code 的 ripgrep 原始缓冲区一致。 |
+| `timeoutMs` | `30000` | 附加到两个工具定义并由 `@deepseek-ai/dsh-timeout-policy` 强制执行的工具调用超时。 |
+
+`globMaxResults` 和 `grepMaxMatches` 使用 `ItemRetainer({ kind: 'head' })`。`grepMaxLineBytes` 针对每条匹配行使用 `TextRetainer({ kind: 'head', maxBytes: grepMaxLineBytes })`,使预览截断保留 UTF-8 边界。这遵循[工具结果保留库](../architecture/2026-07-06-tool-result-retention-library.md)对发现条目的映射:收集完整结果,在内联结果中保留头部条目,并将路径映射、分组和逐行预览放在保留器外部。v1 的 `grep` 不公开 `case_insensitive`、`head_limit`、`offset`、`count`、多行、上下文行、输出模式或文件类型过滤器。模型如需周边上下文,可使用 `read` 读取匹配文件;如需后续结果,则遵循返回的落盘定位符所给出的检索提示。
+
+Claude Code 的数值只是两层预算的参考点,并非面向模型 schema 的先例。其专用搜索工具会缓冲最多 20 MB 的原始 ripgrep 输出用于内部处理;在非 WSL 平台上使用 20 秒 ripgrep 超时,在 WSL 上使用 60 秒,之后才在模型看到结果前应用搜索专用上限:`GrepTool` 默认 `head_limit = 250`,并持久化超过 20,000 个字符的格式化结果;`GlobTool` 默认最多 100 条路径,并持久化超过 100,000 个字符的格式化结果。此 Agent Note 采用同样的原始缓冲区和内联数量默认值,将默认搜索超时设为 30 秒,并通过本 harness 的 `ctx.spillStore.saveText()` 路径恢复格式化结果。
+
+`path` 字段沿用与 Claude Code 相同的区分方式:`grep.path` 是文件或目录形式的 ripgrep 目标,`glob.path` 则是目录搜索根。v1 不为这些工具公开单独的 cwd/workdir 参数。
+
+`include` 是一个正向 glob 过滤器,不是列表,也不是排除语法。系统会预先拒绝逗号分隔或取反的 include 模式,并返回结构化参数错误。shell 命令中使用的每个模型控制值,包括 `pattern`、`path` 和 `include`,都必须经过包私有的 shell 引用辅助模块。
+
+### 执行
+
+`glob` 构建固定的 `rg --files` 命令,并以解析后的目录搜索根为根(提供 `path` 时使用该值,否则使用 bash workdir):`rg --files --glob --sort=modified --no-ignore --hidden`,另加针对 `.git`、`.svn`、`.hg`、`.bzr`、`.jj` 和 `.sl` 的 VCS 元数据排除项。这样既与 Claude Code 的隐藏/忽略文件发现和修改时间排序保持一致,也避免宽泛搜索包含 VCS 内部文件。工具逐行解析路径,只要可行就将结果映射为相对于 bash workdir 的路径,将每条路径推入 `ItemRetainer({ kind: 'head', maxItems: globMaxResults })`;当保留结果达到上限时,它会格式化完整的已排序路径列表,作为落盘产物。
+
+`grep` 构建固定的逐行 `rg --json` 命令,作用于所提供的文件/目录目标(提供 `path` 时使用该值,否则使用 bash workdir),从而无需按冒号拆分,就能解析文件路径、行号和行文本。它消费 `match` 记录,将格式错误的 JSON 或匹配记录视为 `SEARCH_FAILED`;只要可行,就将结果路径映射为相对于 bash workdir 的路径;通过 `grepMaxLineBytes` 应用逐行预览保留,将每个匹配推入 `ItemRetainer({ kind: 'head', maxItems: grepMaxMatches })`,然后只按文件分组内联输出中保留的预览匹配。落盘产物保存完整的格式化匹配列表,而不是只保存省略的尾部,因此检索提示指向模型已经看到的同一逻辑结果。
+
+原始 `rg` stdout 是内部传输细节。工具请求 `stdoutMaxBytes: rawOutputMaxBytes`,并通过 `ctx.bash.resolve()` 解析;只有当执行器在该上限内返回未截断的 stdout 时,工具才解析 `stdout.text`。如果 stdout 超过 `rawOutputMaxBytes`,或者执行器仍返回 `stdout.truncated`,工具会以明确的搜索错误失败,要求模型缩小 `pattern`、`path` 或 `include`。工具绝不向模型公开原始 `rg` 输出或 bash 原始落盘路径。
+
+只有 stdout 是解析源。对于无效模式、注册后运行时 `rg` 消失,以及搜索失败,stderr 作为诊断文本;如果 bash 截断 stderr,工具会使用保留的 stderr 尾部并附加截断说明,不会读取 `stderr.spillPath`。
+
+如果 `ctx.bash.run()` 因工具超时或调用方取消触发而报告 `aborted`,工具会返回结构化失败,而不是假装没有匹配项。如果 bash 自身的超时先触发,工具同样会以明确的超时消息失败。非零 ripgrep 退出语义由工具负责:退出码 0 表示存在匹配并成功;退出码 1 表示没有匹配但成功;无效模式、运行时 `rg` 消失或无法访问搜索 workdir 则表示失败。
+
+搜索失败使用包自有的 `HarnessError` 子类与 `SEARCH_*` 代码,而不使用 `FsErrorCode`,因为这些工具不是 `ctx.fs` 提供方操作。v1 的词汇包括 `SEARCH_INVALID_PATTERN`、`SEARCH_FAILED`、`SEARCH_RAW_OUTPUT_OVERFLOW` 和 `SEARCH_ABORTED`。缺少必填字段、空字符串或不支持的取反/列表式 `include` 值等模型参数验证失败,仍作为普通工具参数错误处理。
+
+### 格式化结果落盘
+
+`ctx.spillStore` 是可选服务,仅用于面向模型的格式化结果。这是代码库中首个工具自有落盘调用模式;此设计有意为之,因为搜索保留属于条目级策略:`globMaxResults` 限制路径数,`grepMaxMatches` 限制匹配数,而工具此时仍持有完整逻辑结果。通用 `dsh-spill-policy` 会在 `tools/post-execute` 阶段限制最终文本字节数;到那时搜索工具已经省略后续路径或匹配,策略无法恢复它们。
+
+当搜索产生的逻辑结果数超过内联上限,且 `ctx.spillStore` 存在时,工具会通过 `saveText()` 保存完整的格式化结果。落盘所有者是调用 agent 的会话头 id(`exec.agent?.session.header.id`);缺少该所有者时,搜索会保留内联结果,并报告完整结果无法保存。落盘来源是工具执行身份:`{ toolName: exec.name, callId: exec.callId, label: 'result' }`。建议文件名为 `grep-results.txt` 和 `glob-results.txt`;落盘后端仍将它们视为提示,而不是路径。
+
+如果落盘存储不存在、调用没有会话所有者,或保存失败,工具仍返回内联页和页脚,说明完整结果无法保存。格式化结果落盘存储不可用本身绝不能把搜索成功变为 `isError` 结果。
+
+bash 原始输出流与格式化搜索落盘产物是两个不同的产物。原始 `rg` stdout 只会在所请求的 bash stdout 上限内于内存中解析;格式化落盘产物则是 `ctx.spillStore.saveText()` 生成的稳定、面向模型的恢复定位符。
+
+### 结果形状
+
+带有成功格式化落盘的受限 `glob` 结果会返回内联页与落盘通知:
+
+```text
+
+
+(Showing N of M paths. Full sorted result stored at: /.../session-abc123/9f8e7d-glob-results.txt. Use read with offset/limit, or grep this path to search within it.)
+```
+
+带有成功格式化落盘的受限 `grep` 结果会返回分组后的预览匹配与落盘通知:
+
+```text
+Found N of M matches
+
+
+Line 12: ...
+
+(Full grep result stored at: /.../session-abc123/9f8e7d-grep-results.txt. Use read with offset/limit, or grep this path to search within it.)
+```
+
+如果完整逻辑结果未超过内联上限,系统不会创建格式化落盘产物。如果完整逻辑结果过大但无法格式化落盘,页脚会说明结果已受限,完整结果无法保存。`truncated`/省略计数是预算事实,并不表示搜索不完整;超时、无效正则表达式、运行时 `rg` 消失、无法访问 workdir、原始输出溢出、跳过二进制文件和解析失败,仍属于工具领域的错误或不完整字段。
+
+## 考虑过的替代方案
+
+**将 `glob`/`grep` 放在 `ctx.fs` 上。** v1 不采用:这会迫使每个文件系统后端增加搜索 API,并使本地 ripgrep 行为成为提供方 seam 的一部分。搜索是有用的产品行为,但不像 `readText` 或 `writeText` 那样属于通用文本存储原语。
+
+**直接从 `dsh-fs-local` spawn ripgrep。** 此 Agent Note 的 v1 不采用:直接 spawn 提供最简洁的 argv 边界、stdout/stderr 控制与提前停止控制,但会重复 bash seam 已负责的进程执行事项,包括环境清理、进程组终止、超时传播、沙箱/远程执行器替换,以及有界输出捕获。如果 bash 支持的搜索被证明过于依赖 shell 字符串,或必须支持前台流式输出,该方案仍是合理优化。
+
+**通过 `ctx.bash.start()` 实现流式提前停止。** 不采用:`start()` 会创建模型可见的后台任务语义,包括 task id、所有者 token、`bash_output`、`bash_kill`、完成通知,并且没有内置超时。`grep` 需要前台工具结果,而不是后台 bash 工作流。如果将来必须流式搜索,正确的抽象是在 bash/进程 seam 上增加前台流式进程句柄,而不是借用公开后台任务 API。
+
+**向模型公开 bash 原始落盘路径。** 不采用:bash 原始落盘路径包含原始 `rg` stdout(对于 grep 即 `rg --json` 记录),并非稳定的格式化搜索结果。搜索只把原始 stdout 当作内部传输;模型恢复使用通过 `ctx.spillStore.saveText()` 保存的格式化结果。
+
+**先为 bash 输出规范化增加 `spillStore.saveFile()`。** 此 Agent Note 的 v1 不采用:未来规范化 bash 时,`saveFile()` 可以帮助将现有执行器落盘文件移动到会话范围的落盘存储,但搜索只需在生成面向模型的产物前,在内存中获取有界的原始 `rg` stdout。`saveText()` 足以保存格式化搜索结果。
+
+**依赖通用 `dsh-spill-policy`。** 不采用:通用 post-execute 落盘只能看到最终工具结果。如果 `grep`/`glob` 内联返回第一页,通用策略无法恢复省略的结果。搜索工具必须在返回有界的面向模型文本前,自行保存完整的格式化结果。
+
+**公开 Claude Code 的完整 `GrepTool` schema。** v1 不采用:`output_mode`、上下文标志、多行、`head_limit`、`offset`、`case_insensitive` 和类型过滤器会使面向模型的接口变成 ripgrep 包装层。本 harness 将常规预算与续传机制保留在部署策略和落盘产物中。
+
+**保留提前停止搜索,并省略格式化落盘产物。** 此提案不采用:提前停止效率更高,却不给模型检查后续结果的路径。所选 v1 优先保证结果可恢复性与实现简洁性,并以 `timeoutMs`、`rawOutputMaxBytes`、bash 后端上限和格式化落盘产物作为安全后备。
+
+**先扩展 bash seam,增加原始输出读取器。** 不采用:可移植的 `readRawOutput(ref, maxBytes)` API 会增加引用生命周期、权限和后端存储语义。逐次运行的 `stdoutMaxBytes` 请求是更窄的 seam:搜索要么在 `rawOutputMaxBytes` 内收到完整 stdout,要么明确失败。
+
+**始终注册,只有执行时才报告缺少 `rg`。** 不采用:模型可见工具 schema 是部署能够尝试该能力的承诺。如果 bash 执行器在加载时找不到 ripgrep,更安全的接口是完全没有 `glob`/`grep` 工具或提示词指引。对于注册后发生环境变化的情况,执行时的 `rg` 缺失分类仍作为防御性回退。
+
+## 测试
+
+- 测试覆盖注册时 `rg` 探测(探测成功会注册两个工具和提示词章节;非零探测会跳过工具与提示词章节并发出警告;基础设施探测失败会拒绝插件加载),证明中止的 `exec.signal` 会到达 bash 后端(通过同一引用的 spec 断言和 `SEARCH_ABORTED` 结果),并覆盖命令构造/引用(恶意模式、带空格路径、以短横线开头的值、引号、换行、glob 元字符:既有单元断言,也针对每个恶意值执行真实 `bash -c` 往返)、将 `grep.path` 用作文件与目录目标、将 `glob.path` 用作目录搜索根、无效模式处理、无匹配、格式错误的 `rg --json` 输出、匹配行预览截断、原始输出溢出、超时/中止、格式化落盘成功/失败、包自有 `SEARCH_*` 错误代码,以及无后台任务不变量。
+- 直接覆盖第一方工具自有落盘先例:落盘后端存在、落盘后端缺失、`saveText()` 失败,以及缺少落盘所有者。
+- 该包通过真实 Loader 路径覆盖命名空间插件的导出形状(`name`、`inject`、`Config` 和 `apply`,且没有默认导出)。
+- 真实执行器集成测试(`dsh-bash-local` + 真实 `rg`)验证外部世界:恶意模式保持惰性、逐会话 cwd 解析、VCS 元数据排除、按修改时间排序,以及真实 ripgrep stderr 分类。如果测试进程的 PATH 中没有 `rg`,该测试会自行跳过(这是与无密钥 e2e 跳过相似的 CI 兼容措施);伪执行器测试覆盖注册和执行时缺少 `rg`,并由逐文件 100% 覆盖率门禁兜底。
+- transcript 可见落盘通知仍有快照缺口:此功能合入时记录了缺口说明,没有快照。快照层会回放 acp-agent 树;在其中加入搜索插件会改变组装的系统提示词,必须使用真实密钥重新录制每一份预期输出,而实现环境没有密钥。落盘通知的确切 transcript 文本由单元测试固定(`formatGlobOutput`/`formatGrepOutput` 以及通过注册表执行的落盘测试);下一次拥有密钥的会话应把插件接入 acp-agent 树,并运行一次 `test:snapshot:record`。
+
+## 后果
+
+- `glob` 和 `grep` 是 `@deepseek-ai/dsh-tool-fs-search` 中的条件式面向模型工具,不是 `ctx.fs` 提供方方法,也不属于现有 `@deepseek-ai/dsh-tool-fs` 根插件。只有 bash 执行器能找到 `rg` 时才会注册;该包注入 `tools`、`systemPrompt` 和 `bash`,不注入 `fs`,并使 `ctx.spillStore` 保持可选,读取时使用 `ctx.get('spillStore')`。
+- Schema 严格为 `glob(pattern, path?)` 和 `grep(pattern, path?, include?)`;搜索上限与超时是带默认值并经过验证的 Config 字段(`globMaxResults`、`grepMaxMatches`、`grepMaxLineBytes`、`rawOutputMaxBytes`、`timeoutMs`)。
+- 工具通过 `ctx.bash.resolve(request)` → `ctx.bash.run(spec)` 执行,转发 `exec.signal`,绝不调用 `ctx.bash.start()`,也绝不公开 bash task id。如果存在 `exec.agent?.session.header.cwd`,bash 请求 workdir 来自该值;解析后的 `spec.workdir` 决定执行与相对路径显示。
+- 工具向 bash seam 请求 `stdoutMaxBytes: rawOutputMaxBytes`,只解析上限内未截断的 stdout,并将超限或仍被截断的原始输出视为明确的搜索失败;绝不向模型公开原始 `rg` 输出。
+- 只要可用,过大的完整格式化结果会通过 `ctx.spillStore.saveText()` 保存,而内联结果保持有界;落盘失败、后端缺失或所有者缺失时,系统保留内联结果并报告未保存的剩余内容,绝不会返回 `isError`。
+- 包 README、生成的配置目录与导出 JSDoc 会记录 Config 字段和 `SEARCH_*` 代码;tui-agent 示例会提供条件式工具插件(acp-agent 树等待完成上述快照重新录制);fs 组 README 会记录 `rg` 可用性以及 bash/文件系统共置部署要求。
+
+## 风险
+
+在宽泛模式下,完整运行的 `grep` 可能比提前停止搜索更慢。v1 为了简化实现并恢复完整结果而接受这项成本,同时通过工具超时、bash 超时、`rawOutputMaxBytes` 和输出上限加以约束。如果实际运行过慢,仍可采用直接 ripgrep 或前台流式替代方案。
+
+Shell 命令构造是最尖锐的安全边界。`ctx.bash` 接受命令字符串而不是 argv 向量,因此实现必须集中处理 shell 引用,并测试恶意模式、带空格路径、以短横线开头的模式、引号、换行和 glob 元字符。
+
+v1 假设 bash 与文件系统共置部署。如果 bash 搜索一个工作区,而 `read` 工具基于另一个根解析路径,返回路径可能无法继续读取。该包会记录这项要求,但不在运行时验证。
+
+落盘定位符由后端负责。当前本地后端返回本地文件系统路径,适用于 `read`/`grep` 能打开这些文件的部署;远程或工作区受限部署可以使用另一种后端,让其定位符和检索提示指向受支持的检索机制。
diff --git a/.agents/notes/archived/feature/2026-07-10-session-query-service.i18n.yaml b/.agents/notes/archived/feature/2026-07-10-session-query-service.i18n.yaml
new file mode 100644
index 0000000000..c918f96bea
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-10-session-query-service.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
+2026-07-10-session-query-service.md: 135385a94a8368af93efeb3937d87c86c129915a
+2026-07-10-session-query-service.zh.md: 283f0be3fb40a07d494f55aea94f8222be521d9f
diff --git a/.agents/notes/implemented/feature/2026-07-10-session-query-service.md b/.agents/notes/archived/feature/2026-07-10-session-query-service.md
similarity index 91%
rename from .agents/notes/implemented/feature/2026-07-10-session-query-service.md
rename to .agents/notes/archived/feature/2026-07-10-session-query-service.md
index 8cc529377b..135385a94a 100644
--- a/.agents/notes/implemented/feature/2026-07-10-session-query-service.md
+++ b/.agents/notes/archived/feature/2026-07-10-session-query-service.md
@@ -1,6 +1,9 @@
# Agent Note: Exact session query service
Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-07-10-session-query-service.zh.md)
## Problem
@@ -10,7 +13,7 @@ Full-text search is related but materially larger. Putting provider coordination
## Decision
-`@deepseek-ai/dsh-session-query` owns the single abstract `ctx.sessionQuery` service over one logical corpus. It concretely implements `listSessions()`, provider-independent `filterSessions(filters)`, `listEvents(sessionId)`, `filterEvents(sessionId, filters)`, bounded `readEvent(request)`, `traceSession(sessionId)`, and `traceEvent(request)`, while concrete backends implement its two full-text methods. The [unified service decision](../architecture/2026-07-23-unified-session-query-service.md) owns that topology, the [SQLite search decision](2026-07-10-sqlite-session-query-provider.md) owns search behavior, and the [tracing decision](2026-07-13-session-query-tracing.md) owns lineage and event-relationship semantics.
+`@deepseek-ai/dsh-session-query` owns the single abstract `ctx.sessionQuery` service over one logical corpus. It concretely implements `listSessions()`, provider-independent `filterSessions(filters)`, `listEvents(sessionId)`, `filterEvents(sessionId, filters)`, bounded `readEvent(request)`, `traceSession(sessionId)`, and `traceEvent(request)`, while concrete backends implement its two full-text methods. The [unified service decision](../../archived/architecture/2026-07-23-unified-session-query-service.md) owns that topology, the [SQLite search decision](2026-07-10-sqlite-session-query-provider.md) owns search behavior, and the [tracing decision](2026-07-13-session-query-tracing.md) owns lineage and event-relationship semantics.
The service observes the optional `ctx.sessionPersistence` binding dynamically but retains no persisted cache or invalidation listener. Each cross-corpus list asks the active backend for authoritative metadata, then overlays a fresh live-store list. Matching ids become one `SessionRecord`: the live header wins and `live`/`persisted` independently report source availability. Immutable header disagreement is `SESSION_QUERY_SOURCE_CONFLICT`.
diff --git a/.agents/notes/archived/feature/2026-07-10-session-query-service.zh.md b/.agents/notes/archived/feature/2026-07-10-session-query-service.zh.md
new file mode 100644
index 0000000000..283f0be3fb
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-10-session-query-service.zh.md
@@ -0,0 +1,43 @@
+# Agent Note: 精确会话查询服务
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-07-10-session-query-service.md) | 中文
+
+## 问题
+
+会话历史存在于两处:当前的 `SessionStore` 对象与可选的持久化后端。需要精确检查的消费方若无统一服务,就不得不各自重复实现活跃/持久化优先级判定、持久化生命周期处理、原始事件的 surface 分类、关系追踪以及防御性克隆。在检查点之间,持久化状态可能落后于活跃日志,因此仅靠持久化并非当前状态的可靠来源。
+
+全文搜索与此相关,但规模大得多。将提供方协调、同步、失效、排序和游标状态放入精确读取服务,会在具体数据库拥有方旁边再创建一个状态机。
+
+## 决策
+
+`@deepseek-ai/dsh-session-query` 拥有面向单一逻辑语料库的唯一抽象 `ctx.sessionQuery` 服务。它具体实现 `listSessions()`、提供方无关的 `filterSessions(filters)`、`listEvents(sessionId)`、`filterEvents(sessionId, filters)`、有界的 `readEvent(request)`、`traceSession(sessionId)` 和 `traceEvent(request)`,而具体后端实现其两个全文搜索方法。[统一服务决策](../../archived/architecture/2026-07-23-unified-session-query-service.md)拥有这一拓扑,[SQLite 搜索决策](2026-07-10-sqlite-session-query-provider.md)拥有搜索行为,[追踪决策](2026-07-13-session-query-tracing.md)拥有血缘与事件关系语义。
+
+该服务动态观察可选的 `ctx.sessionPersistence` 绑定,但不保留持久化缓存或失效监听器。每次跨语料库列表操作向活跃后端请求权威元数据,然后叠加一份新鲜的活跃 store 列表。id 匹配的条目合并为一条 `SessionRecord`:活跃 header 优先,`live`/`persisted` 各自独立报告来源可用性。不可变 header 不一致时产生 `SESSION_QUERY_SOURCE_CONFLICT`。
+
+精确目标读取首先检查活跃 store,快照活跃 header 与事件日志。此路径从不查询持久化,因此持久化后端故障不会导致已知的活跃历史不可读。若活跃 store 中无目标,服务列出当前持久化元数据、证明该 id 存在、加载它,并在列表/加载 header 不一致时拒绝。所有返回的 header 与事件都经过一次 structured-clone 边界。
+
+## Surface 语义
+
+`dsh-session` 导出 `foldSurface(events)`,`SurfaceManager` 使用相同的转换函数维护其增量缓存。fold 返回分离的当前事件 seq 以及每次替换实际移除的 seq。`listEvents()` 和 `traceEvent()` 利用该结果为每个原始事件分类,使检查结果不会在位置替换语义上与 model-history 推导产生分歧。
+
+`readEvent()` 返回完整的目标加上按连续 seq 排列的原始相邻事件。`before` 和 `after` 默认为零,各自受 `readWindowMax`(默认 50)约束。结果携带克隆的 `SessionHeader` 而非来源可用性记录,因为判断活跃目标的 persisted 标志会违反「活跃精确读取不依赖持久化健康状态」这一保证。
+
+## 安全边界
+
+该服务是上下文级别的受信任基础设施,而非授权层。未来面向模型的历史工具或人类 UI 将施加显式的调用方/会话范围。该服务不添加面向模型的工具,也不改变 transcript(文本记录)或快照的 surface。
+
+## 曾考虑的替代方案
+
+- **将逻辑语料库解析直接放在每个消费方中**:否决。来源优先级、冲突处理、可选服务生命周期、克隆与 surface 分类是共享的正确性规则。
+- **仅查询持久化**:否决。检查点可能落后于当前活跃日志。
+- **缓存持久化元数据并监听写入/删除**:否决。精确读取可以直接询问权威来源,而缓存失效在规模尚未要求时就引入了生命周期与并发状态。
+- **将提供方注册放入精确读取服务**:否决。SQLite 包拥有一套对账/事务生命周期;若没有第二个提供方证明其必要性,注册表只会拆分该状态。
+
+## 后果
+
+继承的精确读取实现只有一个来源解析状态变量:当前挂载的持久化服务。它没有提供方队列、指纹、提取器注册表、观察代次或派生索引更新;具体后端单独拥有其全文搜索状态。精确读取、语义扫描和事件追踪在纯活跃部署中仍然可用,在持久化存在时具有确定性。
+
+跨语料库列表、血缘追踪和持久化事件操作在每次调用时执行后端 I/O。这是有意为之:正确性来自当前权威状态,而面向规模的全文搜索方法使用具体后端的 SQLite 派生索引。
diff --git a/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml b/.agents/notes/archived/feature/2026-07-14-time-context-plugin.i18n.yaml
similarity index 65%
rename from .agents/notes/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-14-time-context-plugin.i18n.yaml
index 28ecd2a765..62ca11e69e 100644
--- a/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-14-time-context-plugin.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
-2026-07-14-time-context-plugin.md: 189f75fc12fe12e9dec56fc71ea901ec2eaa8b19
-2026-07-14-time-context-plugin.zh.md: 12671cb891531627fffabb7bd91a1532bc3de6b9
+2026-07-14-time-context-plugin.md: 89d6fca7c473a932e8f014ff0576cecfd6f2e4e0
+2026-07-14-time-context-plugin.zh.md: 11e642f0361209cd29e86441e9ee82d845ea63dd
diff --git a/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.md b/.agents/notes/archived/feature/2026-07-14-time-context-plugin.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-14-time-context-plugin.md
rename to .agents/notes/archived/feature/2026-07-14-time-context-plugin.md
index 189f75fc12..89d6fca7c4 100644
--- a/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.md
+++ b/.agents/notes/archived/feature/2026-07-14-time-context-plugin.md
@@ -1,6 +1,7 @@
# Agent Note: Optional time-context plugin
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-14-time-context-plugin.zh.md)
diff --git a/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.zh.md b/.agents/notes/archived/feature/2026-07-14-time-context-plugin.zh.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-14-time-context-plugin.zh.md
rename to .agents/notes/archived/feature/2026-07-14-time-context-plugin.zh.md
index 12671cb891..11e642f036 100644
--- a/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.zh.md
+++ b/.agents/notes/archived/feature/2026-07-14-time-context-plugin.zh.md
@@ -1,6 +1,7 @@
-# Agent Note:可选时间上下文插件
+# Agent Note: 可选时间上下文插件
Status: implemented
+Archived: 2026-07-26
[English](2026-07-14-time-context-plugin.md) | 中文
diff --git a/.agents/notes/implemented/feature/2026-07-20-tui-startup-slogans.i18n.yaml b/.agents/notes/archived/feature/2026-07-20-tui-startup-slogans.i18n.yaml
similarity index 65%
rename from .agents/notes/implemented/feature/2026-07-20-tui-startup-slogans.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-20-tui-startup-slogans.i18n.yaml
index 3ed957d231..efebe5b58e 100644
--- a/.agents/notes/implemented/feature/2026-07-20-tui-startup-slogans.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-20-tui-startup-slogans.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
-2026-07-20-tui-startup-slogans.md: a2a22baafddd08145cec0d03b65ee56b2f8114b1
-2026-07-20-tui-startup-slogans.zh.md: 58fa5790f315845f27b810d62658bd79428b519b
+2026-07-20-tui-startup-slogans.md: aa847f96ebe13c4b1833531074577561faa2afb9
+2026-07-20-tui-startup-slogans.zh.md: bd667dfe9f54bbe923c48d0adf2889735b1255c5
diff --git a/.agents/notes/implemented/feature/2026-07-20-tui-startup-slogans.md b/.agents/notes/archived/feature/2026-07-20-tui-startup-slogans.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-20-tui-startup-slogans.md
rename to .agents/notes/archived/feature/2026-07-20-tui-startup-slogans.md
index a2a22baafd..aa847f96eb 100644
--- a/.agents/notes/implemented/feature/2026-07-20-tui-startup-slogans.md
+++ b/.agents/notes/archived/feature/2026-07-20-tui-startup-slogans.md
@@ -1,6 +1,7 @@
# Agent Note: Startup slogans replace the configured TUI welcome line
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-20-tui-startup-slogans.zh.md)
diff --git a/.agents/notes/implemented/feature/2026-07-20-tui-startup-slogans.zh.md b/.agents/notes/archived/feature/2026-07-20-tui-startup-slogans.zh.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-20-tui-startup-slogans.zh.md
rename to .agents/notes/archived/feature/2026-07-20-tui-startup-slogans.zh.md
index 58fa5790f3..bd667dfe9f 100644
--- a/.agents/notes/implemented/feature/2026-07-20-tui-startup-slogans.zh.md
+++ b/.agents/notes/archived/feature/2026-07-20-tui-startup-slogans.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 启动 slogan 取代配置化的 TUI 欢迎语
Status: implemented
+Archived: 2026-07-26
[English](2026-07-20-tui-startup-slogans.md) | 中文
diff --git a/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.i18n.yaml b/.agents/notes/archived/feature/2026-07-21-dsh-system-prompt-source-path.i18n.yaml
similarity index 62%
rename from .agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-21-dsh-system-prompt-source-path.i18n.yaml
index f1b9829b73..f8a7157bdd 100644
--- a/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-21-dsh-system-prompt-source-path.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
-2026-07-21-dsh-system-prompt-source-path.md: b54d01488fd7c0b49e06200c93af2b056c9fd00b
-2026-07-21-dsh-system-prompt-source-path.zh.md: 208e3dce072f63c280999e15276dce62ff4e5c43
+2026-07-21-dsh-system-prompt-source-path.md: 9581966d10693e1ccbdce1a860314a34387e225e
+2026-07-21-dsh-system-prompt-source-path.zh.md: 392fcd44d988d483306effc34d4aaf4211803b4e
diff --git a/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.md b/.agents/notes/archived/feature/2026-07-21-dsh-system-prompt-source-path.md
similarity index 97%
rename from .agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.md
rename to .agents/notes/archived/feature/2026-07-21-dsh-system-prompt-source-path.md
index b54d01488f..9581966d10 100644
--- a/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.md
+++ b/.agents/notes/archived/feature/2026-07-21-dsh-system-prompt-source-path.md
@@ -1,6 +1,7 @@
# Agent Note: dsh tells the agent where its own source lives
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-21-dsh-system-prompt-source-path.zh.md)
@@ -16,7 +17,7 @@ The testable logic lives in `dsh-app-boot`, not in `apps/cli`, because `apps/*`
## Scope
-Only the `dsh` CLI adds this. The demo bins (`dsh-tui-demo`, `dsh-acp-demo`) boot their committed trees verbatim and gain no source section: they are not the self-modification surface, and their checkout root is not a fact the model needs.
+Only the `dsh` CLI adds this. The demo bins (`dsh-cli-demo`, `dsh-acp-demo`) boot their committed trees verbatim and gain no source section: they are not the self-modification surface, and their checkout root is not a fact the model needs.
## HMR
diff --git a/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.zh.md b/.agents/notes/archived/feature/2026-07-21-dsh-system-prompt-source-path.zh.md
similarity index 98%
rename from .agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.zh.md
rename to .agents/notes/archived/feature/2026-07-21-dsh-system-prompt-source-path.zh.md
index 208e3dce07..392fcd44d9 100644
--- a/.agents/notes/implemented/feature/2026-07-21-dsh-system-prompt-source-path.zh.md
+++ b/.agents/notes/archived/feature/2026-07-21-dsh-system-prompt-source-path.zh.md
@@ -1,6 +1,7 @@
# Agent Note: dsh 告知 agent 其自身源码所在位置
Status: implemented
+Archived: 2026-07-26
[English](2026-07-21-dsh-system-prompt-source-path.md) | 中文
@@ -16,7 +17,7 @@ Status: implemented
## Scope
-只有 `dsh` CLI 会加入这一段。demo bin(`dsh-tui-demo`、`dsh-acp-demo`)原样引导它们已提交的插件树,不会获得 source 段:它们不是自我修改的接口,其检出根目录也不是模型需要知道的事实。
+只有 `dsh` CLI 会加入这一段。demo bin(`dsh-cli-demo`、`dsh-acp-demo`)原样引导它们已提交的插件树,不会获得 source 段:它们不是自我修改的接口,其检出根目录也不是模型需要知道的事实。
## HMR
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-auto-pane-title.i18n.yaml b/.agents/notes/archived/feature/2026-07-21-tui-auto-pane-title.i18n.yaml
similarity index 65%
rename from .agents/notes/implemented/feature/2026-07-21-tui-auto-pane-title.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-21-tui-auto-pane-title.i18n.yaml
index 737a9da6ca..1060fa28b5 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-auto-pane-title.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-21-tui-auto-pane-title.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
-2026-07-21-tui-auto-pane-title.md: 069fd33a8874d9ad3d4472dd13f5130b2df65f08
-2026-07-21-tui-auto-pane-title.zh.md: 580f36b2563e21231a22cab3f0c1689c6f3e8d9d
+2026-07-21-tui-auto-pane-title.md: 5235ffb12807b7e24ef952df594052ed7aa65cdf
+2026-07-21-tui-auto-pane-title.zh.md: 0b93d5a0a9d31e9f4321ca7ae975c1db74d2229e
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-auto-pane-title.md b/.agents/notes/archived/feature/2026-07-21-tui-auto-pane-title.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-auto-pane-title.md
rename to .agents/notes/archived/feature/2026-07-21-tui-auto-pane-title.md
index 069fd33a88..5235ffb128 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-auto-pane-title.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-auto-pane-title.md
@@ -1,6 +1,7 @@
# Agent Note: Auto-titled terminal from the first message
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-21-tui-auto-pane-title.zh.md)
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-auto-pane-title.zh.md b/.agents/notes/archived/feature/2026-07-21-tui-auto-pane-title.zh.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-auto-pane-title.zh.md
rename to .agents/notes/archived/feature/2026-07-21-tui-auto-pane-title.zh.md
index 580f36b256..0b93d5a0a9 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-auto-pane-title.zh.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-auto-pane-title.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 从首条消息自动命名终端
Status: implemented
+Archived: 2026-07-26
[English](2026-07-21-tui-auto-pane-title.md) | 中文
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-auto-title-default-on.i18n.yaml b/.agents/notes/archived/feature/2026-07-21-tui-auto-title-default-on.i18n.yaml
similarity index 63%
rename from .agents/notes/implemented/feature/2026-07-21-tui-auto-title-default-on.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-21-tui-auto-title-default-on.i18n.yaml
index 830ca3e2e0..6d0a7c59d1 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-auto-title-default-on.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-21-tui-auto-title-default-on.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
-2026-07-21-tui-auto-title-default-on.md: 35809e1ef6bade3e09c34b17608eff5f8fb5bd22
-2026-07-21-tui-auto-title-default-on.zh.md: aa20cfde1359605f2ac5a8f0427f4518c611ecd1
+2026-07-21-tui-auto-title-default-on.md: 498c6095fcd05c40ce2ad48364a9ac02beb9aa05
+2026-07-21-tui-auto-title-default-on.zh.md: 8bd426c8705d84f674f811347c04ef42de7cae15
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-auto-title-default-on.md b/.agents/notes/archived/feature/2026-07-21-tui-auto-title-default-on.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-auto-title-default-on.md
rename to .agents/notes/archived/feature/2026-07-21-tui-auto-title-default-on.md
index 35809e1ef6..498c6095fc 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-auto-title-default-on.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-auto-title-default-on.md
@@ -1,6 +1,7 @@
# Agent Note: Auto-title on by default, re-derived on resume
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-21-tui-auto-title-default-on.zh.md)
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-auto-title-default-on.zh.md b/.agents/notes/archived/feature/2026-07-21-tui-auto-title-default-on.zh.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-auto-title-default-on.zh.md
rename to .agents/notes/archived/feature/2026-07-21-tui-auto-title-default-on.zh.md
index aa20cfde13..8bd426c870 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-auto-title-default-on.zh.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-auto-title-default-on.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 自动标题默认开启,恢复时重新推导
Status: implemented
+Archived: 2026-07-26
[English](2026-07-21-tui-auto-title-default-on.md) | 中文
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-banner-brand-gradient.i18n.yaml b/.agents/notes/archived/feature/2026-07-21-tui-banner-brand-gradient.i18n.yaml
similarity index 63%
rename from .agents/notes/implemented/feature/2026-07-21-tui-banner-brand-gradient.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-21-tui-banner-brand-gradient.i18n.yaml
index 684f23438c..20ee31b643 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-banner-brand-gradient.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-21-tui-banner-brand-gradient.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
-2026-07-21-tui-banner-brand-gradient.md: 41edf5d0bcf856bc7695af6bf651ff04c11adc01
-2026-07-21-tui-banner-brand-gradient.zh.md: 9253c001e8df2a4d0f79f69f32d65c11afd13e22
+2026-07-21-tui-banner-brand-gradient.md: 3516b0dcf9b6949721ec3e0d062f2d135da21083
+2026-07-21-tui-banner-brand-gradient.zh.md: 6fd0f140d474d26860eef77d64d5df550709d940
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-banner-brand-gradient.md b/.agents/notes/archived/feature/2026-07-21-tui-banner-brand-gradient.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-banner-brand-gradient.md
rename to .agents/notes/archived/feature/2026-07-21-tui-banner-brand-gradient.md
index 41edf5d0bc..3516b0dcf9 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-banner-brand-gradient.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-banner-brand-gradient.md
@@ -1,6 +1,7 @@
# Agent Note: TUI banner brand gradient
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-21-tui-banner-brand-gradient.zh.md)
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-banner-brand-gradient.zh.md b/.agents/notes/archived/feature/2026-07-21-tui-banner-brand-gradient.zh.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-banner-brand-gradient.zh.md
rename to .agents/notes/archived/feature/2026-07-21-tui-banner-brand-gradient.zh.md
index 9253c001e8..6fd0f140d4 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-banner-brand-gradient.zh.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-banner-brand-gradient.zh.md
@@ -1,6 +1,7 @@
# Agent Note: TUI 启动横幅品牌渐变
Status: implemented
+Archived: 2026-07-26
[English](2026-07-21-tui-banner-brand-gradient.md) | 中文
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-banner-sweep.i18n.yaml b/.agents/notes/archived/feature/2026-07-21-tui-banner-sweep.i18n.yaml
similarity index 66%
rename from .agents/notes/implemented/feature/2026-07-21-tui-banner-sweep.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-21-tui-banner-sweep.i18n.yaml
index a06145f092..a23f12cb22 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-banner-sweep.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-21-tui-banner-sweep.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
-2026-07-21-tui-banner-sweep.md: c146424d53e75a72b63e346f87a5bbd206d67350
-2026-07-21-tui-banner-sweep.zh.md: 01cc153e88f067b7b8d2eb6317648f3892fe8a5a
+2026-07-21-tui-banner-sweep.md: 3351ff40e50fb3ef4de569cab0a33f311ea24a46
+2026-07-21-tui-banner-sweep.zh.md: e783ec20158c5a3be07b4afdf539463763d40948
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-banner-sweep.md b/.agents/notes/archived/feature/2026-07-21-tui-banner-sweep.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-banner-sweep.md
rename to .agents/notes/archived/feature/2026-07-21-tui-banner-sweep.md
index c146424d53..3351ff40e5 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-banner-sweep.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-banner-sweep.md
@@ -1,6 +1,7 @@
# Agent Note: The banner sweeps in; the subtitle line is gone
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-21-tui-banner-sweep.zh.md)
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-banner-sweep.zh.md b/.agents/notes/archived/feature/2026-07-21-tui-banner-sweep.zh.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-banner-sweep.zh.md
rename to .agents/notes/archived/feature/2026-07-21-tui-banner-sweep.zh.md
index 01cc153e88..e783ec2015 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-banner-sweep.zh.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-banner-sweep.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 横幅整体扫入;副标题行移除
Status: implemented
+Archived: 2026-07-26
[English](2026-07-21-tui-banner-sweep.md) | 中文
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-borderless-banner.i18n.yaml b/.agents/notes/archived/feature/2026-07-21-tui-borderless-banner.i18n.yaml
similarity index 64%
rename from .agents/notes/implemented/feature/2026-07-21-tui-borderless-banner.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-21-tui-borderless-banner.i18n.yaml
index 8732101ab2..972ead6485 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-borderless-banner.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-21-tui-borderless-banner.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
-2026-07-21-tui-borderless-banner.md: 37263854b6cc77283215c3c1378f9908ff966611
-2026-07-21-tui-borderless-banner.zh.md: ca796e49cb9d3a9abc0acd64a39448bc3f9ad50e
+2026-07-21-tui-borderless-banner.md: 09fe713544134865687162c4624090b8e9aa3ebf
+2026-07-21-tui-borderless-banner.zh.md: b11c6d3c8cd327d7779617b4a7939ce55d8b765c
diff --git a/.agents/notes/archived/feature/2026-07-21-tui-borderless-banner.md b/.agents/notes/archived/feature/2026-07-21-tui-borderless-banner.md
new file mode 100644
index 0000000000..09fe713544
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-21-tui-borderless-banner.md
@@ -0,0 +1,47 @@
+# Agent Note: The banner returns, borderless
+
+Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-21-tui-borderless-banner.zh.md)
+
+## Problem
+
+An intermediate no-banner design removed the boxed startup banner: it deleted `HeaderComponent` and its sweep, moved the model into the footer, dropped the session id, and rendered `welcome` as the transcript's first line. The user's verdict reversed that: bring the banner back — "just remove the border". The four-row box frame was the objectionable chrome, not the identifying facts it carried (model, session id) nor the sweep-in motion.
+
+## Decision
+
+- `HeaderComponent` and its left-to-right sweep return, but render **borderless**: no `╭─╮`/`╰─╯` corners and no `│` side bars. Each line is a single leading space plus `truncateToWidth`-clipped content, so the sweep's width clip can never tear an escape sequence and no fixed frame is drawn. The reveal advances through about 24 frames at 15 ms each.
+- The header carries the title (`DEEPSEEK HARNESS`), a ` • ` detail line, and — when `welcome` is set — a muted subtitle. With `welcome` unset the header is title + detail only: there is no fixed or random slogan.
+- The model **also** stays in the footer's left segment, so the driving model remains glanceable after the transient banner scrolls out of view.
+- `welcome` reverts to a banner subtitle; the transcript-first-line notice is removed from `rebuildTranscript`.
+- The sweep animates only when `welcome` is unset. A configured `welcome` renders the whole banner immediately, keeping fixtures and snapshots frame-deterministic. The sweep starts after `ui.start()` succeeds and is cleared through the same `detachListeners` path via `stopBannerReveal`, which also resets the clip so a header disposed mid-sweep re-renders whole.
+
+This note owns the current result of the discarded startup variants: random slogans with a per-character typewriter, a boxed whole-banner sweep, and no banner. The example composition does not set `welcome`; deployments and deterministic fixtures may still provide one. The model's persistent footer home from the no-banner variant remains.
+
+## Alternatives considered
+
+**Keep the box but thin it or use lighter glyphs.** Rejected: the instruction was "just remove the border"; any surrounding glyph is the frame chrome the user objected to.
+
+**Keep a random or fixed slogan when `welcome` is unset.** Rejected because repeated flavor copy becomes wallpaper and the per-character reveal was slow while animating only one line. An unset welcome therefore produces no subtitle, and the whole banner supplies the startup motion.
+
+**Remove the banner entirely.** Rejected because the persistent footer is a good home for the model but not for the full identifying detail, while putting `welcome` in the transcript makes presentation configuration behave like conversation content.
+
+**Reveal the banner top-down.** Rejected because four row-sized steps read as a flicker. The horizontal width clip uses the terminal span for smooth motion and reuses the ANSI-aware truncation path.
+
+**Drop the model from the footer now that the banner shows it again.** Rejected: the banner is transient and scrolls away with the transcript, while the footer keeps the model visible for the whole session; that persistent location is deliberately preserved.
+
+**Leave the session id out of the banner.** Rejected: with the box gone the detail line costs one row, and the user asked for the banner "as before", which carried `model • session-id`.
+
+## Consequences
+
+- Boot output with `welcome` unset is animation-dependent again (the sweep); configured welcomes stay frame-deterministic, so every snapshot and scripted fixture keeps a fixed subtitle.
+- The demo no longer supplies instructional welcome filler; an unset `welcome` means a subtitle-free banner, while the config remains the deterministic escape hatch for deployments and fixtures.
+- The model now appears twice at boot — banner detail and footer — intended redundancy: the banner is transient, the footer persistent.
+- `/clear` empties the transcript but not the header, so the banner and its configured subtitle survive `/clear`, unlike a transcript-based welcome line.
+- All pi-tui terminal snapshots and the examples/tui-agent replay snapshots re-recorded (`test:snapshot:refresh`): banner rows return with no box glyphs; footer rows keep the model prefix.
+- Anything that anchored on banner absence re-anchors on its presence: the PTY smoke boots on the detail line's `main-session-` id (revealed late in the sweep) and asserts `DEEPSEEK`/`HARNESS` present with no box corners.
+
+## Testing
+
+`packages/ui/tui/tests/tui.spec.ts` pins: the borderless banner sweeps to natural completion — no box corners, title and `main-session` detail present — with at least one clipped mid-sweep frame; a configured `welcome` renders the whole banner with no clipped frame; the unset-welcome banner has no subtitle; and dispose clears the sweep interval mid-sweep. Independent color-scheme cases cover reported light/dark transitions, a same-scheme no-op, and a terminal that throws on the DSR query write; `applyColorScheme` relies on `setStatus` to rederive the editor border instead of repeating the dead assignment that had broken per-file coverage. The tui-agent and dsh-CLI PTY smokes boot on the `main-session-` detail marker and assert no box corners. Snapshots verify the full frames.
diff --git a/.agents/notes/archived/feature/2026-07-21-tui-borderless-banner.zh.md b/.agents/notes/archived/feature/2026-07-21-tui-borderless-banner.zh.md
new file mode 100644
index 0000000000..b11c6d3c8c
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-21-tui-borderless-banner.zh.md
@@ -0,0 +1,47 @@
+# Agent Note: 横幅回归,无边框
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-21-tui-borderless-banner.md) | 中文
+
+## Problem
+
+一个中间的无横幅设计删掉了带框的启动横幅:它删除了 `HeaderComponent` 及其扫入动画,把模型移入页脚,丢弃了会话 id,并把 `welcome` 渲染为 transcript 的第一行。用户的裁决把这一切反转:把横幅拿回来——"just remove the border"。令人反感的装饰是那四行盒子边框,而不是它承载的识别信息(模型、会话 id),也不是扫入动效。
+
+## Decision
+
+- `HeaderComponent` 及其从左到右的扫入动画回归,但以**无边框**方式渲染:没有 `╭─╮`/`╰─╯` 边角,也没有 `│` 侧边。每一行都是一个前导空格加上经 `truncateToWidth` 裁剪的内容,因此扫入的宽度裁剪永远不会撕裂转义序列,也不绘制任何固定边框。扫入大约经过 24 帧完成,每帧间隔 15 ms。
+- 头部承载标题(`DEEPSEEK HARNESS`)、一条 ` • ` 详情行,以及——当设置了 `welcome` 时——一条弱化的副标题。`welcome` 未设置时头部只有标题加详情:不含固定或随机标语。
+- 模型**同时**保留在页脚的左段,因此在短暂的横幅滚出视野后,会话使用的模型仍可一瞥可见。
+- `welcome` 恢复为横幅副标题;transcript 第一行的通知从 `rebuildTranscript` 中移除。
+- 仅当 `welcome` 未设置时才播放扫入动画。配置了 `welcome` 会立即渲染整个横幅,使 fixture 和快照保持帧确定性。扫入在 `ui.start()` 成功后启动,并经与之前相同的 `detachListeners` 路径通过 `stopBannerReveal` 清理;后者还会重置裁剪,使扫入中途被销毁的头部重新完整渲染。
+
+本 Agent Note 统一记录几种已弃用启动方案的当前结论:带逐字打字机效果的随机标语、带边框的整幅横幅扫入动画,以及完全移除横幅。示例组装不设置 `welcome`;部署和确定性 fixture 仍可提供该值。无横幅方案为模型设置的常驻页脚位置继续保留。
+
+## Alternatives considered
+
+**保留盒子但做细或改用更轻的字符。** 否决:指令是 "just remove the border";任何环绕的字符都是用户所反对的边框装饰。
+
+**在未设置 `welcome` 时保留随机或固定标语。** 否决:反复出现的氛围文案很快失去信息价值,而逐字揭示仅为一行制作动画,速度又慢。因此,未设置 `welcome` 时不显示副标题,由整个横幅提供启动动效。
+
+**完全移除横幅。** 否决:常驻页脚很适合显示模型,却无法承载完整识别详情;把 `welcome` 放入 transcript 还会使展示配置表现成对话内容。
+
+**自上而下揭示横幅。** 否决:按四行分成四步看起来像闪烁。横向宽度裁剪利用终端横向空间实现平滑动效,并复用 ANSI 感知的截断路径。
+
+**既然横幅重新显示模型,就把模型从页脚移除。** 否决:横幅是短暂的,会随 transcript 滚走,而页脚在整个会话中保持模型可见;这个常驻位置被刻意保留。
+
+**将会话 id 留在横幅之外。** 否决:盒子去掉后详情行只占一行,且用户要求横幅"和以前一样",而以前它承载 `model • session-id`。
+
+## Consequences
+
+- `welcome` 未设置时的启动输出再次依赖动画(扫入);配置了欢迎语则保持帧确定性,因此每个快照和脚本 fixture 都保留一个固定副标题。
+- demo 不再提供教学性质的欢迎填充文案;`welcome` 未设置就表示横幅没有副标题,而该配置仍是部署和 fixture 获得确定性输出的配置手段。
+- 模型现在在启动时出现两次——横幅详情与页脚——这是有意的冗余:横幅短暂,页脚常驻。
+- `/clear` 清空 transcript 但不清头部,因此横幅及其配置的副标题在 `/clear` 后存活,不同于基于 transcript 的欢迎行。
+- 全部 pi-tui 终端快照与 examples/tui-agent 回放快照重新录制(`test:snapshot:refresh`):横幅行以无盒子字符方式回归;页脚行保留模型前缀。
+- 一切锚定横幅缺失的内容改为锚定其存在:PTY 冒烟测试以详情行的 `main-session-` id 为启动标记(它在扫入后段才被揭示),并断言 `DEEPSEEK`/`HARNESS` 出现且无盒子角。
+
+## Testing
+
+`packages/ui/tui/tests/tui.spec.ts` 固定:无边框横幅扫入至自然完成——无盒子角、标题与 `main-session` 详情出现——且至少有一帧扫入中途被裁剪;配置的 `welcome` 完整渲染横幅且无裁剪帧;未设置 `welcome` 的横幅无副标题;销毁会在扫入中途清掉扫入定时器。独立的配色方案用例覆盖终端报告的浅色/深色转换、相同方案下的空操作,以及写入 DSR 查询时抛出异常的终端;`applyColorScheme` 依靠 `setStatus` 重新推导编辑器边框,而不再重复那个导致逐文件覆盖率未达标的无效赋值。tui-agent 与 dsh CLI 的 PTY 冒烟测试以 `main-session-` 详情标记为启动标记并断言无盒子角。快照验证完整帧。
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-footer-cache-hit-rate.i18n.yaml b/.agents/notes/archived/feature/2026-07-21-tui-footer-cache-hit-rate.i18n.yaml
similarity index 63%
rename from .agents/notes/implemented/feature/2026-07-21-tui-footer-cache-hit-rate.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-21-tui-footer-cache-hit-rate.i18n.yaml
index d7cdbb3c3c..cbe0bd6811 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-footer-cache-hit-rate.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-21-tui-footer-cache-hit-rate.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
-2026-07-21-tui-footer-cache-hit-rate.md: aaee8ed31ff8f20370f490d3ce27c8705cda3e16
-2026-07-21-tui-footer-cache-hit-rate.zh.md: 67a7aa474d98878a5bc0bc0a76a8c2ccad004e9b
+2026-07-21-tui-footer-cache-hit-rate.md: 9e6ec734030088f063c049312ea345dc07303554
+2026-07-21-tui-footer-cache-hit-rate.zh.md: ec761df2cb9cad1dd922082f4ff1a8bb25cbdd69
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-footer-cache-hit-rate.md b/.agents/notes/archived/feature/2026-07-21-tui-footer-cache-hit-rate.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-footer-cache-hit-rate.md
rename to .agents/notes/archived/feature/2026-07-21-tui-footer-cache-hit-rate.md
index aaee8ed31f..9e6ec73403 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-footer-cache-hit-rate.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-footer-cache-hit-rate.md
@@ -1,6 +1,7 @@
# Agent Note: TUI footer shows the session cache hit rate
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-21-tui-footer-cache-hit-rate.zh.md)
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-footer-cache-hit-rate.zh.md b/.agents/notes/archived/feature/2026-07-21-tui-footer-cache-hit-rate.zh.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-footer-cache-hit-rate.zh.md
rename to .agents/notes/archived/feature/2026-07-21-tui-footer-cache-hit-rate.zh.md
index 67a7aa474d..ec761df2cb 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-footer-cache-hit-rate.zh.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-footer-cache-hit-rate.zh.md
@@ -1,6 +1,7 @@
# Agent Note: TUI 页脚展示会话缓存命中率
Status: implemented
+Archived: 2026-07-26
[English](2026-07-21-tui-footer-cache-hit-rate.md) | 中文
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.i18n.yaml b/.agents/notes/archived/feature/2026-07-21-tui-no-banner.i18n.yaml
similarity index 67%
rename from .agents/notes/implemented/feature/2026-07-21-tui-no-banner.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-21-tui-no-banner.i18n.yaml
index 56333563f5..2a2b8007b1 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-21-tui-no-banner.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
-2026-07-21-tui-no-banner.md: f5f4b1b847740e741ec3e33a6116e7497e955bd1
-2026-07-21-tui-no-banner.zh.md: 956fe03e2c0b09ea7378ffd53ffbe8d712d1e152
+2026-07-21-tui-no-banner.md: bfe78b2ba958193cc7bfe483b17faf5ae27eb4bb
+2026-07-21-tui-no-banner.zh.md: c92ad46d4176ec0444e5db766350c4e74c137645
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.md b/.agents/notes/archived/feature/2026-07-21-tui-no-banner.md
similarity index 96%
rename from .agents/notes/implemented/feature/2026-07-21-tui-no-banner.md
rename to .agents/notes/archived/feature/2026-07-21-tui-no-banner.md
index f5f4b1b847..bfe78b2ba9 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-no-banner.md
@@ -1,6 +1,7 @@
# Agent Note: No startup banner
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-21-tui-no-banner.zh.md)
@@ -13,7 +14,7 @@ The TUI opened with a boxed product banner ("DEEPSEEK HARNESS" + model/session d
## Decision
- `HeaderComponent`, the sweep animation, and its lifecycle wiring are deleted. The TUI mounts straight into the transcript; startup renders nothing above the separator.
-- The model name moves into the footer status line's left segment (` ↑tokens ↓tokens`), so the session's driving model stays visible at all times, not just at boot. The session id is no longer displayed — it lives in the session log and `./.sessions` filenames, and `RESUME_SESSION_ID` consumers retrieve it there.
+- The model name moves into the footer status line's left segment (` ↑tokens ↓tokens`), so the session's driving model stays visible at all times, not just at boot. The session id is no longer displayed — it lives in the session log and `./.sessions` filenames, where `dsh --resume ` and the `/resume` selector retrieve it.
- `welcome`, when configured, renders as the transcript's first line (a muted notice) inside `rebuildTranscript`, so palette swaps preserve it. Unset renders nothing. Fixtures keep their configured welcomes; the PTY smoke's boot marker becomes the footer's model name, the only mounted-TUI text guaranteed to render regardless of cwd length.
This supersedes the [banner sweep Agent Note](2026-07-21-tui-banner-sweep.md) entirely: both the sweep and the banner it animated are gone.
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.zh.md b/.agents/notes/archived/feature/2026-07-21-tui-no-banner.zh.md
similarity index 96%
rename from .agents/notes/implemented/feature/2026-07-21-tui-no-banner.zh.md
rename to .agents/notes/archived/feature/2026-07-21-tui-no-banner.zh.md
index 956fe03e2c..c92ad46d41 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-no-banner.zh.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-no-banner.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 移除启动横幅
Status: implemented
+Archived: 2026-07-26
[English](2026-07-21-tui-no-banner.md) | 中文
@@ -13,7 +14,7 @@ TUI 启动时展示一个带框的产品横幅("DEEPSEEK HARNESS" + 模型/会
## Decision
- 删除 `HeaderComponent`、扫入动画及其生命周期接线。TUI 直接挂载进 transcript;启动时分隔线之上不渲染任何东西。
-- 模型名移入页脚状态行的左段(` ↑tokens ↓tokens`),会话使用的模型因此始终可见,而不只是启动时。会话 id 不再显示——它存在于会话日志和 `./.sessions` 文件名中,`RESUME_SESSION_ID` 的使用者从那里获取。
+- 模型名移入页脚状态行的左段(` ↑tokens ↓tokens`),会话使用的模型因此始终可见,而不只是启动时。会话 id 不再显示——它存在于会话日志和 `./.sessions` 文件名中,`dsh --resume ` 和 `/resume` 选择器会从中获取该 id。
- 配置了 `welcome` 时,它作为 transcript 的第一行(一条弱化的通知)在 `rebuildTranscript` 内渲染,因此调色板切换会保留它。未设置则什么也不渲染。fixture 保留各自配置的欢迎语;PTY 冒烟测试的启动标记改为页脚的模型名——无论 cwd 多长都保证渲染的唯一挂载后文本。
本 note 完全取代[横幅扫入 Agent Note](2026-07-21-tui-banner-sweep.md):扫入动画和它所动画的横幅都已移除。
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-reload-command.i18n.yaml b/.agents/notes/archived/feature/2026-07-21-tui-reload-command.i18n.yaml
similarity index 65%
rename from .agents/notes/implemented/feature/2026-07-21-tui-reload-command.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-21-tui-reload-command.i18n.yaml
index 05f9ae37b6..f1e467433d 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-reload-command.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-21-tui-reload-command.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
-2026-07-21-tui-reload-command.md: 89bf2f7bb482d7f3889136c1a6ac9918ba0c4919
-2026-07-21-tui-reload-command.zh.md: cfea10690af49f2cf484938a3f9f12d954766a71
+2026-07-21-tui-reload-command.md: e491e8f4510128b7fda03d41bc1d13e4dfdc9f5e
+2026-07-21-tui-reload-command.zh.md: ed96c689fe4ba700f5b6836d7b7727e0252737a2
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-reload-command.md b/.agents/notes/archived/feature/2026-07-21-tui-reload-command.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-reload-command.md
rename to .agents/notes/archived/feature/2026-07-21-tui-reload-command.md
index 89bf2f7bb4..e491e8f451 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-reload-command.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-reload-command.md
@@ -1,6 +1,7 @@
# Agent Note: The /reload command re-reads loader configs on demand
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-21-tui-reload-command.zh.md)
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-reload-command.zh.md b/.agents/notes/archived/feature/2026-07-21-tui-reload-command.zh.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-reload-command.zh.md
rename to .agents/notes/archived/feature/2026-07-21-tui-reload-command.zh.md
index cfea10690a..ed96c689fe 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-reload-command.zh.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-reload-command.zh.md
@@ -1,6 +1,7 @@
# Agent Note: /reload 命令按需重读 loader 配置
Status: implemented
+Archived: 2026-07-26
[English](2026-07-21-tui-reload-command.md) | 中文
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-steering-queue-badge.i18n.yaml b/.agents/notes/archived/feature/2026-07-21-tui-steering-queue-badge.i18n.yaml
similarity index 63%
rename from .agents/notes/implemented/feature/2026-07-21-tui-steering-queue-badge.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-21-tui-steering-queue-badge.i18n.yaml
index ddf4792769..5231dfd7e7 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-steering-queue-badge.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-21-tui-steering-queue-badge.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
-2026-07-21-tui-steering-queue-badge.md: b29a4667e778e65b0678f946fcaa34b79c4d7da0
-2026-07-21-tui-steering-queue-badge.zh.md: 4bfce461e11bce1773d6e0b15aabecf6a6a6144c
+2026-07-21-tui-steering-queue-badge.md: 37e8a11c0dd30a0674107ff33d51a31d92385ada
+2026-07-21-tui-steering-queue-badge.zh.md: 6ef412b26a32c3aa359215404ca81635fca776c1
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-steering-queue-badge.md b/.agents/notes/archived/feature/2026-07-21-tui-steering-queue-badge.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-steering-queue-badge.md
rename to .agents/notes/archived/feature/2026-07-21-tui-steering-queue-badge.md
index b29a4667e7..37e8a11c0d 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-steering-queue-badge.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-steering-queue-badge.md
@@ -1,6 +1,7 @@
# Agent Note: TUI status line badges queued steering messages
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-21-tui-steering-queue-badge.zh.md)
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-steering-queue-badge.zh.md b/.agents/notes/archived/feature/2026-07-21-tui-steering-queue-badge.zh.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-21-tui-steering-queue-badge.zh.md
rename to .agents/notes/archived/feature/2026-07-21-tui-steering-queue-badge.zh.md
index 4bfce461e1..6ef412b26a 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-steering-queue-badge.zh.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-steering-queue-badge.zh.md
@@ -1,6 +1,7 @@
# Agent Note: TUI 状态行标示排队中的 steering 消息
Status: implemented
+Archived: 2026-07-26
[English](2026-07-21-tui-steering-queue-badge.md) | 中文
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-verbose-status-line.i18n.yaml b/.agents/notes/archived/feature/2026-07-21-tui-verbose-status-line.i18n.yaml
similarity index 64%
rename from .agents/notes/implemented/feature/2026-07-21-tui-verbose-status-line.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-21-tui-verbose-status-line.i18n.yaml
index 319f28ea61..a6fc0d00d6 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-verbose-status-line.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-21-tui-verbose-status-line.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
-2026-07-21-tui-verbose-status-line.md: f277afd3a874b30a29dc0ef193740f636d22290b
-2026-07-21-tui-verbose-status-line.zh.md: 9fa7cf29c67245382bbee6b72f2710c5550d7f54
+2026-07-21-tui-verbose-status-line.md: 9ed396b0dbf4325d6fdd2a4f20f8d81b5b408171
+2026-07-21-tui-verbose-status-line.zh.md: 14c9b36646e226715156ba8db8f62cd6090ce9a0
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-verbose-status-line.md b/.agents/notes/archived/feature/2026-07-21-tui-verbose-status-line.md
similarity index 93%
rename from .agents/notes/implemented/feature/2026-07-21-tui-verbose-status-line.md
rename to .agents/notes/archived/feature/2026-07-21-tui-verbose-status-line.md
index f277afd3a8..9ed396b0db 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-verbose-status-line.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-verbose-status-line.md
@@ -1,6 +1,7 @@
# Agent Note: The running status line shows the turn phase and elapsed time
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-21-tui-verbose-status-line.zh.md)
@@ -13,7 +14,7 @@ While a turn ran, the [full-screen TUI](2026-07-17-dedicated-full-screen-tui-fro
- While a turn runs, the status line above the editor shows a derived phase label with elapsed time, keeping the trailing `— Enter sends steering, Esc cancels` hint. The four phases and their labels are `waiting` → "Waiting for the first token", `thinking` → "Thinking", `responding` → "Responding", and `executing` → "Executing tools".
- The phase is presentation state the TUI derives from live session events, not a session event or agent status of its own. `step/start` enters `waiting`; an `assistant/chunk` reasoning delta or reasoning block-start enters `thinking`; a text delta or text block-start enters `responding`; a `tool/call` enters `executing`. The event map is merge-extensible, so every other event kind falls through a default and leaves the phase unchanged.
- The label reports two clocks — ` · total ` — except `waiting`, which shows only the step total. The phase clock resets on a genuine phase change or a new step; the step clock resets on `step/start`. Durations format as `8s` below a minute and `1m05s` at or above one. Tool time between `step/end` and the next `step/start` accrues to the finishing step's total.
-- A single `RunningStatus` controller — the loader, the phase, the two baselines, and a refresh timer — exists only while a turn runs. A one-second `setInterval` refreshes the elapsed time; a phase event refreshes it immediately. `clearStatus` clears the interval, stops the loader, and drops the controller, so any transition to idle or disposed leaves no live timer, matching the [banner sweep](2026-07-21-tui-banner-sweep.md)'s timer hygiene. A mid-turn palette rebuild (`setStatus` re-derives the editor border on a terminal color-scheme change) carries the phase and both baselines across, so a running status never snaps back to `waiting`.
+- A single `RunningStatus` controller — the loader, the phase, the two baselines, and a refresh timer — exists only while a turn runs. A one-second `setInterval` refreshes the elapsed time; a phase event refreshes it immediately. `clearStatus` clears the interval, stops the loader, and drops the controller, so any transition to idle or disposed leaves no live timer, matching the [borderless banner](2026-07-21-tui-borderless-banner.md)'s timer hygiene. A mid-turn palette rebuild (`setStatus` re-derives the editor border on a terminal color-scheme change) carries the phase and both baselines across, so a running status never snaps back to `waiting`.
## Alternatives considered
diff --git a/.agents/notes/implemented/feature/2026-07-21-tui-verbose-status-line.zh.md b/.agents/notes/archived/feature/2026-07-21-tui-verbose-status-line.zh.md
similarity index 96%
rename from .agents/notes/implemented/feature/2026-07-21-tui-verbose-status-line.zh.md
rename to .agents/notes/archived/feature/2026-07-21-tui-verbose-status-line.zh.md
index 9fa7cf29c6..14c9b36646 100644
--- a/.agents/notes/implemented/feature/2026-07-21-tui-verbose-status-line.zh.md
+++ b/.agents/notes/archived/feature/2026-07-21-tui-verbose-status-line.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 运行状态行展示轮次阶段与已用时长
Status: implemented
+Archived: 2026-07-26
[English](2026-07-21-tui-verbose-status-line.md) | 中文
@@ -13,7 +14,7 @@ Status: implemented
- 轮次运行期间,编辑器上方的状态行显示一个派生的阶段标签及已用时长,并保留末尾的 `— Enter sends steering, Esc cancels` 提示。四个阶段及其标签为 `waiting` → "Waiting for the first token"、`thinking` → "Thinking"、`responding` → "Responding"、`executing` → "Executing tools"。
- 阶段是 TUI 从实时会话事件派生出的呈现状态,而非它自有的会话事件或 agent 状态。`step/start` 进入 `waiting`;`assistant/chunk` 的 reasoning 分片或 reasoning 块开始(`block-start`)进入 `thinking`;text 分片或 text 块开始进入 `responding`;`tool/call` 进入 `executing`。该事件映射可合并扩展,因此其余任何事件类型都落入默认分支,保持阶段不变。
- 标签汇报两个时钟——` · total `——但 `waiting` 只显示步骤总时长。阶段时钟在真正发生阶段切换或进入新步骤时重置;步骤时钟在 `step/start` 时重置。时长在不足一分钟时格式化为 `8s`,达到或超过一分钟时格式化为 `1m05s`。`step/end` 与下一个 `step/start` 之间的工具时间计入结束步骤的总时长。
-- 单一的 `RunningStatus` 控制器——loader、阶段、两个基准时刻以及一个刷新定时器——仅在轮次运行期间存在。一个每秒触发的 `setInterval` 刷新已用时长;阶段事件则立即刷新。`clearStatus` 清除该 interval、停止 loader 并丢弃控制器,因此任何向 idle 或 disposed 的转变都不会遗留活动定时器,与 [banner 扫入动画](2026-07-21-tui-banner-sweep.md)的定时器清理保持一致。轮次进行中的调色板重建(终端颜色方案变化时 `setStatus` 会重新派生编辑器边框)会将阶段与两个基准时刻一并沿用过来,因此运行中的状态绝不会退回 `waiting`。
+- 单一的 `RunningStatus` 控制器——loader、阶段、两个基准时刻以及一个刷新定时器——仅在轮次运行期间存在。一个每秒触发的 `setInterval` 刷新已用时长;阶段事件则立即刷新。`clearStatus` 清除该 interval、停止 loader 并丢弃控制器,因此任何向 idle 或 disposed 的转变都不会遗留活动定时器,与[无边框横幅](2026-07-21-tui-borderless-banner.md)的定时器清理保持一致。轮次进行中的调色板重建(终端颜色方案变化时 `setStatus` 会重新派生编辑器边框)会将阶段与两个基准时刻一并沿用过来,因此运行中的状态绝不会退回 `waiting`。
## 曾考虑的替代方案
diff --git a/.agents/notes/implemented/feature/2026-07-23-trajectory-step-cell.i18n.yaml b/.agents/notes/archived/feature/2026-07-23-trajectory-step-cell.i18n.yaml
similarity index 65%
rename from .agents/notes/implemented/feature/2026-07-23-trajectory-step-cell.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-23-trajectory-step-cell.i18n.yaml
index 1702c90c43..cc1d5c06fc 100644
--- a/.agents/notes/implemented/feature/2026-07-23-trajectory-step-cell.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-23-trajectory-step-cell.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
-2026-07-23-trajectory-step-cell.md: 414c3aac856fb5e60f0e4cf42f8e7b410cdf3413
-2026-07-23-trajectory-step-cell.zh.md: aa76b422f165ebf6918b3781fdfe38797a34ba51
+2026-07-23-trajectory-step-cell.md: 871f02d72b74bb6dbeb782fde3b639b237cf71e1
+2026-07-23-trajectory-step-cell.zh.md: 3ebb4becd569242bfdea222df6d042a4c00ad096
diff --git a/.agents/notes/implemented/feature/2026-07-23-trajectory-step-cell.md b/.agents/notes/archived/feature/2026-07-23-trajectory-step-cell.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-23-trajectory-step-cell.md
rename to .agents/notes/archived/feature/2026-07-23-trajectory-step-cell.md
index 414c3aac85..871f02d72b 100644
--- a/.agents/notes/implemented/feature/2026-07-23-trajectory-step-cell.md
+++ b/.agents/notes/archived/feature/2026-07-23-trajectory-step-cell.md
@@ -1,6 +1,7 @@
# Agent Note: Trajectory step cell and turn list chrome
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-23-trajectory-step-cell.zh.md)
diff --git a/.agents/notes/implemented/feature/2026-07-23-trajectory-step-cell.zh.md b/.agents/notes/archived/feature/2026-07-23-trajectory-step-cell.zh.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-23-trajectory-step-cell.zh.md
rename to .agents/notes/archived/feature/2026-07-23-trajectory-step-cell.zh.md
index aa76b422f1..3ebb4becd5 100644
--- a/.agents/notes/implemented/feature/2026-07-23-trajectory-step-cell.zh.md
+++ b/.agents/notes/archived/feature/2026-07-23-trajectory-step-cell.zh.md
@@ -1,6 +1,7 @@
# Agent Note: Trajectory 步骤单元格与轮次列表 chrome
Status: implemented
+Archived: 2026-07-26
[English](2026-07-23-trajectory-step-cell.md) | 中文
diff --git a/.agents/notes/implemented/feature/2026-07-24-new-session-clears-to-empty-state.i18n.yaml b/.agents/notes/archived/feature/2026-07-24-new-session-clears-to-empty-state.i18n.yaml
similarity index 61%
rename from .agents/notes/implemented/feature/2026-07-24-new-session-clears-to-empty-state.i18n.yaml
rename to .agents/notes/archived/feature/2026-07-24-new-session-clears-to-empty-state.i18n.yaml
index 4b7354a322..c545c8d7ee 100644
--- a/.agents/notes/implemented/feature/2026-07-24-new-session-clears-to-empty-state.i18n.yaml
+++ b/.agents/notes/archived/feature/2026-07-24-new-session-clears-to-empty-state.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
-2026-07-24-new-session-clears-to-empty-state.md: 1605f44a05d0f59b61fe95cb5b03a0f9f5c3d4ab
-2026-07-24-new-session-clears-to-empty-state.zh.md: 1f78d99babc33d30ee1300bfa6bf048a78e7132e
+2026-07-24-new-session-clears-to-empty-state.md: c9b57fec3aa093062847aacba4d01b877edf4bd5
+2026-07-24-new-session-clears-to-empty-state.zh.md: 82a4f8b1e933d6aa3531556e8be4839b204ea562
diff --git a/.agents/notes/implemented/feature/2026-07-24-new-session-clears-to-empty-state.md b/.agents/notes/archived/feature/2026-07-24-new-session-clears-to-empty-state.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-24-new-session-clears-to-empty-state.md
rename to .agents/notes/archived/feature/2026-07-24-new-session-clears-to-empty-state.md
index 1605f44a05..c9b57fec3a 100644
--- a/.agents/notes/implemented/feature/2026-07-24-new-session-clears-to-empty-state.md
+++ b/.agents/notes/archived/feature/2026-07-24-new-session-clears-to-empty-state.md
@@ -1,6 +1,7 @@
# Agent Note: New Session clears onto the empty-state launch
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-24-new-session-clears-to-empty-state.zh.md)
diff --git a/.agents/notes/implemented/feature/2026-07-24-new-session-clears-to-empty-state.zh.md b/.agents/notes/archived/feature/2026-07-24-new-session-clears-to-empty-state.zh.md
similarity index 99%
rename from .agents/notes/implemented/feature/2026-07-24-new-session-clears-to-empty-state.zh.md
rename to .agents/notes/archived/feature/2026-07-24-new-session-clears-to-empty-state.zh.md
index 1f78d99bab..82a4f8b1e9 100644
--- a/.agents/notes/implemented/feature/2026-07-24-new-session-clears-to-empty-state.zh.md
+++ b/.agents/notes/archived/feature/2026-07-24-new-session-clears-to-empty-state.zh.md
@@ -1,6 +1,7 @@
# Agent Note: New Session clears onto the empty-state launch
Status: implemented
+Archived: 2026-07-26
[English](2026-07-24-new-session-clears-to-empty-state.md) | 中文
diff --git a/.agents/notes/archived/feature/2026-07-26-code-mode-trajectory-waterfall-spans.i18n.yaml b/.agents/notes/archived/feature/2026-07-26-code-mode-trajectory-waterfall-spans.i18n.yaml
new file mode 100644
index 0000000000..7e7712948d
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-26-code-mode-trajectory-waterfall-spans.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
+2026-07-26-code-mode-trajectory-waterfall-spans.md: 2e158878faff29aa1ccd03b9b07a370b10cbb4f8
+2026-07-26-code-mode-trajectory-waterfall-spans.zh.md: 462968c9dae66880778b7a5e871a54983402e221
diff --git a/.agents/notes/archived/feature/2026-07-26-code-mode-trajectory-waterfall-spans.md b/.agents/notes/archived/feature/2026-07-26-code-mode-trajectory-waterfall-spans.md
new file mode 100644
index 0000000000..2e158878fa
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-26-code-mode-trajectory-waterfall-spans.md
@@ -0,0 +1,32 @@
+# Agent Note: Code Mode sub-calls in the trajectory and waterfall views
+
+Status: implemented
+Archived: 2026-07-28
+
+English | [中文](2026-07-26-code-mode-trajectory-waterfall-spans.zh.md)
+
+> Scope: the final PR of the Code Mode UI stack — sub-dispatch rendering in the two non-chat views. Chat nesting is owned by the [sub-call rows note](2026-07-26-code-mode-chat-subcall-rows.md); the timing this consumes is the [live-parallel note](2026-07-26-code-mode-live-parallel-dispatch.md)'s start/settle pair.
+
+## Problem
+
+Trajectory and waterfall still rendered a `run_code` turn as one opaque Tool cell / one node-count bar. The chat view got nested sub-rows in the earlier PRs, but the two analytical views — whose whole purpose is structure and timing — showed none of the sub-call structure and none of the per-sub-call wall time the dispatch pair now records. Waterfall sub-spans were deliberately deferred until that pair existed: a span without real timing would have been a lie.
+
+## Decision
+
+**Trajectory: `subtool` cells interleaved after their parent Tool cell. Waterfall: real-time sub-lanes under the owning turn row.**
+
+- **Trajectory**: the layout fold takes the snapshot's `codeDispatches` index; after each Tool cell whose `callId` has dispatches (assistant-block calls, orphan results, and running calls alike), it interleaves one `subtool` cell per sub-dispatch in start order — indexes stay sequential across the interleave. A settled sub-call's duration is its start/settle pair (`durationSeconds(sub.time, sub.callTime)`); a running one shows the em dash, exactly the native in-flight convention. The new cell kind wears a `Sub` tag (business tint) and a 28px indent so nesting reads at a glance.
+- **Waterfall**: `deriveSubSpans` folds the dispatch index into per-turn lanes with REAL timing — each parent's dispatch window is first start → last settle, and every lane's offset/width is its fraction of that window, so parallel sub-calls (PR3) visibly overlap. Each lane carries a `timing` provenance tag: `measured` (pair observed), `running` (settle pending — extends to the window end at reduced opacity), or `unknown` (settle-only replay window, `callTime: null` — drawn hollow and titled "duration unknown", never a fabricated 0 ms). Lanes draw under the owning turn's bar row, scaled into a fixed lane budget.
+- Both views read `codeDispatches` through the standard snapshot hook — no new wire data, no new stores; replay renders identically to live by construction.
+
+## Alternatives considered
+
+**Fold sub-calls into the turn-span node counts (weight the existing bars).** Rejected: it hides exactly the structure this stack exists to show, and node-count weighting is already flagged as a stand-in (deviation ledger #3).
+
+**A dedicated sub-call panel instead of in-view nesting.** Rejected: the stack's settled UX is nesting under the parent everywhere; a separate panel would diverge from chat and double the selection plumbing.
+
+**Defer waterfall lanes until the P-III duration-lane redesign.** Rejected: the sub-lane timing is real today (the pair), and the fraction-of-window rendering is independent of whatever the turn-level lanes become; deferring would strand the stack's timing payoff.
+
+## Consequences
+
+The waterfall carries the first REAL wall-time rendering in the client (turn bars remain node-count stand-ins — the contrast is deliberate and labeled by hover titles). Trajectory cell indexes now count sub-calls, so `#N` totals grow on Code Mode turns. Specs pin the interleave order and durations, the running em-dash arm, window fractions (offsets/widths), the running-lane extension, the unknown-timing (settle-only) lane, and the rendered lane under the turn row; the built-client Code Mode fixture snapshot additionally pins both tabs' assembled rendering (sub-cells with real +0.8s durations, measured lanes).
diff --git a/.agents/notes/archived/feature/2026-07-26-code-mode-trajectory-waterfall-spans.zh.md b/.agents/notes/archived/feature/2026-07-26-code-mode-trajectory-waterfall-spans.zh.md
new file mode 100644
index 0000000000..462968c9da
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-26-code-mode-trajectory-waterfall-spans.zh.md
@@ -0,0 +1,32 @@
+# Agent Note: trajectory 与 waterfall 视图中的 Code Mode 子调用
+
+Status: implemented
+Archived: 2026-07-28
+
+[English](2026-07-26-code-mode-trajectory-waterfall-spans.md) | 中文
+
+> 范围:Code Mode UI 堆叠 PR(Pull Request)链的最后一个 PR,涵盖两个非 chat 视图中的子分发渲染。chat 的嵌套归[子调用行 Agent Note](2026-07-26-code-mode-chat-subcall-rows.md)所有;本篇所消费的计时即[实时并行 Agent Note](2026-07-26-code-mode-live-parallel-dispatch.md)的 start/settle 事件对。
+
+## 问题
+
+trajectory 过去仍把一个 `run_code` 轮次渲染为单个不透明的 Tool 单元格,waterfall 则渲染为一根节点计数条。chat 视图在此前的几个 PR 中已获得嵌套子行,但这两个分析视图(其全部意义恰恰是结构与计时)过去既不显示任何子调用结构,也不显示分发事件对如今已记录的逐子调用墙钟时间。waterfall 的子调用 span 曾被刻意推迟到该事件对存在之后:没有真实计时的 span 就是在撒谎。
+
+## 决策
+
+**trajectory:`subtool` 单元格穿插在其父 Tool 单元格之后。waterfall:所属轮次行之下、带真实计时的子泳道(sub-lane)。**
+
+- **trajectory**:布局 fold 接收快照的 `codeDispatches` 索引;凡某个 Tool 单元格的 `callId` 名下存在分发(assistant 块内的调用、孤儿结果与运行中的调用一视同仁),fold 就在该单元格之后按启动顺序为每个子分发穿插一个 `subtool` 单元格,索引在整个穿插序列中保持连续编号。已结算子调用的耗时来自其 start/settle 事件对(`durationSeconds(sub.time, sub.callTime)`);运行中的子调用则显示破折号,与原生的进行中约定完全一致。新增的单元格类型带有 `Sub` 标签(business 色调)与 28px 缩进,嵌套关系一眼可辨。
+- **waterfall**:`deriveSubSpans` 把分发索引折叠成带真实计时的逐轮次泳道:每个父调用的分发窗口为首个 start → 最后一个 settle,每条泳道的偏移/宽度即其在该窗口中的占比,因此并行的子调用(PR3)会肉眼可见地重叠。每条泳道带有 `timing` 来源标记:`measured`(观察到了成对事件)、`running`(settle 未到 — 以较低不透明度延伸至窗口末端)或 `unknown`(回放窗口只含 settle、`callTime: null` — 画成空心并以「duration unknown」为悬停标题,绝不伪造 0 ms)。泳道绘制在所属轮次的条形行之下,并缩放进固定的泳道预算。
+- 两个视图都经由标准的快照 hook 读取 `codeDispatches`:没有新的 wire 数据,也没有新的 store;回放的渲染由构造保证与实时完全一致。
+
+## 曾考虑的替代方案
+
+**把子调用折入轮次 span 的节点计数(给既有的条加权)。** 否决:它隐藏的恰恰是本堆叠 PR 链存在就是为了展示的结构,而且节点计数加权本就已被标记为占位(偏差账本 #3)。
+
+**用专用的子调用面板取代视图内嵌套。** 否决:本堆叠 PR 链已敲定的 UX 是处处嵌套在父级之下;独立面板会与 chat 发生偏差,还会让选中接线翻倍。
+
+**把 waterfall 泳道推迟到 P-III 的时长泳道重新设计。** 否决:子泳道的计时如今已是真实的(即那对事件),而按窗口占比的渲染与轮次级泳道将来的形态无关;推迟只会让本堆叠 PR 链的计时收益搁浅。
+
+## 后果
+
+waterfall 承载了 client 中第一处真实的墙钟时间渲染(轮次条仍是节点计数的占位;这一反差是有意为之,并由悬停标题标注)。trajectory 的单元格索引现在会把子调用计入,因此 Code Mode 轮次上的 `#N` 总数会随之增大。spec 锁定穿插顺序与耗时、运行中的破折号分支、窗口占比(偏移/宽度)、运行中泳道的延伸、unknown 计时(仅 settle)泳道,以及轮次行之下实际渲染出的泳道;构建产物级的 Code Mode fixture 快照另行锁定两个标签页的组装后渲染(带真实 +0.8s 耗时的子单元格、measured 泳道)。
diff --git a/.agents/notes/archived/feature/2026-07-27-user-message-icon-actions.i18n.yaml b/.agents/notes/archived/feature/2026-07-27-user-message-icon-actions.i18n.yaml
new file mode 100644
index 0000000000..49220ee07d
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-27-user-message-icon-actions.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
+2026-07-27-user-message-icon-actions.md: 3a6944143972e8e7f6dcc59033c933d112708b63
+2026-07-27-user-message-icon-actions.zh.md: da80bd9242940da56271c2639114b78e43cd1041
diff --git a/.agents/notes/archived/feature/2026-07-27-user-message-icon-actions.md b/.agents/notes/archived/feature/2026-07-27-user-message-icon-actions.md
new file mode 100644
index 0000000000..3a69441439
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-27-user-message-icon-actions.md
@@ -0,0 +1,28 @@
+# Agent Note: User-message IconActions under the bubble
+
+Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-07-27-user-message-icon-actions.zh.md)
+
+## Problem
+
+The chat user bubble had no under-bubble action chrome. The Harness design (figma `User_Bubble/message_container`) shows three IconActions — copy, branch in new chat, and edit — right-aligned under the bubble, matching the product action-bar pattern used elsewhere.
+
+## Decision
+
+`MessageItem` owns the actions for `kind: 'user'` only. Layout is a column (`align-items: flex-end`, 6px gap): bubble, then a 28px action row with 10px gaps and 28px circular icon buttons (`IconCopyOutline16`, `IconBranchOutline16`, `IconEditOutline16`). Tooltips carry Chinese labels. Actions stay visible by default; `@media (hover: hover)` hides them until the row is hovered or focus-within, so touch / `hover: none` devices keep discoverable controls (opacity alone still hit-tests).
+
+Copy writes the bubble's joined text blocks to the clipboard (`navigator.clipboard.writeText`, with an `execCommand` fallback). Branch and edit are present chrome with no handlers yet — they reserve the design seats without inventing session-fork or edit-resubmit behavior.
+
+Steering bubbles keep the badge-only form and do not show these actions.
+
+## Alternatives considered
+
+**Wire branch/edit to real session fork and draft-edit now.** Rejected for this change: those product flows are not specified; shipping inert buttons matches the requested scope and avoids half-built mutation paths.
+
+**Always hide with `opacity: 0` outside hover.** Rejected for touch: without `@media (hover: hover)`, idle opacity still hit-tests while looking empty. Hover-capable pointers keep the fade; others keep the actions visible.
+
+## Consequences
+
+User messages expose copy immediately; branch/edit remain clickable stubs until a later decision owns their behavior. Tests pin the three buttons, copy payload, and steering exclusion.
diff --git a/.agents/notes/archived/feature/2026-07-27-user-message-icon-actions.zh.md b/.agents/notes/archived/feature/2026-07-27-user-message-icon-actions.zh.md
new file mode 100644
index 0000000000..da80bd9242
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-07-27-user-message-icon-actions.zh.md
@@ -0,0 +1,28 @@
+# Agent Note: 用户消息气泡下方的 IconActions
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-07-27-user-message-icon-actions.md) | 中文
+
+## 问题
+
+聊天用户气泡下方没有操作栏。Harness 设计稿(figma `User_Bubble/message_container`)在气泡下方右对齐展示三个 IconActions——复制、在新对话中分支、编辑——与产品其他位置使用的操作栏模式一致。
+
+## 决策
+
+仅当 `kind: 'user'` 时,`MessageItem` 拥有这些操作。布局为纵向列(`align-items: flex-end`,间距 6px):先是气泡,再是高度 28px 的操作行;行内间距 10px,圆形图标按钮尺寸为 28px(`IconCopyOutline16`、`IconBranchOutline16`、`IconEditOutline16`)。Tooltip 承载中文标签。操作默认保持可见;`@media (hover: hover)` 下在悬停或 focus-within 前隐藏,以便触摸/`hover: none` 设备仍能发现控件(仅靠 opacity 仍会命中测试)。
+
+复制将气泡内拼接后的文本块写入剪贴板(`navigator.clipboard.writeText`,并以 `execCommand` 作为回退)。分支与编辑目前仅有外观、尚无处理函数——它们预留设计席位,但不发明会话 fork 或编辑重提交流程。
+
+steering(中途引导)气泡保持仅徽章形态,不展示这些操作。
+
+## 考虑过的替代方案
+
+**现在就把分支/编辑接到真实的会话 fork 与草稿编辑。**本次变更不予采纳:这些产品流程尚未定稿;交付无行为按钮符合请求范围,也避免半成品的变更路径。
+
+**在悬停外始终以 `opacity: 0` 隐藏。**因触摸不予采纳:若无 `@media (hover: hover)`,空闲 opacity 看起来空白但仍会命中测试。具备悬停能力的指针保留淡入;其他设备保持操作可见。
+
+## 后果
+
+用户消息立即可用复制;分支/编辑仍为可点击的占位,直至后续决策明确其行为。测试钉死三个按钮、复制载荷,以及对 steering 的排除。
diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json
new file mode 100644
index 0000000000..e46d7c34cd
--- /dev/null
+++ b/.agents/notes/archived/manifest.json
@@ -0,0 +1,251 @@
+{
+ "version": 1,
+ "files": {
+ "architecture/2026-06-11-custom-schema-dsl.i18n.yaml": "sha256:f05d94c11762e506183044ddb1494a2b200ca16999ef3cef51c7b3a324eec945",
+ "architecture/2026-06-11-custom-schema-dsl.md": "sha256:71286f2676f8b47d0bd56c6cc43cf8102946e6d195942860a5810b9c534d2b2b",
+ "architecture/2026-06-11-custom-schema-dsl.zh.md": "sha256:999ff59565a4459184a644c4de6ef98c1bb1a174712e529f5c8417342abdd437",
+ "architecture/2026-06-11-tool-schemas-in-prompt-assembly.i18n.yaml": "sha256:37230a2f5b9dbe160b4f36b6906065637846261cbef380c3e2b777cf0d8e9013",
+ "architecture/2026-06-11-tool-schemas-in-prompt-assembly.md": "sha256:6f7b7f15f53f857ccb8477b3fdf65bf2de3a6f96acd93049c03ce4aff1629538",
+ "architecture/2026-06-11-tool-schemas-in-prompt-assembly.zh.md": "sha256:fd5ddfc53f8a1c4afa86599c858c2ad27858166e676754ee6e2ce0881f55cef7",
+ "architecture/2026-06-15-turn-enclosure-invariant.i18n.yaml": "sha256:7eb471a53b7bef104c57e9343b80d672763f062ecb086b01b318b65b488d3c02",
+ "architecture/2026-06-15-turn-enclosure-invariant.md": "sha256:afefa3a268c84f26cf5461e08933245352a9e63cff688d3c398c8064a4ac6e85",
+ "architecture/2026-06-15-turn-enclosure-invariant.zh.md": "sha256:c54fdac980abc922cdc252a8fef59e4bdd7567316c7fbb6f7dbc035e470d95fa",
+ "architecture/2026-06-20-extract-example-app-packages.i18n.yaml": "sha256:d99b612cc1051c86d883d74737c72e921735e7a28e0b5e6351d3870c664bdcc4",
+ "architecture/2026-06-20-extract-example-app-packages.md": "sha256:9c7aca3a1e9a1ccc3729961663bc649b90076e671cae23e3db8203305983ccce",
+ "architecture/2026-06-20-extract-example-app-packages.zh.md": "sha256:19bd50232d9f25d35aa3f9dc72d9af0df457dd0eaca8b982d5aa625e5b95bcff",
+ "architecture/2026-06-20-package-hierarchy.i18n.yaml": "sha256:02b9c383c0fc4e9d6cdd2d28b70b73d02ed913861834db8e2a3111ad82e05dee",
+ "architecture/2026-06-20-package-hierarchy.md": "sha256:a0c6b3022e27f6106996b7aaf5d087b3e3f66f24f7b5fd7b7c12f172ab5dc2f4",
+ "architecture/2026-06-20-package-hierarchy.zh.md": "sha256:40eec478cc5ff8ae76996e567d1f58309ed71b3bf0068ee06b088deae283512a",
+ "architecture/2026-07-02-result-time-applied-hunk-diffs.i18n.yaml": "sha256:e26441a73c76fb4217d7458c2772d0cefc09eaef638ea9d302ebcf5d65c42c18",
+ "architecture/2026-07-02-result-time-applied-hunk-diffs.md": "sha256:537f57877cc2e86b2d3a8a482043db8d7a51880159b1077c5a45b89cd5d0114c",
+ "architecture/2026-07-02-result-time-applied-hunk-diffs.zh.md": "sha256:7661634cb816caea859287327e37c5536381ee7ad9c77e320ff9b8bb7a7e62ce",
+ "architecture/2026-07-03-filesystem-directory-listing-seam.i18n.yaml": "sha256:636a822f3240e0401cdddad6a21f3454af1c1593fff14d4c9ce6613495f7dac1",
+ "architecture/2026-07-03-filesystem-directory-listing-seam.md": "sha256:809a3c79f4d602607e8fa93aafd1ebccf4fae50c31f1fb1b1e386bb7ad089153",
+ "architecture/2026-07-03-filesystem-directory-listing-seam.zh.md": "sha256:13735cd4c9fe990e6df3b028d6da01da89e94fde454dc0e968e517151cbd4281",
+ "architecture/2026-07-05-windows-fs-permissions.i18n.yaml": "sha256:7e61ee9bbd9de4bf3285a6f250d9625bd062e5fb90279dbffd64c820f1f7fe6b",
+ "architecture/2026-07-05-windows-fs-permissions.md": "sha256:03734da511eae3b0736f7cad73d9da76ae2f69f9d5ed09089b0121ccb135a861",
+ "architecture/2026-07-05-windows-fs-permissions.zh.md": "sha256:454848057ea905fe76c88d17264e71e71fb685f08f82088de6976878372865c3",
+ "architecture/2026-07-23-unified-session-query-service.i18n.yaml": "sha256:e8733b6543d9602ec206a087d9e89815f041f60fb57e93bee80e1309b9f03067",
+ "architecture/2026-07-23-unified-session-query-service.md": "sha256:28d003686f29ec5e072e51e73da353575bcdcba5af20fefdfad88340e1ddd32c",
+ "architecture/2026-07-23-unified-session-query-service.zh.md": "sha256:cfbe6525bc3b072fbc6db6bdca7a4d8cb4fc5507b1655bebc6af0589ed29ed31",
+ "architecture/2026-07-24-dsh-commander-argument-adapter.i18n.yaml": "sha256:cf99eda0e58b49630d5f95792459d7095666fafbef61f614165d5cdd031b7118",
+ "architecture/2026-07-24-dsh-commander-argument-adapter.md": "sha256:705654c8a43bcd199f72c21a77d24ca8bfa02447aff1c7f3e4e820be61dcd562",
+ "architecture/2026-07-24-dsh-commander-argument-adapter.zh.md": "sha256:3844f02d7659d18caf5d39e1131ed775c789cbf92dc44b4a446c7d6468aa5d00",
+ "bug-fix/2026-07-20-code-mode-result-card-completeness.i18n.yaml": "sha256:1035dae11d049d32ab09fd7d4f950eceae44bf46ba498b3cfaf3c75102b9fb64",
+ "bug-fix/2026-07-20-code-mode-result-card-completeness.md": "sha256:6ca2c9d4df98be18813ef38b7462db880900b5bcd6944fbcd1b8f2258006b93e",
+ "bug-fix/2026-07-20-code-mode-result-card-completeness.zh.md": "sha256:ed85fa7f935e5f525d566bc37a92014614983e649c75de9a9f244939097a7991",
+ "bug-fix/2026-07-22-collapsed-sidebar-control-rail.i18n.yaml": "sha256:98de4a1ae016608b88010d413a204c2d33695f4b1d7217e5d1b705be09c1b669",
+ "bug-fix/2026-07-22-collapsed-sidebar-control-rail.md": "sha256:b58620a3cf203507a5d651b90554bb7897e9d271613dc7c32d0f3bec992475bb",
+ "bug-fix/2026-07-22-collapsed-sidebar-control-rail.zh.md": "sha256:f36ef24f26ead60b01169c8e4a2a01b396c3f4284f14979e4d52b47c9589075c",
+ "bug-fix/2026-07-23-demo-web-builds-client-bundles.i18n.yaml": "sha256:f657e2166a05d6164c1ca65560bdd168ed969f7354217d8ec7a00274fd6c4307",
+ "bug-fix/2026-07-23-demo-web-builds-client-bundles.md": "sha256:a9d8dfcd153b1d10479e9d42848f07adf89398cb685e1a09afafcecff14e36d9",
+ "bug-fix/2026-07-23-demo-web-builds-client-bundles.zh.md": "sha256:dd06828964980798f343b8aafdeded2580b5b2e4c794305221fe73dab0a7eba2",
+ "bug-fix/2026-07-23-thinking-row-disclosure-target.i18n.yaml": "sha256:fd926967311f30ea4a222e88b845f95d74af75d1e94b24ebef59186593b9ca78",
+ "bug-fix/2026-07-23-thinking-row-disclosure-target.md": "sha256:92815c170972b1b91c3d75dd0c846c070805ec1e99ce368b6aae37b048e19869",
+ "bug-fix/2026-07-23-thinking-row-disclosure-target.zh.md": "sha256:0e09f5f5e14d74214e5157ceb5859c866bab6de701c47e2ce5c450866d75aecf",
+ "bug-fix/2026-07-26-intent-draft-same-tick-echo.i18n.yaml": "sha256:c623947c4fa00e6d4b51792c7972ba09582bbcb7605beb373725c0dd666f2c81",
+ "bug-fix/2026-07-26-intent-draft-same-tick-echo.md": "sha256:fa8b1417b2cdd3deecbf8e55bdddd73dd3a8c6e3486fd399b0b8bdf317e56373",
+ "bug-fix/2026-07-26-intent-draft-same-tick-echo.zh.md": "sha256:00ce72552dbaa11562fbc541343a5d33f9449edabbe6dd354eb879a7d4d530f8",
+ "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",
+ "feature/2026-06-18-acp-terminal-and-tool-rendering.i18n.yaml": "sha256:79592f96bb25713d01865f37972a6919b2bfb3b66368df0f275bbd59d09ebcf6",
+ "feature/2026-06-18-acp-terminal-and-tool-rendering.md": "sha256:946d0c580705ef2e7c7ac1897ada074e72f2ec4209c1e7531b6eccf116e9aecc",
+ "feature/2026-06-18-acp-terminal-and-tool-rendering.zh.md": "sha256:fd815817925a038f79b52b6fab43abdb2d655db3ec07974ce1320ea3674f2afc",
+ "feature/2026-06-25-ask-user-question.i18n.yaml": "sha256:8ed9d420908e79650a427c82970407c0b8fd49e00e129d2bd0bf3ff3e0c63ed4",
+ "feature/2026-06-25-ask-user-question.md": "sha256:d9ecff7baec5ca59c61822f7a41e719c17558044b8d23a30a8573e794ed828b7",
+ "feature/2026-06-25-ask-user-question.zh.md": "sha256:bdeb4869f880d6083563b4e6aefaa601d2fd1de7d46f3a7fa6e0bed376bc34ee",
+ "feature/2026-06-30-subagent-observe-enrich.i18n.yaml": "sha256:08c2478ba394429f46c1e87a9f055e88704a9000e5d250d5600c0c85124cb17f",
+ "feature/2026-06-30-subagent-observe-enrich.md": "sha256:0630975c3e325975a932f58a65a178b79c624dc56ebd29e288e96f5a189cfbfa",
+ "feature/2026-06-30-subagent-observe-enrich.zh.md": "sha256:b9fbb44a7d81f4063faf3baaf97c382a2f5106be533feb4de792ee57b766c1a4",
+ "feature/2026-07-07-plan-mode.i18n.yaml": "sha256:c59b6a6c218d741cdef8edf625f1d015e409a39411fa64e65200fdebb1c49394",
+ "feature/2026-07-07-plan-mode.md": "sha256:7bf1bb8e826edf68f0ec919dfd4f66955b935b46b4400d7de85fac3e4663edbc",
+ "feature/2026-07-07-plan-mode.zh.md": "sha256:5b08cbcd8023f26744e481386177dd0e82423e8b0032829d0dbc22a92cced0cd",
+ "feature/2026-07-07-session-prefix.i18n.yaml": "sha256:943c50f778521f1800009cebed56d9ec192fda23b5ee9025a4f2b0e4dd8bd729",
+ "feature/2026-07-07-session-prefix.md": "sha256:1cfd8e46467111b671b0bd9c2370ef07316ff9b2846fecc0cd29389686c6bbf8",
+ "feature/2026-07-07-session-prefix.zh.md": "sha256:9a15bbed3a53db84ac12e3dfb93b00bf93dabe6b9e39bebfdfc04e00ba8ff135",
+ "feature/2026-07-08-repeat-tool-guard.i18n.yaml": "sha256:e4382e8d25f23d728f1869c5195723698842dfe5422443a22f21fa8ae9b2c63c",
+ "feature/2026-07-08-repeat-tool-guard.md": "sha256:95df2c423624c5c56e0b3bb76ced49e91878c7e5096cfef0faf4b20203d8a2aa",
+ "feature/2026-07-08-repeat-tool-guard.zh.md": "sha256:cbe63d163aa19fc5a63233b05c8f6ed9a8a0ca215cf6f337a3c1e8e48af11b28",
+ "feature/2026-07-09-bash-backed-grep-glob-discovery.i18n.yaml": "sha256:8eb6ec1312c55b93c2d51c6e54585d6f4d5bf6db444fb9c336681848676e3638",
+ "feature/2026-07-09-bash-backed-grep-glob-discovery.md": "sha256:908521563012a17d312de664f380444aeafffd401d2200ec97e7bf004bac9ee7",
+ "feature/2026-07-09-bash-backed-grep-glob-discovery.zh.md": "sha256:52eec49e454357a7e0c3f27c8aaa63840b5822b5d103f49ed48a5c4f25081b00",
+ "feature/2026-07-10-session-query-service.i18n.yaml": "sha256:5c3f2ce6db1add66b9cdb82d3d115dde0f68762810bdbf7558070b9ff98c371e",
+ "feature/2026-07-10-session-query-service.md": "sha256:2d7cdb2629856eb16679c576c3d54b9f0468456ddc5be1cbaa4008d4f353033d",
+ "feature/2026-07-10-session-query-service.zh.md": "sha256:48d5a2d5f679bad90124c19a2b7a563f5d6551274c2bef9b3ce3eb9612ba0649",
+ "feature/2026-07-14-time-context-plugin.i18n.yaml": "sha256:670c093817c77e093562e02f43984d42ed44ebcced7c91d09366839e412d05e1",
+ "feature/2026-07-14-time-context-plugin.md": "sha256:618b121da38a8b610bcadaecf121ca823b2c8c13598c012b350c214b82fd238f",
+ "feature/2026-07-14-time-context-plugin.zh.md": "sha256:1e9eee8ba427a6f2ee08c79e2fcb33c0948e67a80758fdf9f8c9f7dff9aea361",
+ "feature/2026-07-20-tui-startup-slogans.i18n.yaml": "sha256:265d1fd79dae6c785201c81ffe2de3baa9fe9e3b6c0f84aac79c90f4040ced15",
+ "feature/2026-07-20-tui-startup-slogans.md": "sha256:aaaab4b419d35ce24317b7730f15af0029878bf3d17c6f184b05138c2cd44930",
+ "feature/2026-07-20-tui-startup-slogans.zh.md": "sha256:01fba568cd92e9c54857f6dba1a3a5a6a4d0e906f36915d7e64682e67d456708",
+ "feature/2026-07-21-dsh-system-prompt-source-path.i18n.yaml": "sha256:22efaf3237425fecbac1b40a444454e0fc244a3c85c2f6a14535de22ea777719",
+ "feature/2026-07-21-dsh-system-prompt-source-path.md": "sha256:5fa554932c62a8bbd5a619581710d7f8b6b65d79ec1e340129cda96d279c5ae3",
+ "feature/2026-07-21-dsh-system-prompt-source-path.zh.md": "sha256:995cd593074881c72510a6af3ba80108bbf986d49508cce9f698c2fcb493fd23",
+ "feature/2026-07-21-tui-auto-pane-title.i18n.yaml": "sha256:0e9ad2adf0810811b2981435e761fd57b1e2cd89e5aa084522150c41e3cf3876",
+ "feature/2026-07-21-tui-auto-pane-title.md": "sha256:0dd4572eacefc5fba508df8d1ff3f28b55e10b4b178e1f9773db3434a337c527",
+ "feature/2026-07-21-tui-auto-pane-title.zh.md": "sha256:3ac195bf3fc63d40c2d36e6a38d6a41c73d8b21daa5e668412fd28b8a2630aa1",
+ "feature/2026-07-21-tui-auto-title-default-on.i18n.yaml": "sha256:46815dbc1cbacfb11cb9f18f8df0f54dc1c4e5f9c051591dd3af97ad338b6c47",
+ "feature/2026-07-21-tui-auto-title-default-on.md": "sha256:0caad9db58e031f9f667e93a3f53ebcf3c1f0700efc6decd51acafa3657372ac",
+ "feature/2026-07-21-tui-auto-title-default-on.zh.md": "sha256:4e85a028e47caaa3f1cfcb01e143616c2a5a4916662c5f49f2b070386745d2e2",
+ "feature/2026-07-21-tui-banner-brand-gradient.i18n.yaml": "sha256:adc228a5e6797096002619ba5bd8c47d49f2d5e98e40dd168ae5e07bc57bc460",
+ "feature/2026-07-21-tui-banner-brand-gradient.md": "sha256:9b14ab1ae88eab598cd0f8d2d1cfbe53cec89a5374e3e3c765b487c91579e1eb",
+ "feature/2026-07-21-tui-banner-brand-gradient.zh.md": "sha256:111dfde012857af10b2f7b9b8a9b9f783522e4ad14dad3ff5e25706b5bbffcbe",
+ "feature/2026-07-21-tui-banner-sweep.i18n.yaml": "sha256:4cf71f8a8436bd9151be10aa7ae71ed1656206b49f056ae173e2b3e88bf9efea",
+ "feature/2026-07-21-tui-banner-sweep.md": "sha256:87654f4b1960ab4f7455e993de298d2ee75f63ceb66fccea6136238ff0134b7a",
+ "feature/2026-07-21-tui-banner-sweep.zh.md": "sha256:91b98a38c1111a561072d749a985c023ecaef0d749d6c1aaa320bfe0034660bd",
+ "feature/2026-07-21-tui-borderless-banner.i18n.yaml": "sha256:9e80de590085e6e02f0830fedb149289387bb83eaa073c9f99a4eb7af1afba80",
+ "feature/2026-07-21-tui-borderless-banner.md": "sha256:e3237b4de432cd97262a4baf1f64fee6bea48c3180a2e773575f603ed008d44c",
+ "feature/2026-07-21-tui-borderless-banner.zh.md": "sha256:6c65cd654a1aed704d80b5882aba8ae0a2c1090709d672189847f5d0a6f58122",
+ "feature/2026-07-21-tui-footer-cache-hit-rate.i18n.yaml": "sha256:56898ebb26741c83bb1c5de4c6e64bd3ca06b5e3b90ab79107823f19353596eb",
+ "feature/2026-07-21-tui-footer-cache-hit-rate.md": "sha256:c66a1485d21fe6a4b975ffeed56c021c0d9556488bfadc4fb32648b3948c1fea",
+ "feature/2026-07-21-tui-footer-cache-hit-rate.zh.md": "sha256:6fc2efe5817e83a9deb057a2de9b31b4c786700ebf40d38369abb5cefae231d0",
+ "feature/2026-07-21-tui-no-banner.i18n.yaml": "sha256:26d98ba4a5c04504d649ada26d666dec0115026b9724af160483d0fcfa535903",
+ "feature/2026-07-21-tui-no-banner.md": "sha256:a75c8535ac348199c9de0c2a6e266b3b4c21fe188f86ad1388a5241ad03c5a73",
+ "feature/2026-07-21-tui-no-banner.zh.md": "sha256:05659d5e54a10fbace886f4407ec7a457cbab85139af03e3ae59612fa7611988",
+ "feature/2026-07-21-tui-reload-command.i18n.yaml": "sha256:9be416ccd681aed0781fdfd2c44c4821c1e45f2a0deccb1f2b47d46163bde488",
+ "feature/2026-07-21-tui-reload-command.md": "sha256:b8616457822ae87c90062308bc8c0d2badd5f368092ec65847d0d9520b1ac372",
+ "feature/2026-07-21-tui-reload-command.zh.md": "sha256:c24bfcb0df13977a9c11c4d0fe433169e535b5f764995b668430dbb14a8e6b33",
+ "feature/2026-07-21-tui-steering-queue-badge.i18n.yaml": "sha256:a029da558a6e14e1f13269960b98273ca9af0141579967acfbc19b656775f4a5",
+ "feature/2026-07-21-tui-steering-queue-badge.md": "sha256:9aabd68c8910fdc7e7b05674492ddb8dc9285dd691fe554adcb84026fb846cc8",
+ "feature/2026-07-21-tui-steering-queue-badge.zh.md": "sha256:919fd737866c3700f945751628071dab89eabdbf8f809deab93b9e6fbe2c8c59",
+ "feature/2026-07-21-tui-verbose-status-line.i18n.yaml": "sha256:4371b9a46d713d4180aa5d0b1ecde1ff3cae948380a8f56474c895e6113d7824",
+ "feature/2026-07-21-tui-verbose-status-line.md": "sha256:9dcba19ee725b1593e9413a1da5398c205a258aff2e384acd406bb618e86c7f0",
+ "feature/2026-07-21-tui-verbose-status-line.zh.md": "sha256:203c2abac99cedf7afa2540c925367ba66f00b61b926d1cc86472a603ad2bb07",
+ "feature/2026-07-23-trajectory-step-cell.i18n.yaml": "sha256:fe2e935a0affdef877902a40d9861ef5f55b30f40650469f6a52a4d45a92793f",
+ "feature/2026-07-23-trajectory-step-cell.md": "sha256:185e3b87174cb6d2f2d2271fd2a74b1517d03e8570be602570d027bf6002d106",
+ "feature/2026-07-23-trajectory-step-cell.zh.md": "sha256:51f46be43d2f5c4f78a05ed9aeec92d1f33ac988f45cf24d35528e9c43828ef3",
+ "feature/2026-07-24-new-session-clears-to-empty-state.i18n.yaml": "sha256:978638cbf18bc6dce9fea0817654f41cc307f99004a637b85a63ae2208fe9095",
+ "feature/2026-07-24-new-session-clears-to-empty-state.md": "sha256:b6b71d3883a167056070713e3dffb5046de953bdd218074d17c88e7690e03d83",
+ "feature/2026-07-24-new-session-clears-to-empty-state.zh.md": "sha256:82a80b48337487029acd05a0137d268f0850f46801fa44a0e62733cacd00d5e9",
+ "feature/2026-07-26-code-mode-trajectory-waterfall-spans.i18n.yaml": "sha256:916525fdc3a12061928380fd8f7b81cd9763a7873663aad564843fccab0ccedc",
+ "feature/2026-07-26-code-mode-trajectory-waterfall-spans.md": "sha256:a822963e4c34c9737681d6d70d8167731d4350e66ef7f356684ad096c04fa7ab",
+ "feature/2026-07-26-code-mode-trajectory-waterfall-spans.zh.md": "sha256:ef01163adf1245f75cc8291db70d389314dee43b6bff958a9da49b097531218d",
+ "feature/2026-07-27-user-message-icon-actions.i18n.yaml": "sha256:b33e480f19ec58c8c60417a6c03999953d463ca54606a5ac80ec528edf57c49b",
+ "feature/2026-07-27-user-message-icon-actions.md": "sha256:b6332e67c6dad0a3fcdb597cec9e4dc32b44ad33665f39c1a50501cf38d3f5ad",
+ "feature/2026-07-27-user-message-icon-actions.zh.md": "sha256:0fc824eac66a18063f7098e1c395c09b580d5a30b96f2b856608c084212c2ac2",
+ "process/2026-06-11-doc-sync-enforcement.i18n.yaml": "sha256:33b6d5874427bd7a2bd82e7e2f4f482b12448b2464aef15a9c57975edb48554d",
+ "process/2026-06-11-doc-sync-enforcement.md": "sha256:aa2fe83d519fc30d48dff19e596e83c8922aacc9e063e14fe2cc35b769b9100e",
+ "process/2026-06-11-doc-sync-enforcement.zh.md": "sha256:698017bd35f030fdea3eac51df9e43138c48140f504739d687b7251d13fced2b",
+ "process/2026-06-11-tsdown-over-dumble.i18n.yaml": "sha256:22791adb84a4b6c545173d4f1708eea51151d57e426d875e0e5423be9b6e0212",
+ "process/2026-06-11-tsdown-over-dumble.md": "sha256:8d3c35dddd8869cc3361059dfe4b7b8ab6716d29dda232c97c2f37e92c841dc0",
+ "process/2026-06-11-tsdown-over-dumble.zh.md": "sha256:cf11c651c13f5ffef5474e7795006ba3653c5eb08eae75a879be6499353455dc",
+ "process/2026-07-03-documentation-graph-atlas.i18n.yaml": "sha256:b1e1ed4b7865d87f939dbf8c94c0ea1069fdf7af6fa68f695e6c9d6eccbeb123",
+ "process/2026-07-03-documentation-graph-atlas.md": "sha256:b62e92bb12123bfa4c4dac806f584aabb6b60af4c5a6a4ab88f84bb9153e766d",
+ "process/2026-07-03-documentation-graph-atlas.zh.md": "sha256:3485ede4a5e695643bcf9e744a62f8914cff788ae35717dac5eb6bf77e0d65cf",
+ "process/2026-07-04-cordis-jsdoc-completeness-gate.i18n.yaml": "sha256:92ab8aca8856324acb383927d4500d2f2b3b8ad64d24bd855632035a52671032",
+ "process/2026-07-04-cordis-jsdoc-completeness-gate.md": "sha256:a9404acbfeaa83f032d17414627b2cce5e280cad8651c6ff078a2ea6af458285",
+ "process/2026-07-04-cordis-jsdoc-completeness-gate.zh.md": "sha256:e8f4737ecc0925e3dcd60f8319d9cd0fd24de5c7468e1ede4438748d3ad378a1",
+ "process/2026-07-04-persistence-log-catalog.i18n.yaml": "sha256:23119a0769808279d7caef9e84934cc53b6a86580b703c205fa47ad181a2bf76",
+ "process/2026-07-04-persistence-log-catalog.md": "sha256:fbb44218b77fd92f7b0cc7af2559200547018ceb5c34aca560371227c8bf5898",
+ "process/2026-07-04-persistence-log-catalog.zh.md": "sha256:a39ee31bec043e97ad6b7e8433d4c1d24200642049e371ac0167a2092a6e5286",
+ "process/2026-07-06-generated-config-catalog.i18n.yaml": "sha256:48e64ccfc7622559a9b964e2741ad82a9fa37243a0d5c472f0482d9c748d7739",
+ "process/2026-07-06-generated-config-catalog.md": "sha256:c8654a4e084bfbf4f7d8eb78239dc1d5d62e1384b2aed309fa2526a4d6de0f04",
+ "process/2026-07-06-generated-config-catalog.zh.md": "sha256:5ab8d964f12e0cf9a04251e988f0b2a114d7ae8ea7ed337c00f625213cb1dc94",
+ "process/2026-07-06-parallel-github-ci-gates.i18n.yaml": "sha256:0f6ece268d9a51bc20cb8eb929f26d8838761603a64eb08dc24521198f10da36",
+ "process/2026-07-06-parallel-github-ci-gates.md": "sha256:6249bd7396ae7f2d0dc671879ce21cefab33a47ace6ef17a25a70e8650b815af",
+ "process/2026-07-06-parallel-github-ci-gates.zh.md": "sha256:cf7edb9bcf97ab1d4e452330c0df0b127a664509ec3f11597ace3eabeb663a5b",
+ "process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml": "sha256:0a2e97095be592e2c603f23d9b3b8959c4556b5636fa2c6e4539be18cfcc7a6c",
+ "process/2026-07-17-run-ci-examples-from-built-lib.md": "sha256:d7e8901c67e9850b05cef65132f193b8be566be40d0f54b28b11043871955263",
+ "process/2026-07-17-run-ci-examples-from-built-lib.zh.md": "sha256:9bb720b65a0d5933b5804dfbe0104c831a26695687b7f1a72f4ed4cfb4bd4415",
+ "process/2026-07-20-generated-cordis-core-api.i18n.yaml": "sha256:4405f216d4beefdbb33f521dc8126be4f8d3a664abfed3a4cdad04fef84ab0f9",
+ "process/2026-07-20-generated-cordis-core-api.md": "sha256:eed4dde36984f584c211bd00fa88ce4aef9ee296a0814b46a8d759f953a90660",
+ "process/2026-07-20-generated-cordis-core-api.zh.md": "sha256:74f4316abc78500097f1b25bac06b460cb20a7dd3d5f323da875cc0575148991",
+ "process/2026-07-21-doc-sync-through-gate-scheduler.i18n.yaml": "sha256:1dbe70d21dd510bec4f2f56ae39d0fdc7290d5648280ca0b67224cd23b3a02a8",
+ "process/2026-07-21-doc-sync-through-gate-scheduler.md": "sha256:b3eb3f2395ad8f1b77f44aa3fdac79856e5d0b6b4873560d0cc87b63de2ea2e0",
+ "process/2026-07-21-doc-sync-through-gate-scheduler.zh.md": "sha256:e262e02c3d08057b83b0d29281eadb92723f0fe5b3f54424528f47be137bc760",
+ "process/2026-07-22-cordis-tutorial-docs.i18n.yaml": "sha256:4a7d80b72fd1d86379937250817459324a71b03385ebd35cd6e5a7c8cdc32dd9",
+ "process/2026-07-22-cordis-tutorial-docs.md": "sha256:a4332b4357eb1f6f0f576e4f87c0d5697c12c8486dc2d92785556a54f2c7af90",
+ "process/2026-07-22-cordis-tutorial-docs.zh.md": "sha256:dd9edc0e37abbf6f6975fdf0df5d498bc3589acf23bb8b31d72e3216fd930a62",
+ "process/2026-07-22-installer-in-repo-skip-clone.i18n.yaml": "sha256:677aa91c3ccd9eda8a658b10410699ac608d3891d2fa32529898a3432fb56660",
+ "process/2026-07-22-installer-in-repo-skip-clone.md": "sha256:4e30c0dd5429db33638a91a30afdd3386ac1a4705bd259a5eef325b5f86cced8",
+ "process/2026-07-22-installer-in-repo-skip-clone.zh.md": "sha256:1d93c99f5a8d56077e766242c33245621626be55cf481d01c83bb5cbbe9a74d7",
+ "process/2026-07-23-browser-demo-gif-recording.i18n.yaml": "sha256:808ccdda39e540645b440e40a2124baed737b98636265d8f6d8cf036a70f0d50",
+ "process/2026-07-23-browser-demo-gif-recording.md": "sha256:4d3a3dc829c75b66f4f57a6a763b4b9562ce10efb90a19308142f598c5ea8524",
+ "process/2026-07-23-browser-demo-gif-recording.zh.md": "sha256:409e5d31ea87f35c5227fcd1bd105167f580a8a24d29a223ce390b29eb31639a",
+ "process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.i18n.yaml": "sha256:89b6dc255cb0dd9d97ae6f34f37d185412d4260d7bf2f87d6b25e340f01dc26e",
+ "process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.md": "sha256:1742e09435ade4a09349c8843eb381e870a2be2d51b74449f08260422c8096b3",
+ "process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.zh.md": "sha256:220bd53a88617b09ee8970627c46540fa8951724b102acfe296a7ed7d0f7b5fe",
+ "simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.i18n.yaml": "sha256:ad3d1263cb0051b885173bf064de62065e2c646ccaae2d7250723da3b4eab90c",
+ "simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md": "sha256:8fb061d51c8c23b47d2367814bab3623c6d5b972f38d207a273caa9030b579bd",
+ "simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md": "sha256:2ffeaca91f82844a5616d6dcce6b4af514bb8a7c46f78e47f668b204ac6edc04",
+ "simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.i18n.yaml": "sha256:f01960a5e8fab5e4f284f35ced6b84400aab257b243805db797a9c4a00ff525e",
+ "simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md": "sha256:0020f6b80e8bea5a8441b5bf7385a9bcfacbe14485f0e77de5d8b4fe3d2f69d0",
+ "simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md": "sha256:21647760eb06e57f8a38b35196233c634b8284a178b99b14f504a791758e9088",
+ "simplification/2026-06-20-prune-dead-seam-methods.i18n.yaml": "sha256:0594648368c942f429599ac0ff5977d62c89c70a31d4bdbac61b0a30fe15ef3b",
+ "simplification/2026-06-20-prune-dead-seam-methods.md": "sha256:fd3b0eaf600e178eeeef0c6cedc71f2382878733c557f1915d3b47f74a1d0d6d",
+ "simplification/2026-06-20-prune-dead-seam-methods.zh.md": "sha256:4f5feef9331e3a1346bc362ffb39cfa373db2c609041bfeee6d88a10392464b1",
+ "simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml": "sha256:eef600eafd70a576b2ac16a74f5dd5010ee601376008a90be10f1da56d746cee",
+ "simplification/2026-07-02-remove-stream-chunk-mirror.md": "sha256:0c22a896260c6eb8991cc8babd8172f7b6889fc41bb891d748f34650b3eee5ec",
+ "simplification/2026-07-02-remove-stream-chunk-mirror.zh.md": "sha256:ae7a3c2450b16fdf2f8da9e1f83cd987bf387671ecc8ebb76d2e7541695e7ee1",
+ "simplification/2026-07-04-drop-inert-request-knobs.i18n.yaml": "sha256:e4c992a27ae0e37e5ef663c2cddf55eefe20387fd6103bebf655834d8e75e9db",
+ "simplification/2026-07-04-drop-inert-request-knobs.md": "sha256:8735c2b868a85b13235e0491a0fa7b9570dd090eef5170324fc5e93782687b67",
+ "simplification/2026-07-04-drop-inert-request-knobs.zh.md": "sha256:78b243f5d580f2a6fbbdb7d26574295d6ed74feb8d9bba34bbcdf4aa87624b5c",
+ "simplification/2026-07-04-drop-unconsumed-web-observation-surface.i18n.yaml": "sha256:30cbf5f573ad9df5140a2bc57181c6465dc3cb0717d192a8bbbb5b1c68a56f29",
+ "simplification/2026-07-04-drop-unconsumed-web-observation-surface.md": "sha256:2d4d4ad2d0b72c602a20af6082392c22c889e4cf455614177fdc9e892069948e",
+ "simplification/2026-07-04-drop-unconsumed-web-observation-surface.zh.md": "sha256:012b4fb2a346e01d5d88a53913a790df713650907ad7987b744bba456be36bbf",
+ "simplification/2026-07-04-fold-stdio-ui-helper.i18n.yaml": "sha256:e0e476ec897d29a8688b201746a1db47033486b560c07244b37d91058d39e07a",
+ "simplification/2026-07-04-fold-stdio-ui-helper.md": "sha256:d6cb5b0cbada51a19e4c2b1aab8dc738a30760ec8e7348e090ad44c8b80e3955",
+ "simplification/2026-07-04-fold-stdio-ui-helper.zh.md": "sha256:57618fe935bf3310f1d91ab7d1940d8dff7b21919dbe82f1fa0137ae5f7c2189",
+ "simplification/2026-07-04-prune-producerless-vocabulary-variants.i18n.yaml": "sha256:338c2290ae2cdcbeb758e996970e7f9dc8c36261f076302e358d70508604bac6",
+ "simplification/2026-07-04-prune-producerless-vocabulary-variants.md": "sha256:87a269ba0c849084bf16b546fe8fff3e6bba188d3565b10099721109551ada5a",
+ "simplification/2026-07-04-prune-producerless-vocabulary-variants.zh.md": "sha256:1485426f46ae46bf5c25ab95962cb7edc4dd3b43f3bd2211c0e41f02c505e1fc",
+ "simplification/2026-07-04-prune-write-only-fs-surface.i18n.yaml": "sha256:6c8ed11b067c34f1af060d6c36de3685f3a15874786d811620b57e42c2b8d5c6",
+ "simplification/2026-07-04-prune-write-only-fs-surface.md": "sha256:5602e09004f9f2b81f447abed4de10b18a96df5f44b13fd1cc0c06ffd3ce5b4a",
+ "simplification/2026-07-04-prune-write-only-fs-surface.zh.md": "sha256:086f2cce3dc120f0c31c7dbc1855390f72e3940f2ffa92108de20fa175fff86a",
+ "simplification/2026-07-04-remove-agent-steering-mirror.i18n.yaml": "sha256:24fb3c525cae7334841b7daca4c65783f013aa53910a81d20e092b4dd7081cda",
+ "simplification/2026-07-04-remove-agent-steering-mirror.md": "sha256:3351fef50ba8635e5a3829a39cad24333bf3285799602b0891acdec312aa858f",
+ "simplification/2026-07-04-remove-agent-steering-mirror.zh.md": "sha256:75ad399226bc42950128e410be9666cb3d0b76ca673418f31cd2a57a8f56e513",
+ "simplification/2026-07-04-share-app-bin-boot-glue.i18n.yaml": "sha256:bd64279826444b41f6f1dc5d92fecd974edc3663885470f2eae226978926a59b",
+ "simplification/2026-07-04-share-app-bin-boot-glue.md": "sha256:de0f4dca1e89c0c19d649aa37989df1991376d2cac1a5ec72c1a3ca0dce27e49",
+ "simplification/2026-07-04-share-app-bin-boot-glue.zh.md": "sha256:e014ac4c2b609b70c467540ff0985c59a74df0eaf3062c43ffcaf1ac35d18ce2",
+ "simplification/2026-07-04-trim-acp-bridge-unreachable-surface.i18n.yaml": "sha256:9080af48de70cc519f935896ae90134bcecdf4cee56bb5abb3e909672f2dded3",
+ "simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md": "sha256:c11fbdea4bdd14eba517dc6377f8e59eb73fc05779424f38833cc662baf04fd5",
+ "simplification/2026-07-04-trim-acp-bridge-unreachable-surface.zh.md": "sha256:f1bceae26fdea3fc71d8a0e32530eadb00342590a611b9fbc7ff09d0fc8aa3b8",
+ "simplification/2026-07-12-drop-unconsumed-skill-provider-events.i18n.yaml": "sha256:cb9f223b74ea3ba0279f17d2bfd59033b67d1ea7b525b0ebee01fb9ee74da4be",
+ "simplification/2026-07-12-drop-unconsumed-skill-provider-events.md": "sha256:cc78d0f80438e52e7d928b786101a902a15e4317fb0db2a47833c44520937c60",
+ "simplification/2026-07-12-drop-unconsumed-skill-provider-events.zh.md": "sha256:ccb7146536c8a0f956d4799ddafc7b7cfea264fbc642107bd8aa24f06d88932d",
+ "simplification/2026-07-12-prune-unused-web-seam-fields.i18n.yaml": "sha256:896dea8f5430603c445169fa79bfba997421a76d48fa4336349ec693572e6167",
+ "simplification/2026-07-12-prune-unused-web-seam-fields.md": "sha256:e732eb5eed007e95f40f32eddd8d94cd34f0ce579f1a70b16ca072a48a3989b4",
+ "simplification/2026-07-12-prune-unused-web-seam-fields.zh.md": "sha256:ac427d5cf6525c155b12dc7605954ebe3ce1f3edb6da0d0305e8d79cf475460b",
+ "simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml": "sha256:a7e5e21bf8a3a06bbf1272c677a7fff980e7548459405835416b88f1537bfe92",
+ "simplification/2026-07-19-retire-subagent-mock-package.md": "sha256:3df91519b77efcc413a54927adb2f829e944ce7f827211ac6ae66d4b5e0398a7",
+ "simplification/2026-07-19-retire-subagent-mock-package.zh.md": "sha256:c86d96800abc5aebf2d63694cb2cdcb21867091b2867de44493212f302498889",
+ "simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml": "sha256:602ab8fda1facb04a8f04d088267cbbd0426d607a8cc8c3fc056887f4a2696d9",
+ "simplification/2026-07-19-use-one-session-surface-manager.md": "sha256:267882c357527a12d8581c9d78249819a987c766a74a2d47f351dc5b14bf7d0a",
+ "simplification/2026-07-19-use-one-session-surface-manager.zh.md": "sha256:21c68a432c22209a3c19c8424da8e03fe91415d9ce3753cf17d727663077e4c9",
+ "simplification/2026-07-20-retire-readline-front-door.i18n.yaml": "sha256:48b8573d325d280b65e7debde660140e7afdf1db793d4eb32c839c635121a965",
+ "simplification/2026-07-20-retire-readline-front-door.md": "sha256:f632cd22fd81cc470992ff4e5f695a118a2aaed53748a860da7e9a78ca99ebbd",
+ "simplification/2026-07-20-retire-readline-front-door.zh.md": "sha256:3612b25120a87f0e1c9d075c9e3c4b6a9449f38c8b219dd1aef506c6e6f92d98",
+ "simplification/2026-07-21-tui-remove-cancel-command.i18n.yaml": "sha256:17ee6e9a3db867b85d8399879c40552a6771b5d7585f7b58e33601428a1309e3",
+ "simplification/2026-07-21-tui-remove-cancel-command.md": "sha256:e90ad809b5ea241a653641f7331893347a1a0be7c677c99cbfc6bba8c907ab19",
+ "simplification/2026-07-21-tui-remove-cancel-command.zh.md": "sha256:94d388753157eb498b9a8dbd9050dc07e5ee893e9b5a07f4c265b2e8e66f6338",
+ "simplification/2026-07-21-tui-todo-write-opt-in.i18n.yaml": "sha256:633975e45444f179e5fcd258d3c4bce924975583505fa97f18cff21861a88ca2",
+ "simplification/2026-07-21-tui-todo-write-opt-in.md": "sha256:7c4c0818f5cb5b1a506dabb71a56b7d79b811e4b912d492865f1404f4d1ece99",
+ "simplification/2026-07-21-tui-todo-write-opt-in.zh.md": "sha256:2c121b8ea03182f7854e7d834b07967fdb6790af6a2a38c1d24bb0ca968496ba",
+ "simplification/2026-07-22-tui-titles-from-session-title-service.i18n.yaml": "sha256:b02b34f76599eef44ef6cbacac36345716bed52eafe8f30e5300d654a93eecbb",
+ "simplification/2026-07-22-tui-titles-from-session-title-service.md": "sha256:c84b34c2c4700f78e3060e64f49f16625bab002e8211dcd6fce431e7fd076392",
+ "simplification/2026-07-22-tui-titles-from-session-title-service.zh.md": "sha256:99df66f452004df84efb215e3b58bf73e39af0538ddf038bbed4ac6b65c630ea",
+ "simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.i18n.yaml": "sha256:7acf002ea8c1533f052c7bfc0c4e3da013ecf43c5872866a3ee4a8c2691c5e33",
+ "simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.md": "sha256:f18a913096b7defd2192c4bac888a33f68075c3662703a0e28a6146897d17777",
+ "simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.zh.md": "sha256:ff48a37673c97059536fe5b61aff746133eac682145550badb049eb5c83b097c",
+ "testing/2026-06-20-remove-redundant-snapshot-log-expected-output.i18n.yaml": "sha256:4177012c0821a8c22499852ecdf096af56d7263cb91c5d9d1bcd552cc26a3e00",
+ "testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md": "sha256:45234e7cc04b6010c6141f8d5924c04547300098f96262d423c50108e7c7011a",
+ "testing/2026-06-20-remove-redundant-snapshot-log-expected-output.zh.md": "sha256:15e5a4ad3dee0bb711480cabe45cd97ec37bbdba19c2c2b47d1e9c203b07a48b",
+ "testing/2026-06-22-fork-snapshot-scenarios.i18n.yaml": "sha256:d9fb0a30bbf58bbd6fcb45c84bee204f3f97a8a4cf7b867eec7320ba8663abf7",
+ "testing/2026-06-22-fork-snapshot-scenarios.md": "sha256:2bd6458490789f68110ec6a7fb6ea55af544f09df854c163fcb83f04a440da98",
+ "testing/2026-06-22-fork-snapshot-scenarios.zh.md": "sha256:f39e26c527dcb92d364b00bd3294f79bb30960ff35a4309bde00563aa594ec08",
+ "testing/2026-07-04-hook-snapshot-matrix.i18n.yaml": "sha256:f8fe2a2893e929d3a0476151f4f44ce9b4c26790aae84f6ddbf365239ce6a0bb",
+ "testing/2026-07-04-hook-snapshot-matrix.md": "sha256:287b9e0d97ea2e79a3ec6175c02ab01ef1314652528d5911a3a971e008094b6e",
+ "testing/2026-07-04-hook-snapshot-matrix.zh.md": "sha256:25b33993da3b8eb94113b90050ac03b72d40bf085bff72b3119d38105a2d7ee2",
+ "testing/2026-07-04-single-source-acp-replay-config.i18n.yaml": "sha256:cdf1ede909bc51792b1dcd74d5928111f75d4aba5020e8b30b3f395887348329",
+ "testing/2026-07-04-single-source-acp-replay-config.md": "sha256:a94352fe79201949e28028abe4c7d932fd0d2e81d869e7d3b58d22f54a649417",
+ "testing/2026-07-04-single-source-acp-replay-config.zh.md": "sha256:bed4dcd236a07192dd3de6c76e4a5c47dd5ec35963ce830bd5521bbb41d3f3a3",
+ "testing/2026-07-06-pin-request-header-content-in-one-scenario.i18n.yaml": "sha256:4f3ebae0faea8a38ffe0d5291a33b3bcf99ed723f8e0cc5cccecbedbf4fb9ce9",
+ "testing/2026-07-06-pin-request-header-content-in-one-scenario.md": "sha256:050bf8044ce22a27a0f57b5cef84ccff0dc45b1a3f6b70aa41950d41038d0702",
+ "testing/2026-07-06-pin-request-header-content-in-one-scenario.zh.md": "sha256:cac75d4475666239bbe0030b90c0fa7cc66024af5b9f8ef217e53018be64890e",
+ "testing/2026-07-08-shared-acp-snapshot-package.i18n.yaml": "sha256:fc37fcdfe8744f8c8f39eda3f494eed25ee8c4d5322d1f3f671eabd2c5d7026e",
+ "testing/2026-07-08-shared-acp-snapshot-package.md": "sha256:285b4a3c0b1ef7a837e6713cf0192ddc8a26101f6737fa3923682a9e91350c50",
+ "testing/2026-07-08-shared-acp-snapshot-package.zh.md": "sha256:02da3f910c2060f70038a0d86a7ddae4a8890905600440e1373412f54fbdcea8"
+ }
+}
diff --git a/.agents/notes/archived/process/2026-06-11-doc-sync-enforcement.i18n.yaml b/.agents/notes/archived/process/2026-06-11-doc-sync-enforcement.i18n.yaml
new file mode 100644
index 0000000000..25c34c889b
--- /dev/null
+++ b/.agents/notes/archived/process/2026-06-11-doc-sync-enforcement.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
+2026-06-11-doc-sync-enforcement.md: 00fc6e904f1908b5cc4ddbe02b6eecf46b06408b
+2026-06-11-doc-sync-enforcement.zh.md: 9daaf88626d18093f8af7ba9ee5b6ce9e596d011
diff --git a/.agents/notes/implemented/process/2026-06-11-doc-sync-enforcement.md b/.agents/notes/archived/process/2026-06-11-doc-sync-enforcement.md
similarity index 98%
rename from .agents/notes/implemented/process/2026-06-11-doc-sync-enforcement.md
rename to .agents/notes/archived/process/2026-06-11-doc-sync-enforcement.md
index 67cdfd6771..00fc6e904f 100644
--- a/.agents/notes/implemented/process/2026-06-11-doc-sync-enforcement.md
+++ b/.agents/notes/archived/process/2026-06-11-doc-sync-enforcement.md
@@ -1,6 +1,9 @@
# Agent Note: Doc-sync enforcement
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-06-11-doc-sync-enforcement.zh.md)
## Problem
diff --git a/.agents/notes/archived/process/2026-06-11-doc-sync-enforcement.zh.md b/.agents/notes/archived/process/2026-06-11-doc-sync-enforcement.zh.md
new file mode 100644
index 0000000000..9daaf88626
--- /dev/null
+++ b/.agents/notes/archived/process/2026-06-11-doc-sync-enforcement.zh.md
@@ -0,0 +1,33 @@
+# Agent Note: Doc-sync 强制
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-06-11-doc-sync-enforcement.md) | 中文
+
+## 问题
+
+AGENTS.md 承诺文档与代码严格同步,但这一承诺此前仅靠人眼核查。评审曾两次发现漂移:一次是实操手册(cookbook)示例与类型策略矛盾,一次是 README 引用了错误的 `registerAdapter` 调用。失去同步的文档比没有文档更糟;而本代码库主要由 agent(智能体)构建,agent 遵守门禁远比遵守行文约定可靠(机械质量门禁)。有两类文档漂移可以被机械检查:不再能编译的代码块,以及与 `interface Events` 声明重复的事件分类体系表。
+
+## 决策
+
+两道门禁,沿用既有的 `scripts/` 风格(tsx ESM,每个脚本一项职责):
+
+1. **`doc-typecheck`** 从 `README.md`、`docs/**` 和 `packages/*/README.md` 中提取所有 ` ```ts ` 围栏代码块,写入一个继承根 `tsconfig.json` 的临时项目,然后用 `tsc -b` 编译。临时项目复用源码的 `paths` 映射和根 project references,因此文档示例能看到源码,而 vendor 代码仍在其自身的 tsconfig 设置下被检查。刻意作为草图的代码块可通过显式的 ` ```ts ignore-check ` 信息字符串来 opt-out;脚本会报告 opt-out 比例,超过一半即失败,防止该豁免机制悄然成为常态。
+2. **`verify-event-taxonomy`** 从 `packages/*/src` 中的 `interface Events` 块和 `docs/architecture.md` 中的分类体系表分别提取事件名称,断言两个集合完全一致。只校验,不生成:表格保留手写的 Mode/Purpose 列,仅检查名称集合。(落地此门禁时发现了表格遗漏的三个事件:`tools/change`、`llm/adapter-change`、`system-prompt/change`。)**已被取代**:由[生成式 Cordis 目录](2026-06-20-generated-cordis-catalog.md)取代。此门禁及其 `architecture.md` 表格已退役,取而代之的是完全生成的 `docs/cordis-catalog/events.md` + `docs/cordis-catalog/services.md` 及其 `verify-cordis-catalog` 新鲜度门禁。本 Agent Note(agent 决策记录)中的其他门禁(`doc-typecheck` 以及下文修订中的 `verify-md-wrap`)不受影响。
+
+两者都通过 package.json 中共享的 `doc-sync` 脚本运行;贡献者在相关文档变更中调用它,CI 则执行完整检查。[快速本地 Git 钩子](2026-07-22-fast-local-git-hooks.md)决策使这类按变更面选择的工作不进入 commit 和 push 钩子。
+
+**修订(2026-06-17):** 第三道门禁 **`verify-md-wrap`** 随后被纳入 `doc-sync`。它使用 `mdast-util-from-markdown` + GFM 解析范围内的每个 Markdown 文件(`README.md`、`docs/**`、`packages/*/README.md`,加上 `AGENTS.md` / `packages/AGENTS.md`),如果任何 `paragraph` 节点跨越多个源码行则失败,从而强制执行 docs/AGENTS.md 中「一个段落一个物理行」的写作规则。同样遵循只校验不生成的原则:它报告硬换行但从不重写,因此不会引入格式化噪音。`doc-sync` 现在包含三道门禁。
+
+## 曾考虑的替代方案
+
+- **API-extractor 基准报告**([已推迟的提案](../../proposed/process/2026-06-11-api-extractor-reports.md)):有意推迟。对于评审者已能直接看到源码 diff 的内部 monorepo 而言价值有限,且依赖重、配置繁琐。
+- **从源码生成分类体系表**而非仅校验名称:否决,机制比问题本身更重;表格保留了手写的 Mode/Purpose 列,直到[生成式 Cordis 目录](2026-06-20-generated-cordis-catalog.md)完全取代了这项检查。
+
+## 后果
+
+- 可检查类别中的文档漂移会直接使 `doc-sync` 和 CI 失败,而不是等评审人发现。这是「机械门禁优于行文规范」原则的具体应用。
+- 让文档代码片段可编译需要少量 stub import/`declare`;`ignore-check` 比例必须保持低位,否则门禁形同虚设(比例守卫强制执行此约束)。
+- 分类体系检查仅限名称——Mode 或 Purpose 列的错误仍需人工评审。
+- 如果包(package)未来对外发布,API 报告方案仍可重新考虑。
diff --git a/.agents/notes/archived/process/2026-06-11-tsdown-over-dumble.i18n.yaml b/.agents/notes/archived/process/2026-06-11-tsdown-over-dumble.i18n.yaml
new file mode 100644
index 0000000000..0384ce1260
--- /dev/null
+++ b/.agents/notes/archived/process/2026-06-11-tsdown-over-dumble.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
+2026-06-11-tsdown-over-dumble.md: b622fd41b262147e3a42ca590ccb192dd83d0b9f
+2026-06-11-tsdown-over-dumble.zh.md: 2200cb8c1776c5f50913ed7e3cfb9a6c305d31e0
diff --git a/.agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md b/.agents/notes/archived/process/2026-06-11-tsdown-over-dumble.md
similarity index 97%
rename from .agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md
rename to .agents/notes/archived/process/2026-06-11-tsdown-over-dumble.md
index 0629acabb4..b622fd41b2 100644
--- a/.agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md
+++ b/.agents/notes/archived/process/2026-06-11-tsdown-over-dumble.md
@@ -1,6 +1,9 @@
# Agent Note: tsdown for JS bundling instead of dumble
Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-06-11-tsdown-over-dumble.zh.md)
## Problem
diff --git a/.agents/notes/archived/process/2026-06-11-tsdown-over-dumble.zh.md b/.agents/notes/archived/process/2026-06-11-tsdown-over-dumble.zh.md
new file mode 100644
index 0000000000..2200cb8c17
--- /dev/null
+++ b/.agents/notes/archived/process/2026-06-11-tsdown-over-dumble.zh.md
@@ -0,0 +1,31 @@
+# Agent Note: 使用 tsdown 替代 dumble 进行 JS 打包
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-06-11-tsdown-over-dumble.md) | 中文
+
+## 问题
+
+最初的构建使用 **dumble**,即 cordiverse 的零配置 esbuild 包装层——上游 Cordis 自身也用它构建——与 vendor 包(package)的约定最大程度对齐(它读取每个 package.json 并从 `exports` 字段推断入口/格式)。但 dumble 作为本仓库的承重工具存在隐患:v0.2.x,每周约 530 次 npm 下载,实质上只有一位维护者,而且由于它没有 workspace 模式,我们不得不通过自定义编排脚本(`scripts/build.ts`)来调用它。
+
+目前构建产物只在 `pnpm run build` + publint 中有意义(尚未发布任何包;开发/测试/演示通过 tsx 直接运行未打包的源码),因此切换成本现在最低,一旦包开始发布就只会更高。
+
+## 决策
+
+用 **tsdown**(基于 rolldown,每周约 250 万次下载,VoidZero 支持,活跃发布)替代 dumble:
+
+- 根目录 `tsdown.config.ts`,配置 `workspace: ['vendor/*', 'packages/*/*']`(显式 glob 将打包范围限定在 vendor 的 Cordis 与 TypeScript 包目录树内;`workspace: true` 还会发现示例 manifest 和不需要打包的 workspace 成员)。
+- 共享形状:入口为 `lib/types/index.js`,`outDir: 'lib'`,ESM,`platform: node`,`target: es2024`,`fixedExtension: false`(为 `"type": "module"` 包保留 `.js`),`dts: false`(声明归 tsc -b 所有),`clean: false`(lib/ 还保存 TSC 的 `lib/types` 中间树)。入口最初是 `src/index.ts`;[TSC 优先构建 Agent Note(agent 决策记录)](2026-06-17-ts-build-config.md)随后将 tsdown 改为打包 TSC 输出的 JS,使 TypeScript 转换行为统一由一个编译器提供。
+- vendor/ 中有两个按包覆盖的配置(属于我们自己的修改,与重新生成的 tsconfig 类似;记录在 vendor/README.md 中):schemastery(通过 `outExtensions` 输出双格式 `.mjs`/`.cjs`)、logger-console(两次单入口 pass,使共享基类被内联到每个入口而非生成哈希命名的分片,与上游发布形态一致)。
+- `scripts/build.ts` 删除;`pnpm run build` = `tsc -b && tsdown`(根 solution 拥有 emit 图)。
+
+## 曾考虑的替代方案
+
+- **直接编写 esbuild 脚本**:最成熟的引擎,零包装层风险,但需要手动维护 tsdown workspace 模式自动提供的按包规格表。
+- **pkgroll**:理念上最接近的直接替代品,但每周仅 78k 下载且基于 Rollup,维护前景严格弱于 tsdown。
+- **保留 dumble**:与上游完美对齐,但巴士因子不可接受。
+
+## 后果
+
+运行时 bundle 输出仍沿用 dumble 时代的公开入口形状(`lib/index.js`,以及包特有的变体,例如 `schemastery` 的 `lib/index.mjs`/`lib/index.cjs` 与 `logger-console` 的 `lib/browser.js`);根据 [TSC 优先构建 Agent Note](2026-06-17-ts-build-config.md),声明现位于 `lib/types` 下。External 仍来自各包的 dependencies/peerDependencies。我们放弃了 dumble 的 exports 字段推断:采用非默认形状的新包需要逐包提供 `tsdown.config.ts`,不能只依赖 package.json 字段。未来如果 `tsc -b` 成为瓶颈,tsdown 也可以接管声明打包(isolatedDeclarations);这需要另写一份 Agent Note。
diff --git a/.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.i18n.yaml b/.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.i18n.yaml
new file mode 100644
index 0000000000..1c1ed11b5a
--- /dev/null
+++ b/.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.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
+2026-07-03-documentation-graph-atlas.md: 6f2949e3673c43018f961cc954c9925945875f55
+2026-07-03-documentation-graph-atlas.zh.md: 731ae8a97a8437216c706adf86574d1982d7c484
diff --git a/.agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md b/.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.md
similarity index 94%
rename from .agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md
rename to .agents/notes/archived/process/2026-07-03-documentation-graph-atlas.md
index 7969f0e80c..6f2949e367 100644
--- a/.agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md
+++ b/.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.md
@@ -1,6 +1,9 @@
# Agent Note: Documentation graph index for maintainers and SDK users
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-03-documentation-graph-atlas.zh.md)
## Problem
@@ -26,7 +29,7 @@ Every graph page declares one maintenance mode:
### First shipped index
-The index links eleven relationship surfaces. Package topology and tool-package affordances live in the existing generated catalogs that already own those facts; the remaining focused diagrams are generated by `scripts/gen-doc-graphs.ts`.
+The index links ten relationship surfaces. Package topology and tool-package affordances live in the existing generated catalogs that already own those facts; the remaining focused diagrams are generated by `scripts/gen-doc-graphs.ts`.
| Graph | Maintenance mode | Source of truth |
|---|---|---|
@@ -40,7 +43,6 @@ The index links eleven relationship surfaces. Package topology and tool-package
| [event producer/consumer matrix](../../../../docs/event-producer-consumer.md) | hybrid generated | Cordis event declarations, AST-scanned `ctx.on/emit/parallel/serial/waterfall` sites, and explicit dynamic dispatch overrides |
| [agent turn and step lifecycle](../../../../docs/agent-lifecycle.md) | curated | architecture.md loop lifecycle, Cordis catalog links, and session event semantics |
| [tool execution pipeline](../../../../docs/tool-execution-pipeline.md) | curated | tool pipeline semantics and the `tools/execute` waterfall |
-| [ACP snapshot replay](../../../../packages/ui/acp/snapshot-replay.md) | curated | snapshot harness behavior |
### Why generators own the docs
@@ -62,7 +64,7 @@ Committed diagrams use Mermaid because GitHub renders it in Markdown and it adds
## Consequences
-- Maintainers get visual entry points for topology, seams, event flow, lifecycle, app composition, and snapshot behavior.
+- Maintainers get visual entry points for topology, seams, event flow, lifecycle, and app composition.
- SDK users get a path from use case to package composition instead of only bottom-up package references.
- `doc-sync` now includes `verify-doc-graphs` and `verify-mermaid`, so graph drift and Mermaid syntax errors are caught with the other doc freshness gates.
- Future fs and hooks work has a concrete place to land new complexity: fs should expand the capability docs and tool catalog, while hooks should expand the event matrix and tool execution pipeline.
diff --git a/.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.zh.md b/.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.zh.md
new file mode 100644
index 0000000000..731ae8a97a
--- /dev/null
+++ b/.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.zh.md
@@ -0,0 +1,70 @@
+# Agent Note: 面向维护者与 SDK 用户的文档关系图索引
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-03-documentation-graph-atlas.md) | 中文
+
+## 问题
+
+仓库已经有若干高可信文档表面,各自覆盖不同维度:[module-graph.md](../../../../docs/module-graph.md) 根据包(package)的 `peerDependencies` 生成;生成式 [Cordis 事件](../../../../docs/cordis-catalog/events.md)和[服务](../../../../docs/cordis-catalog/services.md)目录根据 Cordis `Events` 和 `Context` 声明生成;[tool-catalog.md](../../../../docs/tool-catalog.md) 通过启动已发布工具插件生成;[core-data-structures/](../../../../docs/core-data-structures/core.md) 则使用 `ts type-equiv` 块使粘贴的类型定义与源码保持同步。
+
+这些参考文档是准确的,但大多是目录式的。维护者仍需自行综合关系:哪些包构成一个能力 seam、哪个应用组装了具体的主干、哪些事件是持久的而哪些是实时的、钩子或策略插件在哪里可以拦截工作、以及哪个面向模型的工具依赖哪个服务。SDK 用户从另一个角度面临同样的问题:「我想要某种行为,应该安装或加载哪个包?应该扩展哪个事件/服务/工具?」
+
+钩子子系统使事件的生产者/消费方拓扑与拦截点变得更加重要;文件系统 seam 使能力 seam、策略否决、工具呈现与 SDK 组装路径变得更加重要。如果关系图的范围仅限于一个小的 bash/todo/subagent 表面,它们会立即陈旧。
+
+## 决策
+
+新增生成式关系图文档,由聚焦的生成器产出并在 [docs/graph-atlas.md](../../../../docs/graph-atlas.md) 建立索引;作为 `doc-sync` 的一部分,通过 `pnpm run verify-doc-graphs` / 现有目录新鲜度检查进行验证。
+
+该索引是既有目录之上的关系层。它不取代精确的参考文档,而是链接到它们并解释各部分如何组合在一起。
+
+### 维护模式
+
+每个关系图页面声明一种维护模式:
+
+- **Generated(生成)**:所有节点和边均从源码发现;如果已提交的产物陈旧,`--check` 失败。
+- **Hybrid generated(混合生成)**:源码发现清单,一个小型 manifest 对不可约的策略进行分类,完整性守卫在发现的条目未被分类时失败。
+- **Curated(人工策划)**:图表解释设计意图、时序或归属;它由生成器输出以使关系图文档保持为可重新生成的整体,但内容是有意撰写的。
+
+### 首批发布的索引
+
+该索引链接十种关系表面。包拓扑和工具包所提供的功能位于已经拥有这些事实的现有生成式目录中;其余聚焦图表由 `scripts/gen-doc-graphs.ts` 生成。
+
+| 关系图 | 维护模式 | 真源 |
+|---|---|---|
+| [模块依赖图](../../../../docs/module-graph.md) | 生成式 | `packages/*/*/package.json` 的对等依赖(peer dependency)与包分组路径 |
+| [工具 schema 目录与包映射](../../../../docs/tool-catalog.md) | 生成式 | 启动后采集的工具 schema,以及工具包服务/效应元数据 |
+| [能力 seam 与核心服务](../../../../docs/capability-seams.md) | 混合生成式 | Cordis 服务声明,以及 `gen-doc-graphs.ts` 中的角色清单 |
+| [tui-agent 应用组合](../../../../examples/tui-agent/composition.md) | 混合生成式 | `examples/tui-agent/cordis.yml` 插件列表,以及人工维护的应用/bundle 展开 |
+| [headless-agent 应用组合](../../../../examples/headless-agent/composition.md) | 混合生成式 | `examples/headless-agent/cordis.yml` 插件列表,以及人工维护的应用/bundle 展开 |
+| [cordis-agent 应用组合](../../../../examples/cordis-agent/composition.md) | 混合生成式 | `examples/cordis-agent/cordis.yml` 插件列表,以及人工维护的应用/bundle 展开 |
+| [acp-agent 应用组合](../../../../examples/acp-agent/composition.md) | 混合生成式 | `examples/acp-agent/cordis.yml` 插件列表加人工策划的应用/bundle 展开 |
+| [事件生产者/消费方矩阵](../../../../docs/event-producer-consumer.md) | 混合生成式 | Cordis 事件声明、经 AST 扫描的 `ctx.on/emit/parallel/serial/waterfall` 位置,以及显式动态分派覆盖 |
+| [agent 轮次与步骤生命周期](../../../../docs/agent-lifecycle.md) | 人工维护 | architecture.md 循环生命周期、Cordis 目录链接,以及会话事件语义 |
+| [工具执行管线](../../../../docs/tool-execution-pipeline.md) | 人工维护 | 工具管线语义与 `tools/execute` waterfall(瀑布式事件)|
+
+### 为什么由生成器拥有文档
+
+包拓扑留在 `gen-module-graph.ts`,工具-包能力映射留在 `gen-tool-catalog.ts`,因为这些生成器已经拥有权威事实和新鲜度门禁。`gen-doc-graphs.ts` 拥有其余关系页面和索引。代价是人工策划的图表需要在 TypeScript 字符串块中编辑,而非直接编辑 Markdown。对于首版来说这是可接受的,因为面向用户的产物仍然是纯 Markdown/Mermaid;未来如果撰写体验比可重新生成更重要,可以将人工策划的页面拆分出去。
+
+### 完整性守卫
+
+混合生成的页面在其 manifest 陈旧时必须显式报错:
+
+- 模块图读取每个包的 `peerDependencies`,并按 `packages//` 路径对包进行分组。
+- 工具目录通过启动收集已发布的工具,并从同一份 manifest 渲染包/服务/副作用映射(其完整性守卫已在检查该 manifest)。
+- 能力 seam 图导入 Cordis 服务收集器,断言每个发现的 harness `ctx.` 都已在 `SERVICE_ROLES` 中分类,且每个已分类的 key 仍然存在。
+- 事件生产者/消费方矩阵标记为 hybrid,因为 subagent 生命周期事件有意使用 `ctx.events.dispatch` 实现逐监听器隔离;这些动态边是显式覆盖而非无声遗漏。
+- `verify-mermaid` 使用 Mermaid 自身的解析器解析仓库中每个 ` ```mermaid ` 围栏,因此语法错误在本地和 CI 的 `doc-sync` 阶段即被捕获,而非在 GitHub 渲染时才显示为损坏的图表。
+
+## 曾考虑的替代方案
+
+已提交的图表使用 Mermaid,因为 GitHub 在 Markdown 中原生渲染它且不引入新的文档构建依赖;密集的多对多数据(如事件生产者/消费方关系)改用 Markdown 表格。**PlantUML、托管图表服务和生成的 SVG** 曾被考虑,但在 Mermaid 成为瓶颈之前有意不采用。
+
+## 后果
+
+- 维护者获得了拓扑、seam、事件流、生命周期与应用组合的可视化入口。
+- SDK 用户获得了从用例到包组合的路径,而非仅有自底向上的包参考。
+- `doc-sync` 现在包含 `verify-doc-graphs` 和 `verify-mermaid`,因此关系图漂移和 Mermaid 语法错误与其他文档新鲜度门禁一起被捕获。
+- 未来的文件系统和钩子工作有了承载新复杂度的具体位置:文件系统应扩展能力文档和工具目录,钩子应扩展事件矩阵和工具执行流水线。
diff --git a/.agents/notes/archived/process/2026-07-04-cordis-jsdoc-completeness-gate.i18n.yaml b/.agents/notes/archived/process/2026-07-04-cordis-jsdoc-completeness-gate.i18n.yaml
new file mode 100644
index 0000000000..d17897d305
--- /dev/null
+++ b/.agents/notes/archived/process/2026-07-04-cordis-jsdoc-completeness-gate.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
+2026-07-04-cordis-jsdoc-completeness-gate.md: 4b033ab52c8bfe79d1ca9db4baf3db2a533223be
+2026-07-04-cordis-jsdoc-completeness-gate.zh.md: f1c1c9976deadaaa700a8a152e4f29a31122ca1a
diff --git a/.agents/notes/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md b/.agents/notes/archived/process/2026-07-04-cordis-jsdoc-completeness-gate.md
similarity index 98%
rename from .agents/notes/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md
rename to .agents/notes/archived/process/2026-07-04-cordis-jsdoc-completeness-gate.md
index 3945382217..4b033ab52c 100644
--- a/.agents/notes/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md
+++ b/.agents/notes/archived/process/2026-07-04-cordis-jsdoc-completeness-gate.md
@@ -1,6 +1,9 @@
# Agent Note: JSDoc completeness gate for the cordis surface
Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-07-04-cordis-jsdoc-completeness-gate.zh.md)
## Problem
diff --git a/.agents/notes/archived/process/2026-07-04-cordis-jsdoc-completeness-gate.zh.md b/.agents/notes/archived/process/2026-07-04-cordis-jsdoc-completeness-gate.zh.md
new file mode 100644
index 0000000000..f1c1c9976d
--- /dev/null
+++ b/.agents/notes/archived/process/2026-07-04-cordis-jsdoc-completeness-gate.zh.md
@@ -0,0 +1,42 @@
+# Agent Note: 针对 Cordis 对外服务接口的 JSDoc 完整性门禁
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-07-04-cordis-jsdoc-completeness-gate.md) | 中文
+
+## 问题
+
+生成的 Cordis 目录此前强制了事件分发模式,但未强制要求完整的服务与事件契约。方法可以缺少描述,参数或返回值可以在跨插件 API 接口上不写文档——而这恰恰是 IDE 引导最重要的地方。
+
+AGENTS.md 中的规则(「每个导出都有解释语义的 JSDoc」)只能靠评审以行文形式检查;本仓库的既定偏好是将不变式编码为机械门禁。「Cordis 服务函数与事件」这一范围有精确的机器定义,只有目录生成器知道:事件是 `declare module 'cordis'` 内 `interface Events` 的成员,服务接口是每个 `interface Context` 键所指向的类的公开方法。ESLint 规则看不到这层映射;生成器在每次运行时计算它。
+
+## 决策
+
+扩展 `scripts/gen-cordis-catalog.ts`——复用同一次遍历和同一套 `@mode` 先例——对它编目的所有内容强制执行 JSDoc 完整性要求。`verify-cordis-catalog` 在 `doc-sync` 内运行,因此相关文档变更和 CI 会执行同一门禁,无需另行接线。
+
+契约如下:
+
+- **事件**需要描述性文字,以及为每个**载荷参数**提供非空的 `@param`。载荷参数是携带事件数据的签名参数;`this` 接收者注解和尾部的 waterfall(瀑布式事件) `next` 免检——`next` 是分发机制,其语义已由 `@mode waterfall` 标签(及其结构交叉检查)拥有,逐事件重述只是样板代码。为免检参数写文档是允许的;只有缺失才被检查。
+- **服务类**需要类级 JSDoc,每个公开方法需要描述性文字、为每个参数提供非空的 `@param`,以及非空的 `@returns`——除非标注的返回类型是 `void`/`Promise`(此时 `@returns` 可选——resolve 时机有时值得记录——但从不强制要求)。
+- **陈旧标签报错**:`@param` 命名了一个不存在的参数即为违规,与 `@mode` 与签名矛盾的检查对称。标签描述必须非空;超出此范围的语义质量由评审负责。
+- **遍历可检查的显式性**:门禁是纯 AST 遍历(不使用类型检查器),因此服务方法必须显式标注返回类型(推断的返回类型无法分类),接口参数必须是简单标识符(解构模式没有名称供 `@param` 匹配)。
+- **违规聚合**为一条错误信息,列出所有违规项——修复时一次看到完整清单。此前快速失败的 `@mode` 检查也移入同一份聚合报告,消息文本不变。
+
+生成器保留同一源码注释的两种视图:`parseJsDoc` 在第一个块标签处结束条目正文,而 `ts cordis-catalog` 签名块包含原始 JSDoc,并完整保留 `@param`、`@returns` 和 `@mode`。因此,读者可以看到完整的源码契约,而块标签文本不会泄漏到周围正文中。
+
+`packages/core/agent/tests/gen-cordis-catalog.spec.ts` 中的负路径测试对合成 fixture(测试前置数据)运行 `collectEvents`/`collectServices`,验证每条守卫都会触发且免检规则成立。撰写规则写在根 [AGENTS.md](../../../../AGENTS.md) 的约定条目中,与 `@mode` 规则并列。
+
+## 曾考虑的替代方案
+
+- **ESLint 规则**:无法看到该范围的机器定义(哪些 `interface Events` 成员、哪些 `ctx.` 类构成 Cordis 对外服务接口);目录生成器在每次运行时恰好计算这层映射,因此门禁放在那里。
+- **将每个方法展开为单独的正文小节**:否决。目录保留一个服务章节和一个签名块,以维持可扫读性;附着于每个声明的 JSDoc 则在原处保留完整的方法契约。
+- **逃逸标签**:不设。该接口面小且经过策展(采纳时 12 个服务、57 个方法、27 个事件),要点在于检查不可豁免。
+
+## 后果
+
+- 新事件或服务方法不能带着未记录的参数或结果落地:生成器会拒绝重新生成,`verify-cordis-catalog` 也会使 `doc-sync` 和 CI 失败。采纳时发现的约 139 处缺口已在同一变更中补齐,因此门禁以绿色状态落地。
+- 服务接口必须显式标注返回类型并使用标识符参数。两项约束在采纳时均未构成限制(所有方法已有标注;不存在解构的 seam 参数);但二者现在是承重要求,违反时会被机械检测到。
+- AGENTS.md 中通用的 JSDoc 规则(「一行能说清就用一行」)在此接口上获得了更严格的特例:仅当方法无参数且返回 void 时,一行摘要才足够。
+- 为 `next` 或 `this` 写 `@param` 合法但不检查——这是有意的不对称:门禁强制载荷契约,拒绝要求样板代码。
+- 每个生成的事件或方法片段都带有其原始 JSDoc,而正文摘要不含标签。因此,源码编辑会同时刷新可读索引和签名旁展示的确切契约。
diff --git a/.agents/notes/archived/process/2026-07-04-persistence-log-catalog.i18n.yaml b/.agents/notes/archived/process/2026-07-04-persistence-log-catalog.i18n.yaml
new file mode 100644
index 0000000000..122f95f28d
--- /dev/null
+++ b/.agents/notes/archived/process/2026-07-04-persistence-log-catalog.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
+2026-07-04-persistence-log-catalog.md: f000b41c24761fa4914f296d0c2c07e4a26770d6
+2026-07-04-persistence-log-catalog.zh.md: 58680e56c5b8f5959baa90318f1747303392c9aa
diff --git a/.agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md b/.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md
similarity index 98%
rename from .agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md
rename to .agents/notes/archived/process/2026-07-04-persistence-log-catalog.md
index bde0f08497..f000b41c24 100644
--- a/.agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md
+++ b/.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md
@@ -1,6 +1,9 @@
# Agent Note: Generated persistence log event catalog
Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-07-04-persistence-log-catalog.zh.md)
## Problem
diff --git a/.agents/notes/archived/process/2026-07-04-persistence-log-catalog.zh.md b/.agents/notes/archived/process/2026-07-04-persistence-log-catalog.zh.md
new file mode 100644
index 0000000000..58680e56c5
--- /dev/null
+++ b/.agents/notes/archived/process/2026-07-04-persistence-log-catalog.zh.md
@@ -0,0 +1,37 @@
+# Agent Note: 生成式持久化日志事件目录
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-07-04-persistence-log-catalog.md) | 中文
+
+## 问题
+
+`SessionEventMap` 是磁盘格式的词汇,但其声明分散在所属的会话包(package)和声明合并中。生成式持久化目录是所有事件、各自完整 payload 声明与源码 JSDoc,以及共享 `SessionEvent` 信封的唯一参考;手工维护的表格会发生漂移,因此被移除。这些记录不是 Cordis 事件——观察者通过唯一的 `session/event` 总线事件接收它们——所以 Cordis 目录无法覆盖。生成器会发现所有声明,文档同步新鲜度门禁会拒绝遗漏或陈旧输出。
+
+## 决策
+
+从源码生成 `docs/persistence-catalog.md`,配合新鲜度门禁,作为第四个参考面:持久化会话日志可以包含的*记录*,与 Cordis 目录(接线)、核心数据结构(词汇)和工具目录(工具)互补。
+
+`gen-persistence-catalog.ts` 使用 TypeScript AST 扫描每个所属及声明合并的 `SessionEventMap`。它从前置 JSDoc 开始渲染每个成员,直至完整的 payload 类型,保留嵌套属性注释且只移除其容器缩进;同时粘贴构成持久化信封的所属 `SessionEventType`、`SurfaceEventType`、`SurfaceOp` 和 `SessionEvent` 声明。派生的 surface 徽章、参考链接和源码位置仍位于声明块之外。文档同步新鲜度检查会拒绝目录尚未重新生成的词汇或信封变更。
+
+具体选择:
+
+- **强制保证 JSDoc 完整性。** 每个成员和渲染出的信封类型都必须带有描述正文,完整的源码 JSDoc 会在目录中保持附着于其声明。`@mode` 标签是硬错误:分派模式属于 Cordis 总线事件,持久化记录没有这种模式。所有违规会汇总为一条错误,列出每个违规项。
+- **surface 徽章由派生得出,而非手工列举。** `SurfaceEventType`(产生 LLM(大语言模型)消息且可能携带 `surfaceOp` 的子集)从拥有方包中的 union 声明解析;如果 union 成员命名了一个未声明的事件,则为硬错误(否则陈旧的 union 成员会静默地不标注任何内容)。其余一律渲染为 **log-only**。
+- **专用围栏。** 声明块使用 ` ```ts persistence-catalog ` 信息字符串,`doc-typecheck` 会识别并跳过这些块,将其排除在 opt-out 比例之外——处理方式与 `ts cordis-catalog` 相同(这些声明引用所属模块中的类型,无法独立编译)。
+- **仓库范围。** 目录枚举本仓库中的包,与兄弟文档的 packages-only 范围一致;下游插件可以合并更多事件类型,它们在设计上不在目录范围内。遍历过程用硬错误保护自身假设:拥有方的顶层 `interface SessionEventMap` 必须是 `@deepseek-ai/dsh-session` 中唯一的导出声明(无关的、局部的或同名重复的接口不能被当作磁盘词汇编入目录);任何声明不得携带 `extends`(继承的键会加入 `keyof SessionEventMap` 却没有对应的目录行);每个成员必须是带有显式 payload 类型的属性签名(方法形式的成员会加入 `keyof` 却在静默遍历中被漏过);跨声明的重复成员也会失败。
+
+本方案取代了手工副本:session.md 的 `hook/*` 表格、精简版 README 的事件表格、hook-protocol README 的 payload 条目列表,以及会话 README 的名称列表现在链接到目录,而不再重述 payload(周围的语义说明文字保留原位)。hook-protocol 合并成员上的两个误加的 `@mode emit` 标签已被移除——新门禁将它们作为类别错误拒绝。
+
+## 曾考虑的替代方案
+
+- **基于启动的生成器(类似工具目录)**:日志词汇完全是静态的,AST 遍历无需启动任何东西即可读取全部真相。
+- **保留手工副本**:手工副本只能检查作者已经写下的名称;目录落地时,会话 README 的合并说明已经漂移。
+
+## 后果
+
+- 目录不会发生漂移:提交文件未反映的词汇或信封变化会使 `doc-sync` 和 CI 中的 `verify-persistence-catalog` 失败,而没有 JSDoc 的新增合并事件会直接使生成器失败——插件不能再添加未记录的磁盘记录类型。
+- 事件正文只有一个归属,即声明处的 JSDoc;目录会保留该 JSDoc 和所有嵌套字段注释,不会将其扁平化或复述。
+- `SurfaceEventType` union 现在对文档具有结构性承载作用:重命名事件而不更新 union(或反过来)会导致生成器失败,而不仅仅是编译器失败。
+- 徽章派生假设 union 始终是一组封闭的字符串字面量且只有一个拥有方;如果重构偏离了这一形状,必须在同一个变更中更新生成器。
diff --git a/.agents/notes/archived/process/2026-07-06-generated-config-catalog.i18n.yaml b/.agents/notes/archived/process/2026-07-06-generated-config-catalog.i18n.yaml
new file mode 100644
index 0000000000..439ef8fde1
--- /dev/null
+++ b/.agents/notes/archived/process/2026-07-06-generated-config-catalog.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
+2026-07-06-generated-config-catalog.md: b7c69f21c0e2152235f57e90f88f670769eb9d3b
+2026-07-06-generated-config-catalog.zh.md: 59d632df0219c09d4afac9c859e2ed0a88d62550
diff --git a/.agents/notes/implemented/process/2026-07-06-generated-config-catalog.md b/.agents/notes/archived/process/2026-07-06-generated-config-catalog.md
similarity index 98%
rename from .agents/notes/implemented/process/2026-07-06-generated-config-catalog.md
rename to .agents/notes/archived/process/2026-07-06-generated-config-catalog.md
index f876191be4..b7c69f21c0 100644
--- a/.agents/notes/implemented/process/2026-07-06-generated-config-catalog.md
+++ b/.agents/notes/archived/process/2026-07-06-generated-config-catalog.md
@@ -1,6 +1,9 @@
# Agent Note: Generated plugin config catalog
Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-07-06-generated-config-catalog.zh.md)
## Problem
diff --git a/.agents/notes/archived/process/2026-07-06-generated-config-catalog.zh.md b/.agents/notes/archived/process/2026-07-06-generated-config-catalog.zh.md
new file mode 100644
index 0000000000..59d632df02
--- /dev/null
+++ b/.agents/notes/archived/process/2026-07-06-generated-config-catalog.zh.md
@@ -0,0 +1,41 @@
+# Agent Note: 生成式插件配置目录
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-07-06-generated-config-catalog.md) | 中文
+
+## 问题
+
+仓库此前没有以源码为后盾的插件配置参考。各包(package)的 README 对字段的记录方式不一致,未列举哪些包可被加载,也未校验运行时 schema 与声明的配置类型是否一致。
+
+## 决策
+
+`scripts/gen-config-catalog.ts` 根据各插件声明的 config 类型和 JSDoc 生成 [docs/config-catalog.md](../../../../docs/config-catalog.md),并包含注入要求、被引用类型的链接和源码位置。包内类型会以传递方式纳入;workspace 类型和外部类型则会链接或点名。确定性的 `--write` 和 `--check` 模式使提交页面成为生成产物。
+
+此处采用纯 AST 生成是正确的,原因与事件/服务目录相同,而与工具目录不同:配置类型是静态声明,仓库中每个 schemastery schema 都是静态的 `z.object`/`z.intersect` 字面量,因此源码即全部真相——配置表面没有任何部分是运行时组合的。
+
+具体选择:
+
+- **配置类型是第二参数的类型。** catalog 记录的是 `apply(ctx, config)` / 服务构造函数 `(ctx, config)` 的声明参数类型——即 Cordis 实际传入的值——而非按命名约定定位的 `Config` 导出。这使得遍历是全量的:无论接口叫 `AcpConfig` 还是 `BasicCompactConfig`,无论类型声明在兄弟文件中,还是插件完全没有验证 schema,都能正常工作。
+- **分类是全量的。** 每个 `packages//` 条目都会被解析(镜像 Loader 的 `unwrapExports`:`exports.default ?? exports`),归入可配置插件、无配置插件、抽象 seam 类或库之一——各自渲染在独立小节中——无法归类的条目直接报错。新包不可能被悄悄遗漏。
+- **逐字段 JSDoc 强制要求。** 粘贴的声明中每个属性(包括嵌套的类型字面量)都需要非空的 JSDoc 描述,否则生成失败。粘贴本身就是文档,因此这与 events catalog 通过 `@mode` 施加的强制函数相同:源码文档过于单薄时门禁报错,而非产出单薄的 catalog。
+- **Schema 键与声明类型做比对。** 生成器通过局部和 workspace 类型解析嵌套的对象与数组路径。确定缺失的路径报错;无法枚举的外部或动态形状则跳过。比对有意设计为单向的,因为声明类型可能包含被排除在 loader 配置之外的运行时专用字段。
+- **专用围栏。** 粘贴的声明使用 ` ```ts config-catalog ` 信息字符串,`doc-typecheck` 会跳过它(引用了导入类型的孤立声明无法独立编译),并将其排除在 opt-out 比例之外——与 `cordis-catalog` 和 `persistence-catalog` 围栏的处理方式相同。
+- **单文件 `docs/config-catalog.md`**,而非一个单文件目录:该页面面向单一受众(`cordis.yml` 的编写者),只有一个维度,不同于 `cordis-catalog/`(其中包含两个并列页面)。
+
+各包 README 中的 `## Config` 小节保留。重叠是有意接受的:README 是经过策划的逐包契约(在部署上下文中描述配置语义,连同限制与扩展点),catalog 则是穷举式的生成枚举。由于 catalog 是生成的,二者不一致时说明 README 有误,修复方式是编辑 README——catalog 不会漂移。
+
+## 曾考虑的替代方案
+
+- **合成式逐字段渲染**:为每个字段生成项目符号列表、表格或带注释的 YAML 片段,从解析的 JSDoc 加 schema 元数据组装。否决,改用逐字粘贴:接口连同其 JSDoc 本身就是以原始形式撰写的契约,合成渲染器会重新格式化它不拥有的行文,增加一个可能歪曲原意的渲染层。
+- **运行时启动 + schema 内省(如工具目录所做的那样)**:否决。此处没有任何内容是运行时组合的,且 schema 本身对配置表面的文档化不足(以行文记录的默认值、运行时专用字段、完全没有 schema 的插件)。启动只会增加脆弱性而不增加真相。
+- **双向 schema/接口等价检查**:否决,改用子集检查。声明类型合理地包含 schema 拒绝从配置接受的成员(运行时专用 seam)。
+- **在同一变更中废除 README `## Config` 小节**:否决。保留可接受的重叠使逐包契约在原处可读,而清理工作需要先把每个 README 的额外事实折入字段 JSDoc——这是可分离的工作,catalog 不依赖它。
+
+## 后果
+
+- 目录不会发生漂移:提交文件未反映的源码变化会使 `doc-sync` 和 CI 中的 `verify-config-catalog` 失败。config 字段未记录、被引用类型名无法解析,或 schema 键未出现在 config 类型中,都会直接使生成器失败。
+- 配置行文现在有了声明处的强制函数:编写新配置字段意味着编写其 JSDoc,而该 JSDoc 将逐字成为 catalog 条目。
+- 生成器对无法静态遍历的形状直接报错——别名化的包内配置导入、非 `object`/`intersect` 组合构建的 schema、未列入的全局类型名。引入此类形状时必须同时教会生成器(否则该形状不能进入仓库),这正是设计意图:catalog 始终是全部真相。
+- `gen-cordis-catalog.ts` 导出其 JSDoc/指针辅助函数与 `LINK_MAP` 供复用,因此两个 catalog 以相同方式交叉链接类型,新增一条 link-map 条目同时服务于两者。
diff --git a/.agents/notes/archived/process/2026-07-06-parallel-github-ci-gates.i18n.yaml b/.agents/notes/archived/process/2026-07-06-parallel-github-ci-gates.i18n.yaml
new file mode 100644
index 0000000000..32db8ee132
--- /dev/null
+++ b/.agents/notes/archived/process/2026-07-06-parallel-github-ci-gates.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
+2026-07-06-parallel-github-ci-gates.md: 0a621e0cf0b37d3ba6f612aaf1d9d7d052496929
+2026-07-06-parallel-github-ci-gates.zh.md: 340e5301b941fcb4774e2625e6706ab092a78c26
diff --git a/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md b/.agents/notes/archived/process/2026-07-06-parallel-github-ci-gates.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md
rename to .agents/notes/archived/process/2026-07-06-parallel-github-ci-gates.md
index fef5852153..0a621e0cf0 100644
--- a/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md
+++ b/.agents/notes/archived/process/2026-07-06-parallel-github-ci-gates.md
@@ -1,6 +1,9 @@
# Agent Note: Parallel GitHub CI gates
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-06-parallel-github-ci-gates.zh.md)
## Problem
diff --git a/.agents/notes/archived/process/2026-07-06-parallel-github-ci-gates.zh.md b/.agents/notes/archived/process/2026-07-06-parallel-github-ci-gates.zh.md
new file mode 100644
index 0000000000..340e5301b9
--- /dev/null
+++ b/.agents/notes/archived/process/2026-07-06-parallel-github-ci-gates.zh.md
@@ -0,0 +1,51 @@
+# Agent Note: 并行 GitHub CI 门禁
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-06-parallel-github-ci-gates.md) | 中文
+
+## 问题
+
+无密钥 GitHub CI 门禁大多相互正交:类型检查、lint、文档新鲜度、覆盖率、快照重放、构建、包(package)的发布卫生检查、demo 冒烟和已构建二进制冒烟会因不同原因失败,也不需要彼此的运行时状态。将它们作为一条有序命令链运行,会使工作流墙钟时间等于所有门禁耗时之和;而把每个短小叶子拆成独立 GitHub job,又会反复执行 checkout、Node 设置、pnpm 恢复和安装,直到编排开销成为瓶颈。
+
+随着 workspace 增长,原有的宽车道拆分不再满足这一平衡。PR(Pull Request)#404 合并时,Linux 的静态、覆盖率、快照和产物 job 分别耗时 148、195、94 和 230 秒;Windows 的静态和产物 job 分别耗时 251 和 482 秒。每个包都调用一次包管理器打包,主导了两个产物验证器的耗时;覆盖率在仅运行源码的套件前无谓地重建输出;CPU 密集型门禁则在静态与覆盖率车道内争用资源。
+
+产物边界仍然承载关键约束。`publint`、`verify-node-next-types`、已编译不变量加载和已构建二进制冒烟测试都需要生成的 `lib/` 输出。分片不能让这些消费方抢在构建前运行,也不能用源码执行取代它们对已发布产物的信号。
+
+## 决策
+
+下述生产拓扑已经成为历史,并由[基于证据采用更大的托管 runner](2026-07-22-evidence-based-larger-hosted-runners.md) 取代。更大 runner 的决策移除了其分片选择器和工作流 job;本文保留早期拓扑为何被实现的记录。
+
+[CI](../../../../.github/workflows/ci.yml) 将非 Windows job 的一分钟和 Windows job 的三分钟视为观测所得的性能目标,而非取消截止时间。托管 runner 的波动应留下完整计时证据和有用的失败日志,而不是取消本来正确的门禁。[串行跨平台 CI 参考](2026-07-21-serial-cross-platform-ci-reference.md)会在 Linux、macOS 和 Windows 上独立运行完整、未分片的主 Node 聚合,使优化后的车道清单不会成为自身完整性的唯一判据。
+
+在该拓扑中,[scripts/run-gates.ts](../../../../scripts/run-gates.ts) 是通用的有界调度器,GitHub 则为昂贵的门禁族提供显式分片名称。`scripts/static-shards.ts` 将静态门禁划分为基础、文档类型、API 契约、目录、正文、文档投影和文档构建等归属,并拒绝缺失或重复的门禁分配。Linux lint 使用互不重叠的 A-C、D-M、N-S、T-Z 包源码和包测试车道,Windows 则使用完整的包源码与包测试车道;两者都包含从 `.` 开始的仓库补集,使新增顶层目标无法消失在分片之间,并负责唯一一次跨文件重复检查。`scripts/coverage-shards.ts` 把每个 workspace 包恰好分配给一个源码覆盖率车道。目录过滤器保留尾部分隔符,因为 Vitest 位置过滤器按子字符串匹配,否则会纳入具有同名前缀的相邻项。每个覆盖率车道只包含其拥有的源码文件,重复运行穷尽式伴随拓扑测试,并且不先执行构建,因为从删除了所有生成式 `lib/` 的树开始,完整覆盖率套件仍可通过。
+
+快照重放使用两个显式多文件车道,以及大型 ACP(Agent Client Protocol)文件的八个场景分区。`scripts/snapshot-shards.ts` 拥有该清单,其测试会发现快照配置允许的每个文件。每个快照 job 在其 Linux runner 准备 Bubblewrap 的同时安装依赖,随后构建已发布运行时,并且只运行分配给它的重放表面。该套件保留五个子进程的有界并发,因为重放的大部分时间都在等待子进程协议 I/O。fixture(测试前置数据)守卫仍会在每个分区中检查完整 ACP 场景表。
+
+冷启动的独立文档类型检查会重建完整的项目引用图,因此专用文档类型车道只构建一次,再用这些声明检查 Markdown 块。Linux 文档车道使用 VitePress 的 MPA 构建,在观测所得的非 Windows 目标内保留页面渲染与死链接验证;单独的阻塞式 Windows 构建和生产站点车道保留已生成包与已发布站点检查,同时避免把两条关键路径放进同一个 job。
+
+产物使用两个车道:一个元数据车道负责 `publint`、NodeNext 声明和已编译不变量加载,另一个负责已构建二进制冒烟。每个车道都会在其消费方之前自行构建。重复短时构建会消耗 runner 分钟数,但避免了上传/下载依赖,并使每个 job 的关键路径保持有界。
+
+[scripts/publint-all.ts](../../../../scripts/publint-all.ts) 在进程内针对内存发布视图调用 publint 支持的 API;该视图由每份清单声明的文件和 npm 强制元数据文件构成。这样无需生成 103 次包管理器打包命令,也能保留 workspace 文件与已发布文件之间的区别。[scripts/verify-built-package-invariants.mjs](../../../../scripts/verify-built-package-invariants.mjs) 在真实包下暂存这些经过结构验证、由清单声明的 `lib/` 文件,再通过纯 Node 和 Cordis Loader 规范化导入已编译的自引用。若伴随项触及未声明的运行时分片,仍会失败。
+
+兼容性车道会在每条声明支持的 Node 版本线上运行源码 worker 和 Zstandard 运行时冒烟。TypeScript 在专用的主 Node 24 车道中只检查一次源码图;在运行时兼容性 job 中重复同一编译器分析只会增加耗时,不会提供运行时特有信号。
+
+工作流缓存 pnpm store,将每个不可变 ESLint 缓存的键绑定到其所属 lint 分片,为 Windows 测量保留原生 PowerShell,并保留一个聚合的 `all checks passed` 状态用于分支保护。Windows 复用三个穷尽式 lint 分区,并在共享 runner 设置后组合基础/目录/正文门禁与文档类型/API 契约门禁;只有调度方式与 Linux 分区不同。Windows 构建和生产站点验证继续阻塞,而更广泛的 Windows 静态、lint 和产物矩阵仍为观察性检查。
+
+## 曾考虑的替代方案
+
+- **保留宽车道**:最大限度减少工作流 YAML,但会保留观测到的数分钟反馈周期。
+- **让每个叶子门禁分别成为 GitHub job**:最大化扇出,但短小的生成器和正文检查准备 runner 的时间会超过检查仓库的时间。
+- **向产物消费方上传一次构建**:避免重复编译,但上传/下载和依赖调度会延长墙钟时间;干净构建足够短,可以在有界车道内重复。
+- **在两个发布门禁中保留包管理器打包**:把清单选择委托给 pnpm,但会重复启动 200 多个包管理器进程。清单结构门禁加发布视图 fixture 使优化后的清单契约显式化,并会在存在磁盘上有但未发布的依赖时失败。
+- **在覆盖率前保留构建**:提供源码套件已不再消费的生成输出;干净树覆盖率证明表明这只是纯粹的延迟。
+- **在每个 Node 版本上执行类型检查**:重复编译器工作,而兼容性冒烟已经验证实际的 Node 特有加载与压缩行为。
+
+## 后果
+
+上述分片清单和矩阵 job 不属于当前仓库契约。取而代之的更大 runner 决策在单个进程中保留完整主清单,并以串行套件作为独立完整性判据。
+
+优化后的发布验证器依赖由 `verify-package-invariants` 强制执行的清单 `files` 契约。如果发布规则超出该契约,结构门禁和两个暂存视图必须一起变化。
+
+兼容性 job 不再声称 TypeScript 本身已在每个 Node 运行时下执行。它们证明 Node 22、24 和 26 上对运行时敏感的源码加载,而主运行时负责唯一一次源码图类型检查。
diff --git a/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml b/.agents/notes/archived/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml
similarity index 62%
rename from .agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml
rename to .agents/notes/archived/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml
index 31ec0bdb07..b346069713 100644
--- a/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml
+++ b/.agents/notes/archived/process/2026-07-17-run-ci-examples-from-built-lib.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
-2026-07-17-run-ci-examples-from-built-lib.md: 22f69ed56bfc479281648bfb40df5acbd129ebc0
-2026-07-17-run-ci-examples-from-built-lib.zh.md: 74b985f578dd25f785e556c0cd493a7a9292fc43
+2026-07-17-run-ci-examples-from-built-lib.md: 3063bdcc8a170713151229a2a4cee22a91f1e8a9
+2026-07-17-run-ci-examples-from-built-lib.zh.md: b70367c1d25f4942043a60458b0197288c8094ba
diff --git a/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.md b/.agents/notes/archived/process/2026-07-17-run-ci-examples-from-built-lib.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.md
rename to .agents/notes/archived/process/2026-07-17-run-ci-examples-from-built-lib.md
index 22f69ed56b..3063bdcc8a 100644
--- a/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.md
+++ b/.agents/notes/archived/process/2026-07-17-run-ci-examples-from-built-lib.md
@@ -1,6 +1,7 @@
# Agent Note: Run CI examples from built lib
Status: implemented
+Archived: 2026-07-27
English | [中文](2026-07-17-run-ci-examples-from-built-lib.zh.md)
diff --git a/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.zh.md b/.agents/notes/archived/process/2026-07-17-run-ci-examples-from-built-lib.zh.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.zh.md
rename to .agents/notes/archived/process/2026-07-17-run-ci-examples-from-built-lib.zh.md
index 74b985f578..b70367c1d2 100644
--- a/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.zh.md
+++ b/.agents/notes/archived/process/2026-07-17-run-ci-examples-from-built-lib.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 在 CI 中从构建后的 lib 运行示例
Status: implemented
+Archived: 2026-07-27
[English](2026-07-17-run-ci-examples-from-built-lib.md) | 中文
diff --git a/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.i18n.yaml b/.agents/notes/archived/process/2026-07-20-generated-cordis-core-api.i18n.yaml
similarity index 63%
rename from .agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.i18n.yaml
rename to .agents/notes/archived/process/2026-07-20-generated-cordis-core-api.i18n.yaml
index 6bae3b4d87..9d9515dd97 100644
--- a/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.i18n.yaml
+++ b/.agents/notes/archived/process/2026-07-20-generated-cordis-core-api.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
-2026-07-20-generated-cordis-core-api.md: 848dec2dba6f432c706798c40abe98e8937da651
-2026-07-20-generated-cordis-core-api.zh.md: c40a480224f4e1387b71ade9264458cd84403584
+2026-07-20-generated-cordis-core-api.md: b6a66e518744354771f25d44d662e2655fa7c758
+2026-07-20-generated-cordis-core-api.zh.md: 82dbb308386b7adc678c6e4bf8ef3f06c3b2df77
diff --git a/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.md b/.agents/notes/archived/process/2026-07-20-generated-cordis-core-api.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.md
rename to .agents/notes/archived/process/2026-07-20-generated-cordis-core-api.md
index 848dec2dba..b6a66e5187 100644
--- a/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.md
+++ b/.agents/notes/archived/process/2026-07-20-generated-cordis-core-api.md
@@ -1,6 +1,7 @@
# Agent Note: Generate the Cordis core API reference
Status: implemented
+Archived: 2026-07-27
English | [中文](2026-07-20-generated-cordis-core-api.zh.md)
diff --git a/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.zh.md b/.agents/notes/archived/process/2026-07-20-generated-cordis-core-api.zh.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.zh.md
rename to .agents/notes/archived/process/2026-07-20-generated-cordis-core-api.zh.md
index c40a480224..82dbb30838 100644
--- a/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.zh.md
+++ b/.agents/notes/archived/process/2026-07-20-generated-cordis-core-api.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 生成 Cordis 核心 API 参考文档
Status: implemented
+Archived: 2026-07-27
[English](2026-07-20-generated-cordis-core-api.md) | 中文
diff --git a/.agents/notes/implemented/process/2026-07-21-doc-sync-through-gate-scheduler.i18n.yaml b/.agents/notes/archived/process/2026-07-21-doc-sync-through-gate-scheduler.i18n.yaml
similarity index 61%
rename from .agents/notes/implemented/process/2026-07-21-doc-sync-through-gate-scheduler.i18n.yaml
rename to .agents/notes/archived/process/2026-07-21-doc-sync-through-gate-scheduler.i18n.yaml
index 8bd4745529..8ca2ff1db1 100644
--- a/.agents/notes/implemented/process/2026-07-21-doc-sync-through-gate-scheduler.i18n.yaml
+++ b/.agents/notes/archived/process/2026-07-21-doc-sync-through-gate-scheduler.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
-2026-07-21-doc-sync-through-gate-scheduler.md: b7e41ba4aeac8ea03c706acadd481eee26abd5c2
-2026-07-21-doc-sync-through-gate-scheduler.zh.md: 56699747b1ba97fd90f7d53ab0deebc73ac775ef
+2026-07-21-doc-sync-through-gate-scheduler.md: ba7eeb75e65e5fc342ad7d3b9055d60a4e6f5770
+2026-07-21-doc-sync-through-gate-scheduler.zh.md: 7723a409e2a5d1ee046869403d9f4aadee10757d
diff --git a/.agents/notes/implemented/process/2026-07-21-doc-sync-through-gate-scheduler.md b/.agents/notes/archived/process/2026-07-21-doc-sync-through-gate-scheduler.md
similarity index 86%
rename from .agents/notes/implemented/process/2026-07-21-doc-sync-through-gate-scheduler.md
rename to .agents/notes/archived/process/2026-07-21-doc-sync-through-gate-scheduler.md
index b7e41ba4ae..ba7eeb75e6 100644
--- a/.agents/notes/implemented/process/2026-07-21-doc-sync-through-gate-scheduler.md
+++ b/.agents/notes/archived/process/2026-07-21-doc-sync-through-gate-scheduler.md
@@ -1,6 +1,7 @@
# Agent Note: doc-sync through the gate scheduler
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-21-doc-sync-through-gate-scheduler.zh.md)
@@ -10,7 +11,7 @@ English | [中文](2026-07-21-doc-sync-through-gate-scheduler.zh.md)
## Decision
-`doc-sync` in `package.json` delegates to the existing bounded scheduler — `tsx scripts/run-gates.ts doc-sync` — like the `check:ci:*` scripts ([parallel gate scheduling](2026-07-06-parallel-pre-push-gates.md), [parallel GitHub CI gates](2026-07-06-parallel-github-ci-gates.md)). The `doc-sync` mode expands to exactly `docSyncLeafGates()`, making the leaf list in `run-gates.ts` the single source of truth for the member set. The local mode caps default concurrency at four workers because several doc gates each build a full `ts.Program`; `DSH_GATE_CONCURRENCY` still overrides.
+`doc-sync` in `package.json` delegates to the existing bounded scheduler — `tsx scripts/run-gates.ts doc-sync` — like the `check:ci:*` scripts ([parallel gate scheduling](2026-07-06-parallel-pre-push-gates.md), [current CI topology](2026-07-22-evidence-based-larger-hosted-runners.md)). The `doc-sync` mode expands to exactly `docSyncLeafGates()`, making the leaf list in `run-gates.ts` the single source of truth for the member set. The local mode caps default concurrency at four workers because several doc gates each build a full `ts.Program`; `DSH_GATE_CONCURRENCY` still overrides.
`docSyncLeafGates` includes `verify-cordis-api`, so relevant local documentation checks and CI gate the generated runtime API catalog alongside the other generated docs.
diff --git a/.agents/notes/implemented/process/2026-07-21-doc-sync-through-gate-scheduler.zh.md b/.agents/notes/archived/process/2026-07-21-doc-sync-through-gate-scheduler.zh.md
similarity index 86%
rename from .agents/notes/implemented/process/2026-07-21-doc-sync-through-gate-scheduler.zh.md
rename to .agents/notes/archived/process/2026-07-21-doc-sync-through-gate-scheduler.zh.md
index 56699747b1..7723a409e2 100644
--- a/.agents/notes/implemented/process/2026-07-21-doc-sync-through-gate-scheduler.zh.md
+++ b/.agents/notes/archived/process/2026-07-21-doc-sync-through-gate-scheduler.zh.md
@@ -1,6 +1,7 @@
# Agent Note: doc-sync 走门禁调度器
Status: implemented
+Archived: 2026-07-26
[English](2026-07-21-doc-sync-through-gate-scheduler.md) | 中文
@@ -10,7 +11,7 @@ Status: implemented
## 决策
-`package.json` 中的 `doc-sync` 委托给既有的有界调度器——`tsx scripts/run-gates.ts doc-sync`——与各 `check:ci:*` 脚本的做法一致([并行门禁调度](2026-07-06-parallel-pre-push-gates.md)、[并行 GitHub CI 门禁](2026-07-06-parallel-github-ci-gates.md))。`doc-sync` 模式恰好展开为 `docSyncLeafGates()`,使 `run-gates.ts` 里的叶子列表成为成员集合的唯一真源。本地模式把默认并发上限设为四个 worker,因为多个文档门禁各自要构建完整的 `ts.Program`;`DSH_GATE_CONCURRENCY` 仍可覆盖。
+`package.json` 中的 `doc-sync` 委托给既有的有界调度器——`tsx scripts/run-gates.ts doc-sync`——与各 `check:ci:*` 脚本的做法一致([并行门禁调度](2026-07-06-parallel-pre-push-gates.md)、[当前 CI 拓扑](2026-07-22-evidence-based-larger-hosted-runners.md))。`doc-sync` 模式恰好展开为 `docSyncLeafGates()`,使 `run-gates.ts` 里的叶子列表成为成员集合的唯一真源。本地模式把默认并发上限设为四个 worker,因为多个文档门禁各自要构建完整的 `ts.Program`;`DSH_GATE_CONCURRENCY` 仍可覆盖。
`docSyncLeafGates` 包含 `verify-cordis-api`,因此相关的本地文档检查与 CI 会同其他生成文档一起把关生成的运行时 API 目录。
diff --git a/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.i18n.yaml b/.agents/notes/archived/process/2026-07-22-cordis-tutorial-docs.i18n.yaml
similarity index 65%
rename from .agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.i18n.yaml
rename to .agents/notes/archived/process/2026-07-22-cordis-tutorial-docs.i18n.yaml
index b2aa854cf3..25159413b2 100644
--- a/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.i18n.yaml
+++ b/.agents/notes/archived/process/2026-07-22-cordis-tutorial-docs.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
-2026-07-22-cordis-tutorial-docs.md: 45abb8f524218ce0b2678606ae62c7bf2dbca00b
-2026-07-22-cordis-tutorial-docs.zh.md: cd1a62e2a6e7f2e28bc32109dd67746840f8b8b7
+2026-07-22-cordis-tutorial-docs.md: a882f3f58fe4be5388bf142424e0e99e140410fc
+2026-07-22-cordis-tutorial-docs.zh.md: 4caf03ee05a120584dabef91f6ebafc78594d43a
diff --git a/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.md b/.agents/notes/archived/process/2026-07-22-cordis-tutorial-docs.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.md
rename to .agents/notes/archived/process/2026-07-22-cordis-tutorial-docs.md
index 45abb8f524..a882f3f58f 100644
--- a/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.md
+++ b/.agents/notes/archived/process/2026-07-22-cordis-tutorial-docs.md
@@ -1,6 +1,7 @@
# Agent Note: Tutorial-style Cordis docs under docs/cordis-tutorial
Status: implemented
+Archived: 2026-07-27
English | [中文](2026-07-22-cordis-tutorial-docs.zh.md)
diff --git a/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.zh.md b/.agents/notes/archived/process/2026-07-22-cordis-tutorial-docs.zh.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.zh.md
rename to .agents/notes/archived/process/2026-07-22-cordis-tutorial-docs.zh.md
index cd1a62e2a6..4caf03ee05 100644
--- a/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.zh.md
+++ b/.agents/notes/archived/process/2026-07-22-cordis-tutorial-docs.zh.md
@@ -1,6 +1,7 @@
# Agent Note: `docs/cordis-tutorial` 下的 Cordis 实操教程文档
Status: implemented
+Archived: 2026-07-27
[English](2026-07-22-cordis-tutorial-docs.md) | 中文
diff --git a/.agents/notes/implemented/process/2026-07-22-installer-in-repo-skip-clone.i18n.yaml b/.agents/notes/archived/process/2026-07-22-installer-in-repo-skip-clone.i18n.yaml
similarity index 62%
rename from .agents/notes/implemented/process/2026-07-22-installer-in-repo-skip-clone.i18n.yaml
rename to .agents/notes/archived/process/2026-07-22-installer-in-repo-skip-clone.i18n.yaml
index a6becde554..20649748cb 100644
--- a/.agents/notes/implemented/process/2026-07-22-installer-in-repo-skip-clone.i18n.yaml
+++ b/.agents/notes/archived/process/2026-07-22-installer-in-repo-skip-clone.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
-2026-07-22-installer-in-repo-skip-clone.md: f63c438205f7bd6aeb8dd78941bbe0880a8e31a1
-2026-07-22-installer-in-repo-skip-clone.zh.md: f9fe4865ad1090211c094fc8fba843b623512cc9
+2026-07-22-installer-in-repo-skip-clone.md: 607ce3baf842d437b64050a5ef17f3c7e4cffba5
+2026-07-22-installer-in-repo-skip-clone.zh.md: 17260427d514b5fd1c87c1d7faae5b1c8c1d9b0b
diff --git a/.agents/notes/implemented/process/2026-07-22-installer-in-repo-skip-clone.md b/.agents/notes/archived/process/2026-07-22-installer-in-repo-skip-clone.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-07-22-installer-in-repo-skip-clone.md
rename to .agents/notes/archived/process/2026-07-22-installer-in-repo-skip-clone.md
index f63c438205..607ce3baf8 100644
--- a/.agents/notes/implemented/process/2026-07-22-installer-in-repo-skip-clone.md
+++ b/.agents/notes/archived/process/2026-07-22-installer-in-repo-skip-clone.md
@@ -1,6 +1,7 @@
# Agent Note: installer skips the clone when run from inside a checkout
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-22-installer-in-repo-skip-clone.zh.md)
diff --git a/.agents/notes/implemented/process/2026-07-22-installer-in-repo-skip-clone.zh.md b/.agents/notes/archived/process/2026-07-22-installer-in-repo-skip-clone.zh.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-07-22-installer-in-repo-skip-clone.zh.md
rename to .agents/notes/archived/process/2026-07-22-installer-in-repo-skip-clone.zh.md
index f9fe4865ad..17260427d5 100644
--- a/.agents/notes/implemented/process/2026-07-22-installer-in-repo-skip-clone.zh.md
+++ b/.agents/notes/archived/process/2026-07-22-installer-in-repo-skip-clone.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 在检出目录内运行时安装脚本跳过克隆
Status: implemented
+Archived: 2026-07-26
[English](2026-07-22-installer-in-repo-skip-clone.md) | 中文
diff --git a/.agents/notes/implemented/process/2026-07-23-browser-demo-gif-recording.i18n.yaml b/.agents/notes/archived/process/2026-07-23-browser-demo-gif-recording.i18n.yaml
similarity index 63%
rename from .agents/notes/implemented/process/2026-07-23-browser-demo-gif-recording.i18n.yaml
rename to .agents/notes/archived/process/2026-07-23-browser-demo-gif-recording.i18n.yaml
index 1aee1563ad..9c3f69671d 100644
--- a/.agents/notes/implemented/process/2026-07-23-browser-demo-gif-recording.i18n.yaml
+++ b/.agents/notes/archived/process/2026-07-23-browser-demo-gif-recording.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
-2026-07-23-browser-demo-gif-recording.md: 096edf453d6b61c4d9046b284ef67a460edf4e88
-2026-07-23-browser-demo-gif-recording.zh.md: f5b8eac1c8dd57a59e9c2293ecc71511078a4896
+2026-07-23-browser-demo-gif-recording.md: 844e976c5be5acfca6b1f933256fb5a8a82f980a
+2026-07-23-browser-demo-gif-recording.zh.md: 12702daaca35dc26f8e9ad7aa1b803f9e4192a5c
diff --git a/.agents/notes/implemented/process/2026-07-23-browser-demo-gif-recording.md b/.agents/notes/archived/process/2026-07-23-browser-demo-gif-recording.md
similarity index 75%
rename from .agents/notes/implemented/process/2026-07-23-browser-demo-gif-recording.md
rename to .agents/notes/archived/process/2026-07-23-browser-demo-gif-recording.md
index 096edf453d..844e976c5b 100644
--- a/.agents/notes/implemented/process/2026-07-23-browser-demo-gif-recording.md
+++ b/.agents/notes/archived/process/2026-07-23-browser-demo-gif-recording.md
@@ -1,6 +1,7 @@
# Agent Note: Browser demo GIF recording
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-23-browser-demo-gif-recording.zh.md)
@@ -10,9 +11,9 @@ Browser demonstrations have been assembled with one-off capture and encoding com
## Decision
-The repository provides the [`record-browser-gif`](../../../skills/record-browser-gif/SKILL.md) skill for local browser-demo artifacts. It uses the available browser-control workflow, establishes whether the requested flow is real, fixture-backed, or otherwise simulated, and captures a small storyboard only after semantically observable UI states. Frames and the output live outside the Git worktree by default.
+The repository provides the [`record-browser-gif`](../../../skills/record-browser-gif/SKILL.md) skill for local browser-demo artifacts. It uses the available browser-control workflow, establishes whether the requested flow is real, fixture-backed, or otherwise simulated, and captures a small storyboard only after semantically observable UI states. Frames live under the repository's gitignored `.playwright-mcp/` directory — the browser tool writes only under its allowed roots — and never dirty the worktree.
-The bundled `encode_gif.py` helper orders frames lexically, assigns explicit hold durations, uses an `ffmpeg` palette pipeline, and validates source dimensions plus the encoded frame count, dimensions, duration, and byte limit through `ffprobe`. The workflow stops after returning the verified absolute GIF path; uploading the artifact and mutating a pull request, issue, or document remain separate workflows.
+The bundled `encode_gif.py` helper orders frames lexically, assigns explicit hold durations, uses an `ffmpeg` palette pipeline, and validates source dimensions plus the encoded frame count, dimensions, duration, and byte limit through `ffprobe`. Recording stops after returning the verified absolute GIF path; when the task includes attaching the GIF to a pull request, the [GUI-PR GIF evidence decision](2026-07-26-gui-pr-gif-evidence-and-assets-branch.md) owns the mandatory-evidence policy and the assets-branch publication step that follows.
## Alternatives considered
@@ -20,7 +21,7 @@ The bundled `encode_gif.py` helper orders frames lexically, assigns explicit hol
**Keep an inline `ffmpeg` recipe in the skill.** Reconstructing quoting, timing manifests, palette filters, overwrite behavior, and post-encode checks in every run is error-prone. A bundled helper keeps those mechanics executable while the skill owns capture judgment.
-**Include GitHub attachment and description editing.** Upload and remote mutation require separate authentication, confirmation, and recovery rules. Excluding them keeps invocation of a recording skill local and reversible.
+**Include GitHub attachment and description editing.** Upload and remote mutation require separate authentication, confirmation, and recovery rules. Keeping recording itself local and reversible preserves that boundary; the [GUI-PR GIF evidence decision](2026-07-26-gui-pr-gif-evidence-and-assets-branch.md) owns the bounded publication step for tasks that do attach the GIF to a pull request.
**Use a fixture whenever it is easier to stage.** Fixtures are valid when the requested demonstration is explicitly fixture-backed, but they do not substantiate a real-server or real-API claim. The skill preserves the requested provenance and reports a missing prerequisite instead of silently changing it.
diff --git a/.agents/notes/implemented/process/2026-07-23-browser-demo-gif-recording.zh.md b/.agents/notes/archived/process/2026-07-23-browser-demo-gif-recording.zh.md
similarity index 78%
rename from .agents/notes/implemented/process/2026-07-23-browser-demo-gif-recording.zh.md
rename to .agents/notes/archived/process/2026-07-23-browser-demo-gif-recording.zh.md
index f5b8eac1c8..12702daaca 100644
--- a/.agents/notes/implemented/process/2026-07-23-browser-demo-gif-recording.zh.md
+++ b/.agents/notes/archived/process/2026-07-23-browser-demo-gif-recording.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 浏览器演示 GIF 录制
Status: implemented
+Archived: 2026-07-26
[English](2026-07-23-browser-demo-gif-recording.md) | 中文
@@ -10,9 +11,9 @@ Status: implemented
## 决策
-仓库提供 [`record-browser-gif`](../../../skills/record-browser-gif/SKILL.md) skill(技能),用于生成本地浏览器演示产物。该 skill 使用当前可用的浏览器控制工作流,先确认请求的流程是真实流程、由 fixture 支撑,还是采用其他模拟方式,再仅在 UI 达到语义上可观察的状态后截取一组精简的分镜帧。帧文件与输出产物默认存放在 Git worktree 之外。
+仓库提供 [`record-browser-gif`](../../../skills/record-browser-gif/SKILL.md) skill(技能),用于生成本地浏览器演示产物。该 skill 使用当前可用的浏览器控制工作流,先确认请求的流程是真实流程、由 fixture 支撑,还是采用其他模拟方式,再仅在 UI 达到语义上可观察的状态后截取一组精简的分镜帧。帧文件存放在仓库 `.gitignore` 忽略的 `.playwright-mcp/` 目录下(浏览器工具只能写入其允许的根目录),不会弄脏 worktree。
-随附的 `encode_gif.py` 辅助脚本按词法顺序排列各帧,为每帧设置明确的停留时长,通过 `ffmpeg` 调色板流水线编码,并借助 `ffprobe` 校验源图像尺寸以及编码结果的帧数、尺寸、时长和字节上限。工作流在返回已验证的 GIF 绝对路径后即结束;上传产物以及修改 PR、issue 或文档仍属于独立的工作流。
+随附的 `encode_gif.py` 辅助脚本按词法顺序排列各帧,为每帧设置明确的停留时长,通过 `ffmpeg` 调色板流水线编码,并借助 `ffprobe` 校验源图像尺寸以及编码结果的帧数、尺寸、时长和字节上限。录制在返回已验证的 GIF 绝对路径后即结束;当任务包含把 GIF 附到 PR 时,[GUI PR 的 GIF 证据决策](2026-07-26-gui-pr-gif-evidence-and-assets-branch.md)拥有强制证据政策以及随后的 assets 分支发布步骤。
## 曾考虑的替代方案
@@ -20,7 +21,7 @@ Status: implemented
**在 skill 中保留内联 `ffmpeg` 配方。**每次运行都重新组装引号转义、时序清单、调色板过滤器、覆盖行为和编码后检查,容易出错。随附的辅助脚本使这些机制保持可执行,skill 则负责判断何时截取画面。
-**纳入 GitHub 附件上传与描述编辑。**上传和远程修改需要各自独立的身份认证、确认与恢复规则。将它们排除在外,可以使录制 skill 的调用保持本地且可撤销。
+**纳入 GitHub 附件上传与描述编辑。**上传和远程修改需要各自独立的身份认证、确认与恢复规则。让录制本身保持本地且可撤销即维护了这一边界;对确需把 GIF 附到 PR 的任务,[GUI PR 的 GIF 证据决策](2026-07-26-gui-pr-gif-evidence-and-assets-branch.md)拥有那个有边界的发布步骤。
**每当 fixture 更容易布置时就使用它。**当请求明确要求由 fixture 支撑演示时,使用 fixture 是有效的;但它无法为真实服务器或真实 API 的声明提供证据。该 skill 会保持请求指定的演示来源,并在缺少先决条件时报告问题,不会擅自更改来源。
diff --git a/.agents/notes/archived/process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.i18n.yaml b/.agents/notes/archived/process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.i18n.yaml
new file mode 100644
index 0000000000..4403ee72d8
--- /dev/null
+++ b/.agents/notes/archived/process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.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
+2026-07-26-gui-pr-gif-evidence-and-assets-branch.md: 18e56e44190e0c44fcf2cae8bf1d66f91ef9a4c7
+2026-07-26-gui-pr-gif-evidence-and-assets-branch.zh.md: 945dd7a0242d9829334ddd79ab569c6723a64185
diff --git a/.agents/notes/archived/process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.md b/.agents/notes/archived/process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.md
new file mode 100644
index 0000000000..18e56e4419
--- /dev/null
+++ b/.agents/notes/archived/process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.md
@@ -0,0 +1,40 @@
+# Agent Note: GUI pull request GIF evidence and assets-branch publication
+
+Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-07-26-gui-pr-gif-evidence-and-assets-branch.zh.md)
+
+## Problem
+
+A pull request that changes what a product user sees in the GUI is otherwise reviewed through prose and test names, neither of which shows the rendered result. The [browser-demo GIF recording](../../../skills/record-browser-gif/SKILL.md) skill produces truthful local GIFs but deliberately stopped at the local artifact, so each pull request that wanted to show one re-derived publication on its own — and committing the GIF to the pull request branch is never acceptable, because binary media in history bloats every future clone permanently.
+
+The recording procedure itself also kept being re-learned failure by failure: screenshots written outside the browser tool's allowed roots or into missing directories fail at capture time, transient UI states polled across separate tool calls are lost because the turn settles between calls, substring completion predicates match the echo of the user's own prompt, and an inline environment-variable assignment on the encoder command expands too late to take effect.
+
+## Decision
+
+Every pull request that changes product-user-visible GUI behavior includes a demonstration GIF recorded with the [record-browser-gif skill](../../../skills/record-browser-gif/SKILL.md), with real provenance — a real server booted from that pull request's own branch tree, a real API key, and real model rounds — stated next to the embed. Fixture provenance is acceptable only when the user explicitly asked for it.
+
+The GIF is published to a dedicated orphan assets branch — no parent commit, media only — never to the pull request branch; one assets branch serves a whole pull request series (existing branches: `code-mode-ui-assets`, `pr-613-assets`). Publication works in a shallow single-branch scratch clone, commits as `assets: gif (#)`, and the pull request body embeds the blob URL with the required `?raw=true` suffix. Assets branches are append-only: merged pull request bodies reference their URLs forever, so an assets branch is never rewritten or deleted.
+
+Recording itself stays side-effect-free; publication is a bounded final step the skill performs only when the task includes attaching the GIF to a pull request. The [record-browser-gif skill](../../../skills/record-browser-gif/SKILL.md) remains the current contract for the recording half.
+
+The skill folds in the operational lessons recording earned: frames go under `.playwright-mcp/`, ignored by the repository `.gitignore` and created before capture, because the browser tool writes only under its allowed roots and resolves relative names against the repository root; each pull request stages its own built tree with a fresh scratch workspace and a new session per scenario, and servers are stopped by PID rather than a broad process-name pattern; transient states are captured by driving a slow foreground operation and polling a concrete DOM marker inside one browser-script call; completion predicates match an exact-text element rather than a substring; and the encoder runs with `GIF_SKILL_DIR` exported on its own line, per-frame durations holding the settled state longest, and both a JSON-summary check and a visual read of the encoded GIF.
+
+## Alternatives considered
+
+**Commit the GIF to the pull request branch.** Binary media merged into the default branch stays in history for every future clone and fetch; a demo GIF's value ends at review while its cost never does.
+
+**Attach the GIF as a GitHub upload.** Drag-and-drop `user-attachments` uploads are not available to a command-line workflow, cannot be re-created or audited from the repository, and leave the media's lifecycle outside repository control.
+
+**Store GIFs with Git LFS.** LFS still couples media to the code branch's history, adds an infrastructure dependency to every clone and CI fetch, and buys nothing over an isolated branch that ordinary git already supports.
+
+**One assets branch per pull request.** A branch per pull request sprawls the ref namespace and multiplies scratch clones during a series; one branch per series keeps publication a single push while staying isolated from code history.
+
+**Keep publication out of the recording skill.** That was the prior state; it preserved a clean boundary but made every pull request re-derive the same procedure. The boundary survives as an explicit gate — publication runs only when the task includes attaching the GIF to a pull request — instead of as omission.
+
+**Leave the GIF optional per pull request.** Optional evidence disappears under schedule pressure exactly where it matters most; a GUI change reviewed without a recording asks reviewers to imagine the rendered result or rebuild the branch themselves.
+
+## Consequences
+
+Every GUI pull request carries visual evidence with stated provenance, and reviewers see the change without rebuilding the branch. Repository history stays free of media; the cost moves to append-only assets branches that grow forever, stay cheap to clone shallowly, and can never be deleted. Mandatory real-provenance recording adds a real-key, real-model round to every GUI pull request's workflow — deliberate, because that run is the evidence. The recording half remains locally reversible, and a GIF request whose task does not include attaching it to a pull request still ends at the verified local artifact.
diff --git a/.agents/notes/archived/process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.zh.md b/.agents/notes/archived/process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.zh.md
new file mode 100644
index 0000000000..945dd7a024
--- /dev/null
+++ b/.agents/notes/archived/process/2026-07-26-gui-pr-gif-evidence-and-assets-branch.zh.md
@@ -0,0 +1,40 @@
+# Agent Note: GUI PR 的 GIF 证据与 assets 分支发布
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-07-26-gui-pr-gif-evidence-and-assets-branch.md) | 中文
+
+## 问题
+
+改变产品用户在 GUI 中所见行为的 PR(Pull Request),此前只能通过文字描述和测试名称接受评审,两者都无法展示渲染结果。[浏览器演示 GIF 录制](../../../skills/record-browser-gif/SKILL.md)对应的 skill(技能)能生成真实可信的本地 GIF,但刻意止步于本地产物,于是每个想展示 GIF 的 PR 都得各自重新摸索发布方式;而把 GIF 提交到 PR 分支从来不可接受:进入历史的二进制媒体会永久增大之后每一次克隆的体积。
+
+录制流程本身也在靠一次次失败反复重新学习:截图写到浏览器工具允许的根目录之外或写入不存在的目录,会在截取时直接失败;跨多次工具调用轮询的瞬态 UI 状态会丢失,因为调用之间轮次已经结算;用子串匹配做完成判定会命中用户自己提示词的回显;在编码器命令上内联赋值环境变量则因参数先于赋值展开而不生效。
+
+## 决策
+
+每个改变产品用户可见 GUI 行为的 PR 都包含一个用 [record-browser-gif skill](../../../skills/record-browser-gif/SKILL.md) 录制的演示 GIF,其来源必须真实:从该 PR 自身分支树启动的真实服务器、真实 API 密钥、真实的模型轮次,并在嵌入处注明来源。只有当用户明确要求 fixture(测试前置数据)来源时才可使用 fixture。
+
+GIF 发布到专用的孤儿(orphan)assets 分支上:该分支没有父提交、只含媒体,GIF 绝不进入 PR 自己的分支;一个 assets 分支服务整个 PR 系列(现有分支:`code-mode-ui-assets`、`pr-613-assets`)。发布在浅层单分支的临时克隆中进行,提交信息形如 `assets: gif (#)`,PR 正文用带必需 `?raw=true` 后缀的 blob URL 嵌入。assets 分支只允许追加:已合并的 PR 正文会永远引用其 URL,因此 assets 分支绝不重写或删除。
+
+录制本身保持无副作用;发布是一个有边界的收尾步骤,仅当任务包含把 GIF 附到 PR 时才由该 skill 执行。[record-browser-gif skill](../../../skills/record-browser-gif/SKILL.md) 仍是录制部分的现行契约。
+
+该 skill 还吸收了录制实践换来的操作经验:帧文件放在仓库 `.gitignore` 忽略的 `.playwright-mcp/` 目录下并在截取前先创建,因为浏览器工具只能写入其允许的根目录,相对文件名也相对仓库根目录解析;每个 PR 从自己构建的分支树启动服务,配以全新的临时工作区目录,每个录制场景新开会话,停止服务器时按 PID 精确匹配而不是用宽泛的进程名模式;瞬态状态靠驱动一个缓慢的前台操作、并在同一次浏览器脚本调用内轮询具体的 DOM 标记来截取;完成判定匹配精确文本元素而非子串;编码器在单独一行 export `GIF_SKILL_DIR` 之后运行,逐帧时长让最终稳定状态停留最久,并同时核对 JSON 摘要与目视检查编码后的 GIF。
+
+## 曾考虑的替代方案
+
+**把 GIF 提交到 PR 分支。**合入默认分支的二进制媒体会留在历史中,影响之后的每一次克隆和拉取;演示 GIF 的价值止于评审,代价却永不消失。
+
+**作为 GitHub 附件上传。**拖拽产生的 `user-attachments` 上传对命令行工作流不可用,无法从仓库重建或审计,媒体的生命周期也脱离仓库的控制。
+
+**用 Git LFS 存储 GIF。**LFS 仍把媒体耦合进代码分支的历史,给每次克隆和 CI 拉取增加一项基础设施依赖,相比普通 git 即可支持的隔离分支没有任何额外收益。
+
+**每个 PR 一个 assets 分支。**按 PR 建分支会让 ref 命名空间蔓延,并在一个系列内成倍增加临时克隆;每个系列一个分支让发布只需一次推送,同时仍与代码历史隔离。
+
+**把发布留在录制 skill 之外。**这是此前的状态;它保住了干净的边界,却让每个 PR 重新摸索同一套流程。这个边界如今以显式条件的形式保留:仅当任务包含把 GIF 附到 PR 时才执行发布,而不是靠省略来体现。
+
+**让 GIF 在每个 PR 中保持可选。**可选的证据恰恰会在最需要它的进度压力下消失;没有录制的 GUI 变更评审,等于要求评审人自行想象渲染结果或重新构建分支。
+
+## 后果
+
+每个 GUI PR 都携带注明来源的可视证据,评审人无需重新构建分支即可看到变更。仓库历史保持不含媒体;代价转移到只追加的 assets 分支上:它们会持续增长、可以低成本地浅克隆、且永远不能删除。强制的真实来源录制给每个 GUI PR 的工作流增加一次真实密钥、真实模型轮次的运行,这是有意为之,因为这次运行本身就是证据。录制部分仍然在本地可撤销;任务不包含附到 PR 的 GIF 请求,仍以已验证的本地产物结束。
diff --git a/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.i18n.yaml b/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.i18n.yaml
new file mode 100644
index 0000000000..fe51aa6304
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.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
+2026-06-20-drop-unconsumed-llm-adapter-change-event.md: ad05be999158a225230b4a1d760983b71075aada
+2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md: 0efc0193b3d63b7df6e1671afed0d1faebeaa2af
diff --git a/.agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md b/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md
similarity index 97%
rename from .agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md
rename to .agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md
index d8d4015b0d..ad05be9991 100644
--- a/.agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md
+++ b/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md
@@ -1,6 +1,9 @@
# Agent Note: Drop the unconsumed `llm/adapter-change` event
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md)
## Problem
diff --git a/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md b/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md
new file mode 100644
index 0000000000..0efc0193b3
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md
@@ -0,0 +1,37 @@
+# Agent Note: 移除未被消费的 `llm/adapter-change` 事件
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-06-20-drop-unconsumed-llm-adapter-change-event.md) | 中文
+
+## 问题
+
+`LlmService.registerAdapter()` 在注册和 dispose(资源释放)时发出 `llm/adapter-change` 事件([packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts))。在 `packages/*/src` 和 `examples/*/src` 中搜索 `llm/adapter-change`,只能找到声明、emit 站点、文档和测试;没有任何生产环境的监听器订阅它。
+
+这与 `tools/change` 和 `system-prompt/change` 不同。如今这两个事件同样没有消费方,但它们有望成为未来实时工具/提示词 UI 的注册表变更信号。LLM(大语言模型)适配器注册更像是启动时的实现细节:适配器不是用户可见的选项面板,真正的模型调用拦截 seam 是 `llm/stream`。保留一个没有监听器的适配器变更事件,只是在更小范围内重复[删除无用 summary](2026-06-19-drop-mutable-session-summary.md) 的模式。
+
+这个事件并非零成本。`registerAdapter()` 在发出 `llm/adapter-change` 之前先 yield 回滚 disposer,这样抛出异常的监听器会回退变更而非泄漏适配器条目;包内还有针对该监听器抛出路径的测试。这种防御性排序保护的是一个只有测试才能触发的失败模式。
+
+## 决策
+
+只移除 `llm/adapter-change`:包括 `dsh-llm` 的 `interface Events` 中的声明、`ctx.emit('llm/adapter-change')` 调用,以及 `LlmService.registerAdapter` JSDoc 中“在注册和释放时发出 `llm/adapter-change`”的句子。`registerAdapter()` 的效应生成器为 HMR(热模块替换)/释放保留变更与回滚 disposer,但移除仅因该事件而存在的监听器抛错回滚顺序。适配器 disposer 测试断言返回的 disposer 会移除适配器,不再订阅事件;监听器抛错回滚测试则随其测试对象一起消失。[docs/architecture.md](../../../../docs/architecture.md) 和 [packages/llm/llm/README.md](../../../../packages/llm/llm/README.md) 中的事件分类也在同一变更中更新。
+
+## 曾考虑的替代方案
+
+### 为什么不移除所有注册表变更事件?
+
+由注册表通告变更的微内核是一种一致的约定。当 UI 能够实时刷新可用工具或提示词章节时,`tools/change` 和 `system-prompt/change` 可能会有用。本 Agent Note(agent 决策记录)在存在合理用户侧消费方的位置保留该约定,只删除当前及可能的未来消费方都不明确的适配器变更事件。
+
+如果将来需要 LLM 适配器浏览器或动态模型选择器用到此信号,届时再连同消费方一起重新引入,并提供比「something changed」更清晰的 payload。
+
+## 验证
+
+`llm/adapter-change` 及其 emit 已消失,重新生成的 Cordis 目录保持新鲜;HMR 安全性仍成立(释放贡献该适配器的 fiber 会移除它);`tools/change` 和 `system-prompt/change` 仍有文档与测试;ACP(Agent Client Protocol)快照和无密钥 Headless Loader 冒烟则固定了未变的生产路径。
+
+## 后果
+
+- **移除一个已文档化的 emit 事件属于公开接口变更。** 它出现在分类体系表中,读起来像有意设计的 API。但「已声明且已发出」不等于「已被消费」——这与移除可变 summary 时的判断依据相同。分类体系表在同一个变更中更新,因此文档不会漂移。
+- **注册表变更约定变得不均匀。** 这是可接受的,因为 LLM 适配器注册与工具或提示词段落不是同一层面的面向用户概念。不均匀但诚实,胜过统一但无用。
+
+这是一个小裁剪,但它退役了一条守护着并不存在的消费方的正确性不变式。
diff --git a/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.i18n.yaml b/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.i18n.yaml
new file mode 100644
index 0000000000..46c237e63f
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.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
+2026-06-20-drop-unconsumed-llm-assembled-surfaces.md: fd3d48e0918f8395b6c94e407b889ec3a6de7fbe
+2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md: 83d631329ccddb0f8880f223884ae4dc75a14d09
diff --git a/.agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md b/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
similarity index 97%
rename from .agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
rename to .agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
index b482a444b5..fd3d48e091 100644
--- a/.agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
+++ b/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
@@ -1,6 +1,9 @@
# Agent Note: Drop unconsumed assembled LLM convenience surfaces
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md)
## Problem
diff --git a/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md b/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md
new file mode 100644
index 0000000000..83d631329c
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md
@@ -0,0 +1,40 @@
+# Agent Note: 移除未被消费的 LLM 组装便捷接口
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-06-20-drop-unconsumed-llm-assembled-surfaces.md) | 中文
+
+## 问题
+
+`LlmService`([packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts))在模型之上暴露了三个调用接口:
+
+- `stream()`:原始 `StreamChunk`,通过 `llm/stream` waterfall(瀑布式事件)分发。
+- `streamBlocks()`:一个「便捷视图」,将分片送入 `BlockAssembler` 并按流顺序产出已组装的 `ContentBlock`([index.ts:137-144](../../../../packages/llm/llm/src/index.ts))。
+- `generate()`:一个完整组装的 `GenerateResult`,通过第二条 `llm/generate` waterfall 分发([index.ts:151-157](../../../../packages/llm/llm/src/index.ts))。
+
+LLM(大语言模型)服务唯一的生产消费方是 agent loop(智能体循环),它只使用 `stream()`:将原始分片送入自己的 `BlockAssembler`,以便在并行组装的同时记录分片,保证回放保真度([packages/core/agent-loop/src/loop.ts](../../../../packages/core/agent-loop/src/loop.ts),`ctx.llm.stream(req)` 步骤)。在 `packages/*/src` 和 `examples/*/src` 中 grep `streamBlocks` 与 `ctx.llm.generate`,找不到任何生产调用方。仅有的引用来自服务方法定义、文档和测试;适配器测试用 `generate()` 作为便捷驱动,但它们完全可以通过同一个 assembler 辅助函数手动消费 `stream()`,无需为此保留一个公开的生产 API。
+
+这属于[删除可变会话 summary](2026-06-19-drop-mutable-session-summary.md) 的同类模式:带有受测契约的组装视图 API,由测试而非生产代码消费。它们是为不关心 token 级增量的消费方推测性构建的,但唯一的真实消费方恰恰关心增量,以便持久化高保真重放数据。
+
+`streamBlocks()` 拖带了 `BlockAssembler` 的一块专用逻辑:`flushReady()` 与 `flushRemaining()`([packages/llm/llm/src/assembler.ts:138-168](../../../../packages/llm/llm/src/assembler.ts))以及 `flushed` 游标字段,仅为支持按序增量产出而存在。`generate()` 拖带了 `GenerateResult`、`BlockAssembler.result()` 以及 `llm/generate` waterfall——在同一底层流之上的第二个拦截面。agent loop 对 assembler 的使用仅限于 `push()` / `message()` / `usage` / `finish`,不涉及流式 flush 或一次性服务组装。
+
+## 决策
+
+`stream()` 是唯一的公开 LLM 调用接口。移除 `streamBlocks`、`generate`、其事件/结果类型,以及仅被该路径使用的 assembler 辅助方法。适配器测试通过本地辅助函数对公开流进行组装;`BlockAssembler` 仅保留有生产消费方的操作。
+
+## 曾考虑的替代方案
+
+**保留 `generate()` 作为仅供测试的便捷方法**:否决。适配器测试通过共享 assembler 手动消费 `stream()`,走的是与生产完全相同的流式路径;一个唯一调用方只有测试的公开方法,正是 [drop-mutable-summary 先例](2026-06-19-drop-mutable-session-summary.md)所淘汰的死接口形态。未来如果有消费方需要不带增量的组装块,届时再为该消费方引入一个聚焦的辅助方法。
+
+## 验证
+
+`streamBlocks`、`generate`、`llm/generate` 及仅供它们使用的 assembler 辅助函数均已移除,且未产生新的无用导出;两个真实适配器都通过 `stream()` 和共享 assembler 接受测试;循环行为保持一致(ACP(Agent Client Protocol)快照预期输出未变);README、架构文档和模块文档也不再提及已删除表面。
+
+## 后果
+
+- **从一个核心词汇包中移除了公开方法。** 未来如果有插件需要不带增量的组装块,它需要直接调用 `stream()` 并使用 `BlockAssembler`,或在有真实消费方时重新引入一个聚焦的辅助方法。鉴于预发布阶段「基础优先于预设未来」的立场([AGENTS.md](../../../../AGENTS.md)),现在正是裁剪仅供测试的公开接口的合适时机。
+- **适配器测试变得更显式。** 它们失去了便捷的 `generate()` 包装层,但这是有益的压力:测试走的是与生产相同的流式路径。
+- **waterfall 使用者失去 `llm/generate`。** 不存在生产监听者。未来的缓存/重试/日志插件应包装 `llm/stream`,它仍然是唯一的提供方调用路径。
+
+改动规模不大,但它从 LLM 包中干净地移除了预设的接口面积,为生产和测试留下唯一一份模型调用契约。
diff --git a/.agents/notes/archived/simplification/2026-06-20-prune-dead-seam-methods.i18n.yaml b/.agents/notes/archived/simplification/2026-06-20-prune-dead-seam-methods.i18n.yaml
new file mode 100644
index 0000000000..20e305dae0
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-06-20-prune-dead-seam-methods.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
+2026-06-20-prune-dead-seam-methods.md: 4f292803ff34e504502d8b2b427ebbc769088968
+2026-06-20-prune-dead-seam-methods.zh.md: 325d41536d21686b95618d5d92ef8a946f7b8ddb
diff --git a/.agents/notes/implemented/simplification/2026-06-20-prune-dead-seam-methods.md b/.agents/notes/archived/simplification/2026-06-20-prune-dead-seam-methods.md
similarity index 82%
rename from .agents/notes/implemented/simplification/2026-06-20-prune-dead-seam-methods.md
rename to .agents/notes/archived/simplification/2026-06-20-prune-dead-seam-methods.md
index f74de250ef..4f292803ff 100644
--- a/.agents/notes/implemented/simplification/2026-06-20-prune-dead-seam-methods.md
+++ b/.agents/notes/archived/simplification/2026-06-20-prune-dead-seam-methods.md
@@ -1,6 +1,9 @@
# Agent Note: Prune dead methods from the persistence seam
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-06-20-prune-dead-seam-methods.zh.md)
> **Implementation note:** Only `SessionPersistence.has()` and `.delete()` were removed. `BashExecutor.get()` and `.list()` remain because removing their one-line lookup surface required substantially more completion-tracking machinery in consumers. Their id branding is covered by the [branded-ids Agent Note](../architecture/2026-06-20-branded-ids.md).
@@ -10,7 +13,7 @@ A capability seam ([interface / implementation / consumer](../architecture/2026-
### `SessionPersistence.has()` and `.delete()`
-The abstract service declared its operations beyond create/append: `load`, `list`, `has`, `delete`. Production consumers of `ctx.sessionPersistence` use only two: the agent-loop resume path calls `load()` ([packages/core/agent-loop/src/index.ts:176](../../../../packages/core/agent-loop/src/index.ts)), and the ACP bridge calls `list()` for `session/list` ([packages/ui/acp/src/index.ts:494](../../../../packages/ui/acp/src/index.ts)). Grepping every `sessionPersistence.*` / `persistence.*` use across `packages/*/src` and `examples/` finds no `has(` and no `delete(` on the service. The `.has(`/`.delete(` calls in `packages/ui/acp/src/index.ts` are on the in-memory `SessionStore` and a local `Set` of loading ids, not persistence. The only callers of `has`/`delete` were the contract suites and per-backend specs.
+The abstract service declared its operations beyond create/append: `load`, `list`, `has`, `delete`. Production consumers use `load()` and `list()` for resume and session discovery, while no production caller uses persistence `has()` or `delete()`. The similarly named in-memory collection calls in protocol and UI code are unrelated. The only callers of persistence `has`/`delete` were the contract suites and per-backend specs.
`has()` was not just unused: it added a tracked-vs-untracked coordinator probe and a contract branch even though `loadStored(id)` already owns durable existence checks. `delete()` dragged the `deleteStored` backend hook that every backend had to implement. This is the [drop-mutable-session-summary](2026-06-19-drop-mutable-session-summary.md) pattern: a contract test exercised both, but no shipping code asks "is this session persisted?" or removes one.
@@ -31,7 +34,7 @@ Re-adding a seam method with a live consumer is cheap and better-designed than t
## Verification
-`has`/`delete`/`deleteStored` are gone from the persistence seam, impl, and contract suites with no new dead exports; the remaining operations (`create`/`append`/`load`/`list`) are untouched, with ACP `session/list` and crash-recovery behaving identically; and the seam README and `docs/architecture.md` list only the surviving methods.
+`has`/`delete`/`deleteStored` are gone from the persistence seam, impl, and contract suites with no new dead exports; the remaining operations (`create`/`append`/`load`/`list`) are untouched, with persistence-backed session queries and crash recovery behaving identically; and the seam README and `docs/architecture.md` list only the surviving methods.
## Consequences
diff --git a/.agents/notes/archived/simplification/2026-06-20-prune-dead-seam-methods.zh.md b/.agents/notes/archived/simplification/2026-06-20-prune-dead-seam-methods.zh.md
new file mode 100644
index 0000000000..325d41536d
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-06-20-prune-dead-seam-methods.zh.md
@@ -0,0 +1,44 @@
+# Agent Note: 从持久化 seam 中移除无用方法
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-06-20-prune-dead-seam-methods.md) | 中文
+
+> **实现说明:** 仅移除了 `SessionPersistence.has()` 和 `.delete()`。`BashExecutor.get()` 和 `.list()` 仍然保留,因为删除它们的单行查找表面会要求消费方增加显著更多的完成跟踪机制。其 id 品牌化由[品牌化 id Agent Note(agent 决策记录)](../architecture/2026-06-20-branded-ids.md)负责。
+
+## 问题
+
+能力 seam([接口 / 实现 / 消费方](../architecture/2026-06-13-capability-seams.md))承载了没有消费方调用的抽象方法。seam 的存在是为了让实现和消费方独立演进——但没有消费方以之编程的方法不是 seam,而是每个实现仍必须实现和测试的推测性表面。
+
+### `SessionPersistence.has()` 与 `.delete()`
+
+该抽象服务在 create/append 之外声明了更多操作:`load`、`list`、`has`、`delete`。生产消费方用 `load()` 和 `list()` 完成恢复与会话发现,而没有任何生产调用方使用持久化的 `has()` 或 `delete()`。协议和 UI 代码中名称相似的内存集合调用与此无关。持久化 `has`/`delete` 的唯一调用者是契约测试套件和各后端的 spec。
+
+`has()` 不仅未被使用:在 `loadStored(id)` 已负责持久化存在性检查的情况下,它仍增加了协调器的已跟踪/未跟踪探测和一个契约分支。`delete()` 则拖入每个后端都必须实现的 `deleteStored` 后端钩子。这属于[删除可变会话 summary](2026-06-19-drop-mutable-session-summary.md) 的同类模式:契约测试覆盖了两者,但已发布代码从不会询问“这个会话是否已持久化?”或删除某个会话。
+
+## 决策
+
+没有消费方使用的方法被移除——从抽象 seam、实现,以及仅为覆盖它们而存在的契约/spec 测试套件中移除:
+
+- `SessionPersistence.has()` / `.delete()` 已移除:抽象声明、协调器的 `has`/`delete`/`deleteCore`,以及 `PersistenceBackend.deleteStored` 钩子均消失(jsonl 和 sqlite 都只是为了满足该钩子才实现 `deleteStored`,这些实现也一并移除)。后端属于[双后端](../architecture/2026-06-14-session-persistence.md)设计,其他方面不在范围内;删除它们为没有消费方的钩子所做的实现,是删除钩子的一部分,而非重新设计后端。
+- 所有文档和源码注释引用都已更新为保留下来的四方法、仅含 `list()` 的契约——不仅包括字面上的 `has(`/`delete(`/`deleteStored` 拼写,还包括 `{@link has}`/`{@link delete}` JSDoc 链接和“六个公共方法”的计数——涉及 seam 和后端 README、[docs/architecture.md](../../../../docs/architecture.md)、[会话持久化](../architecture/2026-06-14-session-persistence.md)与[写入协调器](../architecture/2026-06-18-shared-persistence-write-coordinator.md) Agent Note,以及协调器/后端 JSDoc。
+
+## 曾考虑的替代方案
+
+### 为什么不以「seam 应当完整」为由保留?
+
+「持久化 seam 理应提供 delete」这种直觉是真实的——但它恰恰是预发布阶段所警惕的投机性完整([AGENTS.md](../../../../AGENTS.md):为正确的基础优化,而非为你并不拥有的假想调用者优化)。`delete()` 是一个方法,等消费方真正需要时再加回来即可:一个删除旧会话的会话管理 UI 会需要它——到那时再加,基于该 UI 的真实需求来设计(软删除?级联?确认?),而非现在猜测。
+
+在有活跃消费方的情况下重新添加一个 seam 方法,成本低且设计更优,因为消费方锚定了契约。在无人使用的情况下保留它,意味着每个实现(以及未来的每个后端)都必须实现和测试一个无实际作用的方法。
+
+## 验证
+
+`has`/`delete`/`deleteStored` 已从持久化 seam、实现和契约测试套件中移除,没有新增无用导出;剩余操作(`create`/`append`/`load`/`list`)未受影响,基于持久化的会话查询和崩溃恢复行为完全一致;seam README 和 `docs/architecture.md` 仅列出存留的方法。
+
+## 后果
+
+- **`delete()` 是产品最终会需要的操作。** 确实如此,但「最终」正是关键。现在删除、将来基于真实消费方重新添加,严格优于发布一份猜测的契约。两个后端各自减少了一个 `deleteStored` 实现,这是在本次范围之外的包中的有限改动。
+- **低耦合。** 移除局限于持久化 seam + 实现 + 测试;没有跨包消费方引用被移除的方法,因此除文档外没有涟漪效应。
+
+规模不大,但它将 seam 从「实现必须为无人提供什么」恢复为「恰好是消费方使用的东西」。
diff --git a/.agents/notes/archived/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml b/.agents/notes/archived/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml
new file mode 100644
index 0000000000..c2f6f7d751
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-02-remove-stream-chunk-mirror.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
+2026-07-02-remove-stream-chunk-mirror.md: 99086a179dfd1bbaac570e437a66b9be19c05ef4
+2026-07-02-remove-stream-chunk-mirror.zh.md: 7a03e476dd8d0ba725d16d220e9aef3a3fc445f6
diff --git a/.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md b/.agents/notes/archived/simplification/2026-07-02-remove-stream-chunk-mirror.md
similarity index 79%
rename from .agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md
rename to .agents/notes/archived/simplification/2026-07-02-remove-stream-chunk-mirror.md
index c79202b95a..99086a179d 100644
--- a/.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md
+++ b/.agents/notes/archived/simplification/2026-07-02-remove-stream-chunk-mirror.md
@@ -1,10 +1,13 @@
# Agent Note: Stop mirroring the token stream as an agent event
Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-07-02-remove-stream-chunk-mirror.zh.md)
## Problem
-The loop records every model token delta as a durable `assistant/chunk` session event AND emitted a parallel live `agent/stream-chunk` Cordis event carrying the identical data. In `packages/core/agent-loop/src/loop.ts` the two sat one line apart:
+The loop records every model token delta as a durable `assistant/chunk` session event AND emitted a parallel live `agent/stream-chunk` Cordis event carrying the identical data. In `packages/core/agent-loop/src/agent.ts` the two sat one line apart:
```ts ignore-check
const chunkEvent = session.append('assistant/chunk', { turn, step, chunk })
@@ -25,7 +28,7 @@ The premise the deferral hinged on is settled: chunk persistence is authoritativ
Remove `agent/stream-chunk` from the agent event taxonomy. The token stream is read off `session/event` as `assistant/chunk`, the same feed persistence and replay already use — `session/event` is the single live transcript stream (assistant chunks, turn/step boundaries, tool activity, todos).
-**Consumers.** The only production consumer that mattered — the ACP bridge (`dsh-acp`), the real editor-facing streaming surface — already renders `assistant/chunk` off `session/event`, never `agent/stream-chunk`, so it is unaffected. The stdio UI (`dsh-ui-stdio`, a disposable test REPL) was the sole live consumer; it already had a `session/event` listener (from the boundary migration), so its chunk rendering folded into that listener as an `assistant/chunk` case. Consolidating to one listener also removed a latent hazard: the `inReasoning` dim-SGR flag was previously shared across two separate listeners (`agent/stream-chunk` and `session/event`), so a chunk and a boundary racing on it had no defined order; a single listener over the append order makes the interleaving deterministic.
+**Consumers.** Persistence, replay, and interactive renderers consume the authoritative session stream directly. The [automation-only ACP bridge](2026-07-23-acp-automation-only-protocol.md) emits committed `assistant/message` text rather than raw chunks, so it needs neither event. No production consumer requires an `Agent`-first token mirror.
## Scope
@@ -33,7 +36,7 @@ Removed: `agent/stream-chunk`.
Not touched:
- `assistant/chunk` (the durable session event) — the authoritative token stream, kept exactly as-is. This Agent Note removes the LIVE MIRROR, not the persistence (the persistence-removal proposal was separately rejected — see above).
-- `agent/steering` — not touched by THIS decision (a control signal, not the token stream). Its durable twin is `steering/message`, and the mirror emit was removed by its own follow-up: [Remove the `agent/steering` mirror emit](2026-07-04-remove-agent-steering-mirror.md).
+- `agent/steering` — not touched by THIS decision (a control signal, not the token stream). Its durable twin is `steering/message`, and the mirror emit was removed by its own follow-up: [Remove the `agent/steering` mirror emit](../../archived/simplification/2026-07-04-remove-agent-steering-mirror.md).
- `agent/status`, `agent/error`, `agent/created`/`agent/disposed`, `agent/queued`, `agent/session-start` — lifecycle/control events that are not transcript data and have no durable duplicate.
## Alternatives considered
diff --git a/.agents/notes/archived/simplification/2026-07-02-remove-stream-chunk-mirror.zh.md b/.agents/notes/archived/simplification/2026-07-02-remove-stream-chunk-mirror.zh.md
new file mode 100644
index 0000000000..7a03e476dd
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-02-remove-stream-chunk-mirror.zh.md
@@ -0,0 +1,48 @@
+# Agent Note: 停止将 token 流镜像为 agent 事件
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-07-02-remove-stream-chunk-mirror.md) | 中文
+
+## 问题
+
+agent loop(智能体循环)将模型的每个 token delta 同时记录为持久的 `assistant/chunk` 会话事件,并发射一个携带相同数据的并行实时 `agent/stream-chunk` Cordis 事件。在 `packages/core/agent-loop/src/agent.ts` 中,二者仅相隔一行:
+
+```ts ignore-check
+const chunkEvent = session.append('assistant/chunk', { turn, step, chunk })
+chunkSeqs.push(chunkEvent.seq)
+ctx.emit('agent/stream-chunk', agent, turn, step, chunk) // ← the mirror
+```
+
+- 持久事件:`assistant/chunk: { turn, step, chunk }`。
+- 实时发射:`agent/stream-chunk(agent, turn, step, chunk)`——相同的 `StreamChunk`,相同的 `turn`/`step`。
+
+实时发射相比会话事件唯一多出的东西是实时的 `Agent` 句柄,而唯一的消费方直接丢弃了它(其处理函数签名为 `(_agent, _turn, _step, chunk)`)。
+
+这与[移除边界镜像](2026-06-20-remove-agent-boundary-mirror-events.md)为轮次/步骤边界消除的重复相同:消费方面对同一持久事实的两个真源,每次变更都必须同时触及两者。该 Agent Note(agent 决策记录)没有把分片流一并纳入,而是推迟处理(“`assistant/chunk` 持久化仍承载关键约束,所以以后可以将分片流作为镜像评估,但那是一项独立决策”)。本 Agent Note 就是那项独立决策。
+
+推迟所依赖的前提已经明确:分片持久化是权威的,且将保留。停止持久化分片、仅保留瞬态实时流事件的提案已被[否决](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md)——高保真回放、部分失败的流以及快照回放都依赖持久化的 `assistant/chunk` 序列。因此 `session/event` 上的 `assistant/chunk` 是持久的、承重的 token 流,而 `agent/stream-chunk` 是它的纯冗余镜像。
+
+## 决策
+
+从 agent 事件分类体系中移除 `agent/stream-chunk`。token 流通过 `session/event` 以 `assistant/chunk` 的形式读取——持久化与回放已经使用的正是同一个序列。`session/event` 是唯一的实时 transcript(文本记录)流(assistant 分片、轮次/步骤边界、工具活动、todo)。
+
+**消费方。** 持久化、回放和交互式渲染器直接消费权威的会话流。[仅面向自动化的 ACP(Agent Client Protocol)桥接层](2026-07-23-acp-automation-only-protocol.md)发出已提交的 `assistant/message` 文本而非原始分片,因此两种事件它都不需要。没有生产消费方需要一个 `Agent` 优先的 token 镜像。
+
+## 范围
+
+移除:`agent/stream-chunk`。
+
+未触及:
+- `assistant/chunk`(持久会话事件)——权威 token 流,原样保留。本 Agent Note 移除的是实时镜像,而非持久化(移除持久化的提案已单独遭到拒绝——见上文)。
+- `agent/steering`——本决策未触及(它是控制信号,不是 token 流)。其持久孪生事件是 `steering/message`,镜像发射由其自身的后续 Agent Note 移除:[移除 `agent/steering` 镜像发射](../../archived/simplification/2026-07-04-remove-agent-steering-mirror.md)。
+- `agent/status`、`agent/error`、`agent/created`/`agent/disposed`、`agent/queued`、`agent/session-start`——生命周期/控制事件,不是 transcript 数据,也没有持久副本。
+
+## 曾考虑的替代方案
+
+**移除持久化、仅保留瞬态实时流**——反向裁剪,已被[单独否决](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md):高保真回放、部分失败的流以及快照回放都依赖持久化的 `assistant/chunk` 序列。在此前提确定后,实时发射才是配对中冗余的那一半。
+
+## 后果
+
+插件不能再从 `Agent` 优先事件观察 token 增量。它需要订阅 `session/event`、过滤 `assistant/chunk`,并在需要时通过 `ctx.agents.get(session.id)` 直接查找对应的实时 handle。没有生产消费方需要在分片时刻取得实时 `Agent`;这与移除边界镜像所作的取舍相同,均可接受。
diff --git a/.agents/notes/archived/simplification/2026-07-04-drop-inert-request-knobs.i18n.yaml b/.agents/notes/archived/simplification/2026-07-04-drop-inert-request-knobs.i18n.yaml
new file mode 100644
index 0000000000..c5e8631a70
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-drop-inert-request-knobs.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
+2026-07-04-drop-inert-request-knobs.md: d4ef5f00caf3b2f580877374adaf6ddcd84e7326
+2026-07-04-drop-inert-request-knobs.zh.md: dfb57080e91039d48b9c173bf958c583005c3c32
diff --git a/.agents/notes/implemented/simplification/2026-07-04-drop-inert-request-knobs.md b/.agents/notes/archived/simplification/2026-07-04-drop-inert-request-knobs.md
similarity index 98%
rename from .agents/notes/implemented/simplification/2026-07-04-drop-inert-request-knobs.md
rename to .agents/notes/archived/simplification/2026-07-04-drop-inert-request-knobs.md
index dadab43f76..d4ef5f00ca 100644
--- a/.agents/notes/implemented/simplification/2026-07-04-drop-inert-request-knobs.md
+++ b/.agents/notes/archived/simplification/2026-07-04-drop-inert-request-knobs.md
@@ -1,6 +1,9 @@
# Agent Note: Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-04-drop-inert-request-knobs.zh.md)
## Problem
diff --git a/.agents/notes/archived/simplification/2026-07-04-drop-inert-request-knobs.zh.md b/.agents/notes/archived/simplification/2026-07-04-drop-inert-request-knobs.zh.md
new file mode 100644
index 0000000000..dfb57080e9
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-drop-inert-request-knobs.zh.md
@@ -0,0 +1,36 @@
+# Agent Note: 移除 `GenerateOptions.prefill` 与 `ToolSchema.strict`——无端到端可用路径的请求旋钮
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-04-drop-inert-request-knobs.md) | 中文
+
+## 问题
+
+两个请求契约旋钮贯穿了整条请求流水线,却都无法产生任何效果:
+
+- **`prefill`**(`packages/llm/llm/src/types.ts`)没有生产级的 setter:agent loop(智能体循环)组装的是 `model`/`system`/`tools`/`messages` 加 `sessionId`/`signal`,上下文压缩(context compaction)后端只追加 `maxTokens`;而且两个适配器都拒绝它:`packages/llm/llm-deepseek/src/serialize.ts` 和 `packages/llm/llm-pi-ai/src/adapter.ts` 各自在 `prefill` 非 undefined 时抛出 `LlmError('UNSUPPORTED')`。该字段的全部可观测行为就是两个 throw,各由一条适配器测试固定。DeepSeek 的 chat-prefix completion 是一个 Beta 功能,运行在两个适配器都未指向的 base URL 上。
+- **`strict`**(`ToolSchema`,同一文件)穿过了 `DefineToolOptions`/`defineTool`(`packages/core/tools/src/schema.ts`)、注册表的 `schemas()` 允许列表(`packages/core/tools/src/index.ts`)、deepseek 协议格式(wire format)映射(`packages/llm/llm-deepseek/src/serialize.ts`,其 wire-type 注释记录了 strict 模式需要适配器未使用的 `/beta` base URL)、`packages/llm/llm-pi-ai/src/adapter.ts` 中的逐工具 payload 修补逻辑,以及 tool-catalog 渲染器(`scripts/gen-tool-catalog.ts`)中的条件 `Strict:` 行。没有任何已发布的工具设置过它——在所有 `tool-*` 包的 src 和 `examples/` 中执行 `rg` 搜索,`strict:` 的生产者为零;唯一的 setter 出现在 dsh-tools 单元测试中。
+
+两个旋钮在适配器间是对称的,因此移除操作将它们从两个孪生适配器中一并剥离——[孪生适配器设计](../architecture/2026-06-13-twin-llm-adapters.md)不受影响。
+
+## 决策
+
+- 从 `GenerateOptions` 中移除 `prefill`,同时移除两个适配器的 UNSUPPORTED 守卫、固定抛错行为的测试、[core.md](../../../../docs/core-data-structures/core.md) 中的粘贴行,以及记录该拒绝行为的适配器 README 表格行。实操手册中的 UNSUPPORTED 指导([adding-an-llm-adapter.md](../../../../docs/cookbook/adding-an-llm-adapter.md))改为通用表述规则——提供方无法遵守的 `GenerateOptions` 字段应抛出 `LlmError(..., 'UNSUPPORTED')`——而不再以 prefill 为例。[内容块词汇 Agent Note(agent 决策记录)](../architecture/2026-06-11-content-block-vocabulary.md)的后果按照 [implemented/AGENTS.md](../AGENTS.md),将 prefill 记录为由生产者门控,而不是已有归属。
+- 从 `ToolSchema`、`DefineToolOptions`、`defineTool`、`schemas()` 允许列表、deepseek 序列化分支及其 wire-type 字段,以及工具目录渲染器的 `Strict:` 行中移除 `strict`。pi-ai 的 payload 修补逻辑简化为对 pi-ai 自身逐工具 strict 默认值的无条件清除(pi-ai 在每个序列化的工具上打 `strict: false`;手写的孪生适配器不发送此字段,因此清除逻辑为保持协议格式对等而保留,由其序列化器测试固定)。setter 测试和 core.md 粘贴行已移除;`GenerateOptions` 与 `ToolSchema` 在 `scripts/type-equiv.manifest.json` 中保留各自的行,因为两个类型只是少了一个字段,本身仍然存在。
+
+本 Agent Note 刻意不触及 `temperature`、`stop` 或 `maxTokens`:两个适配器都会端到端遵守它们,而且它们自然是 `agent/request` 上修改请求的钩子插件首批目标。
+
+## 曾考虑的替代方案
+
+### 为什么不保留?
+
+「显式的 UNSUPPORTED throw 是诚实的契约行为」——但一个在两个孪生适配器中唯一的实现就是拒绝的旋钮,什么也没承诺;删除它反而升级了失败模式:意外的 setter 变成编译错误而非运行时 throw。「Strict schema 遵循是官方文档记载的提供方功能,且管道完整」——但一个旋钮在有已发布的工具设置它并且有端点兑现它之前,不构成产品表面;今天两者都不成立。它们各自随首个真实 producer 回归:`prefill` 随实现了 chat-prefix completion 的适配器(以及对不支持该功能的适配器的明确策略)一起回来;`strict` 随需要它的工具和 beta 端点方案一起回来。
+
+## 验证
+
+`rg prefill` 只返回 Agent Note 记录(本文及[内容块词汇 Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)中由生产者门控的后果);限定在工具 schema 范围内的 `rg strict` 只返回本 Agent Note、保留下来的 pi-ai 清理逻辑,以及 `strictEqual` 等无关正文。两个适配器的契约测试都能在没有守卫的情况下通过,pi-ai 修正仍会清理库的 strict 默认值——其 serializer 测试固定了线协议一致性。
+
+## 后果
+
+已发布的钩子桥接不设置任何请求字段,而请求变更插件(`agent/request` waterfall(瀑布式事件)监听器)使用的是 `temperature`/`stop`(保留且可用),而非适配器拒绝的字段。如果 chat-prefix completion 或 strict 模式成为产品功能,重新添加将随适配器/端点工作一起落地,届时契约能说明实际发生了什么,而不是「所有人都 throw」。
diff --git a/.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.i18n.yaml b/.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.i18n.yaml
new file mode 100644
index 0000000000..3079ec531b
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.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
+2026-07-04-drop-unconsumed-web-observation-surface.md: e63df08e486862b5bbb37db1e2c15b6d7e9d67e2
+2026-07-04-drop-unconsumed-web-observation-surface.zh.md: f9d1c09f90ec5065f25d4941dc319cff817caf97
diff --git a/.agents/notes/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md b/.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md
similarity index 97%
rename from .agents/notes/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md
rename to .agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md
index 80faa2ac07..e63df08e48 100644
--- a/.agents/notes/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md
+++ b/.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md
@@ -1,6 +1,9 @@
# Agent Note: Drop the unconsumed web observation surface — the `providers-change` event and the status methods
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-04-drop-unconsumed-web-observation-surface.zh.md)
## Problem
diff --git a/.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.zh.md b/.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.zh.md
new file mode 100644
index 0000000000..f9d1c09f90
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.zh.md
@@ -0,0 +1,35 @@
+# Agent Note: 移除未被消费的 web 观测接口——`providers-change` 事件与 status 方法
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-04-drop-unconsumed-web-observation-surface.md) | 中文
+
+## 问题
+
+`WebService` 暴露了一组没有任何生产代码观测的观测接口:
+
+- **`web/providers-change`**(`packages/web/web/src/index.ts`)在每次提供方注册和 dispose(资源释放)时声明并发出,且每个注册 effect 的回滚 yield 被刻意排在 emit 之前,唯一目的是让抛出异常的 change listener 能回退注册。在该包自身的两个单元测试之外没有任何 listener(其中一个测试的存在仅仅是为了固定那个回滚顺序)。
+- **`searchStatus()` / `fetchStatus()` 与 `WebCapabilityStatus` 联合类型**(同一包)没有生产调用方:`dsh-tool-web` 直接通过 `ctx.web.search()`/`fetch()` 执行,并把不可用性呈现为 seam 在执行时抛出的结构化 `WebError` code(`packages/web/tool-web/src/search.ts`、`packages/web/tool-web/src/fetch.ts`);唯一的 status 调用方是 web 包自己的测试。`packages/web/tool-web/README.md` 和 [architecture.md](../../../../docs/architecture.md) 中的正文声称工具“只读取聚合的 `searchStatus()`/`fetchStatus()`”——这种漂移之所以存续,只是因为没有机制对照调用位置检查正文。
+
+seam 自身的设计使这两个接口天然没有消费方:工具注册跟随产品 ENABLEMENT 而非提供方可用性(`packages/web/tool-web/src/index.ts`),提供方选择在执行时解析且从不缓存——因此没有需要失效的缓存、没有需要重算的注册集合、也没有调用方需要一个有别于「执行并路由结构化错误」的可用性探测。HMR(热模块替换)清理由 effect disposer 自身承载。
+
+这与[删除无人消费的 `llm/adapter-change` 事件](2026-06-20-drop-unconsumed-llm-adapter-change-event.md)相呼应;后者从 `LlmService` 移除了相同的通知形状、相同的 emit 前回滚机制和相同的监听器抛错测试。该 Agent Note(agent 决策记录)的保留/删除标准是:为可能面向用户的工具列表消费方保留 `tools/change`,删除启动时后端注册表信号。按这一标准,web 提供方注册表明确属于删除一侧;status 方法则是把同一判断应用于拉取表面,而非推送表面。
+
+## 决策
+
+移除注册表变更事件、聚合 status 方法与类型,以及它们的专属测试。提供方私有的 status 保留用于执行时选择。面向调用方的覆盖率现在断言成功执行或结构化的选择错误,web 文档描述该按需调用契约。
+
+## 曾考虑的替代方案
+
+### 为什么不保留?
+
+web seam Agent Note 刻意规定了两者——事件作为最小 HMR 可见性信号,status 方法作为工具的聚合诊断——未来也可以设想提供方状态面板。但同一 Agent Note 的其他选择让它们失去了生存条件:调用时派生选择和基于启用状态的注册,使任何消费方都不可能需要其中任一项;已发布工具展示了真实模式(执行并路由结构化错误);发生漂移的 README 句子则表明承诺中的消费方从未出现。按照 AGENTS.md 所述“Agent Note 是提案,而非绝对真理”,代码后来证明提案中的这些部分超出了需要;未来的观察者应根据真实消费方的形状,重新引入它实际消费的最小信号或查询。
+
+## 验证
+
+除 Agent Note 历史外,不再存在 `providers-change`、`searchStatus`、`fetchStatus` 或 `WebCapabilityStatus` 拼写;目录保持新鲜(`verify-cordis-catalog` 为绿色);注册/释放 HMR 安全性测试通过执行行为证明清理;tool-web README 和架构段落也描述了工具实际拥有的执行时错误路由契约。
+
+## 后果
+
+未来若有提供方选择器 UI 或诊断面板需要变更通知或 status 查询,它将重新添加自身所消费的最小接口;相同的判断及其反转条件已记录在 LLM(大语言模型)先例中。
diff --git a/.agents/notes/archived/simplification/2026-07-04-fold-stdio-ui-helper.i18n.yaml b/.agents/notes/archived/simplification/2026-07-04-fold-stdio-ui-helper.i18n.yaml
new file mode 100644
index 0000000000..ae31e636f8
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-fold-stdio-ui-helper.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
+2026-07-04-fold-stdio-ui-helper.md: 72211108820bd59235d6171c6008d2d63ad79c40
+2026-07-04-fold-stdio-ui-helper.zh.md: e9329ac3a038a37ee2bd51c65f976cbf7bb143c0
diff --git a/.agents/notes/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md b/.agents/notes/archived/simplification/2026-07-04-fold-stdio-ui-helper.md
similarity index 87%
rename from .agents/notes/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md
rename to .agents/notes/archived/simplification/2026-07-04-fold-stdio-ui-helper.md
index b05dd22357..7221110882 100644
--- a/.agents/notes/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md
+++ b/.agents/notes/archived/simplification/2026-07-04-fold-stdio-ui-helper.md
@@ -1,6 +1,9 @@
# Agent Note: Fold the stdio UI helper into the stdio app
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-04-fold-stdio-ui-helper.zh.md)
The later [redundant-agent removal](2026-07-20-remove-stdio-and-echo-agents.md) supersedes this package-placement decision and removes the folded package, app, and line-oriented surface entirely.
@@ -20,7 +23,7 @@ The earlier support helper package was removed: its manifest, tsconfig reference
### Why not promote it to `ui/` instead?
-Promotion would have resolved the support-vs-product mismatch while keeping the boundary — the right call only if the readline UI were an independently swappable integration or had a second composer, and the consumer census said neither. The structured ACP bridge stays its own package because it is the product protocol surface with its own contract and snapshot tiers; the readline helper is scaffolding for one app's front door. Re-extraction stays cheap pre-release: if a second product app wants the readline UI, split it back out then, with that consumer shaping the package contract.
+Promotion would have resolved the support-vs-product mismatch while keeping the boundary — the right call only if the readline UI were an independently swappable integration or had a second composer, and the consumer census said neither. The structured ACP bridge stays its own package because it is an automation protocol surface with its own contract and snapshot tiers; the readline helper is scaffolding for one app's front door. Re-extraction stays cheap pre-release: if a second product app wants the readline UI, split it back out then, with that consumer shaping the package contract.
## Consequences
diff --git a/.agents/notes/archived/simplification/2026-07-04-fold-stdio-ui-helper.zh.md b/.agents/notes/archived/simplification/2026-07-04-fold-stdio-ui-helper.zh.md
new file mode 100644
index 0000000000..e9329ac3a0
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-fold-stdio-ui-helper.zh.md
@@ -0,0 +1,31 @@
+# Agent Note: 将 stdio UI 辅助模块折入 stdio 应用
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-04-fold-stdio-ui-helper.md) | 中文
+
+后来的[冗余 agent(智能体)移除](2026-07-20-remove-stdio-and-echo-agents.md)取代了这项包放置决策,并完整移除合并后的包、应用和面向行的表面。
+
+## 问题
+
+readline UI 曾是一个完整的包(`packages/support/` 下的 `@deepseek-ai/dsh-ui-stdio`),其唯一的运行时导入方是应用包 `@deepseek-ai/dsh-stdio-demo`。示例通过加载应用来使用 readline UI,从不自行组合该辅助模块;仓库中所有其他引用都是因为包边界存在而存在的机械性或描述性表面:manifest(元数据清单)与 tsconfig 条目、生成的 module-graph 行、依赖图与 README 行,以及命名该包的文档注释。ui 组 README 记录了 support 放置的理由("主要为示例和覆盖率门禁而存在,`ui/` 保留给作为产品交付的界面"),这留下了一个持续的张力:一个已交付的产品应用依赖一个被明确标注为非产品表面的 support 包。
+
+这条边界换来的是:包元数据、workspace 与 tsconfig 引用、module-graph 行、README 条目,以及 publint 表面——服务于一个并不可独立替换的辅助模块:stdio 应用的前门集群始终包含 readline UI,且没有其他消费方能有意义地使用它。
+
+## 决策
+
+当时,该辅助函数移入 `@deepseek-ai/dsh-stdio`,成为终端通道插件。`createStdioChat`、其 `StdioRuntime` 测试 seam 和单元测试随之一同迁移,使 EOF 处理、渲染、释放以及管道/TTY 行为继续受逐文件覆盖率门禁约束,而不会劫持进程全局量。该模块保留应用挂载所消费的具名 `name`/`inject`/`Config`/`apply` 导出形状;当时的 Echo 和 REPL Loader 冒烟证明组合树,插件形状套件则固定显式 `unwrapExports` 行为。上方取代本文的移除记录负责当前包和示例状态。
+
+早期的支持辅助包已移除:其清单、tsconfig 引用、模块图行和 README 行均已消失,其余文档改为描述包内模块。
+
+## 曾考虑的替代方案
+
+### 为什么不将其提升到 `ui/` 而是折入?
+
+提升可以解决 support 与 product 之间的错位,同时保留边界——只有在 readline UI 是一个可独立替换的集成或有第二个组合方时才是正确选择,而消费方普查表明两者皆非。结构化的 ACP(Agent Client Protocol)桥接保留为独立包,因为它是具有自身契约和快照层级的自动化协议表面;readline 辅助模块只是一个应用前门的脚手架。在发布前重新拆分成本很低:如果将来有第二个产品应用需要 readline UI,届时再拆出来,由那个消费方来塑造包契约。
+
+## 后果
+
+- stdio 应用完整拥有自己的前门;叶子 `cordis.yml` 仍然只加载一个应用包,演示的形态没有变化。
+- 未来如果有独立的终端 UI 需要将该辅助模块作为包使用,届时由那个第二消费方驱动重新引入,而非仓库为假设性的复用保留一条边界。
diff --git a/.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.i18n.yaml b/.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.i18n.yaml
new file mode 100644
index 0000000000..f35edf4a1b
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.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
+2026-07-04-prune-producerless-vocabulary-variants.md: c1544fa9d72c994518f690d482a62d506ff3f83e
+2026-07-04-prune-producerless-vocabulary-variants.zh.md: e48f55c49d35d4a675f0d9540dab034bcb864cd3
diff --git a/.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md b/.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md
similarity index 98%
rename from .agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md
rename to .agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md
index aa61e859d2..c1544fa9d7 100644
--- a/.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md
+++ b/.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md
@@ -1,6 +1,9 @@
# Agent Note: Prune producer-less vocabulary variants (block cache hints, the `agent` message source, the `continuation` turn trigger)
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-04-prune-producerless-vocabulary-variants.zh.md)
## Problem
diff --git a/.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.zh.md b/.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.zh.md
new file mode 100644
index 0000000000..e48f55c49d
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.zh.md
@@ -0,0 +1,34 @@
+# Agent Note: 裁剪无生产者的词汇变体(块缓存提示、`agent` 消息来源、`continuation` 轮次触发器)
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-04-prune-producerless-vocabulary-variants.md) | 中文
+
+## 问题
+
+可合并扩展的词汇映射表设计上通过声明合并来增长,代码库已在 `TurnEndReasonMap`(`packages/core/session/src/types.ts`)上明确了准入策略:像 `refusal` 这样的变体「在适配器或循环首次发出它之前,有意不纳入」。三个已声明的词汇项违反了该策略——每个都既无生产者也无消费方,其中两个甚至没有测试:
+
+- **`TextBlock`/`ToolResultBlock` 上的 `CacheHint` 及其 `cache?: CacheHint` 块字段**(`packages/llm/llm/src/types.ts`;图像块曾有第三个此类字段,已随图像块一同移除——参见[删除图像 Agent Note(agent 决策记录)](2026-07-04-drop-image-content-block.md))。任何地方都没有构造带 `cache:` 的块——src、测试和文档粘贴均为空——两个适配器也都不读取 `.cache`:DeepSeek 的提示词缓存是自动的,因此适配器会从响应中映射出 `prompt_cache_hit_tokens`,却从不向请求中发送 hint。这是没有任何提供方能够遵守的 Anthropic 风格 `cache_control` 表面。
+- **`MessageSourceMap.agent`**(`{ kind: 'agent'; agentId: string }`,同一文件)。零个构造点,包括测试在内。它预期的生产者在实现时并未使用它:subagent 后端将父级的提示词发送给子级时不带 `source`,因此记录为 `{ kind: 'user' }`,通用信封渲染器在插值 `source.kind` 时也从未对其做路由。
+- **`TurnTriggerMap.continuation`**(`packages/core/session/src/types.ts`)。agent loop(智能体循环)在结构上不可能发出它——continuation 发生在一个轮次*内部*作为后续步骤,而非作为新轮次——循环只构造 `message` 和 `injection` 触发器。唯一的写入者是一个手工构建的测试 fixture(测试前置数据),它只需要一个任意的非消息触发器(`packages/support/llm-replay/tests/llm-replay.spec.ts`),`injection` 触发器同样满足需求;唯一的生产环境触发器读取方 ACP(Agent Client Protocol)桥接层只过滤 `kind === 'message'`。
+
+## 决策
+
+`CacheHint`、其 `cache?` 块字段、`agent` 消息来源变体和 `continuation` 轮次触发器变体均已删除:已发布词汇不再携带它们。llm-replay fixture 使用 `injection` 触发器(任何非 `message` 触发器都能满足其用途)。[core.md](../../../../docs/core-data-structures/core.md) 和 [session.md](../../../../docs/core-data-structures/session.md) 中的 type-equiv 粘贴与裁剪后的 map 匹配——两个符号仍保留在 `scripts/type-equiv.manifest.json` 中的行,因为每个 map 都只是少了一个成员而继续存在——并且[内容块词汇 Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)的后果按照 [implemented/AGENTS.md](../AGENTS.md),将 cache hint 记录为由生产者门控,而不是已有归属。
+
+每个变体在获得真正的生产者之日回归,这正是映射表设计的增长方式:缓存功能连同传输它的适配器一起重新添加 `cache`;subagent 归属连同打标的后端和路由它的消费方一起重新添加 `agent`;真正启动新轮次的自动续行功能连同发出它的插件一起重新添加 `continuation`。
+
+## 曾考虑的替代方案
+
+### 为什么不保留它们?
+
+[内容块词汇 Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)曾把“cache hint……有了归属”列为设计后果,预留槽位也确实能表明意图。但空槽位是每个实现和消费方都必须考虑的契约表面(我的适配器是否必须遵守 `cache`?我的 renderer 是否必须路由 `agent` 来源?),而相邻 map 自身的 JSDoc 已经拒绝“无 emitter 先预留”——`refusal` 和 `max_turn_requests` 被点名为*首次有内容发出它们时*再添加的变体,而不是提前声明。让已经声明但无用的变体遵守同一标准,才能使词汇真正有意义:只要它位于 map 中,就必须有内容生产它。
+
+## 验证
+
+对 `CacheHint`、`agent` 消息来源拼写和 `continuation` 触发器拼写运行 `rg`,只会返回 Agent Note 记录(本文,以及[删除图像 Agent Note](2026-07-04-drop-image-content-block.md)对图像块自身 `cache` 字段的说明);llm-replay fixture 使用 `injection` 触发器断言相同的重放行为;核心数据结构粘贴和 type-equiv 清单保持同步。
+
+## 后果
+
+操作行为没有变化——原本就没有内容能够构造这些值。镜像事件移除([边界镜像 Agent Note](2026-06-20-remove-agent-boundary-mirror-events.md)、[流分片 Agent Note](2026-07-02-remove-stream-chunk-mirror.md))只触及瞬态 `agent/*` 事件,从不触及持久词汇,因此不存在冲突。其他位置已经遵守准入策略:`rejected`、`prompt/blocked` 和 `hook/invoked`/`hook/result` 都有实时生产者——本 Agent Note 将同一门槛扩展到缺少生产者的三个变体。图像块自身的 `cache?` 字段归属[删除图像 Agent Note](2026-07-04-drop-image-content-block.md),后者将其与该块一同移除;本 Agent Note 覆盖剩余块类型上的两个字段。
diff --git a/.agents/notes/archived/simplification/2026-07-04-prune-write-only-fs-surface.i18n.yaml b/.agents/notes/archived/simplification/2026-07-04-prune-write-only-fs-surface.i18n.yaml
new file mode 100644
index 0000000000..21b28645b6
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-prune-write-only-fs-surface.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
+2026-07-04-prune-write-only-fs-surface.md: e1b6bb6bd9721120015aaced8ffae84048396b60
+2026-07-04-prune-write-only-fs-surface.zh.md: 2f8135b2b1fa9892270a110ea3c3112e6ea20700
diff --git a/.agents/notes/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md b/.agents/notes/archived/simplification/2026-07-04-prune-write-only-fs-surface.md
similarity index 98%
rename from .agents/notes/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md
rename to .agents/notes/archived/simplification/2026-07-04-prune-write-only-fs-surface.md
index 97652ef50c..e1b6bb6bd9 100644
--- a/.agents/notes/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md
+++ b/.agents/notes/archived/simplification/2026-07-04-prune-write-only-fs-surface.md
@@ -1,6 +1,9 @@
# Agent Note: Prune write-only fields and a dead routing knob from the fs seam
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-04-prune-write-only-fs-surface.zh.md)
## Problem
diff --git a/.agents/notes/archived/simplification/2026-07-04-prune-write-only-fs-surface.zh.md b/.agents/notes/archived/simplification/2026-07-04-prune-write-only-fs-surface.zh.md
new file mode 100644
index 0000000000..2f8135b2b1
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-prune-write-only-fs-surface.zh.md
@@ -0,0 +1,33 @@
+# Agent Note: 从 fs seam 中移除只写字段与一个无效的路由旋钮
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-04-prune-write-only-fs-surface.md) | 中文
+
+## 问题
+
+[fs seam 拆分](2026-06-26-fsspec-style-fs-seam.md)将读取路由与策略从后端移至 `dsh-tool-fs` 和 `dsh-fs-policy`。有四处接口保留了拆分前的形态——每次调用都填充,却无人读取:
+
+1. **`dsh-fs-local` 中的 `STREAM_MIN_SIZE` + `FsIoInternals.streamMinSize`**——*在本次变更之前已被「禁止硬编码可调参数」审计移除,该审计将路由阈值改为 `dsh-tool-fs` 的 `readStreamMinSize` 配置;此处记录是为了完整呈现整次清理。* 原始位置(`packages/fs/fs-local/src/fsio.ts`,从 `packages/fs/fs-local/src/index.ts` 重导出):包括 fs-local 自身源码和测试在内,全仓库零读取者。后端没有读取路由——`readWholeText`/`streamWholeText` 是调用方自行选择的两个独立原语——真正的路由常量位于消费方(`packages/fs/tool-fs/src/read.ts`,与 `info.size` 比较)。同一个 10 MiB 事实的两份镜像;后端那份是死代码,且该旋钮的 JSDoc 声称提供一个实际不存在的「read routing」覆盖。
+2. **`FsTarget.inputPath`**(`packages/fs/fs/src/types.ts`):每个后端和每个测试 mock 都必须为这个「仅供诊断」的字段编造一个值,而生产环境零读取者——策略插件和所有错误消息使用的是 `targetKey`/`displayPath`。`listDir` 的生产者暴露了语义上的摇摆:目录子项得到的是裸条目名,这不是任何人的「input」。
+3. **`FsEditOutcome.replacements` + `.replaceAll`**(`packages/fs/fs/src/types.ts`):`replacements` 生产环境零读取者(单匹配策略本身保留——它由后端内部 `FS_AMBIGUOUS_EDIT`/`FS_EDIT_NOT_FOUND` 抛出来强制执行,错误消息保留了内部计数);`replaceAll` 仅被 `packages/fs/tool-fs/src/edit.ts` 中的 `formatEditOutput` 读取——作为工具本身已持有的 `replace_all` 参数的回声。精简后,`FsEditOutcome` 变为 `{ version, before, after }`,与 `FsWriteOutcome` 中真正由后端发现的字段对齐。
+4. **`FileReadOutcome.limit` + `.version`**(`packages/fs/tool-fs/src/read-render.ts`):由读取工具填充,但 `formatReadOutput` 只渲染 `offset`/`lines`/`totalLines`/`truncatedByBytes`,且 `fs/observed` 事件发射直接使用 `info.version` 而非 outcome 的副本。
+
+## 决策
+
+删除 fs-local 常量、其再导出和 `streamMinSize` 配置项(其余 `FsIoInternals` 配置项确实由原子写入测试使用);从 `FsTarget` 删除 `inputPath`;将 `FsEditOutcome` 收窄为 `{ version, before, after }`,并把解析参数中的 `replaceAll` 传给 `formatEditOutput`;从 `FileReadOutcome` 删除 `limit`/`version`。[filesystem.md](../../../../docs/core-data-structures/filesystem.md) 中的粘贴、`packages/fs/fs/README.md`,以及不得不虚构已删除字段的测试 fake 都随类型一同收窄。
+
+## 曾考虑的替代方案
+
+### 为什么不保留?
+
+未来的权限/隔离层可能需要解析前的路径来生成错误文本——但它需要的是*请求*,每个调用点仍然持有请求。「替换了 N 处」可能成为面向模型的文本——这是一个需要时再设计的行为变更,且后端内部的计数为其错误消息而保留。读取页脚可能展示 `limit`——但页脚展示的一切已经可以从 `lines`/`totalLines` 推导。与此同时,每个现有和未来的后端(远程、原生)都必须编造无人消费的协议字段,每个测试 mock 都必须满足它们。
+
+## 验证
+
+已删除表面不复存在——`dsh-fs-local` 中的 `STREAM_MIN_SIZE`/`streamMinSize`、`FsTarget.inputPath`、`FsEditOutcome.replacements`/`.replaceAll`,以及 `FileReadOutcome.limit`/`.version`——而请求侧 `replaceAll`(`FsEditRequest`)和其他 outcome 类型上的版本字段保持不变;测试 fake 随类型一同收窄。`formatEditOutput` 在两个 `replace_all` 分支中生成的文本都没有变化,因此没有快照预期输出发生改动。
+
+## 后果
+
+后端不增加新义务,反而卸下了四个无人消费的字段。fs 发现功能(glob/grep 工具)涉及相同的 `dsh-fs` 类型文件——这是文本层面而非设计层面的重叠,可以机械地合并解决。
diff --git a/.agents/notes/archived/simplification/2026-07-04-remove-agent-steering-mirror.i18n.yaml b/.agents/notes/archived/simplification/2026-07-04-remove-agent-steering-mirror.i18n.yaml
new file mode 100644
index 0000000000..e6790ee816
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-remove-agent-steering-mirror.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
+2026-07-04-remove-agent-steering-mirror.md: 0d7c7f8ac9592033423156d38f3bbe6d037afd07
+2026-07-04-remove-agent-steering-mirror.zh.md: be30e0add3a2d7392cb7a59ed4bc425ec0ad5588
diff --git a/.agents/notes/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md b/.agents/notes/archived/simplification/2026-07-04-remove-agent-steering-mirror.md
similarity index 97%
rename from .agents/notes/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md
rename to .agents/notes/archived/simplification/2026-07-04-remove-agent-steering-mirror.md
index ebfc774792..0d7c7f8ac9 100644
--- a/.agents/notes/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md
+++ b/.agents/notes/archived/simplification/2026-07-04-remove-agent-steering-mirror.md
@@ -1,6 +1,9 @@
# Agent Note: Remove the `agent/steering` mirror emit
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-04-remove-agent-steering-mirror.zh.md)
## Problem
diff --git a/.agents/notes/archived/simplification/2026-07-04-remove-agent-steering-mirror.zh.md b/.agents/notes/archived/simplification/2026-07-04-remove-agent-steering-mirror.zh.md
new file mode 100644
index 0000000000..be30e0add3
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-remove-agent-steering-mirror.zh.md
@@ -0,0 +1,34 @@
+# Agent Note: 移除 `agent/steering` 镜像 emit
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-04-remove-agent-steering-mirror.md) | 中文
+
+## 问题
+
+`agent/steering` 是最后一个仍存在的、对持久会话事件的瞬态镜像。agent loop(智能体循环)的 steering(中途引导)drain 逻辑先追加持久事件 `steering/message { turn, content, source }`,紧接着下一行就 emit `agent/steering(agent, turn, content, source)`——同一个事实以 fire-and-forget 事件的形式重复发出(`packages/core/agent-loop/src/loop.ts`,`drainSteering`)。它在生产环境中没有任何监听者:唯一的订阅方是一个 agent loop 回归测试,断言 emit 携带了 `source`——而这同一个事实已经由上一行的持久事件记录。
+
+`agent/steering` 以相同的 payload 重复了紧接其前的持久事件 `steering/message`。`agent/queued` 仍保留为纯瞬态信号,因为它在持久化之前触发,覆盖了可能在进入日志前被取消的工作。
+
+Steering 承载真实生产流量——钩子 bridge 的轮次延续决策通过 `inbox.steer()` 注入其理由,最终成为由钩子矩阵预期输出固定的持久 `steering/message` 事件——而这些消费方无一例外都观察持久事件。没有任何内容观察镜像。
+
+## 决策
+
+`agent/steering` 已从 agent 事件分类中移除:包括 `packages/core/agent/src/types.ts` 中的声明(以及其中实时事件 JSDoc 列表对它的提及)、`drainSteering` 中的 emit(当时已无用的 `ctx` 参数也随之移除)、`packages/core/agent/README.md` 中的表格行,以及循环伪代码块(`packages/core/agent-loop/src/loop.ts` 模块文档和 [architecture.md](../../../../docs/architecture.md))中的 emit 行;Cordis 目录重新生成后不再包含它。唯一的回归测试改为在持久 `steering/message` 事件上固定来源保留行为——所固定的事实存在于日志上。
+
+三份已实现 Agent Note(agent 决策记录)曾说明保留该事件;按照 [implemented/AGENTS.md](../AGENTS.md),每份记录都已修改并指向本文作为移除记录:包括[边界 Agent Note](2026-06-20-remove-agent-boundary-mirror-events.md) 的保留列表条目、[流分片 Agent Note](2026-07-02-remove-stream-chunk-mirror.md) 的范围条款,以及[事件域语义 Agent Note](../architecture/2026-06-30-event-domain-semantics.md) 的瞬态 emit 枚举。
+
+## 曾考虑的替代方案
+
+### 为什么不保留?
+
+“它是控制信号,不是边界”——但该分类的实际区分是镜像/仅实时,而非控制/边界,并且该事件确实是镜像。希望在入队时收到通知的消费方可以使用 `agent/queued`(及其 steering 标记);希望在排空时收到通知的消费方,本质上是在要求获知 `steering/message` 被追加的时刻,而 `session/event` 会交付相同 payload 并附带持久性。遭拒绝的[退役轮次中途 steering Agent Note](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md)所捍卫的是 steering *功能*——`steer()`、持久事件、强制延续——本次移除不会触及其中任何一项。
+
+## 验证
+
+`agent/steering` 拼写只存在于 Agent Note 正文中(本 Agent Note、上方三份已修改 Agent Note,以及已冻结的[遭拒绝 steering 功能 Agent Note](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md),其正文记录了它所否决的提案);目录已重新生成;重新定向的测试在 `steering/message` 上固定来源保留行为。
+
+## 后果
+
+生产环境中没有需要迁移的监听者,两种瞬态通知需求各有归宿:入队时由 `agent/queued`(带 `steering` flag)承载,drain 时由 `session/event` 在持久事件 `steering/message` 落地时承载。
diff --git a/.agents/notes/archived/simplification/2026-07-04-share-app-bin-boot-glue.i18n.yaml b/.agents/notes/archived/simplification/2026-07-04-share-app-bin-boot-glue.i18n.yaml
new file mode 100644
index 0000000000..0bed1b52c6
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-share-app-bin-boot-glue.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
+2026-07-04-share-app-bin-boot-glue.md: 8b2aeef73ecfc07f3102f852d4b0a44b336b7d92
+2026-07-04-share-app-bin-boot-glue.zh.md: ed5146030ca8d23a653497d6c097ad6c3b4445b1
diff --git a/.agents/notes/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md b/.agents/notes/archived/simplification/2026-07-04-share-app-bin-boot-glue.md
similarity index 97%
rename from .agents/notes/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md
rename to .agents/notes/archived/simplification/2026-07-04-share-app-bin-boot-glue.md
index f054f168a2..8b2aeef73e 100644
--- a/.agents/notes/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md
+++ b/.agents/notes/archived/simplification/2026-07-04-share-app-bin-boot-glue.md
@@ -1,6 +1,9 @@
# Agent Note: Share the app bins' boot glue instead of maintaining twin copies
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-04-share-app-bin-boot-glue.zh.md)
## Problem
diff --git a/.agents/notes/archived/simplification/2026-07-04-share-app-bin-boot-glue.zh.md b/.agents/notes/archived/simplification/2026-07-04-share-app-bin-boot-glue.zh.md
new file mode 100644
index 0000000000..ed5146030c
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-share-app-bin-boot-glue.zh.md
@@ -0,0 +1,28 @@
+# Agent Note: 共享应用 bin 的启动胶水代码,而非维护两份副本
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-04-share-app-bin-boot-glue.md) | 中文
+
+## 问题
+
+stdio 和 ACP(Agent Client Protocol)两个 bin 各自重复了环境加载、fail-loud 处理、入口校验与启动逻辑,包括微妙的 Loader 失败行为。两份副本已经发生漂移,且位于自执行文件中、被排除在单元测试覆盖率之外,导致其导出的辅助函数无法被复用。
+
+## 决策
+
+辅助函数只存在一处:[`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot)(`packages/ui/app-boot`,归入 `ui` 分组,因为 bin 是已发布产物,其运行时依赖本身也必须是已发布的包,而非 `support/`)。包含:`resolveConfigPath`(快照感知,两个 bin 共用的唯一路径解析器)、`loadEnv`、`installFailLoud`、`assertEntriesLoaded` 与 `boot`,每个函数都通过 bin 的诊断前缀参数化,并在其副作用 seam(warn sink、process slice)处支持注入,使单元测试套件能覆盖每个分支——包括 `boot()` 在进程内驱动真实 Loader、使用相对路径 specifier 配置的场景,既覆盖已稳定树的正常路径,也覆盖无 fiber 入口的拒绝路径。该包启用逐文件 100% 覆盖率门禁;Loader 失败的相关知识只有一个归属地。
+
+每个 `bin.ts` 都是在共享辅助函数之上加应用特有生命周期的精简自执行组合(ACP bin:重放模式环境变量跳过和 stdin EOF 释放;stdio bin:没有额外逻辑)。这些 bin 仍排除在覆盖率之外且不导出任何内容;已发布产物守卫保持不变——按照“真实入口路径即已发布产物”的防御模式,已构建 bin 冒烟仍在具有 node_modules 形状的临时目录中用纯 node 运行每个 bin(现在也会符号链接 `ui/app-boot`),并继续断言缺失配置时以非零状态退出。[提取示例应用包 Agent Note(agent 决策记录)](../architecture/2026-06-20-extract-example-app-packages.md)中的 bin 归属事实已据此修改。
+
+## 曾考虑的替代方案
+
+### 为何不保留重复?
+
+这些 bin 当时被定位为归属相互独立的已发布产物,而新包会带来固定开销(清单、README、tsconfig 引用、publint 表面),与去重的行数相当。但创建 bin 的 Agent Note 从未权衡应用间共享——它把三份示例 `start.ts` 副本合并进 bin 后便止步于此;漂移是已经观察到的事实;覆盖率缺口的理由也独立于去重理由:这是仓库中唯一免受逐文件 100% 门禁约束的非平凡运行时逻辑。记录的后备方案(只将纯逻辑提取到各应用模块)会结束豁免,但会继续让相关知识拥有两个归属。
+
+## 后果
+
+- 启动胶水代码的变更(新增守卫、修复路径解析)只需落地一次,两个已发布 bin 自动继承;bin 之间不会再次漂移。
+- `dsh-app-boot` 保持轻量依赖(cordis + loader/include 对)——它是启动机制,不是应用表面积。
+- bin 自身的文件几乎是平凡的组合;所有含分支的逻辑都在覆盖率门禁之下。
diff --git a/.agents/notes/archived/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.i18n.yaml b/.agents/notes/archived/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.i18n.yaml
new file mode 100644
index 0000000000..b3fa52eab4
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.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
+2026-07-04-trim-acp-bridge-unreachable-surface.md: 2b0e6bba085f30959cd3c33362cd369ca7aec422
+2026-07-04-trim-acp-bridge-unreachable-surface.zh.md: ab924775588e61841f835bfe73ccae6d9886fd2a
diff --git a/.agents/notes/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md b/.agents/notes/archived/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md
similarity index 51%
rename from .agents/notes/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md
rename to .agents/notes/archived/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md
index 7d1065d250..2b0e6bba08 100644
--- a/.agents/notes/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md
+++ b/.agents/notes/archived/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md
@@ -1,17 +1,22 @@
# Agent Note: Trim unreachable ACP bridge surface — the branding knobs and the kind-sniffing fallback
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-04-trim-acp-bridge-unreachable-surface.zh.md)
+
+> The handshake-identity simplification remains current. The generic-card fallback was removed when [ACP became automation-only](2026-07-23-acp-automation-only-protocol.md); UI transports retain the provider-neutral presentation contract.
## Problem
Two pieces of `dsh-acp` surface were unreachable from any shipped configuration:
-1. **`AcpConfig.agentName` / `agentVersion`** (`packages/ui/acp/src/index.ts`). The shipped app package hands the bridge only `{ model }` (`packages/examples/acp-demo/src/index.ts`), so no leaf `cordis.yml` — the only production config surface — could set the knobs at all; they were settable solely by direct-mounting the bridge, which only a unit test did. Every snapshot expected output — the hook-matrix scenarios included — pins the schema defaults (`deepseek-harness-acp` / `0.0.1`). The pair also carried a live `TODO(double-default)`: the literals existed twice (schema `.default(...)` plus `??` fallbacks), with the TODO asking to pick one home.
+1. **`AcpConfig.agentName` / `agentVersion`** (`packages/acp/acp/src/index.ts`). The shipped app package hands the bridge only its agent target (`packages/examples/acp-demo/src/index.ts`), so no leaf `cordis.yml` — the only production config surface — could set the knobs at all; they were settable solely by direct-mounting the bridge, which only a unit test did. Every snapshot expected output — the hook-matrix scenarios included — pins the schema defaults (`deepseek-harness-acp` / `0.0.1`). The pair also carried a live `TODO(double-default)`: the literals existed twice (schema `.default(...)` plus `??` fallbacks), with the TODO asking to pick one home.
2. **The `toolKindFor` name heuristic** (same file) special-cased `bash*`/`read*`/`write`/`edit*` tool names in the generic-fallback path. Since the [render-intent union](../architecture/2026-07-02-tool-render-intent-union.md), every first-party tool those arms matched ships its own `presentCall` carrying its kind, and the presenter-less production tools (`subagent`, `subagent_fork`) fell through to `other` anyway. The arms were production-reachable only when a tool declined to present its own call — a `presentCall` that THROWS (the containment fallback), or model arguments that fail the tool's schema so `defineTool`'s `presentCall` wrapper returns `undefined` (e.g. a `bash` call missing the required `description`) — and the bridge's own module doc states the design rule the heuristic violated: "the bridge never special-cases tool names".
## Decision
-Hardcode the existing handshake identity `{ name: 'deepseek-harness-acp', version: '0.0.1' }` at initialization and remove the unreachable config fields and duplicate defaults. Replace `toolKindFor` with neutral `'other'` at both presenter fallbacks. Normal first-party presentations are unchanged; malformed or failed presentations now render an honest generic card instead of inferring a kind from the tool name. Initialize tests and snapshots pin the handshake; only the malformed calls in `hook-codex-posttool-block` change fallback card kind.
+Hardcode the existing handshake identity `{ name: 'deepseek-harness-acp', version: '0.0.1' }` at initialization and remove the unreachable config fields and duplicate defaults. The original implementation also replaced `toolKindFor` with neutral `'other'` at both presenter fallbacks; ACP no longer projects tool cards, so that fallback has left the transport entirely. Initialize tests and snapshots pin the handshake.
## Alternatives considered
@@ -21,4 +26,4 @@ Branding can return when the app package exposes it to deployments. Inferring pr
## Consequences
-Nothing beyond the fallback rendering trade described above — degenerate paths whose neutral card is more diagnosable than an inferred first-party one.
+The bridge exposes no branding knobs. UI transports own generic presentation fallback without tool-name inference, while ACP carries no tool-card surface.
diff --git a/.agents/notes/archived/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.zh.md b/.agents/notes/archived/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.zh.md
new file mode 100644
index 0000000000..ab92477558
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.zh.md
@@ -0,0 +1,29 @@
+# Agent Note: 裁剪不可达的 ACP 桥接层表面——品牌配置项与 kind 嗅探回退
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-04-trim-acp-bridge-unreachable-surface.md) | 中文
+
+> 握手标识简化仍然有效。通用卡片回退已随 [ACP 转为仅面向自动化](2026-07-23-acp-automation-only-protocol.md)一并移除;UI 传输层保留提供方无关的展示契约。
+
+## 问题
+
+`dsh-acp` 有两处对外表面在任何已交付的配置中都不可达:
+
+1. **`AcpConfig.agentName` / `agentVersion`**(`packages/acp/acp/src/index.ts`)。已发布应用包只向 bridge 传递其 agent 的提供方/模型目标(`packages/examples/acp-demo/src/index.ts`),因此没有任何叶子 `cordis.yml`——唯一的生产配置表面——能够设置这些配置项;只有直接挂载 bridge 才能设置它们,而这种做法只存在于一个单元测试中。每份快照预期输出——包括钩子矩阵场景——都固定 schema 默认值(`deepseek-harness-acp` / `0.0.1`)。这对配置项还带有一个尚未解决的 `TODO(double-default)`:字面量存在两次(schema `.default(...)` 加 `??` 后备值),TODO 要求为它们选择一个归属。
+2. **`toolKindFor` 名称启发式**(同一文件)在通用回退路径中对 `bash*`/`read*`/`write`/`edit*` 工具名做了特殊处理。自[render-intent 联合类型](../architecture/2026-07-02-tool-render-intent-union.md)以来,这些分支匹配到的每个第一方工具都自带 `presentCall` 并携带其 kind,而没有 presenter 的生产工具(`subagent`、`subagent_fork`)本来就落入 `other`。这些分支只有在工具拒绝自行呈现调用时才在生产中可达:`presentCall` 抛出异常(容错回退),或模型参数未通过工具 schema 导致 `defineTool` 的 `presentCall` 包装层返回 `undefined`(例如 `bash` 调用缺少必需的 `description`)。而桥接层自身的模块文档明确声明了该启发式所违反的设计规则:"桥接层绝不对工具名做特殊处理"。
+
+## 决策
+
+在初始化时硬编码现有的握手标识 `{ name: 'deepseek-harness-acp', version: '0.0.1' }`,移除不可达的配置字段与重复默认值。最初的实现还在两个 presenter 回退处将 `toolKindFor` 替换为中性的 `'other'`;ACP 不再投影工具卡片,因此该回退已完全离开传输层。初始化测试和快照固定握手标识。
+
+## 曾考虑的替代方案
+
+### 为什么不保留?
+
+品牌配置可以在 app 包将其暴露给部署环境时再回来。从未知工具名推断呈现方式违反了 render-intent 契约;中性回退卡片还能为格式错误的调用和损坏的 presenter 保留原始输入。
+
+## 后果
+
+桥接层不暴露品牌配置项。UI 传输层拥有不做工具名推断的通用展示回退,而 ACP 不承载任何工具卡片表面。
diff --git a/.agents/notes/archived/simplification/2026-07-12-drop-unconsumed-skill-provider-events.i18n.yaml b/.agents/notes/archived/simplification/2026-07-12-drop-unconsumed-skill-provider-events.i18n.yaml
new file mode 100644
index 0000000000..374974a45d
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-12-drop-unconsumed-skill-provider-events.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
+2026-07-12-drop-unconsumed-skill-provider-events.md: 88ce8d01dfbae8347e8671b52597f45735d6890e
+2026-07-12-drop-unconsumed-skill-provider-events.zh.md: ac4e3ff0277867e3b68d9b5c8f999b8819166ae4
diff --git a/.agents/notes/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md b/.agents/notes/archived/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md
similarity index 96%
rename from .agents/notes/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md
rename to .agents/notes/archived/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md
index a719e20f1e..88ce8d01df 100644
--- a/.agents/notes/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md
+++ b/.agents/notes/archived/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md
@@ -1,6 +1,9 @@
# Agent Note: Drop unconsumed skill provider events
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-12-drop-unconsumed-skill-provider-events.zh.md)
## Problem
diff --git a/.agents/notes/archived/simplification/2026-07-12-drop-unconsumed-skill-provider-events.zh.md b/.agents/notes/archived/simplification/2026-07-12-drop-unconsumed-skill-provider-events.zh.md
new file mode 100644
index 0000000000..ac4e3ff027
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-12-drop-unconsumed-skill-provider-events.zh.md
@@ -0,0 +1,30 @@
+# Agent Note: 移除无消费方的 skill 提供方事件
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-12-drop-unconsumed-skill-provider-events.md) | 中文
+
+## 问题
+
+skill(技能)注册表产出两个通知事件,但没有生产环境的监听方。生成的生产者/消费方矩阵以及对事件名的精确搜索表明,`skill/provider-added` 与 `skill/provider-removed` 仅出现在声明、emit 站点、测试、生成的 catalog 和行文中。
+
+skill 发现按需读取当前的提供方映射表,提供方注册时同步清除已完成的 catalog,而 await 后的版本检查阻止了陈旧的发现结果进入缓存。没有兄弟插件通过这些事件等待 skill 提供方——与之形成对比的是活跃的 `subagent/provider-added` 消费方,它容忍兄弟并发加载。
+
+`tools/change` 与 `system-prompt/change` 明确不在本提案范围内。既有的简化决策将它们保留为面向实时工具和提示词 UI 的有意观测点,且自引用的已挂载插件已在使用 `tools/change`。本提案同样不改动 `subagent/provider-added`/`removed`,因为 `tool-subagent` 有生产环境的生命周期消费方。
+
+## 决策
+
+skill 注册表不再声明和 emit 提供方成员变更事件。提供方的注册与 dispose(资源释放)仍为 effect 所有的直接状态变更,同步使已完成的 catalog 失效;查找与发现按需读取当前提供方映射表。测试通过提供方查找和收集到的输出来观察清理行为,而非依赖生命周期通知。
+
+生成式事件目录、API 目录和生产者/消费方矩阵均不再包含已删除通知。skill system Agent Note(agent 决策记录)和包文档改为通过其由 effect 直接拥有的状态与 cache 失效契约描述注册。
+
+## 曾考虑的替代方案
+
+**为未来插件保留 skill 提供方通知。** 第三方插件可能想观察提供方的可用性,但直接提供方注册与按需查找才是扩展契约;当前没有消费方需要推送信号。如果将来出现兄弟加载竞态,可以像 subagent 注册表那样,引入一个带有该消费方实际所需的身份与就绪语义的通知。
+
+## 后果
+
+生成的事件矩阵中不再有 `skill/provider-added` 或 `skill/provider-removed` 的行。skill 发现、直接运行时注册、提供方 effect 回滚/dispose、缓存失效与注册表查找清理保持不变;监听方触发的回滚随事件一起消失。`tools/change`、`system-prompt/change` 以及已被消费的 subagent 提供方生命周期事件不受影响。
+
+预发布消费方失去 skill 提供方观测点,但仍保留两种贡献 skill 的方式:直接运行时注册与提供方注册。未来若有消费方需要实时的提供方可用性信息,必须新增一个带有其实际所需的身份与就绪语义的专用通知。
diff --git a/.agents/notes/archived/simplification/2026-07-12-prune-unused-web-seam-fields.i18n.yaml b/.agents/notes/archived/simplification/2026-07-12-prune-unused-web-seam-fields.i18n.yaml
new file mode 100644
index 0000000000..856d5e4d46
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-12-prune-unused-web-seam-fields.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
+2026-07-12-prune-unused-web-seam-fields.md: 4ad6dbb2efe314977d5e82f49e849dda9408146d
+2026-07-12-prune-unused-web-seam-fields.zh.md: 58706f1248132606d3d2d27603f7283eae310f66
diff --git a/.agents/notes/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md b/.agents/notes/archived/simplification/2026-07-12-prune-unused-web-seam-fields.md
similarity index 97%
rename from .agents/notes/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md
rename to .agents/notes/archived/simplification/2026-07-12-prune-unused-web-seam-fields.md
index 68ced83876..4ad6dbb2ef 100644
--- a/.agents/notes/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md
+++ b/.agents/notes/archived/simplification/2026-07-12-prune-unused-web-seam-fields.md
@@ -1,6 +1,9 @@
# Agent Note: Prune unused web seam fields
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-12-prune-unused-web-seam-fields.zh.md)
## Problem
diff --git a/.agents/notes/archived/simplification/2026-07-12-prune-unused-web-seam-fields.zh.md b/.agents/notes/archived/simplification/2026-07-12-prune-unused-web-seam-fields.zh.md
new file mode 100644
index 0000000000..58706f1248
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-12-prune-unused-web-seam-fields.zh.md
@@ -0,0 +1,28 @@
+# Agent Note: 裁剪 web seam 中未使用的字段
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-12-prune-unused-web-seam-fields.md) | 中文
+
+## 问题
+
+web 能力携带的 request/result/status 值,虽然每个已交付的实现都会填充,但没有任何生产环境的消费方读取它们。`WebSearchResult.providerId`、`query` 与 `WebFetchResult.providerId` 是结果回显;`tool-web` 只格式化 content/sources/truncation 或最终 URL/status/body/truncation,没有其他运行时读取这些字段。搜索提供方返回 `WebProviderStatus.reason`,但可用性检查只看 `available`,并有意输出一条通用的不可用诊断信息。
+
+`WebFetchRequest.timeoutMs` 同样从未被生产调用方设置。`tool-web` 只提供 URL,使用工具定义的 timeout 加 `exec.signal` 作为调用方截止时间,并依赖本地提供方的配置默认值作为兜底。这个未使用的逐请求覆盖迫使 `web-fetch-local` 暴露 `maxTimeoutMs`、对两个 timeout 来源做 clamp,并为没有任何产品路径能选中的优先级规则编写文档和测试。`WebExecContext` 则是另一个单字段包装层:每个调用方分配 `{ signal }`,每个提供方立即解包 `exec?.signal`;不存在第二个执行控制字段。
+
+## 决策
+
+web seam 移除搜索/抓取结果中的 `providerId` 回显和搜索的 `query` 回显;调用方本身已持有请求和提供方选择信息。提供方以返回布尔值的方法暴露可用性。抓取请求不再有逐请求 timeout 或 `maxTimeoutMs` clamp;本地提供方保留其可配置的默认 timeout,工具保留自身的截止时间。提供方方法直接接收一个可选的 `AbortSignal`,而非单字段的 `WebExecContext` 包装层。
+
+所有 web 实现与面向模型的工具使用更精简的契约。接口/实现/消费方的包(package)拆分、提供方选择、来源引用、最终 URL/状态数据、截断报告与安全限制保持不变。
+
+## 曾考虑的替代方案
+
+**保留自描述结果、逐请求截止时间与可扩展的执行上下文对象。** 结果回显可以帮助通用遥测,请求级 timeout 可以帮助受信的程序化调用方,包装层则为未来的控制字段留出空间。但目前不存在这样的消费方或第二个字段;在每个提供方中携带重复的身份标识、第二套截止时间策略以及包装/解包管道,使当前契约更难实现和解释。如果遥测或逐调用预算控制到来,届时应当定义哪个截止时间优先、在哪里观测提供方身份,以及多个控制字段是否足以证明需要一个上下文对象。
+
+## 后果
+
+保留下来的每个 web request/result 字段,要么被生产代码消费,要么是执行提供方请求所必需的。工具可见的搜索/抓取输出、提供方回退、中止行为、可配置的 timeout 兜底、截断与引用仍然被覆盖,无需请求级 timeout 优先级分支或执行上下文包装层。
+
+预发布阶段的程序化调用方失去了结果来源回显和逐请求的抓取截止时间。提供方仍具备部署级可配置 timeout 并尊重取消信号,因此这次精简移除的是可配置性,而非安全边界。
diff --git a/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml b/.agents/notes/archived/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml
similarity index 62%
rename from .agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml
rename to .agents/notes/archived/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml
index 2b0b5c067d..0259af5b2a 100644
--- a/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml
+++ b/.agents/notes/archived/simplification/2026-07-19-retire-subagent-mock-package.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
-2026-07-19-retire-subagent-mock-package.md: 4a7fa32fdb0d8e656d61c39491a49bbd85e0adf3
-2026-07-19-retire-subagent-mock-package.zh.md: 7de72abb18050fb737000a2013e514dde3dae521
+2026-07-19-retire-subagent-mock-package.md: 9fce0d2f8ea20b4f31e2042acba4df34152e3201
+2026-07-19-retire-subagent-mock-package.zh.md: d82650c9e74f8fff1386185f0930762d21a7243f
diff --git a/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.md b/.agents/notes/archived/simplification/2026-07-19-retire-subagent-mock-package.md
similarity index 99%
rename from .agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.md
rename to .agents/notes/archived/simplification/2026-07-19-retire-subagent-mock-package.md
index 4a7fa32fdb..9fce0d2f8e 100644
--- a/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.md
+++ b/.agents/notes/archived/simplification/2026-07-19-retire-subagent-mock-package.md
@@ -1,6 +1,7 @@
# Agent Note: Retire the standalone subagent mock package
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-19-retire-subagent-mock-package.zh.md)
diff --git a/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.zh.md b/.agents/notes/archived/simplification/2026-07-19-retire-subagent-mock-package.zh.md
similarity index 99%
rename from .agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.zh.md
rename to .agents/notes/archived/simplification/2026-07-19-retire-subagent-mock-package.zh.md
index 7de72abb18..d82650c9e7 100644
--- a/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.zh.md
+++ b/.agents/notes/archived/simplification/2026-07-19-retire-subagent-mock-package.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 撤销独立的 subagent mock 包
Status: implemented
+Archived: 2026-07-26
[English](2026-07-19-retire-subagent-mock-package.md) | 中文
diff --git a/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml b/.agents/notes/archived/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml
similarity index 61%
rename from .agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml
rename to .agents/notes/archived/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml
index cd03e02285..a499c4bef7 100644
--- a/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml
+++ b/.agents/notes/archived/simplification/2026-07-19-use-one-session-surface-manager.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
-2026-07-19-use-one-session-surface-manager.md: dee1a2a1cb6642730c87035de071d77ad38bd238
-2026-07-19-use-one-session-surface-manager.zh.md: ce538f1569c91e317af347d2ac20db624215eac8
+2026-07-19-use-one-session-surface-manager.md: 741e949ee04150cdee3328a2ff04d79688bd484d
+2026-07-19-use-one-session-surface-manager.zh.md: 87502c600c6c2d546ad728819ebbf08f7e55bda3
diff --git a/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.md b/.agents/notes/archived/simplification/2026-07-19-use-one-session-surface-manager.md
similarity index 99%
rename from .agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.md
rename to .agents/notes/archived/simplification/2026-07-19-use-one-session-surface-manager.md
index dee1a2a1cb..741e949ee0 100644
--- a/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.md
+++ b/.agents/notes/archived/simplification/2026-07-19-use-one-session-surface-manager.md
@@ -1,6 +1,7 @@
# Agent Note: Use one surface manager per session
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-19-use-one-session-surface-manager.zh.md)
diff --git a/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.zh.md b/.agents/notes/archived/simplification/2026-07-19-use-one-session-surface-manager.zh.md
similarity index 99%
rename from .agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.zh.md
rename to .agents/notes/archived/simplification/2026-07-19-use-one-session-surface-manager.zh.md
index ce538f1569..87502c600c 100644
--- a/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.zh.md
+++ b/.agents/notes/archived/simplification/2026-07-19-use-one-session-surface-manager.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 每个会话只使用一个表层管理器
Status: implemented
+Archived: 2026-07-26
[English](2026-07-19-use-one-session-surface-manager.md) | 中文
diff --git a/.agents/notes/implemented/simplification/2026-07-20-retire-readline-front-door.i18n.yaml b/.agents/notes/archived/simplification/2026-07-20-retire-readline-front-door.i18n.yaml
similarity index 63%
rename from .agents/notes/implemented/simplification/2026-07-20-retire-readline-front-door.i18n.yaml
rename to .agents/notes/archived/simplification/2026-07-20-retire-readline-front-door.i18n.yaml
index 232fec495b..815d23408b 100644
--- a/.agents/notes/implemented/simplification/2026-07-20-retire-readline-front-door.i18n.yaml
+++ b/.agents/notes/archived/simplification/2026-07-20-retire-readline-front-door.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
-2026-07-20-retire-readline-front-door.md: 7ebcfdc246bdf6971418609c61acbd4019aa90cb
-2026-07-20-retire-readline-front-door.zh.md: cf4d03594ed3a0cf31bed96eb2133bd37959084a
+2026-07-20-retire-readline-front-door.md: ecc9967b0ff97a998f9a1ac5c23e16ed82f1d797
+2026-07-20-retire-readline-front-door.zh.md: ea97ee79a2f901e350ef62f1afe6bfed3d7cb249
diff --git a/.agents/notes/implemented/simplification/2026-07-20-retire-readline-front-door.md b/.agents/notes/archived/simplification/2026-07-20-retire-readline-front-door.md
similarity index 91%
rename from .agents/notes/implemented/simplification/2026-07-20-retire-readline-front-door.md
rename to .agents/notes/archived/simplification/2026-07-20-retire-readline-front-door.md
index 7ebcfdc246..ecc9967b0f 100644
--- a/.agents/notes/implemented/simplification/2026-07-20-retire-readline-front-door.md
+++ b/.agents/notes/archived/simplification/2026-07-20-retire-readline-front-door.md
@@ -1,6 +1,7 @@
# Agent Note: Retire the readline front door and the repl-agent example
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-20-retire-readline-front-door.zh.md)
@@ -26,13 +27,13 @@ Pipes remain the default test medium. PTY-driven subprocess tests are sanctioned
- `examples/echo-agent/tests/echo.e2e.ts` proves the Loader boot + mock-model tool round-trip through `stream-json` records instead of readline transcript lines.
- The CI demo-smoke gate (`scripts/run-gates.ts`, AGENTS.md) runs `demo:echo --output-format stream-json -p "echo ci smoke"` and parses the records structurally.
-- `packages/examples/tui-demo/tests/built-bin.e2e.ts` proves the built bin's piped-launch refusal (nonzero exit + pointer at `dsh-cli-demo`); the echo-round-trip-under-plain-Node and missing-config fail-loud proofs live in `cli-demo`'s built-bin suite.
+- The TUI's piped-launch refusal (nonzero exit + pointer at the one-shot CLI) is covered by `apps/cli/tests/built-bin.e2e.ts` (the `dsh` TTY guard under plain Node); the echo-round-trip-under-plain-Node and missing-config fail-loud proofs live in `cli-demo`'s built-bin suite.
- `packages/context/time-context/tests/time-context.e2e.ts` runs one one-shot turn; multi-turn elapsed rendering stays unit-covered in its spec.
## Accepted losses
- **Piped multi-turn in one process** — the readline channel could script several turns over stdin; the one-shot bin runs one task per process. Multi-turn continuity is covered by `RESUME_SESSION_ID`/resume e2es and the TUI's scripted PTY conversation.
-- **Non-TTY `ask_user_question`** — the readline provider was the only non-TTY terminal implementation of `ctx.userInteraction`. A headless run whose model calls `ask_user_question` now fails that tool call (no provider); the ACP bridge remains the non-terminal provider. A future headless deployment that needs it composes its own provider.
+- **Non-TTY `ask_user_question`** — the readline provider was the only non-TTY terminal implementation of `ctx.userInteraction`. A headless or ACP automation run whose model calls `ask_user_question` fails that tool call unless its composition supplies a provider; Web owns the shipped non-terminal provider.
## Alternatives considered
diff --git a/.agents/notes/implemented/simplification/2026-07-20-retire-readline-front-door.zh.md b/.agents/notes/archived/simplification/2026-07-20-retire-readline-front-door.zh.md
similarity index 91%
rename from .agents/notes/implemented/simplification/2026-07-20-retire-readline-front-door.zh.md
rename to .agents/notes/archived/simplification/2026-07-20-retire-readline-front-door.zh.md
index cf4d03594e..ea97ee79a2 100644
--- a/.agents/notes/implemented/simplification/2026-07-20-retire-readline-front-door.zh.md
+++ b/.agents/notes/archived/simplification/2026-07-20-retire-readline-front-door.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 退役 readline 前端与 repl-agent 示例
Status: implemented
+Archived: 2026-07-26
[English](2026-07-20-retire-readline-front-door.md) | 中文
@@ -26,13 +27,13 @@ Status: implemented
- `examples/echo-agent/tests/echo.e2e.ts` 通过 `stream-json` 记录证明 Loader 启动 + mock 模型的工具往返,而不是匹配 readline 文本记录行。
- CI 演示冒烟门禁(`scripts/run-gates.ts`、AGENTS.md)运行 `demo:echo --output-format stream-json -p "echo ci smoke"` 并结构化解析记录。
-- `packages/examples/tui-demo/tests/built-bin.e2e.ts` 证明构建产物 bin 对管道启动的拒绝(非零退出 + 指向 `dsh-cli-demo` 的提示);纯 Node 下的 echo 往返证明与缺失配置的快速失败证明位于 `cli-demo` 的 built-bin 套件。
+- TUI 对管道启动的拒绝(非零退出 + 指向单次任务 CLI 的提示)由 `apps/cli/tests/built-bin.e2e.ts`(纯 Node 下的 `dsh` TTY 守卫)覆盖;纯 Node 下的 echo 往返证明与缺失配置的快速失败证明位于 `cli-demo` 的 built-bin 套件。
- `packages/context/time-context/tests/time-context.e2e.ts` 运行一个单次任务轮次;多轮 elapsed 渲染仍由其单元测试覆盖。
## 接受的损失
- **单进程内的管道多轮对话**——readline 通道可以通过 stdin 脚本化多个轮次;单次任务 bin 每个进程只运行一个任务。多轮连续性由 `RESUME_SESSION_ID`/resume e2e 和 TUI 的脚本化 PTY 对话覆盖。
-- **非 TTY 的 `ask_user_question`**——readline 提供方是 `ctx.userInteraction` 唯一的非 TTY 终端实现。模型调用 `ask_user_question` 的 headless 运行现在会让该工具调用失败(没有提供方);ACP 桥接仍是非终端提供方。未来需要它的 headless 部署自行组合提供方。
+- **非 TTY 的 `ask_user_question`**——readline 提供方是 `ctx.userInteraction` 唯一的非 TTY 终端实现。模型调用 `ask_user_question` 的 headless 或 ACP 自动化运行会让该工具调用失败,除非其组合提供相应的 provider;Web 拥有已交付的非终端 provider。
## 曾考虑的替代方案
diff --git a/.agents/notes/implemented/simplification/2026-07-21-tui-remove-cancel-command.i18n.yaml b/.agents/notes/archived/simplification/2026-07-21-tui-remove-cancel-command.i18n.yaml
similarity index 63%
rename from .agents/notes/implemented/simplification/2026-07-21-tui-remove-cancel-command.i18n.yaml
rename to .agents/notes/archived/simplification/2026-07-21-tui-remove-cancel-command.i18n.yaml
index 62bf9574c0..ca9895d9ed 100644
--- a/.agents/notes/implemented/simplification/2026-07-21-tui-remove-cancel-command.i18n.yaml
+++ b/.agents/notes/archived/simplification/2026-07-21-tui-remove-cancel-command.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
-2026-07-21-tui-remove-cancel-command.md: f9bad74e7b8f04a162a32e8045d2f874991b9d5d
-2026-07-21-tui-remove-cancel-command.zh.md: 6a4c0af1d2ac345afd775566db21f7a7c0b262da
+2026-07-21-tui-remove-cancel-command.md: eba4ada458cd7926cc06130d081b669da514fe79
+2026-07-21-tui-remove-cancel-command.zh.md: 9954af56682591287abc955d9f2485f02e1ce6b8
diff --git a/.agents/notes/implemented/simplification/2026-07-21-tui-remove-cancel-command.md b/.agents/notes/archived/simplification/2026-07-21-tui-remove-cancel-command.md
similarity index 99%
rename from .agents/notes/implemented/simplification/2026-07-21-tui-remove-cancel-command.md
rename to .agents/notes/archived/simplification/2026-07-21-tui-remove-cancel-command.md
index f9bad74e7b..eba4ada458 100644
--- a/.agents/notes/implemented/simplification/2026-07-21-tui-remove-cancel-command.md
+++ b/.agents/notes/archived/simplification/2026-07-21-tui-remove-cancel-command.md
@@ -1,6 +1,7 @@
# Agent Note: Drop the TUI `/cancel` slash command
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-21-tui-remove-cancel-command.zh.md)
diff --git a/.agents/notes/implemented/simplification/2026-07-21-tui-remove-cancel-command.zh.md b/.agents/notes/archived/simplification/2026-07-21-tui-remove-cancel-command.zh.md
similarity index 99%
rename from .agents/notes/implemented/simplification/2026-07-21-tui-remove-cancel-command.zh.md
rename to .agents/notes/archived/simplification/2026-07-21-tui-remove-cancel-command.zh.md
index 6a4c0af1d2..9954af5668 100644
--- a/.agents/notes/implemented/simplification/2026-07-21-tui-remove-cancel-command.zh.md
+++ b/.agents/notes/archived/simplification/2026-07-21-tui-remove-cancel-command.zh.md
@@ -1,6 +1,7 @@
# Agent Note: Drop the TUI `/cancel` slash command
Status: implemented
+Archived: 2026-07-26
[English](2026-07-21-tui-remove-cancel-command.md) | 中文
diff --git a/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.i18n.yaml b/.agents/notes/archived/simplification/2026-07-21-tui-todo-write-opt-in.i18n.yaml
similarity index 64%
rename from .agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.i18n.yaml
rename to .agents/notes/archived/simplification/2026-07-21-tui-todo-write-opt-in.i18n.yaml
index 4e0393bede..5f3c3b7d0d 100644
--- a/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.i18n.yaml
+++ b/.agents/notes/archived/simplification/2026-07-21-tui-todo-write-opt-in.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
-2026-07-21-tui-todo-write-opt-in.md: f89f76a462f4d30960254833ab71973f6a4f7655
-2026-07-21-tui-todo-write-opt-in.zh.md: f80d2639612819975f03aea9771019cd5237a2ee
+2026-07-21-tui-todo-write-opt-in.md: fd2ad9bb89bd20ffddf4d509e427a34c5ec0cf2b
+2026-07-21-tui-todo-write-opt-in.zh.md: 5b1a7f19a48c87686cfc2052d4ad19ed45ab1fb9
diff --git a/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.md b/.agents/notes/archived/simplification/2026-07-21-tui-todo-write-opt-in.md
similarity index 99%
rename from .agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.md
rename to .agents/notes/archived/simplification/2026-07-21-tui-todo-write-opt-in.md
index f89f76a462..fd2ad9bb89 100644
--- a/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.md
+++ b/.agents/notes/archived/simplification/2026-07-21-tui-todo-write-opt-in.md
@@ -1,6 +1,7 @@
# Agent Note: Ship the TUI without `todo_write`; keep it a one-line opt-in
Status: implemented
+Archived: 2026-07-26
English | [中文](2026-07-21-tui-todo-write-opt-in.zh.md)
diff --git a/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.zh.md b/.agents/notes/archived/simplification/2026-07-21-tui-todo-write-opt-in.zh.md
similarity index 99%
rename from .agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.zh.md
rename to .agents/notes/archived/simplification/2026-07-21-tui-todo-write-opt-in.zh.md
index f80d263961..5b1a7f19a4 100644
--- a/.agents/notes/implemented/simplification/2026-07-21-tui-todo-write-opt-in.zh.md
+++ b/.agents/notes/archived/simplification/2026-07-21-tui-todo-write-opt-in.zh.md
@@ -1,6 +1,7 @@
# Agent Note: Ship the TUI without `todo_write`; keep it a one-line opt-in
Status: implemented
+Archived: 2026-07-26
[English](2026-07-21-tui-todo-write-opt-in.md) | 中文
diff --git a/.agents/notes/implemented/simplification/2026-07-22-tui-titles-from-session-title-service.i18n.yaml b/.agents/notes/archived/simplification/2026-07-22-tui-titles-from-session-title-service.i18n.yaml
similarity index 60%
rename from .agents/notes/implemented/simplification/2026-07-22-tui-titles-from-session-title-service.i18n.yaml
rename to .agents/notes/archived/simplification/2026-07-22-tui-titles-from-session-title-service.i18n.yaml
index 72e2341385..8e0bf97bf0 100644
--- a/.agents/notes/implemented/simplification/2026-07-22-tui-titles-from-session-title-service.i18n.yaml
+++ b/.agents/notes/archived/simplification/2026-07-22-tui-titles-from-session-title-service.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
-2026-07-22-tui-titles-from-session-title-service.md: b54b99647230255cf241415f94aa21b2630c44cd
-2026-07-22-tui-titles-from-session-title-service.zh.md: 67cc3332f0694887d5af0d71997d140b74669f46
+2026-07-22-tui-titles-from-session-title-service.md: 946ef283f91249c70f82280b754ef991665dbaa3
+2026-07-22-tui-titles-from-session-title-service.zh.md: f2fa4772c9fa404540dcf8a514cf571a284d4967
diff --git a/.agents/notes/archived/simplification/2026-07-22-tui-titles-from-session-title-service.md b/.agents/notes/archived/simplification/2026-07-22-tui-titles-from-session-title-service.md
new file mode 100644
index 0000000000..946ef283f9
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-22-tui-titles-from-session-title-service.md
@@ -0,0 +1,36 @@
+# Agent Note: TUI titles come from the session-title service
+
+Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-07-22-tui-titles-from-session-title-service.zh.md)
+
+## Problem
+
+A per-session title makes terminal panes and tabs distinguishable, but a TUI-local model call would create a second title pipeline beside [log-backed session titles](../feature/2026-07-21-log-backed-session-titles.md). The local path needs its own prompt, cap, one-shot latch, resume derivation, cancellation, and failure fallback, while its process-local result remains invisible to session listings, forks, Web consumers, and replay. If both paths run, one session can also be titled twice by different strategies.
+
+## Decision
+
+The session-title service is the one title source. The TUI contains no `autoTitle` config, title-model request, latch, abort controller, prompt, or output cap. It folds the latest logged title on mount (`foldSessionTitle`), renders it as the banner subtitle, and calls `runtime.terminal.setTitle` with ` — ` on every accepted `session/title` event. The same terminal-safe OSC 0 path handles the configured fallback title, resumed sessions, and live revisions without renaming tmux windows or adding another terminal-control surface.
+
+Model-made titles are a composition choice: `examples/tui-agent/cordis.yml` (and the scripted PTY fixture) mount `@deepseek-ai/dsh-session-title-first-message-llm`, which inherits the main request's route and replaces the spine's deterministic fallback with a short model summary. Deployments without the provider keep the fallback title from `dsh-agent-spine-demo`'s bundled `SessionTitleService`.
+
+## Alternatives considered
+
+**Keep both, letting the logged title win.** This was the first merge resolution: auto-title owned the whole window title until a logged `session/title` arrived in suffix form. It preserved behavior but doubled the model calls on every fresh session and left the TUI's title unobservable in the log, violating model-visible ⟺ logged in spirit and splitting the title contract across two owners.
+
+**Port auto-title's prompt and cap into the service as a third provider.** The first-message-llm provider already exists with the same cadence, a reviewed prompt contract, durable request records, and supersession fencing; a second near-identical provider would be pure duplication.
+
+**Use only a truncated first prompt or only a model title.** A deterministic fallback provides an immediate, free title, while an optional model provider improves quality without delaying the main turn. Forcing either strategy removes that deployment choice.
+
+**Make model titles a TUI default or block the first turn for them.** The cost and route belong to composition, and auxiliary title latency must stay off the interaction critical path. The TUI consumes accepted state instead of owning generation policy.
+
+**Rename a tmux window or use a separate terminal escape.** Rejected because the existing terminal adapter's OSC 0 path labels the pane or tab without acquiring tmux ownership or adding a second control API.
+
+## Verification
+
+TUI tests pin restored and live `session/title` consumption, terminal-safe title rendering, the configured fallback, and the absence of a TUI-owned model path. The keyless PTY smoke boots the real composition, accepts a logged provider title, and observes the resulting terminal title. The [log-backed title decision](../feature/2026-07-21-log-backed-session-titles.md) owns provider, persistence, resume, fork, cancellation, and stale-completion coverage.
+
+## Consequences
+
+One title pipeline is durable, replayable, visible to every consumer, and fenced against stale completions by the service. The TUI has no `llm`-streaming title path. Model quality requires a provider plugin in the composition, while deployments without one keep the deterministic fallback; the terminal title consistently uses the suffixed ` — ` shape.
diff --git a/.agents/notes/archived/simplification/2026-07-22-tui-titles-from-session-title-service.zh.md b/.agents/notes/archived/simplification/2026-07-22-tui-titles-from-session-title-service.zh.md
new file mode 100644
index 0000000000..f2fa4772c9
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-22-tui-titles-from-session-title-service.zh.md
@@ -0,0 +1,36 @@
+# Agent Note: TUI 标题来自 session-title 服务
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-07-22-tui-titles-from-session-title-service.md) | 中文
+
+## 问题
+
+每会话标题让终端窗格和标签页易于区分,但 TUI 本地模型调用会在[日志承载的会话标题](../feature/2026-07-21-log-backed-session-titles.md)旁形成第二条标题管线。本地路径需要自己的提示词、截断上限、一次性闩锁、恢复推导、取消和失败回退,而其进程本地结果仍对会话列表、fork、Web 消费方和回放不可见。若两条路径同时运行,同一会话还可能被不同策略命名两次。
+
+## 决策
+
+session-title 服务是唯一的标题来源。TUI 不包含 `autoTitle` 配置、标题模型请求、闩锁、abort controller、提示词或输出上限。TUI 在挂载时折叠最新的已记录标题(`foldSessionTitle`),将其渲染为横幅副标题,并在每个被接受的 `session/title` 事件上调用 `runtime.terminal.setTitle`,传入 ` — `。同一条终端安全的 OSC 0 路径会处理配置的回退标题、恢复的会话和实时修订,既不重命名 tmux 窗口,也不增加另一套终端控制接口。
+
+模型生成的标题是组合选择:`examples/tui-agent/cordis.yml`(以及脚本化 PTY fixture)挂载 `@deepseek-ai/dsh-session-title-first-message-llm`,它继承主请求的确切路由,用简短的模型摘要替换 spine 的确定性回退。未挂载该 provider 的部署保留 `dsh-agent-spine-demo` 内置 `SessionTitleService` 的回退标题。
+
+## 备选方案
+
+**两者并存,已记录标题胜出。** 这是第一版合并决议:auto-title 独占整个窗口标题,直到已记录的 `session/title` 以后缀形式到达。它保留了行为,但每个新会话产生双倍模型调用,且 TUI 的标题在日志中不可观察,实质上违反 model-visible ⟺ logged,并把标题契约拆给两个所有者。
+
+**把 auto-title 的提示词和截断移植为服务的第三个 provider。** first-message-llm provider 已经存在,节奏相同,且有经过评审的提示词契约、持久的请求记录和替换围栏;再造一个近乎相同的 provider 纯属重复。
+
+**只使用截断后的首条提示词,或只使用模型标题。** 确定性回退可以立即且免费地提供标题,而可选模型 provider 可以提升质量,不会延迟主轮次。强制采用任一种策略都会移除这项部署选择。
+
+**让模型标题成为 TUI 默认行为,或为此阻塞第一个轮次。** 成本与路由归组合所有,辅助标题的延迟不得进入交互关键路径。TUI 只消费已接受的状态,不拥有生成策略。
+
+**重命名 tmux 窗口,或使用另一种终端转义序列。** 不予采纳,因为现有终端适配器的 OSC 0 路径可以标记窗格或标签页,无需取得 tmux 归属,也无需增加第二套控制 API。
+
+## 验证
+
+TUI 测试锁定恢复后和实时的 `session/title` 消费、终端安全的标题渲染、配置的回退标题,以及不存在 TUI 自有模型路径。无密钥 PTY 冒烟测试启动真实组合,接收已记录的 provider 标题,并观察由此产生的终端标题。[日志承载标题决策](../feature/2026-07-21-log-backed-session-titles.md)拥有 provider、持久化、恢复、fork、取消和陈旧完成结果的覆盖。
+
+## 影响
+
+唯一的标题管线持久、可回放、对所有消费方可见,并由服务防止陈旧完成结果生效。TUI 不再有 `llm` 流式标题路径。若要提升模型标题质量,组合中必须挂载 provider 插件;未挂载的部署保留确定性回退。终端标题始终采用 ` — ` 后缀形式。
diff --git a/.agents/notes/archived/simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.i18n.yaml b/.agents/notes/archived/simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.i18n.yaml
new file mode 100644
index 0000000000..6c3719ecf4
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.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
+2026-07-26-consolidate-gate-scripts-on-existing-deps.md: 9ad27ff8b2fe7a68511c01a1125dd01289aa4a7f
+2026-07-26-consolidate-gate-scripts-on-existing-deps.zh.md: b01133fad22c8d8244d787f8740d2d1c29b2c41e
diff --git a/.agents/notes/archived/simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.md b/.agents/notes/archived/simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.md
new file mode 100644
index 0000000000..9ad27ff8b2
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.md
@@ -0,0 +1,34 @@
+# Agent Note: Consolidate gate scripts on already-present deps and builtins
+
+Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-07-26-consolidate-gate-scripts-on-existing-deps.zh.md)
+
+## Problem
+
+The `scripts/` gates mostly used the right tools (`node:fs` `globSync` in 15+ gates, mdast/micromark in the markdown gates), but a handful of stragglers hand-rolled what a sibling gate already did with an existing dependency or builtin:
+
+- **Duplicated fence scanners.** `scripts/md-fences.ts` (~55 lines, consumed by `doc-typecheck.ts`) and `extractEquivBlocks` in `scripts/verify-type-equiv.ts` (~39 lines) were two copies of the same regex line-scanner for fenced code blocks, while `scripts/verify-mermaid.ts` already extracted fences by visiting mdast `code` nodes — and `markdownProseLines` in `scripts/markdown.ts` itself parsed to mdast but then hand-tracked fence state with a second regex. The regex scanners only recognized backtick fences at column 0, so they silently disagreed with the mdast-based gates on tilde and indented fences.
+- **Hand-rolled argv parsing.** `parseOptions` in `scripts/publint-all.ts` and its near-identical copy in `scripts/verify-built-package-invariants.mjs` (~26 lines) stepped argv indexes manually, while sibling scripts (`verify-runtime-closure.ts`, `build-exe-for-python-sdk.ts`, `packages/sdk/scripts/src/args.ts`) already used the `node:util` `parseArgs` builtin.
+- **Hand-rolled directory walks.** Five sites re-derived nested `readdirSync` walks that `globSync` covers: `verify-runtime-closure.ts` (packages + vendor manifests), `dev-web.ts` `discoverPluginDirs`, `verify-package-paths.ts` `realPackageNames`, `verify-client-domain-graph.ts` `listSources`, and `publint-all.ts` `addPath` (~55–65 lines total). `scripts/package-invariants.ts` shows the one-line `globSync` template.
+
+No new dependency was needed anywhere; every replacement is an existing devDep or a Node builtin.
+
+## Decision
+
+- A shared mdast fence helper, `markdownFences` in `scripts/markdown.ts`, visits `code` nodes for the language, full info string, body, and 1-based opening-fence line; `doc-typecheck.ts` and `verify-type-equiv.ts` extract fences through it. `md-fences.ts` and the duplicated `extractEquivBlocks` scanner are deleted, and `markdownProseLines` derives fenced lines from the parsed `code` nodes' positions instead of a second regex.
+- Both CLIs parse argv via `parseArgs`; unknown options and missing values still fail loud, with `parseArgs`'s own error text instead of the bespoke usage strings.
+- The five straggler walks use `globSync`. The walks in `check-workspace-constraints.ts` and `clean.ts` stay: they need dirent-level detail to diagnose malformed trees, which glob-by-pattern cannot report.
+
+## Alternatives considered
+
+- **A new glob/walking dependency (`tinyglobby`, `fdir`).** Rejected: the builtin already won repo-wide; these were stragglers, not a gap.
+- **`p-map` for `publint-all.ts`'s ~19-line ordered worker pool.** Deliberately left out: one new devDep for one small deletion is at the edge of the [dependency policy](../process/2026-07-26-dependencies-over-hand-rolling.md) bar, and the pool's requirements (bounded workers, deterministic order, env override) are documented in the [parallel-gates note](../process/2026-07-06-parallel-pre-push-gates.md). Fold it in only if `p-map` earns a second consumer.
+- **Leaving the fence scanners.** Rejected: two drifting copies of a parser beside a third correct implementation is exactly the duplication the shared `markdown.ts` helper exists to prevent, and the column-0-backtick-only limitation was a latent inconsistency between sibling gates.
+
+## Consequences
+
+- One fence parser: every markdown gate now classifies fences through mdast, so tilde, indented, and 4-backtick container fences behave identically everywhere. The docs tree contained no fence shape the regex scanners mishandled, so gate results are unchanged on the tree that landed the swap: `pnpm run doc-sync` and each rewritten gate ran before and after with byte-identical output (`doc-typecheck` block/opt-out counts, `verify-type-equiv` match counts, `publint`, `verify-built-package-invariants`, `verify-runtime-closure`, `verify-package-paths`, `verify-client-domain-graph`, and both package-README prose gates).
+- `verify-type-equiv` still rejects an unterminated type-equivalence fence: mdast silently closes an unterminated block at end-of-file (its comparisons could then pass), so the shared helper reports whether a closing delimiter exists and the gate errors on an unclosed block, preserving the removed scanner's rejection. The `doc-typecheck` scanner never had that error path.
+- `parseArgs` keeps the last value of a duplicated option instead of erroring — a dev-tool edge case the tests don't pin, accepted in exchange for deleting the two bespoke parsers. (Strict mode still rejects a `--`-prefixed token where a value is expected, matching the replaced parsers.)
diff --git a/.agents/notes/archived/simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.zh.md b/.agents/notes/archived/simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.zh.md
new file mode 100644
index 0000000000..b01133fad2
--- /dev/null
+++ b/.agents/notes/archived/simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.zh.md
@@ -0,0 +1,34 @@
+# Agent Note: 把门禁脚本统一到已有依赖与内置模块上
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-07-26-consolidate-gate-scripts-on-existing-deps.md) | 中文
+
+## 问题
+
+`scripts/` 下的门禁大多本已在用正确的工具(15 个以上的门禁使用 `node:fs` 的 `globSync`,markdown 门禁使用 mdast/micromark),但少数几个掉队的脚本曾手写同类门禁早已用既有依赖或内置模块完成的事情:
+
+- **重复的围栏扫描器。**`scripts/md-fences.ts`(约 55 行,由 `doc-typecheck.ts` 消费)和 `scripts/verify-type-equiv.ts` 中的 `extractEquivBlocks`(约 39 行)曾是同一个围栏代码块正则行扫描器的两份拷贝,而 `scripts/verify-mermaid.ts` 早已通过访问 mdast `code` 节点来提取代码围栏;`scripts/markdown.ts` 自己的 `markdownProseLines` 也曾先解析成 mdast,再用第二个正则手工跟踪围栏状态。这两个正则扫描器只识别第 0 列的反引号围栏,因此在波浪线围栏和缩进围栏上与基于 mdast 的门禁悄悄不一致。
+- **手写的 argv 解析。**`scripts/publint-all.ts` 中的 `parseOptions` 和 `scripts/verify-built-package-invariants.mjs` 中与之几乎相同的拷贝(约 26 行)曾手工推进 argv 下标,而同类脚本(`verify-runtime-closure.ts`、`build-exe-for-python-sdk.ts`、`packages/sdk/scripts/src/args.ts`)早已在使用 `node:util` 的内置 `parseArgs`。
+- **手写的目录遍历。**五处代码曾各自重写 `globSync` 已覆盖的嵌套 `readdirSync` 遍历:`verify-runtime-closure.ts` 对 packages 与 vendor manifest(元数据清单)的扫描、`dev-web.ts` 的 `discoverPluginDirs`、`verify-package-paths.ts` 的 `realPackageNames`、`verify-client-domain-graph.ts` 的 `listSources`,以及 `publint-all.ts` 的 `addPath`(合计约 55–65 行)。`scripts/package-invariants.ts` 展示了一行式的 `globSync` 模板。
+
+所有替换都不需要引入新依赖;每一处替换用的都是既有的 devDependency 或 Node 内置模块。
+
+## 决策
+
+- `scripts/markdown.ts` 中的共享 mdast 围栏辅助函数 `markdownFences` 访问 `code` 节点,读取语言、完整 info string、块体以及以 1 起始的开围栏行号;`doc-typecheck.ts` 和 `verify-type-equiv.ts` 通过它提取代码围栏。`md-fences.ts` 和重复的 `extractEquivBlocks` 扫描器已删除,`markdownProseLines` 也改为从解析出的 `code` 节点位置推导围栏内的行,而不再用第二个正则。
+- 两个 CLI 都改用 `parseArgs` 解析 argv;未知选项和缺失取值仍然大声失败,只是错误文案换成了 `parseArgs` 自带的文本,而非原先手写的用法字符串。
+- 那五处掉队的目录遍历改用 `globSync`。`check-workspace-constraints.ts` 和 `clean.ts` 中的遍历保留:它们需要 dirent 级别的细节来诊断结构异常的目录树,按模式匹配的 glob 报告不了这些信息。
+
+## 曾考虑的替代方案
+
+- **新的 glob/目录遍历依赖(`tinyglobby`、`fdir`)。**不予采纳:内置模块已在全仓库范围内胜出;这几处只是掉队者,不是能力缺口。
+- **用 `p-map` 替换 `publint-all.ts` 中约 19 行的有序 worker 池。**刻意未纳入:为一次小删除引入一个新 devDependency,正处在[依赖策略](../process/2026-07-26-dependencies-over-hand-rolling.md)门槛的边缘,而且该池的需求(worker 数量有界、确定性顺序、环境变量覆盖)已记录在[并行 pre-push 门禁决策记录](../process/2026-07-06-parallel-pre-push-gates.md)中。仅当 `p-map` 赢得第二个消费方时再顺带纳入。
+- **保留这两个围栏扫描器。**不予采纳:在第三个正确实现旁边放着两份逐渐漂移的解析器拷贝,正是共享的 `markdown.ts` 辅助函数要防止的那种重复;「只认第 0 列反引号」的限制也是同类门禁之间的潜在不一致。
+
+## 后果
+
+- 只剩一个围栏解析器:所有 markdown 门禁现在都经由 mdast 归类代码围栏,因此波浪线围栏、缩进围栏和四反引号容器围栏在各处的行为完全一致。文档树中不存在正则扫描器处理有误的围栏形态,所以在落地这次替换的代码树上门禁结果不变:`pnpm run doc-sync` 及每个被改写的门禁在改动前后各跑一遍,输出逐字节相同(`doc-typecheck` 的块数/opt-out 计数、`verify-type-equiv` 的匹配计数、`publint`、`verify-built-package-invariants`、`verify-runtime-closure`、`verify-package-paths`、`verify-client-domain-graph`,以及两个包 README 散文门禁)。
+- `verify-type-equiv` 仍然拒绝未闭合的类型等价围栏:mdast 会在文件末尾静默闭合未闭合的代码块(其比较随后可能通过),因此共享辅助函数会报告闭合定界符是否存在,门禁在块未闭合时报错,保留了被删扫描器的这条拒绝路径。`doc-typecheck` 的扫描器本来就没有这条错误路径。
+- `parseArgs` 对重复出现的选项保留最后一个值而不报错——一个测试未固定的开发工具边缘用例,作为删除两份手写解析器的交换被接受。(严格模式下,需要取值处遇到以 `--` 开头的 token 仍会拒绝,与被替换的解析器行为一致。)
diff --git a/.agents/notes/archived/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.i18n.yaml b/.agents/notes/archived/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.i18n.yaml
new file mode 100644
index 0000000000..ed26cc462d
--- /dev/null
+++ b/.agents/notes/archived/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.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
+2026-06-20-remove-redundant-snapshot-log-expected-output.md: 306e1c67cc8370b629bd83c0335baf84037938f5
+2026-06-20-remove-redundant-snapshot-log-expected-output.zh.md: 552fbb36fcea5dca5e9699348caa3a4fef1bb293
diff --git a/.agents/notes/implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md b/.agents/notes/archived/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md
similarity index 97%
rename from .agents/notes/implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md
rename to .agents/notes/archived/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md
index b17ecad098..306e1c67cc 100644
--- a/.agents/notes/implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md
+++ b/.agents/notes/archived/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md
@@ -1,6 +1,9 @@
# Agent Note: Use `session.jsonl` as the only snapshot session-log artifact
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-06-20-remove-redundant-snapshot-log-expected-output.zh.md)
## Problem
diff --git a/.agents/notes/archived/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.zh.md b/.agents/notes/archived/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.zh.md
new file mode 100644
index 0000000000..552fbb36fc
--- /dev/null
+++ b/.agents/notes/archived/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.zh.md
@@ -0,0 +1,38 @@
+# Agent Note: 使用 `session.jsonl` 作为唯一的快照会话日志产物
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-06-20-remove-redundant-snapshot-log-expected-output.md) | 中文
+
+## 问题
+
+驱动模型的 ACP(Agent Client Protocol)快照场景同时包含 `session.jsonl` 和 `session.expected.jsonl`。对于普通记录场景,`session.jsonl` 是从真实运行采集的重放 fixture(测试前置数据);重放测试会规范化新持久化的日志,并将其与 `session.expected.jsonl` 比较。在当前 fixture 中,普通记录场景的两份规范化日志完全相同。
+
+手工编写的 override 场景(`error-finish`、`cancel`)目前使用 `replay.override.json` 驱动模型行为,并把 `session.jsonl` 保留为最小 dummy fixture,而 `session.expected.jsonl` 存放预期的持久化日志。override 文件是由 `ReplayEntry` 对象组成的 JSON 数组:`{ "kind": "chunks", "chunks": StreamChunk[] }`、`{ "kind": "throw", "chunks": StreamChunk[], "message": string, "code": string }` 或 `{ "kind": "hang" }`。这种拆分同样没有必要:override sidecar 存在时,`llm-replay` 会替换派生脚本,不需要从 `session.jsonl` 取得模型分片,因此 `session.jsonl` 仍可作为场景的预期会话日志产物。
+
+## 决策
+
+彻底移除 `session.expected.jsonl` 概念。每个场景最多只有一个已提交会话日志产物,即 `session.jsonl`:
+
+- 对于录制场景,`session.jsonl` 仍是原始采集的日志。回放仍从中派生模型分片,快照测试将回放运行归一化后的持久化日志与归一化后的 `session.jsonl` 进行比较。
+- 对于手工编写的覆盖场景,`replay.override.json` 驱动模型行为,`session.jsonl` 存放预期产出的会话日志。当覆盖文件存在时,回放适配器不从 fixture 获取模型分片,因此同一个文件既可作为预期日志,又不影响回放行为。
+- 对于无模型场景,`session.jsonl` 可保留为引导 `llm-replay` 所需的最小 fixture;除非场景创建了持久化会话,否则无需进行会话日志比较。
+
+Stdout 预期输出保持不变;它们是面向编辑器的投影,与会话 fixture 并不重复。
+
+## 曾考虑的替代方案
+
+**对两侧基于共享的(回放运行)上下文做归一化**:否决。`normalizeSessionLog` 通过精确字符串匹配擦除 cwd,因此 fixture 中录制的 cwd 不会被擦除,每次比较都会失败。两侧各自基于自身 header 派生的上下文做归一化——下方的实现说明描述了具体机制。
+
+## 验证
+
+快照 harness、fixture、孤立项守卫和文档中都不再出现 `session.expected.jsonl`;对于每个模型场景,快照测试都从 `session.jsonl` 派生预期会话日志;手工编写 sidecar 的场景把预期生成日志提交为 `session.jsonl`,并以 `replay.override.json` 覆盖模型行为;孤立 fixture 守卫知道每种场景类型所需的文件。[ACP 快照测试 Agent Note(agent 决策记录)](2026-06-19-acp-snapshot-tests.md)描述了精简后的 fixture 集合。
+
+## 后果
+
+评审者失去了一个能在视觉上区分预期持久化日志与重放 fixture 的产物名。stdout 预期输出仍然保护编辑器 transcript(文本记录),而将重放输出与 `session.jsonl` 比较,无需复制文件即可保留循环/持久化回归检查。
+
+## 实现说明
+
+两侧各自基于自身 header 值做归一化,因为录制与回放具有不同的 id、路径和时间戳。`fixtureContext()` 从 fixture 的 header 派生上下文,使已归一化的 fixture 具有幂等性。会话日志使用普通相等比较而非文件快照更新,因此比较过程不会改写 fixture。
diff --git a/.agents/notes/archived/testing/2026-06-22-fork-snapshot-scenarios.i18n.yaml b/.agents/notes/archived/testing/2026-06-22-fork-snapshot-scenarios.i18n.yaml
new file mode 100644
index 0000000000..ca654248d0
--- /dev/null
+++ b/.agents/notes/archived/testing/2026-06-22-fork-snapshot-scenarios.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
+2026-06-22-fork-snapshot-scenarios.md: 806ce2c1f3681810f609141936741c199e48fb67
+2026-06-22-fork-snapshot-scenarios.zh.md: 72f242784efef56f38590c393935e2d344cb89e2
diff --git a/.agents/notes/implemented/testing/2026-06-22-fork-snapshot-scenarios.md b/.agents/notes/archived/testing/2026-06-22-fork-snapshot-scenarios.md
similarity index 98%
rename from .agents/notes/implemented/testing/2026-06-22-fork-snapshot-scenarios.md
rename to .agents/notes/archived/testing/2026-06-22-fork-snapshot-scenarios.md
index 272e62ba77..806ce2c1f3 100644
--- a/.agents/notes/implemented/testing/2026-06-22-fork-snapshot-scenarios.md
+++ b/.agents/notes/archived/testing/2026-06-22-fork-snapshot-scenarios.md
@@ -1,6 +1,9 @@
# Agent Note: Record fork and mixed spawn+fork snapshot scenarios
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-06-22-fork-snapshot-scenarios.zh.md)
## Problem
diff --git a/.agents/notes/archived/testing/2026-06-22-fork-snapshot-scenarios.zh.md b/.agents/notes/archived/testing/2026-06-22-fork-snapshot-scenarios.zh.md
new file mode 100644
index 0000000000..72f242784e
--- /dev/null
+++ b/.agents/notes/archived/testing/2026-06-22-fork-snapshot-scenarios.zh.md
@@ -0,0 +1,32 @@
+# Agent Note: 记录 fork 与混合 spawn+fork 快照场景
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-06-22-fork-snapshot-scenarios.md) | 中文
+
+## 问题
+
+[seed 边界 Agent Note(agent 决策记录)](2026-06-22-fork-child-replay-seed-boundary.md)让 fork 子项重放能够正确路由:`dsh-llm-replay` 根据持久化 `seedLength` 边界处及其后的事件派生子项脚本,因此 fork 子项继承的父前缀不会作为子项自身的模型调用重放。但落地时**没有记录式 fork 场景**——slice 只由 `llm-replay` 单元测试(合成子项 fixture(测试前置数据))和持久化往返测试覆盖。完整 transcript(文本记录)快照层——会启动真实 `acp-agent` 并重放端到端嵌套 transcript 的那张网——只有 spawn 子项(`subagent-spawn`、`subagent-multi`)。如果 fork 路由回归没有让单元测试变红,它仍会逃过专为捕获 transcript 回归而构建的这一层。
+
+表达 fork 场景所需的快照基础设施已经就位:两个进程内后端都在 `cordis.yml` / `cordis.snapshot.yml` 中以两个面向模型的工具接入(`subagent` → spawn、`subagent_fork` → fork),harness 会收集每个子会话的日志,回放按 `seedLength` 为键转发各子会话的 fixture。缺少的是一个*已记录的场景*来驱动 fork 子会话走完这条路径。
+
+## 决策
+
+针对真实 API 记录两个场景,均在默认门禁中以无密钥方式回放:
+
+- **`subagent-fork`**:父会话完成一个轮次以建立一个事实,然后通过 `subagent_fork` 委派一个子任务。fork 子会话继承对话(其日志携带非零 `seedLength`),因此可以从父会话的上下文中作答。这是聚焦的回归守卫:子会话 fixture 的 `seedLength` 就是回放切片所依赖的边界,来自真实 fork 的记录而非手工合成。
+- **`subagent-mixed`**——父项完成一个轮次,随后在同一 transcript 中通过 `subagent` 委托一次(全新 spawn 子项,`seedLength` 为 0),再通过 `subagent_fork` 委托一次(fork 子项,`seedLength` 非零)。这是 seed 边界与逐会话重放 Agent Note 都点名作为未来新增项的 spawn+fork 混合场景:一份 transcript 覆盖两种传输方式和 slice 的两个分支(`seedLength` 为 0 = 无操作,`seedLength > 0` = 裁剪继承前缀),两个子项按 `createdAt` 排列为先 spawn、后 fork。
+
+### 为什么需要一个已完成的第一轮次
+
+fork 后端使用父项的**已配平完整轮次前缀**为子项提供 seed。父项若在第一个轮次就执行 fork,没有已完成轮次可供继承,因此 seed 为空(≡ 全新 spawn,`seedLength` 为 0)——这不会覆盖 slice。因此,两个场景都使用双提示词输入:第一个提示词完成一个轮次(建立稍后要求子项回忆的 codeword),第二个提示词委托 fork。子项 transcript 中回忆出的 codeword 只是模型行为的附带结果;承载关键约束的产物是子项 fixture 中记录、由重放 slice 消费的 `seedLength`。
+
+## 后果
+
+- fork 路由切片现在由全 transcript 层守卫,而不仅仅是单元测试。移除 `slice(seedLength)`(回放整个子会话日志)会让**两个**新场景变红——fork 子会话收到的是父会话记录的分片而非自己的——证明守卫确实生效(场景落地时已验证红→绿)。
+- `subagent-mixed` 是第一个在同一个 transcript 中驱动两种*不同* subagent 后端的快照场景,同时覆盖了跨 spawn 和 fork 子会话的逐会话回放键控。
+- 进程外(ACP(Agent Client Protocol))subagent 回放形态不同(每个子会话是独立进程、有自己的回放),仍以 `TODO(acp-subagent-replay)` 跟踪——本文场景仅限进程内。
+- 重新录制(`pnpm run test:snapshot:record`)会从真实 API 重新生成全部四个 fork/spawn fixture;两个新场景在无密钥时自动跳过,与所有已录制场景一致。
+
+
diff --git a/.agents/notes/archived/testing/2026-07-04-hook-snapshot-matrix.i18n.yaml b/.agents/notes/archived/testing/2026-07-04-hook-snapshot-matrix.i18n.yaml
new file mode 100644
index 0000000000..106e643572
--- /dev/null
+++ b/.agents/notes/archived/testing/2026-07-04-hook-snapshot-matrix.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
+2026-07-04-hook-snapshot-matrix.md: 6c7229ee3b13867e01f657528265aabc2e4430df
+2026-07-04-hook-snapshot-matrix.zh.md: 7e46626db0234e269796338e8e0a4406547e1b71
diff --git a/.agents/notes/implemented/testing/2026-07-04-hook-snapshot-matrix.md b/.agents/notes/archived/testing/2026-07-04-hook-snapshot-matrix.md
similarity index 94%
rename from .agents/notes/implemented/testing/2026-07-04-hook-snapshot-matrix.md
rename to .agents/notes/archived/testing/2026-07-04-hook-snapshot-matrix.md
index 7c2c33460c..6c7229ee3b 100644
--- a/.agents/notes/implemented/testing/2026-07-04-hook-snapshot-matrix.md
+++ b/.agents/notes/archived/testing/2026-07-04-hook-snapshot-matrix.md
@@ -1,12 +1,15 @@
# Agent Note: Hook snapshot matrix — end-to-end expected outputs for both bridges
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-04-hook-snapshot-matrix.zh.md)
## Problem
The hook bridges — [`dsh-hooks-claude`](../../../../packages/hooks/hooks-claude) (7 Claude Code hook points) and [`dsh-hooks-codex`](../../../../packages/hooks/hooks-codex) (5 Codex points) — map external hook commands onto the harness interception seams. They carry deep unit and coverage-spec coverage (every decision arm, every payload dialect, driven against a mocked seam) plus one key-gated e2e (`hooks.e2e.ts`, a live `PreToolUse` block). But the full-transcript snapshot tier — the one net that boots the real `acp-agent` subprocess, replays a recorded session keyless, and diffs the normalized ACP stdout + re-persisted log against committed expected outputs — covered exactly ONE hook: a Claude `UserPromptSubmit` block (`hook-cc-promptsubmit-block`).
-That is the tier a mocked unit test structurally cannot be: it exercises the REAL bridge translating a REAL hook process's outcome into the REAL seam decision, then the REAL loop's reaction, rendered exactly as an editor sees it. A bridge-translation or loop-structure regression that left every unit green would still escape it for every hook point but one — and for the Codex bridge, the ACP example did not even LOAD it, so no Codex hook could fire end-to-end at all.
+That is the tier a mocked unit test structurally cannot be: it exercises the REAL bridge translating a REAL hook process's outcome into the REAL seam decision, then the REAL loop's reaction through the automation wire and persisted log. A bridge-translation or loop-structure regression that left every unit green would still escape it for every hook point but one — and for the Codex bridge, the ACP example did not even LOAD it, so no Codex hook could fire end-to-end at all.
## Decision
diff --git a/.agents/notes/archived/testing/2026-07-04-hook-snapshot-matrix.zh.md b/.agents/notes/archived/testing/2026-07-04-hook-snapshot-matrix.zh.md
new file mode 100644
index 0000000000..7e46626db0
--- /dev/null
+++ b/.agents/notes/archived/testing/2026-07-04-hook-snapshot-matrix.zh.md
@@ -0,0 +1,53 @@
+# Agent Note: 钩子快照矩阵——覆盖两种 bridge 的端到端预期输出测试
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-04-hook-snapshot-matrix.md) | 中文
+
+## 问题
+
+钩子 bridge——[`dsh-hooks-claude`](../../../../packages/hooks/hooks-claude)(7 个 Claude Code 钩子点)和 [`dsh-hooks-codex`](../../../../packages/hooks/hooks-codex)(5 个 Codex 点)——把外部钩子命令映射到 harness 拦截 seam。它们有深入的单元与覆盖率规格覆盖(每个决策分支、每种 payload dialect,针对 mock seam 驱动),外加一个受密钥门控的 e2e(`hooks.e2e.ts`,实时 `PreToolUse` 阻止)。但完整 transcript(文本记录)快照层——会启动真实 `acp-agent` 子进程、无需密钥重放已记录会话,并将规范化 ACP(Agent Client Protocol)stdout + 重新持久化日志与已提交预期输出进行 diff 的那张网——只覆盖了一个钩子:Claude `UserPromptSubmit` 阻止(`hook-cc-promptsubmit-block`)。
+
+这正是 mock 单元测试在结构上无法替代的层级:它验证的是真实 bridge 将真实钩子进程的结果翻译到真实 seam 决策,再经由自动化线协议和持久化日志检验真实 agent loop(智能体循环)的反应。一个 bridge 翻译或 loop 结构的回归,即使让所有单元测试保持绿色,也会在除那一个钩子点之外的所有点上逃逸;而对于 Codex bridge,ACP 示例甚至没有加载它,因此没有任何 Codex 钩子能端到端触发。
+
+## 决策
+
+实现由两个耦合部分组成:
+
+### 1. ACP 示例同时加载两种钩子 bridge
+
+`examples/acp-agent/cordis.yml` 和 `cordis.snapshot.yml` 现在同时加载 `dsh-hooks-codex` 与 `dsh-hooks-claude`,各自指向自己的配置文件(Claude 用 `./hooks.json`,Codex 用 `./codex-hooks.json`——两种方言无法共用一个文件)。这是一个真正的产品接口变更,而非仅用于测试的接线:交付的 ACP 服务器(以及 `demo:acp` 入口)现在同时携带两种 bridge。
+
+这是安全的,因为配置文件不存在时 bridge 是**静默无操作**的:`apply()` 捕获读取失败、通过 `ctx.logger` 记录日志、不注册任何东西——零监听器、零会话事件。`acp-agent` 应用不附带 stdout logger,因此警告不会到达 ACP JSON-RPC 通道。只需要 Claude 钩子的场景(或真实项目)只提供 `hooks.json`;Codex bridge 找不到 `codex-hooks.json` 便自动消失。这已通过实验验证:在两种 bridge 同时加载的情况下,所有既有快照(均不附带 `codex-hooks.json`)逐字节一致。
+
+同时加载是让快照层能够在产品交付的同一个真实应用上验证每种方言的最低要求。录制(启动 `cordis.yml`)天然加载两者,回放以同样方式继承:`cordis.snapshot.yml` 是 `cordis.yml` 的 include-overlay,只替换 llm 入口(见[单一来源 acp-agent 回放配置](2026-07-04-single-source-acp-replay-config.md)),因此添加到运行时树的 bridge 无需第二次编辑即出现在回放树中。
+
+### 2. 每个钩子点 × 其主要结果各一个快照场景,覆盖两种方言
+
+`examples/acp-agent/tests/snapshots/` 下共 13 个场景,命名为 `hook---`:
+
+- **手工编写、无模型轮次**(无密钥、无 sidecar——派生的回放脚本为空;比对的是携带 `hook/*` 事件的 `rejected` 轮次):`hook-cc-promptsubmit-block`、`hook-codex-promptsubmit-block`。
+- **对真实 API 录制、录制期间钩子活跃**(模型对决策的反应是捕获的 transcript 的一部分,此后无密钥回放):`hook-{cc,codex}-promptsubmit-context`(allow + additionalContext 折叠)、`hook-cc-pretool-deny` / `hook-codex-pretool-block`(deny → `isError` 工具结果)、`hook-cc-pretool-ask`(ask → 降级为 deny 并附带 approval-required 原因)、`hook-{cc,codex}-posttool-block`(阻止并附带反馈)、`hook-{cc,codex}-posttool-context`(accept + additionalContext)、`hook-{cc,codex}-stop-continue`(阻塞性 Stop 钩子通过 steering(中途引导)强制多走一步)。
+
+每个钩子命令只输出固定字面量字符串(无时间戳/pid/`$RANDOM`/cwd 回显);快照规范化器擦除 `hook/result` 携带的唯一不稳定字段(`durationMs`)。`Stop` 场景通过标记文件(`.stop_fired`)自限,使 force-continue 不会循环——`stop_hook_active` 循环守卫仍是 bridge 的一个 `TODO`,因此无条件的 Stop 钩子会在每一步都 force-continue。
+
+`PostToolUse` 阻止场景会在其证明的机制处自行限制。Claude 钩子在首次拒绝后持久化一个 workspace 标记,因此允许一次恢复调用;Codex 提示词发起一次调用并报告注入结果。每份预期输出固定一次遭阻止调用,不会重复阻止/重试循环。
+
+### 三个钩子点被有意排除在快照之外
+
+在构建矩阵过程中发现,记录于此是因为这些遗漏是决策而非疏忽:
+
+- **`SessionStart` 和 `SubagentStart`** 通过脱离且尽力而为的 `void runPoint(...).then(agent.inject())` 注入上下文,没有轮次绑定。由此产生的 `context/message` 会与它应先于的工作(首次模型请求 / 子项的第一个轮次)竞速,并落在不确定的日志位置。记录的预期输出甚至无法在自己的重放中复现——对两者执行 10 次重放稳定性检查,结果均为 10/10 次失败。它们继续留在 bridge 的单元覆盖率中,那里会直接驱动 seam 而不存在时序竞速。(如果注入未来改为绑定轮次且具备确定性——`TODO(session-start-gating)` 所指方向——它们就能接受快照测试。)
+- **`SubagentStop`** 只观察:其 `subagent/end` handler 不传递轮次(因此没有 `hook/*` 日志事件),也不执行注入。它不会向 transcript 写入任何内容,因此预期输出会与无钩子运行逐字节相同,永远无法证明失败——一道咬不住问题的守卫。它继续由单元覆盖率负责(`bridge.spec.ts` 已断言仅观察调用)。
+
+因此,该矩阵覆盖了所有具有确定性、可观测 transcript 足迹的钩子点,涵盖两种方言。
+
+## 后果
+
+- 现在,两种 dialect 中每个具有可观察 transcript 的 bridge seam 映射,都在真实应用的完整 transcript 层受到守护——包括此前完全没有端到端覆盖的 Codex bridge。记录的预期输出捕获模型对遭拒绝/遭阻止/强制继续轮次的真实反应,而手工编写的 transcript 只能猜测这种反应。
+- `UserPromptSubmit` 阻止场景无需密钥即可编写(没有模型轮次);其余场景从已记录 fixture(测试前置数据)无需密钥重放。`pnpm run test:snapshot:record` 从实时 API 重新生成记录式 fixture,并像所有记录场景一样在缺少密钥时自行跳过。
+- 证明会变红的准则仍成立:篡改钩子配置输出(例如改变拒绝理由)会让相应场景在重放时变红——钩子进程在重放期间真实运行(只有模型被重放),因此预期输出守护的是实际钩子→seam→循环路径,而非其 mock。
+- `acp-agent` 演示现在加载了一个通常会无操作的 Codex bridge(典型项目中没有 `codex-hooks.json`),这正是预期的柔性失败行为,而非代价。
+
+
diff --git a/.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.i18n.yaml b/.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.i18n.yaml
new file mode 100644
index 0000000000..e596c0ae47
--- /dev/null
+++ b/.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.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
+2026-07-04-single-source-acp-replay-config.md: 7d6217fe781209d449d87bb2e0411159e5f7eaa0
+2026-07-04-single-source-acp-replay-config.zh.md: a040c14e6e08f9c60e9ad53449ad66c1203831c2
diff --git a/.agents/notes/implemented/testing/2026-07-04-single-source-acp-replay-config.md b/.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.md
similarity index 97%
rename from .agents/notes/implemented/testing/2026-07-04-single-source-acp-replay-config.md
rename to .agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.md
index 3de645deeb..7d6217fe78 100644
--- a/.agents/notes/implemented/testing/2026-07-04-single-source-acp-replay-config.md
+++ b/.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.md
@@ -1,6 +1,9 @@
# Agent Note: Single-source the acp-agent replay config
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-04-single-source-acp-replay-config.zh.md)
## Problem
diff --git a/.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.zh.md b/.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.zh.md
new file mode 100644
index 0000000000..a040c14e6e
--- /dev/null
+++ b/.agents/notes/archived/testing/2026-07-04-single-source-acp-replay-config.zh.md
@@ -0,0 +1,28 @@
+# Agent Note: 将 acp-agent 回放配置改为单一来源
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-04-single-source-acp-replay-config.md) | 中文
+
+## 问题
+
+`examples/acp-agent` 发布了两份手工维护的配置:`cordis.yml`(实时树)和逐条镜像它、只替换 llm 后端的 `cordis.snapshot.yml`——去除注释后,两者的全部差异就是八行 `llm-deepseek` stanza 与两行 `llm-replay` stanza。每次应用形状变化都必须修改两遍,也没有任何机制约束对称性:如果副本发生漂移,快照层会悄然覆盖与已发布应用不同的应用——快照层本就是为了弥合[“单元测试绿色,产品损坏”这类缺口](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md),如今同类缺口在上一层重新出现,只能依靠评审者警惕。
+
+## 决策
+
+`cordis.snapshot.yml` include 正式配置,通过 id 和 name 禁用指定的 DeepSeek 适配器,并插入回放适配器。其余所有条目因此来自正式运行树。回放时选择 overlay;录制仍然启动 `cordis.yml`,加载守卫允许被有意禁用的条目。
+
+overlay 有意依赖一项 vendored 插件事实:include 加载文件时会应用 `patches`,而其 `refresh()`/`internal/update` 路径会重新读取但不重新打补丁——这恰好足以满足一次性重放启动(重放应用不加载 `hmr`,运行中也没有内容重写配置)。快照套件就是证明:所有场景都能在 overlay 上原样通过,包括逐字节相同的预期输出。
+
+## 曾考虑的替代方案
+
+### 为何不采用这些替代方案?
+
+保留完整的双副本并加一道对称性校验门禁是记录在案的退路——它能消除静默漂移这一类问题,但仍保留一份 125 行的近乎复制品,其全部内容只是一个条目的差异,且随应用每增加一个插件而增长。在 bin 侧做替换(解析配置、替换条目、删除文件)则会把 YAML 手术放进发布产物,并把回放差异藏到视线之外;overlay 让差异保持声明式、可读,且紧邻基础配置——这正是双副本支持者真正看重的教学价值。
+
+## 后果
+
+- 向 `cordis.yml` 添加插件即自动进入回放树,无需第二次编辑;漂移这一类问题从结构上消失,而非靠门禁拦截。
+- overlay 依赖条目携带稳定的 `id:`。禁用补丁上的 `name` 断言防止误定位(id 被复用时补丁跳过而非禁用错误的插件)。如果 id 被重命名,补丁退化为跳过,其警告需要一个回放应用有意不具备的 logger——可观测结果是一条无效的无密钥 `llm-deepseek` 条目与 `llm-replay` 并存,回放输出仍然正确(`llm-replay` 拥有流的短路权);这属于配置腐烂,留给评审发现,不会产生错误的快照。顶层插入一个 id 与既有条目冲突的新条目时,loader 的 id map 以后者为准;当前配置无冲突,新增补丁行才是引入冲突的场所。
+- 如果未来回放树需要第二处差异(另一个后端被替换),只需多加一行补丁,而非再 fork 一份文件。
diff --git a/.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.i18n.yaml b/.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.i18n.yaml
new file mode 100644
index 0000000000..fdc781de9e
--- /dev/null
+++ b/.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.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
+2026-07-06-pin-request-header-content-in-one-scenario.md: cadb4f74a1e8556eb32a72285b78e1339f457514
+2026-07-06-pin-request-header-content-in-one-scenario.zh.md: 5fdc38b9aa4cdce54026f23ad375b81d96b9c8c0
diff --git a/.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md b/.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md
similarity index 98%
rename from .agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md
rename to .agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md
index 1dcfc2e085..cadb4f74a1 100644
--- a/.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md
+++ b/.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md
@@ -1,6 +1,9 @@
# Agent Note: Pin request-header content in one snapshot scenario
Status: implemented
+Archived: 2026-07-26
+
+English | [中文](2026-07-06-pin-request-header-content-in-one-scenario.zh.md)
## Problem
diff --git a/.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.zh.md b/.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.zh.md
new file mode 100644
index 0000000000..5fdc38b9aa
--- /dev/null
+++ b/.agents/notes/archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.zh.md
@@ -0,0 +1,36 @@
+# Agent Note: 在单个快照场景中固定请求头内容
+
+Status: implemented
+Archived: 2026-07-26
+
+[English](2026-07-06-pin-request-header-content-in-one-scenario.md) | 中文
+
+## 问题
+
+一个 ACP(Agent Client Protocol)快照测试套件需要证明每个 `request/header` 中实际发送的组合系统提示词与工具 schema 列表,但如果在每个 `session.jsonl` 中重复这些内容,一次提示词或 schema 编辑就会改写数十条巨大的单行 JSON 记录。保留一份原始 header 可以避免重复,但提示词的评审体验仍然很差:行文被 JSON 转义到一行中,与数千字符的工具 schema 混在一起。
+
+## 决策
+
+每种请求头组合类别恰好有一个场景标记为 `pinsHeader`。其目录按评审格式拆分固定内容:`system-prompt.expected.md` 以普通 Markdown 包含规范化的完整提示词序列;`tool-schemas.expected.json` 以结构化 JSON 包含对应的完整 schema 序列;`session.jsonl` 保留 config、reason 和所有模型可见前缀,同时将 `header.system` 与 `header.tools` 存为 `"{{system}}"` / `"{{tools}}"`。其他每份 JSONL 都使用相同的提示词与工具 token,并同样将会话前缀内容 token 化。固定机制位于 [`dsh-acp-snapshot`](../../../../packages/support/acp-snapshot/README.md),其套件 factory 强制每种类别恰好有一个固定场景。
+
+纯 `scrubSystemPrompts` 和 `scrubToolSchemas` 规范化器会分别将每个已存储完整请求头 token 化。`scrubRequestHeaders` 还会为非固定场景把会话前缀内容 token 化,同时保留请求头数量、字段存在性、config、reason 和前缀消息数量。记录与刷新写回会在写入 JSONL 前应用适当清理,并根据规范化的实时完整请求头序列重新生成两个 sidecar,因此两条路径都无法把大段提示词/schema 重新引入 JSONL,也不会留下陈旧的评审产物。
+
+守卫使这种拆分能够自我强制。在磁盘上,每个 `session*.jsonl` 都是提示词和 schema 清理器的固定点;只有非固定 fixture(测试前置数据)必须是完整请求头清理的固定点;两个 sidecar 恰好位于固定 fixture 旁,并采用规范、以换行符结尾的格式;每种类别都有一个固定场景。在实时运行中,由父项、spawn 子项、fork 子项、初始请求、恢复或实例内变化产生的每个 `request/header`,都必须在易变值规范化后与重建的类别序列匹配。请求头若没有字符串提示词、没有数组值工具列表,或超过固定场景声明的变更请求头数量,就会响亮失败。
+
+一个固定场景覆盖整个套件,因为每个会话(parent、spawn 子会话、fork 子会话)组合出的工具列表完全相同、提示词除 cwd 外完全相同,而一致性守卫会在这一前提不再成立时立即使套件失败。如果 header 组合将来在设计上变为会话相关的(例如受限的 subagent 工具集),那么分歧的形态将获得自己的固定场景。
+
+## 曾考虑的替代方案
+
+- **每次变更重新录制或手动编辑所有 fixture**:保留了精确的 header,但行为差异被重复的提示词和 schema 内容淹没。
+- **仅在比较时 scrub,fixture 保持原始内容**:比较能通过,但已提交的 fixture 保留着陈旧的重复内容,下次录制时会整体重写。存储 token 诚实地表明每个 JSONL 没有固定什么。
+- **全部 scrub,不做任何固定**:丢失了组合 header 实际发送内容(提示词组装、已注册工具顺序、完整 schema)的唯一端到端记录。生成的工具目录只孤立地记录每个工具;只有真实 fixture 才能固定组合后的完整集合。
+- **将完整固定内容全部保留在 JSONL 中**:消除了套件范围的重复,但提示词和 schema 变更仍然是一行转义文本。Markdown 和结构化 JSON 为每种内容提供其自然的评审格式,同时不削弱重建 header 的断言。
+- **收窄会话日志本身(记录内容 digest,把请求头存到其他位置)**——违反可重建性契约:产品日志必须逐 bit 复现每个请求([可重建请求 Agent Note(agent 决策记录)](../architecture/2026-07-05-reconstructable-requests.md))。请求头体积是测试产物问题,应在测试规范化中解决;实时日志保持不变。
+
+## 验证
+
+该套件针对拆分后的固定内容回放每个场景。单元覆盖率会覆盖独立与完整清理器、两种完整请求头 sidecar 格式、记录/刷新重新生成、规范化提示词/schema 提取、固定点强制、必需文件对称性、重建请求头一致性,以及变更请求头数量拒绝。
+
+## 后果
+
+系统提示词变更在每个受影响的组合类别中产生一个面向行的 Markdown diff;工具描述变更在每个类别中产生一个结构化 JSON diff;普通行为 fixture 不受影响。会话 fixture 对省略的内容显示 token,运行时一致性守卫使每个拆分固定场景对其类别内的所有会话具有权威性。每个固定场景携带两个生成的、换行规范化的 sidecar 文件。
diff --git a/.agents/notes/archived/testing/2026-07-08-shared-acp-snapshot-package.i18n.yaml b/.agents/notes/archived/testing/2026-07-08-shared-acp-snapshot-package.i18n.yaml
new file mode 100644
index 0000000000..0472aed0a3
--- /dev/null
+++ b/.agents/notes/archived/testing/2026-07-08-shared-acp-snapshot-package.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write
+2026-07-08-shared-acp-snapshot-package.md: 6952f79eff8a764cd296bef98e9cd7b40e59cf01
+2026-07-08-shared-acp-snapshot-package.zh.md: 35246fc4f98f616d70e31eb55499cca15f04c94e
diff --git a/.agents/notes/implemented/testing/2026-07-08-shared-acp-snapshot-package.md b/.agents/notes/archived/testing/2026-07-08-shared-acp-snapshot-package.md
similarity index 89%
rename from .agents/notes/implemented/testing/2026-07-08-shared-acp-snapshot-package.md
rename to .agents/notes/archived/testing/2026-07-08-shared-acp-snapshot-package.md
index 8d6032d1cc..6952f79eff 100644
--- a/.agents/notes/implemented/testing/2026-07-08-shared-acp-snapshot-package.md
+++ b/.agents/notes/archived/testing/2026-07-08-shared-acp-snapshot-package.md
@@ -1,6 +1,9 @@
# Agent Note: Extract the ACP snapshot suite into a support package
Status: implemented
+Archived: 2026-07-27
+
+English | [中文](2026-07-08-shared-acp-snapshot-package.zh.md)
## Problem
@@ -10,7 +13,7 @@ A second ACP example wanting snapshot coverage — the sandbox/approval composit
## Decision
-The machinery lives in [`packages/support/acp-snapshot`](../../../../packages/support/acp-snapshot/README.md) (`@deepseek-ai/dsh-acp-snapshot`); an example's `*.snapshot.ts` is its scenario table, its agent paths, and one factory call, over its own `snapshots/` fixtures and `cordis.snapshot.yml` overlay ([single-source replay config](2026-07-04-single-source-acp-replay-config.md)). Reading `DSH_SNAPSHOT` stays at that edge — the library takes a resolved `mode`.
+The machinery lives in [`packages/support/acp-snapshot`](../../../../packages/support/acp-snapshot/README.md) (`@deepseek-ai/dsh-acp-snapshot`); an example's `*.snapshot.ts` is its scenario table, its agent paths, and one factory call, over its own `snapshots/` fixtures and `cordis.snapshot.yml` overlay ([single-source replay config](../../archived/testing/2026-07-04-single-source-acp-replay-config.md)). Reading `DSH_SNAPSHOT` stays at that edge — the library takes a resolved `mode`.
**`src/launcher.ts`** — `launchAcpTestAgent` owns the common unbuilt-process boundary: absolute tsx loader resolution, `TSX_TSCONFIG_PATH`, isolated harness homes, stdio wiring, a raw-byte stdout tee, stderr and update capture, fail-closed permission fallback, update waiters, and graceful or signalled shutdown. Snapshot scenarios and ordinary e2e suites supply the same `AgentUnderTest` (`binScript`, `configPath`, `tsconfigPath`); a test that plays a user supplies only its permission handler. The ACP and hook e2e suites plus the sandbox/approval e2e suite use this launcher instead of rebuilding the SDK client boundary.
@@ -18,7 +21,7 @@ The machinery lives in [`packages/support/acp-snapshot`](../../../../packages/su
**`src/normalize.ts`** — the pure normalizers, hook-free by policy: when a future event carries a new volatile field (an approval duration, say), the shared normalizer learns it in the same change, keeping one home for what "normalized" means rather than per-suite scrub extensions.
-**`src/suite.ts`** — the `Scenario` type and `defineAcpSnapshotSuite(options)`, registering the per-scenario compares, record/refresh fixture write-back, the header pin with its live uniformity guard, and the fixture guard block (no orphan scenario dirs, required files present, exactly one pin per class, every JSONL a `scrubSystemPrompts` fixed point, non-pinning fixtures also `scrubRequestHeaders` fixed points). Refresh expands packed timing envelopes before aligning existing volatile event times, so switching between packed and unpacked layouts cannot shift later records; fresh chunk-fragment arrays remain authoritative because their boundaries are replay behavior. A scenario directory's `session.jsonl` plus contiguous `session..jsonl` siblings are its ordered primary/child inventory, so the scenario table declares policy without duplicating a child count. The pinned-header contract ([pinned-header Agent Note](2026-07-06-pin-request-header-content-in-one-scenario.md)) is per-suite: each header class flags exactly one `pinsHeader` scenario, whose `system-prompt.expected.md` and JSONL tool list split the composed header into reviewable artifacts; the uniformity guard compares both against every live header in that class. A pinning scenario declares any legitimate changed-header count, and its Markdown artifact records every full changed prompt. The pure helpers (`sessionFixtureNames`, `fixtureContext`, `normalizedHeaders`, `normalizedSystemPrompts`, `formatSystemPromptSnapshot`, `headerChangeCount`) are exported from the module for direct unit coverage.
+**`src/suite.ts`** — the `Scenario` type and `defineAcpSnapshotSuite(options)`, registering the per-scenario compares, record/refresh fixture write-back, the header pin with its live uniformity guard, and the fixture guard block (no orphan scenario dirs, required files present, exactly one pin per class, every JSONL a `scrubSystemPrompts` fixed point, non-pinning fixtures also `scrubRequestHeaders` fixed points). Refresh expands packed timing envelopes before aligning existing volatile event times, so switching between packed and unpacked layouts cannot shift later records; fresh chunk-fragment arrays remain authoritative because their boundaries are replay behavior. A scenario directory's `session.jsonl` plus contiguous `session..jsonl` siblings are its ordered primary/child inventory, so the scenario table declares policy without duplicating a child count. The pinned-header contract ([pinned-header Agent Note](../../archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)) is per-suite: each header class flags exactly one `pinsHeader` scenario, whose `system-prompt.expected.md` and JSONL tool list split the composed header into reviewable artifacts; the uniformity guard compares both against every live header in that class. A pinning scenario declares any legitimate changed-header count, and its Markdown artifact records every full changed prompt. The pure helpers (`sessionFixtureNames`, `fixtureContext`, `normalizedHeaders`, `normalizedSystemPrompts`, `formatSystemPromptSnapshot`, `headerChangeCount`) are exported from the module for direct unit coverage.
## Alternatives considered
diff --git a/.agents/notes/archived/testing/2026-07-08-shared-acp-snapshot-package.zh.md b/.agents/notes/archived/testing/2026-07-08-shared-acp-snapshot-package.zh.md
new file mode 100644
index 0000000000..35246fc4f9
--- /dev/null
+++ b/.agents/notes/archived/testing/2026-07-08-shared-acp-snapshot-package.zh.md
@@ -0,0 +1,41 @@
+# Agent Note: 将 ACP 快照套件提取为支持包
+
+Status: implemented
+Archived: 2026-07-27
+
+[English](2026-07-08-shared-acp-snapshot-package.md) | 中文
+
+## 问题
+
+ACP(Agent Client Protocol)快照层([快照 Agent Note(agent 决策记录)](2026-06-19-acp-snapshot-tests.md))由位于一个示例测试目录内的三个模块构建:`snapshot-harness.ts`(启动真实 bin 子进程,通过 ACP JSON-RPC 驱动它,采集持久化日志)、`snapshot-normalize.ts`(纯预期输出规范化器),以及 `acp.snapshot.ts` 中约 150 行的场景主体与 fixture(测试前置数据)守卫(记录/回放模式、stdout 预期输出与日志比较、固定请求头一致性守卫、孤立项/必需文件/单一固定项元测试)。
+
+第二个希望获得快照覆盖的 ACP 示例——直接消费方是沙箱/approval 组合——只能复制这些模块,恰好分叉了绝不能漂移的逻辑:记录写回、请求头清理、子会话采集顺序。spawn/client 胶水也在 `acp.e2e.ts`、`hooks.e2e.ts` 和 harness 中重复三份。文件位置决定了测试严格度:逐文件 100% 覆盖率门禁只测量 `packages/*/*/src`,因此这些机制完全未被测量——正是同一种缺口,曾推动 `dsh-llm-replay` 从 `examples/` 移入 [packages/support](../../../../packages/support/README.md)。此外,harness 的 ACP client 硬编码 `requestPermission → cancelled`,因此 approval 往返——沙箱组合的主打行为——完全无法在快照层表达。
+
+## 决策
+
+这些机制位于 [`packages/support/acp-snapshot`](../../../../packages/support/acp-snapshot/README.md)(`@deepseek-ai/dsh-acp-snapshot`);示例的 `*.snapshot.ts` 只包含场景表、agent 路径和一次工厂调用,依赖自己的 `snapshots/` fixture 与 `cordis.snapshot.yml` overlay([单源回放配置](../../archived/testing/2026-07-04-single-source-acp-replay-config.md))。读取 `DSH_SNAPSHOT` 留在边缘层——库接收的是已解析的 `mode`。
+
+**`src/launcher.ts`**——`launchAcpTestAgent` 拥有通用的未构建进程边界:绝对 tsx loader 解析、`TSX_TSCONFIG_PATH`、隔离的 harness home、stdio 接线、原始字节 stdout tee、stderr 与更新捕获、失败关闭的权限后备、更新 waiter,以及优雅或信号式关闭。快照场景和普通 e2e 套件提供相同的 `AgentUnderTest`(`binScript`、`configPath`、`tsconfigPath`);扮演用户的测试只提供其权限 handler。ACP 与钩子 e2e 套件以及沙箱/approval e2e 套件都使用该 launcher,而不再重新构建 SDK client 边界。
+
+**`src/harness.ts`**——`runScenario` 和输入脚本/结果类型在 launcher 之上叠加确定性步骤、临时 workspace、快照环境和持久化日志采集。其 `session/request_permission` handler 消费可选的 `InputScript.permissionAnswers` FIFO 队列,每个条目按选项**类型**进行选择(id 是 agent 生成的随机值,已提交脚本无法预知;类型是 ACP 稳定词汇,会在回答时映射到已提供的 `optionId`);队列不存在或耗尽时回答 `cancelled`,若请求从未提供某种类型则拒绝该次运行——agent 自身收到的回答是 `cancelled`,因此场景 bug 会使 harness 失败,而不会被吸收为 agent 侧拒绝。由此,approval 套件可以根据 `input.json` 确定性地驱动允许/拒绝往返。
+
+**`src/normalize.ts`** 是纯规范化器,按策略不含钩子:当未来某个事件携带新的易变字段(例如审批耗时),共享规范化器在同一个变更中学会它,保持「规范化」的含义只有一个归属,而非各套件各自扩展清洗逻辑。
+
+**`src/suite.ts`**——包含 `Scenario` 类型和 `defineAcpSnapshotSuite(options)`,注册各场景比较、记录/刷新 fixture 写回、带实时一致性守卫的请求头固定项,以及 fixture 守卫块(没有孤立场景目录、必需文件存在、每种类别恰好一个固定项、每份 JSONL 都是 `scrubSystemPrompts` 固定点、非固定 fixture 同时也是 `scrubRequestHeaders` 固定点)。刷新会先展开打包的计时信封,再对齐现有易变事件时间,因此在打包与未打包布局之间切换不会移动后续记录;全新的分片片段数组仍为权威,因为其边界属于回放行为。场景目录中的 `session.jsonl` 加连续的 `session..jsonl` 同级文件构成有序主项/子项清单,因此场景表可以声明策略而不重复子项数量。固定请求头契约([固定请求头 Agent Note](../../archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md))按套件生效:每种请求头类别恰好标记一个 `pinsHeader` 场景,其 `system-prompt.expected.md` 和 JSONL 工具列表把组合请求头拆成可评审产物;一致性守卫会将两者与该类别的每个实时请求头比较。固定场景可以声明任何合法的变更请求头数量,其 Markdown 产物记录每个完整的已变提示词。纯辅助函数(`sessionFixtureNames`、`fixtureContext`、`normalizedHeaders`、`normalizedSystemPrompts`、`formatSystemPromptSnapshot`、`headerChangeCount`)从模块导出,以便直接进行单元覆盖。
+
+## 曾考虑的替代方案
+
+- **把模块复制到每个示例中**——这正是本 Agent Note 要防止的分叉:记录/守卫逻辑恰好是必须在各套件间保持逐字节相同的代码,而示例位于覆盖率门禁之外,所以每份副本也都无法测量。
+- **在 `examples/` 下建共享模块目录**:代码仍在覆盖率门禁之外,且需要跨示例边界的相对导入,违反包名导入约定;`examples/` 的叶子节点按设计应保持轻薄。
+- **`dsh-acp-demo` 的 `/testing` 子路径导出**:将测试基础设施耦合到产品包的对外服务接口与依赖集中;`packages/support/` 的存在正是为了真实但兼容性承诺较低的开发/测试包,`dsh-llm-replay` 是先例,本包与之配套。
+- **导出原始测试体函数而非套件工厂**:每个示例将重新拥有 `describe`/`it` 骨架(每套件约 80 行注册样板),却无灵活性收益;工厂使消费方只需一张场景表加一次调用,而导出的纯辅助函数在工厂设计内保留了可单元测试性。
+- **使用可注入 ACP `Client` factory 代替声明式 `permissionAnswers`**——灵活性最大,但会把 SDK client 构造泄漏给每个消费方,并恰好在正在统一的层重新引入逐示例漂移;声明式队列让 `input.json` 保持为唯一脚本表面,并与预期输出规范化兼容。
+- **泛化到 ACP 之外(传输无关的快照 harness)**:不存在第二种传输方式;harness 端到端都是 ACP 形态(SDK 客户端、JSON-RPC 帧、`session/update` 等待器),推测性的抽象将是一个超前于任何消费方的 seam 拆分。
+
+## 测试
+
+提取一致性得到机械证明:迁移后,`pnpm run test:snapshot` 的结果与基准提交匹配,`examples/acp-agent/tests/snapshots/` 下没有任何字节变化。包的 `src/` 在门禁单元运行中保持逐文件 100% 语句/分支/函数/行覆盖,并通过脚本化 fake ACP bin(`tests/fixtures/fake-acp-agent.ts`,每个场景由 fixture 旁的 `behavior.json` 编排行为)经过真实 launcher 驱动:`harness.spec.ts` 直接覆盖 launcher 默认值、捕获、更新等待、关闭以及环境/配置变体,随后覆盖每种场景步骤操作、两个 expect-error 分支、权限队列(选择、后备、不可能点击)、workspace seed,以及采集顺序/噪音/后备分支;`suite.spec.ts` 在收集时真实运行 factory——一个针对已提交合成 fixture 的回放套件和一个针对临时副本的记录套件(写回从不触及已提交树;`ACP_SNAPSHOT_SPEC_BOOTSTRAP=1` 会重新引导它)——并包含纯辅助函数的直接用例。fake bin 会把 `session/new` cwd 而非 `process.cwd()` 代入脚本化日志,与真实 bin 请求头携带的内容一致(darwin 会将 `/var/folders/…` realpath 为 `/private/var/folders/…`)。
+
+## 后果
+
+新示例通过场景表加 fixture 即可获得完整快照层,普通 ACP e2e 则通过一次 launcher 调用获得同一条经过测试的进程/client 边界。代价是:`suite.ts` 导入 vitest,因此包入口只能在 vitest 运行中导入——其他包都没有这种形状,其 README 已说明;每个套件还要固定自己的约 8 KB 请求头 fixture(真正不同的组合理应拥有自己的固定项;相同组合则会被该套件的一致性守卫捕获)。
diff --git a/.agents/notes/implemented/AGENTS.md b/.agents/notes/implemented/AGENTS.md
index 5fb3fde8e2..b8da5dc8ef 100644
--- a/.agents/notes/implemented/AGENTS.md
+++ b/.agents/notes/implemented/AGENTS.md
@@ -6,6 +6,8 @@ These Agent Notes describe shipped decisions. Follow the [root instructions](../
Keep paths, symbols, defaults, and mechanisms current in the same change that alters them. Rewrite stale facts in place; do not append change history.
+When a shipped note is unlikely to guide future work, archive its complete triplet through [`dsh-archive-agent-notes`](../../skills/dsh-archive-agent-notes/SKILL.md) instead of continuing to maintain it.
+
### This is not a license to rewrite the *decision*
-Update factual realization in place. A reversal of the decision or its rationale requires a new Agent Note and cross-link; see the [Agent Note contract](../README.md).
+Update factual realization in place. A reversal of the decision or its rationale requires a new Agent Note and cross-link; a fully superseded old note may be deleted only through the consolidation rule in the [Agent Note contract](../README.md).
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
new file mode 100644
index 0000000000..b8d807098d
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.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
+2026-06-11-content-block-vocabulary.md: d926c28e7e197aff28c7b1c09d085febf866832b
+2026-06-11-content-block-vocabulary.zh.md: 6361f00abe109bffdb5bd3ff5652df67d6b3c8a1
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 1133b990c3..d926c28e7e 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
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-11-content-block-vocabulary.zh.md)
+
## Problem
The harness needs one internal language for messages that the loop, session log, and all plugins speak.
@@ -21,6 +23,6 @@ In-session context injection (`context/message`) and mid-turn steering (`steerin
- Reasoning has a core home without provider-specific shapes.
- Multimodal blocks return only with coordinated adapter, UI, and compaction support; see [the drop-image Agent Note](../simplification/2026-07-04-drop-image-content-block.md).
-- Cache hints and assistant prefill remain absent until a shipping adapter can honor them; see the [producer-less variants](../simplification/2026-07-04-prune-producerless-vocabulary-variants.md) and [inert request knobs](../simplification/2026-07-04-drop-inert-request-knobs.md) Agent Notes.
+- Cache hints and assistant prefill remain absent until a shipping adapter can honor them; see the [producer-less variants](../../archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md) and [inert request knobs](../../archived/simplification/2026-07-04-drop-inert-request-knobs.md) Agent Notes.
- Every adapter pays a translation cost; the first real adapters have since validated the streaming protocol, and new adapters should continue proving their provider-specific mapping in adapter-local tests.
- IDs that cross package boundaries are branded (`CallId`, the shared agent/session `SessionId`) — nominal typing at zero runtime cost.
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
new file mode 100644
index 0000000000..6361f00abe
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md
@@ -0,0 +1,28 @@
+# Agent Note: 由 dsh-llm 拥有的提供方无关内容块词汇
+
+Status: implemented
+
+[English](2026-06-11-content-block-vocabulary.md) | 中文
+
+## 问题
+
+harness 需要一套统一的内部消息语言,供 agent loop(智能体循环)、会话日志和所有插件共同使用。
+
+## 决策
+
+自主拥有词汇:消息是类型化内容块的数组(`text`、`reasoning`、`tool-call`、`tool-result`),其联合类型派生自可合并扩展的 `ContentBlockMap`,插件通过声明合并添加新的块类型。同一可合并扩展映射模式为所有「字符串化」字段提供类型(`MessageSource`、`FinishReason`、`TurnTrigger`、`TurnEndReason`)。流式输出采用原始分片协议;`BlockAssembler` 是唯一的共享组装实现。适配器负责转换为提供方的协议格式(wire format)——映射成本留在适配器中,正是它该在的地方。
+
+会话内上下文注入(`context/message`)和轮次中途 steering(`steering/message`)最初渲染为带标签的 user-role 信封(system-reminder 模式),而非引入新角色,因此适配器无需承担额外负担。如今两者都投影为无包装的普通用户内容;见[注入内容信封 Agent Note](../simplification/2026-07-20-unwrap-injected-content-envelopes.md)。实际适配器验证已确认此渲染方式符合当前 DeepSeek 的行为;如果未来某提供方出现不兼容,应在该适配器内处理,而非引入新的规范角色。
+
+## 曾考虑的替代方案
+
+- **镜像 DeepSeek/OpenAI chat-completions 结构**:对第一个提供方零映射成本,但对富内容(推理、结构化块形式的工具结果)处理不便。
+- **原样采用 Anthropic Messages 块结构**:经过实战检验,但规范类型将镜像一个 harness 并非首要对接的第三方 API。
+
+## 后果
+
+- 推理(reasoning)在核心层有了归属,无需依赖提供方特有的结构。
+- 多模态块只有在适配器、UI 和上下文压缩(context compaction)三方协同支持后才会回归;见 [drop-image Agent Note](../simplification/2026-07-04-drop-image-content-block.md)。
+- 缓存提示与 assistant prefill 在有实际适配器能兑现之前保持缺席;见[无生产者的词汇变体](../../archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md)与[无端到端可用路径的请求旋钮](../../archived/simplification/2026-07-04-drop-inert-request-knobs.md) Agent Note。
+- 每个适配器都需承担翻译成本;首批真实适配器已验证了流式输出协议,新适配器应继续在适配器本地测试中验证其提供方特有的映射。
+- 跨包(package)边界的 ID 使用品牌类型(`CallId`、agent 与会话共享的 `SessionId`)——零运行时开销的名义类型。
diff --git a/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml
new file mode 100644
index 0000000000..d5b172cb13
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.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-06-11-dev-invariants-over-deep-readonly.md
+2026-06-11-dev-invariants-over-deep-readonly.md: f1a741927cf63b43b1aaf558efc148ff80d2d881
+2026-06-11-dev-invariants-over-deep-readonly.zh.md: 67439c674f5a77112fc0619c774f7897cf49e835
diff --git a/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md
index fb1dc93159..f1a741927c 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-11-dev-invariants-over-deep-readonly.zh.md)
+
## Problem
The session log needs two different protections: immutable ownership of each stored fact, and checks for relationships among facts across time and service seams. Conflating them in an optional development plugin would leave production history vulnerable; trying to express both through TypeScript readonly types would not create a runtime boundary or describe relational rules.
@@ -38,7 +40,7 @@ When the session companion attaches to an existing or seeded session, it replays
### Pervasive deep-readonly types
-[The rejected immutable-public-surfaces proposal](../../rejected/architecture/2026-06-11-immutable-public-surfaces.md) would apply a recursive readonly type across public log and message surfaces. That provides editor feedback but not a runtime guarantee: TypeScript types are erased and plugin code can cast through them. It also pushes readonly types into consumers where mutation is intentional. Runtime ownership at the `Session` boundary protects every caller without that type propagation.
+A rejected companion proposal would apply a recursive `DeepReadonly` type across public log and message surfaces, flipping session read paths (`events`, `session/event` listeners, `deriveMessages()`) to deep-readonly while keeping in-flight waterfalls mutable. That provides editor feedback but not a runtime guarantee: TypeScript types are erased and plugin code can cast through them. It also pushes readonly types into consumers where mutation is intentional. Runtime ownership at the `Session` boundary protects every caller without that type propagation.
### Development-only freezing
diff --git a/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.zh.md b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.zh.md
new file mode 100644
index 0000000000..67439c674f
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.zh.md
@@ -0,0 +1,60 @@
+# Agent Note: 源端拥有的会话不可变性与开发模式不变式
+
+Status: implemented
+
+[English](2026-06-11-dev-invariants-over-deep-readonly.md) | 中文
+
+## 问题
+
+会话日志需要两种不同的保护:对每条已存储事实的不可变所有权,以及对跨时间和服务 seam 的事实之间关系的检查。如果将二者混为一个可选的开发插件,生产环境的历史记录将失去保护;如果试图通过 TypeScript readonly 类型同时表达两者,既无法建立运行时边界,也无法描述关系规则。
+
+会话日志是回放、请求重建、持久化与用户可见历史的持久真源。会话包(package)外部的代码必须能检视历史,但不能保留一个可在之后改写历史的引用;从调用方接受的输入也不能继续连接到调用方拥有的可变对象。
+
+单个值的不可变性只是契约的一半。一份日志可以包含完全不可变的记录,但其序列、轮次/步骤嵌套、工具调用配对、作用域分发或重建的模型请求是错误的。这些规则涉及多条记录或多个服务,无法通过冻结单个对象来建立。
+
+TypeScript readonly 类型不是充分的运行时边界。它们在程序运行时消失,类型转换可以绕过它们,而递归的 `DeepReadonly` 会扩散到每个日志和消息消费方,尽管某些下游请求处理 API 有意使用可变值。
+
+## 决策
+
+职责在始终启用的存储边界与可选的开发断言之间分离。
+
+### Session 拥有不可变历史
+
+`Session` 仅在一次递归遍历完成无损 JSON 快照的物化之后才接受事件。该遍历拒绝不支持的值,并产出进入日志的确切分离记录,因此验证与存储不会从有状态的 getter 观察到不同的值,也不会保留调用方拥有的嵌套引用。
+
+被接受的事件及其所有后代在发布前被深度冻结。`append()` 返回该拥有的冻结事件,`session/event` 观察者接收同一记录,`session.events` 返回冻结的数组快照。先前返回的数组不会因后续 append 而增长。种子记录在构造成功前经过相同的验证、快照与冻结边界。
+
+此保证属于 `Session` 而非可选监听器,因为每种组合都依赖可信的历史。无论是否注册了开发支持插件,生产部署、聚焦测试或自定义嵌入都获得相同的存储语义。
+
+### 派生请求保持分离
+
+`deriveMessages()` 将已记录的表面事件投影为分离的、深度冻结的 `Message` 对象,并返回一份新的数组快照。因此请求组装可以将派生历史与其他输入组合,而不会暴露一条回到日志的路径。缓存复用安全的不可变投影,而非为每次模型调用重新克隆完整历史。
+
+### 包拥有的不变式配套插件检查关系
+
+`dsh-invariants` 注册可配置的 `ctx.invariants` 服务,本身不包含产品检查。每个包发布一个 `./invariant` 所有权配套插件;`dsh-session`、`dsh-agent`、`dsh-scope` 和 `dsh-agent-loop` 目前添加需要 trace 状态或观察另一个 seam 的规则:单调递增的序列号、轮次与步骤嵌套、工具调用/结果配对、合法的 agent(智能体)状态转换、主体正确的作用域分发,以及循环构建的请求与从其会话日志前缀重建的请求之间的等价性。全局启用和包名 regex 过滤器归该服务所有(见[包拥有的不变式服务](2026-07-19-package-owned-invariant-service.md))。
+
+当会话配套插件附加到已有或已播种的会话时,它回放不可变日志以重建跟踪状态。服务为每项贡献提供一个可 dispose(资源释放)的子 fiber,因此轮次中途热重载是安全的,同时不赋予诊断逻辑对会话存储的所有权。
+
+## 曾考虑的替代方案
+
+### 全面的 deep-readonly 类型
+
+一个被否决的姊妹提案会在公共日志和消息表面上应用递归 `DeepReadonly` 类型,将会话读取路径(`events`、`session/event` 监听器、`deriveMessages()`)翻转为深只读,同时保持进行中的 waterfall 可变。这能提供编辑器反馈,但无法提供运行时保证:TypeScript 类型在运行时被擦除,插件代码可以通过类型转换绕过。它还会将 readonly 类型推入有意进行修改的消费方。在 `Session` 边界处的运行时所有权保护所有调用方,无需这种类型传播。
+
+### 仅在开发模式冻结
+
+仅当不变式插件安装时才冻结历史,会使核心保证依赖于组合方式。代码可能通过开发测试,却在生产环境或省略了该插件的聚焦组合中破坏历史。因此存储不可变性始终启用,而开销更大的关系检查则保持为可选的开发支持。
+
+### 仅在派生消息时克隆
+
+分离 `deriveMessages()` 能保护最常见的请求路径,但 `session.events` 的其他读取者、append 返回值和会话事件观察者仍能修改持久历史。日志必须保护自身的边界;派生投影是额外的隔离边界,而非替代品。
+
+## 后果
+
+- 每个被接受的实时或种子会话事件在任何观察者接收之前,都已从调用方拥有的输入中分离并深度不可变。
+- `session.events` 暴露稳定的不可变快照,而非私有的增长数组。
+- 请求侧的修改无法通过派生消息触及已存储的历史。
+- 开发构建可以启用关系断言而不改变存储行为;dispose 或过滤一个配套插件不会削弱日志不可变性。
+- `dsh-invariants` 配置全局启用状态以及包允许/阻止 regex 列表;每项检查仍由其产品包拥有并测试。
+- 运行时边界对每个被接受的事件产生一次递归快照与冻结的开销;后续读取者和缓存投影复用已拥有的不可变记录。
diff --git a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml
new file mode 100644
index 0000000000..6ea6fce11e
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.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
+2026-06-11-event-sourced-sessions.md: 15ba7b23d5eae48e7dee2328b5924493d54aeeb0
+2026-06-11-event-sourced-sessions.zh.md: da3be5965a6900076f253cad065b847c6f5ce17e
diff --git a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md
index bab36ee783..15ba7b23d5 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-11-event-sourced-sessions.zh.md)
+
## Problem
The MVP requires strict event-based tracing with fully replayable sessions (严格的基于事件的trace、logging系统,session完全可回放).
diff --git a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md
new file mode 100644
index 0000000000..da3be5965a
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md
@@ -0,0 +1,28 @@
+# Agent Note: 事件溯源的会话与派生消息历史
+
+Status: implemented
+
+[English](2026-06-11-event-sourced-sessions.md) | 中文
+
+## 问题
+
+MVP 要求严格的基于事件的追踪,以及完全可回放的会话(严格的基于事件的 trace、logging 系统,会话完全可回放)。
+
+## 决策
+
+`Session` 是一份仅追加的、类型化的 `SessionEvent` 日志,是唯一的真源。LLM(大语言模型)消息历史从日志*派生*(`deriveMessages()`);原始流分片被记录以保证 token 级别的回放保真度,而组装后的 `assistant/message` 事件才是派生的权威依据。回放/fork = 用已有日志初始化一个新会话。
+
+追加操作是同步的(热路径从不阻塞于 I/O);`session/event` 是同步通知;持久化插件在后台缓冲写入,并在每个轮次结束时触发的 `session/flush` 检查点处等待排空。
+
+顺序契约:agent loop(智能体循环)*先*追加到会话,再发出对应的 Cordis 事件;`agent/step-result` waterfall(瀑布式事件)在 `assistant/message` 追加之前运行,因此日志记录的是工具调度实际使用的消息。回归测试固定了这一顺序。
+
+## 曾考虑的替代方案
+
+**可变消息数组 + 事件仅作通知发出**:更简单,但状态与日志可能分歧;采用事件溯源后,日志本身即是状态,分歧在结构上不可能发生。
+
+## 后果
+
+- 回放、追踪与遥测在结构上得到保证,而非事后附加。
+- 持久化仍是插件关注点;内存存储随 dsh-session 一起提供。
+- 事件词汇可通过合并扩展(插件可添加如压缩(compaction)事件);[会话持久化](2026-06-14-session-persistence.md)在日志变为持久后冻结了其形状。
+- 派生成本随日志长度增长,压缩(未来插件)是预期的缓解手段,而非日志变更。
diff --git a/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml
new file mode 100644
index 0000000000..a15ebcfddf
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.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
+2026-06-11-microkernel-event-taxonomy.md: 8bf05b7deba5f054d4ec8ecf104c3b8798e42d4e
+2026-06-11-microkernel-event-taxonomy.zh.md: 4ff2ab632ca02e98137a15f19a7996a740a519b0
diff --git a/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md
index abdadb447b..8bf05b7deb 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-11-microkernel-event-taxonomy.zh.md)
+
## Problem
The product principle is "everything is a plugin": hooks, /goal, /loop, dynamic workflows, compaction, sandboxing, permissions, UI, persistence, MCP, skills must all be writable as plugins without modifying the core.
diff --git a/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.zh.md b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.zh.md
new file mode 100644
index 0000000000..4ff2ab632c
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.zh.md
@@ -0,0 +1,31 @@
+# Agent Note: 微内核——通过 Cordis 事件分类体系实现扩展,唯一具体循环
+
+Status: implemented
+
+[English](2026-06-11-microkernel-event-taxonomy.md) | 中文
+
+## 问题
+
+产品原则是「一切皆插件」:钩子、/goal、/loop、动态工作流、上下文压缩(context compaction)、沙箱、权限、UI、持久化、MCP、skill(技能)都必须能以插件形式编写,无需修改核心。
+
+## 决策
+
+纯 Cordis 事件分类体系。agent loop(智能体循环)的扩展 seam 是带类型的事件,具有明确的分发模式:
+
+- **waterfall(瀑布式事件)**(around-middleware):插件可变换、否决、恢复或包装:`agent/prompt-submit`、`agent/request`、`agent/request-error`、`agent/step-result`、`agent/turn-continuation`、`tools/pre-execute`、`tools/execute`、`tools/post-execute`、`llm/stream`、`system-prompt/assemble`。
+- **serial**(按监听器顺序依次 await;bail 值会阻止后续监听器执行):用于有序检查点。所有 `agent/pre-step` 和 `agent/post-step` 监听器在全部弃权时才继续运行,而 `agent/turn-stop` 返回的第一个 stop 值即为最终的终止决策。
+- **parallel**(await 扇出):每个监听器都必须获得独立执行的机会:`session/flush` 持久性检查点。
+- **emit**(同步 fire-and-forget):用于通知:轮次/步骤边界、流分片、生命周期、错误,以及包含不可变 `tools/result` 观测的事件。
+
+事件词汇定义在接口包中(dsh-agent 声明 agent/* 事件);`@deepseek-ai/dsh-agent-loop` 是唯一的具体循环插件,且自身可替换——外部不得依赖它。
+
+## 曾考虑的替代方案
+
+**专用中间件栈(koa-compose 风格)** 与**显式阶段状态机(插件向其中插入阶段)**:两者都需要重新实现 Cordis 原生事件系统已提供的分发、dispose(资源释放)与重载语义;作为 Cordis effect,监听器天然获得 HMR(热模块替换)与 dispose 能力。
+
+## 后果
+
+- 每个 MVP 功能都映射到一个监听器([功能→机制映射](../../../../docs/cookbook/extension-cookbook.md#the-feature--mechanism-map)是证明义务,保持更新)。
+- HMR 与 dispose 无需额外工作:监听器和注册均为 Cordis effect。
+- waterfall 语义(调用 `next()` 或短路)不直观,需要教学——在 AGENTS.md 中记录,并由组合测试覆盖。
+- 循环必须具备防御性:插件异常在轮次级别被隔离,任何 seam 发出的 steering(中途引导)永远不会被搁置(有回归测试保障)。
diff --git a/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml
new file mode 100644
index 0000000000..0697332171
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.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
+2026-06-11-runtime-arg-validation.md: e0bca0ff24c5adc7ca58007932dff6580694b01d
+2026-06-11-runtime-arg-validation.zh.md: 09958147766b4015d6bebf786c4947b9d2941f74
diff --git a/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md
index 94b0c6af60..e0bca0ff24 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-11-runtime-arg-validation.zh.md)
+
## Problem
`defineTool` ([the unified schema DSL](2026-07-20-unified-json-value-schema-dsl.md)) gives tool authors a typed `execute(args)` via the `InferArgs` mapping. But that type is a compile-time claim about a value that arrives at runtime as model-generated JSON: nothing forced the model to honor the schema, so a malformed call — missing a required key, a string where a number was declared, or a literal outside the declared set — reached `execute` typed-in-name-only. The tool body then either crashed on the bad shape or silently misbehaved.
diff --git a/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md
new file mode 100644
index 0000000000..0995814776
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md
@@ -0,0 +1,24 @@
+# Agent Note: 模型边界处的运行时参数校验
+
+Status: implemented
+
+[English](2026-06-11-runtime-arg-validation.md) | 中文
+
+## 问题
+
+`defineTool`([统一 schema DSL](2026-07-20-unified-json-value-schema-dsl.md))为工具作者的 `execute(args)` 提供了经 `InferArgs` 映射的类型化参数。但该类型只是对运行时值的编译期声明,而这个值实际上是模型生成的 JSON:没有任何机制强制模型遵守 schema,因此畸形调用(缺少必需键、声明为数字的位置传入字符串,或字面量超出声明的集合)会以「仅名义类型化」的状态到达 `execute`。工具函数体随后要么在错误形状上崩溃,要么静默地行为异常。
+
+## 决策
+
+`validateArgs(spec, args): string[]` 编译 `ParameterSchemaSpec`,并委托共享的 `validateJsonSchemaValue()` 遍历器,对格式正确的声明返回可读的违规列表。`defineTool` 在定义时对编译后的参数 schema 创建快照,并在调用类型化函数体之前执行校验;存在违规时会抛出 `ToolArgsError`(`INVALID_ARGS`),注册表将其作为模型可据以修正的错误结果返回。
+
+校验器与编译器因此共享完全一致的语义:隐式参数根是开放对象;必需键仅来自 `required: true`;默认值仍是注解;显式嵌套对象遵循其声明的开放性;数组通过 `items` 递归;标量字面量约束保证类型正确;`oneOf` 仅在恰好一个分支匹配时才接受。原始注册的工具自行负责输入校验。
+
+## 后果
+
+- 模型在自身畸形调用上获得可操作的反馈,而非不透明的崩溃,弥合了 `InferArgs` 的承诺与运行时现实之间的鸿沟。
+- 校验器与 `InferArgs` 必须保持一致;一项[属性测试](../testing/2026-06-11-property-based-testing.md)生成满足 spec 的参数并断言它们通过 `validateArgs`(同时断言定向破坏的参数被拒绝),以机械方式封堵漂移风险。
+- `ToolArgsError` 目前是带 `code` 字段的普通 `Error`;如果日后引入 harness 级别的错误分类体系,它将变为子类,但不影响读取 `.message` 的调用方。
+- 校验开销相对于一次模型调用可忽略不计。
+
+
diff --git a/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.i18n.yaml
new file mode 100644
index 0000000000..ca9d2117ec
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.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
+2026-06-11-structured-error-taxonomy.md: 9122193b3d01cf5a4c315e6f7a7218153fd4a60a
+2026-06-11-structured-error-taxonomy.zh.md: 56a196ccd10a81b51953887f18e522412cd9463b
diff --git a/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md b/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md
index b2f4ea66b4..9122193b3d 100644
--- a/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-11-structured-error-taxonomy.zh.md)
+
## Problem
Failures crossed seams as bare strings. A tool error flattened to a text block — name, code, and stack lost — so a future sandbox/retry plugin couldn't tell ENOENT from EACCES, and the model got less actionable feedback than it could. A non-Error throw degraded further: the loop wrapped it in `new Error(String(x))`, dropping any code. And `LlmError` was the only typed error in the system, with no shared base, so there was nothing for a consumer to `instanceof` against generically.
diff --git a/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.zh.md b/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.zh.md
new file mode 100644
index 0000000000..56a196ccd1
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.zh.md
@@ -0,0 +1,26 @@
+# Agent Note: 结构化错误分类体系
+
+Status: implemented
+
+[English](2026-06-11-structured-error-taxonomy.md) | 中文
+
+## 问题
+
+故障跨越 seam 时只是裸字符串。工具错误被扁平化为一个文本块(name、code 和 stack 全部丢失),导致未来的沙箱/重试插件无法区分 ENOENT 和 EACCES,模型得到的反馈也不如本可以那样具有可操作性。非 Error 的 throw 退化更严重:agent loop(智能体循环)将其包装为 `new Error(String(x))`,丢弃了所有 code。而 `LlmError` 是系统中唯一的类型化错误,没有共享基类,消费方无法对其进行通用的 `instanceof` 判断。
+
+## 决策
+
+在 `dsh-llm`(叶子包,所有其他包都已依赖它,不引入新的依赖边)中引入一个 `HarnessError extends Error` 基类:稳定的 `code`(与 `message` 分离)、通过 `ErrorOptions` 进行 `cause` 链接、`name` 默认为子类名。`isHarnessError` 在 seam 处做类型收窄。
+
+- `LlmError` 和 `ToolArgsError`(dsh-tools)继承该基类,保留各自既有的 code。
+- `ToolExecutionResult` 新增可选字段 `error: { name, code }`,在注册表的 catch 中当抛出值为 `HarnessError` 时填充。agent loop 将其转发到 `tool/result` 会话事件(该事件也新增了同一可选字段),使结构化的失败信息存活到日志中,供重试/沙箱插件和回放使用。面向模型的文本块保持不变。
+- agent loop 的 `toError` 将非 Error 的 throw 包装为 `HarnessError`(`code: 'UNKNOWN'`,原始值作为 `cause` 链接),而非裸 `Error`;这样即使是不规范的 throw 也能携带可路由的 code 进入会话的 `error` 事件(该事件此前已暴露 `code`)。
+
+## 后果
+
+- 错误端到端可机器路由:插件可以基于 `error.code` 分支,而无需对消息做子串匹配。
+- 一个基类被广泛导入,但它位于所有包已经依赖的包中,代价仅是一条 import 语句,而非新的依赖边。
+- `deriveMessages` 不会将 `error` 暴露到模型历史中——模型仍然看到文本块;结构化字段服务于代码和回放。
+- 参数校验保留其既有的 code 和行为;包自有的诊断不变式独立携带稳定 code,使不变式注册表无需导入产品包。共享基类增加了跨 seam 的路由元数据,不改变面向模型的文本。
+
+
diff --git a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml
new file mode 100644
index 0000000000..63063f8d5f
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.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
+2026-06-13-capability-seams.md: 7c755dced7825d2831acc0901f6412b8e5afe95a
+2026-06-13-capability-seams.zh.md: 4148c79cb5e1930dca77eaf3afd2024f508275b5
diff --git a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.md b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.md
index 5ca299abc0..7c755dced7 100644
--- a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.md
+++ b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.md
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-13-capability-seams.zh.md)
+
## Problem
The harness has swappable capabilities — bash execution today, sandboxed/remote executors and alternative model providers tomorrow. A capability has three concerns that change at different rates and for different reasons: the *contract* (what the capability is), the *implementation* (how it runs), and the *consumer surface* (what the model and other plugins program against). Bundling them in one package couples those rates of change — swapping a local executor for a sandboxed one would churn the tool schemas the model sees, even though the model-facing contract never changed.
diff --git a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md
new file mode 100644
index 0000000000..4148c79cb5
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md
@@ -0,0 +1,32 @@
+# Agent Note: 能力 seam——接口/实现/消费方三分
+
+Status: implemented
+
+[English](2026-06-13-capability-seams.md) | 中文
+
+## 问题
+
+harness 具有可替换的能力:当前是 bash 执行,未来会有沙箱化/远程执行器和替代模型提供方。一项能力涉及三个关注点,它们以不同速率、因不同原因变化:*契约*(这项能力是什么)、*实现*(它如何运行)、*消费方接口*(模型和其他插件面向什么编程)。将三者捆绑在一个包(package)中会耦合这些变化速率——把本地执行器换成沙箱化执行器时,模型看到的工具 schema 也会被搅动,尽管面向模型的契约从未改变。
+
+这与「谁在运行时提供、谁需要一项能力」是不同的问题,后者 Cordis 已通过服务 + `inject` 解决(提供方注册 `ctx.bash`;消费方声明 `inject: ['bash']`,其 fiber 挂起直到服务存在)。该机制是必要的,但不决定包的边界;本 Agent Note 决定的是包的边界。
+
+## 决策
+
+一项可替换的能力由**三个包**构成:
+
+1. **接口**——一个抽象服务加词汇类型,拥有 `ctx.`,仅依赖其词汇依赖(例如 `dsh-bash`:`BashExecutor`、`BashRunResult`、`BashProcess`)。
+2. **实现**——一个具体子类,以插件形式加载(例如 `dsh-bash-local`:子进程、进程组 kill、溢出文件截断)。沙箱化/远程后端是实现同一接口的兄弟包。
+3. **消费方**——模型和插件看到的内容(例如 `dsh-tool-bash`:`bash` schema,后台句柄注册到通用任务运行时)。消费方 `inject` 接口键,从不导入实现类型。
+
+实现与消费方由此独立演进:沙箱化执行器替换 `dsh-bash-local` 时无需触碰任何工具 schema。
+
+当各部分确实属于同一个关注点时,三分并非强制:LLM(大语言模型) seam 将接口 + 消费方合并为 `dsh-llm`(消费方是 agent loop(智能体循环)本身,而非可替换的 schema 表面),适配器作为实现包。不要预防性地拆分——如果一项能力只有一种可设想的实现和一个消费方,就保持为一个包,直到第二种出现。
+
+## 曾考虑的替代方案
+
+- **单一合并包**:否决。因为它重新耦合了三分设计本要分离的三种变化速率(这正是拆分的意义所在)。
+- **`@cordisjs/plugin-capability`**:这是完全不同的维度。它是一个权限/能力*安全*服务(具名权限加继承,通过 `ctx.capability.test` 对会话进行检测),是延后的权限/沙箱工作(`tools/pre-execute` deny/ask seam)的候选方案,不是替换实现的机制。混淆这两个「能力」概念正是本 Agent Note 所指出的陷阱。
+
+## 后果
+
+每项能力需要更多包和更多样板代码(一组 `package.json`/`tsconfig`/README,加上 inject 接线)。换来的是:实现与消费方独立发布和版本管理,新后端永远不会波及面向模型的契约。该规则记录在 [AGENTS.md](../../../../AGENTS.md) § Conventions(「Capability seams are three packages」)和 [architecture.md](../../../../docs/architecture.md) §「Capability seams」中;bash 三件套是参考模板。何时合并、何时拆分是一个判断问题,架构文档对此有详细说明——本 Agent Note 记录的是*为什么*默认选择拆分。
diff --git a/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml
new file mode 100644
index 0000000000..dc88be13a2
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.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
+2026-06-13-twin-llm-adapters.md: b922891d4438553fd96a7f4f4226f378e66e8ad2
+2026-06-13-twin-llm-adapters.zh.md: d98b57a0a2e7c92453046022e8cb50aa52994c0f
diff --git a/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md
index 7f2f5933ad..b922891d44 100644
--- a/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md
+++ b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-13-twin-llm-adapters.zh.md)
+
## Problem
`dsh-llm` owns a provider-neutral streaming vocabulary — the `StreamChunk` protocol (`block-start`, `text-delta`, `reasoning-delta`, `tool-call-delta`, `block-end`, `usage`, `finish`) and the content-block types ([the content-block vocabulary](2026-06-11-content-block-vocabulary.md)). A vocabulary defined against a single adapter risks baking that adapter's quirks into the "neutral" contract: anything the one implementation happens to do becomes the de-facto spec, and the abstraction is unverified until a second provider arrives — by which point the leak is expensive to fix.
@@ -10,10 +12,10 @@ Status: implemented
Ship **two** adapters against the one contract from the start, deliberately built on different internals:
-- `dsh-llm-deepseek` — hand-rolled `fetch` + SSE parsing against the DeepSeek API.
+- `dsh-llm-deepseek` — direct `fetch` + in-repo translation against the DeepSeek API; SSE framing is delegated to `eventsource-parser` ([the SSE-parser swap](../simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md)). The twin identity is owning the fetch/translate internals rather than delegating to a full provider SDK, not hand-rolling transport plumbing.
- `dsh-llm-pi-ai` — the same endpoint through the `@earendil-works/pi-ai` library (its own event vocabulary).
-The rule they enforce: **anything the StreamChunk vocabulary cannot express for BOTH implementations is a core-vocabulary bug**, caught immediately rather than at the next provider. The pair pinned down conventions now documented on `StreamChunk` in `dsh-llm/src/types.ts`: usage emitted before finish, nothing after finish, tool-call `arguments` as raw JSON strings end-to-end, and the two sanctioned error paths (throw from `stream()` *or* end with `finish {kind:'error'|'aborted'}`) that a consumer must handle on both sides — a divergence the library-backed adapter surfaced that a single hand-rolled adapter would have hidden.
+The rule they enforce: **anything the StreamChunk vocabulary cannot express for BOTH implementations is a core-vocabulary bug**, caught immediately rather than at the next provider. The pair pinned down conventions now documented on `StreamChunk` in `dsh-llm/src/types.ts`: usage emitted before finish, nothing after finish, tool-call `arguments` as raw JSON strings end-to-end, and the two sanctioned error paths (throw from `stream()` *or* end with `finish {kind:'error'|'aborted'}`) that a consumer must handle on both sides — a divergence the library-backed adapter surfaced that a single direct-fetch adapter would have hidden.
## Alternatives considered
@@ -22,4 +24,4 @@ The rule they enforce: **anything the StreamChunk vocabulary cannot express for
## Consequences
-The twin doubles adapter and key-gated e2e maintenance—both cover V4 Flash and Pro across representative reasoning modes—in exchange for continuous seam-neutrality validation and a second implementation example. Both use `apiKey`, `baseURL`, and `models`; the hand-rolled adapter exposes `thinking`/`reasoningEffort`, while pi-ai exposes one `reasoning` level. A future conformance suite could justify retiring one adapter through a superseding Agent Note.
+The twin doubles adapter and key-gated e2e maintenance—both cover V4 Flash and Pro across representative reasoning modes—in exchange for continuous seam-neutrality validation and a second implementation example. Both use `apiKey`, `baseURL`, and `models`; the direct-fetch adapter exposes `thinking`/`reasoningEffort`, while pi-ai exposes one `reasoning` level. A future conformance suite could justify retiring one adapter through a superseding Agent Note.
diff --git a/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md
new file mode 100644
index 0000000000..d98b57a0a2
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md
@@ -0,0 +1,27 @@
+# Agent Note: 以两个 LLM 适配器作为设计验证孪生体
+
+Status: implemented
+
+[English](2026-06-13-twin-llm-adapters.md) | 中文
+
+## 问题
+
+`dsh-llm` 拥有一套提供方无关的流式词汇:`StreamChunk` 协议(`block-start`、`text-delta`、`reasoning-delta`、`tool-call-delta`、`block-end`、`usage`、`finish`)以及内容块类型([内容块词汇](2026-06-11-content-block-vocabulary.md))。如果词汇仅针对单个适配器定义,就有可能将该适配器的特异行为烘焙进「中立」契约:唯一实现碰巧做了什么,什么就成为事实上的规范;在第二个提供方到来之前,抽象层未经验证——而届时泄漏已代价高昂。
+
+## 决策
+
+从一开始就针对同一份契约交付**两个**适配器,刻意基于不同的内部实现构建:
+
+- `dsh-llm-deepseek`:直接 `fetch` + 仓库内翻译逻辑对接 DeepSeek API;SSE(Server-Sent Events)分帧委托给 `eventsource-parser`([SSE 解析器替换](../simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md))。孪生身份在于自行持有 fetch/translate 内部实现而非委托给完整的提供方 SDK,不在于手写传输层管道。
+- `dsh-llm-pi-ai`:通过 `@earendil-works/pi-ai` 库访问同一端点(该库有自己的事件词汇)。
+
+二者共同执行的规则是:**凡 StreamChunk 词汇无法为两个实现同时表达的内容,都是核心词汇的缺陷**——立即暴露,而非等到下一个提供方接入时才发现。这对孪生体确定了现已记录在 `dsh-llm/src/types.ts` 中 `StreamChunk` 上的约定:usage 在 finish 之前发出、finish 之后不再有任何事件、工具调用的 `arguments` 全程以原始 JSON 字符串传递,以及消费方必须在两侧都处理的两条合法错误路径(`stream()` 抛异常,*或者*以 `finish {kind:'error'|'aborted'}` 结束)。后一项分歧正是由基于库的适配器暴露出来的,单一直接 fetch 适配器会将其隐藏。
+
+## 曾考虑的替代方案
+
+- **单一适配器**:代码更少、e2e 成本减半,但「提供方无关」的声明无从验证;词汇会默默编码 DeepSeek-via-fetch 的假设。
+- **mock 第二适配器**:更便宜,但不会触及真实提供方的协议格式(wire format)怪癖,因此证明力有限。孪生体是真实对真实的验证。
+
+## 后果
+
+孪生体使适配器和需要密钥的 e2e 维护量翻倍——两者都覆盖 V4 Flash 和 Pro 在各代表性推理(reasoning)模式下的行为——换来的是持续的 seam 中立性验证和第二份实现示例。两个适配器均使用 `apiKey`、`baseURL` 和 `models`;直接 fetch 适配器暴露 `thinking`/`reasoningEffort`,pi-ai 适配器暴露一个 `reasoning` 级别。未来如果有一致性测试套件,可以通过后续 Agent Note 论证退役其中一个适配器。
diff --git a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml
new file mode 100644
index 0000000000..a29fa6e073
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.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
+2026-06-14-session-persistence.md: 75e13b860f621ed407849b3b4c62ff7287ab4812
+2026-06-14-session-persistence.zh.md: a6bd400a053779c742940236737447d1687622de
diff --git a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md
index 4e105ef598..75e13b860f 100644
--- a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md
+++ b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md
@@ -2,9 +2,11 @@
Status: implemented
+English | [中文](2026-06-14-session-persistence.zh.md)
+
## Problem
-Sessions lived only in memory. The example `session-jsonl.ts` plugin (duplicated byte-for-byte in both examples) was write-only telemetry: it buffered `session/event` and appended JSON lines, with no read/replay path, no crash-safety (no fsync, no atomic write, a fire-and-forget dispose drain), no listing, and no format versioning. Nothing could rehydrate a past session from disk into a live agent, so durable resume ("continue yesterday's task"), durable forking, and the ACP `session/load` method ([ACP support](../feature/2026-06-14-acp-agent-client-protocol.md)) were all impossible.
+Sessions lived only in memory. The example `session-jsonl.ts` plugin (duplicated byte-for-byte in both examples) was write-only telemetry: it buffered `session/event` and appended JSON lines, with no read/replay path, no crash-safety (no fsync, no atomic write, a fire-and-forget dispose drain), no listing, and no format versioning. Nothing could rehydrate a past session from disk into a live agent, so durable resume, durable forking, and host-side session browsing were all impossible.
The [event-sourced model](2026-06-11-event-sourced-sessions.md) makes the append-only log the single source of truth and derives LLM history from it. Persistence had to stay faithful to that: persist the existing `SessionEvent` directly, with no parallel "persisted message" type that the log is converted to and from. The backend also had to be swappable — a file store now, a database store later — behind one interface.
@@ -13,22 +15,22 @@ The [event-sourced model](2026-06-11-event-sourced-sessions.md) makes the append
Persistence is an abstract **capability seam** ([capability seams](2026-06-13-capability-seams.md), the `dsh-bash` template), not loop or core logic:
1. **Interface** (`dsh-session-persistence`, `ctx.sessionPersistence`) — an abstract `SessionPersistence` service: `create`/`append`/`load`/`list`. Its persisted unit IS the existing `SessionEvent` (`{ type, seq, time, data }`), reused verbatim — no conversion type.
-2. **Implementation** (`dsh-session-persistence-jsonl`) — an append-only logical JSONL log per session (a `SessionHeader` line then one `SessionEvent` per line, verbatim **including `assistant/chunk`**), encoded as [checksummed Zstandard frames by default](2026-07-19-zstandard-jsonl-session-logs.md) or raw lines by configuration.
+2. **Implementation** (`dsh-session-persistence-jsonl`) — an append-only logical JSONL log per session: a `SessionHeader` line followed by storage records that losslessly represent the contiguous `SessionEvent` stream. Eligible `assistant/chunk` delta runs use packed rows by default; [checksummed Zstandard frames](2026-07-19-zstandard-jsonl-session-logs.md) are the default physical encoding, with raw lines configurable.
Key choices recorded here because they are durable, contested, and surprising:
-- **The canonical durable log persists every `SessionEvent` verbatim, including `assistant/chunk`.** `deriveMessages()` skips chunks, and a chunk-filtered rollout (Codex's `policy.rs`) is tempting — but `seq = log.length` and the load-validation `events[i].seq === i` require a *contiguous* log; filtering chunks out would leave holes and break both the contract and resume. A chunk-filtered projection is possible later as a derived view with its own renumbering, but it is NOT the canonical log.
+- **The canonical durable log persists every `SessionEvent` losslessly, including `assistant/chunk`.** JSONL storage may encode a consecutive delta run as one packed row, but `load` reconstructs the exact event boundaries, sequence numbers, and timestamps. `deriveMessages()` skips chunks, and a chunk-filtered rollout (Codex's `policy.rs`) is tempting — but `seq = log.length` and the load-validation `events[i].seq === i` require a *contiguous* logical log; filtering chunks out would leave holes and break both the contract and resume. A chunk-filtered projection is possible later as a derived view with its own renumbering, but it is NOT the canonical log.
- **Append-only; a crashed turn is closed, never truncated.** Flushed events are never rewritten. The [semantic checkpoint policy](../bug-fix/2026-07-21-semantic-session-checkpoints.md) drains the request before model dispatch, a recorded top-level call before tool dispatch, and the complete response/result batch after a step; the loop drains the final turn boundary. Because one interrupted turn may contain substantial valid work, `load` preserves its contiguous, parseable events and appends risk-classified error results for unanswered assistant calls, a missing `step/end`, and `turn/end` with `{ kind: 'interrupted' }`. The synthetic results keep resumed provider transcripts valid. Only an incomplete final record is discarded; a parse error or sequence gap at or before the last real `turn/end` is corruption and makes the session unloadable.
-- **File backend canonical, DB backend a proven drop-in.** `SessionEvent` maps 1:1 onto a row `(session_id, seq, type, time, data)` — `append` is INSERT (in a transaction asserting the contiguous-seq contract), `load` is SELECT … ORDER BY seq. `dsh-session-persistence-sqlite` is exactly this: a `SessionPersistence` subclass with no interface change (opencode runs this exact shape on SQLite/WAL), and it passes the same `runPersistenceContract` suite as the JSONL backend — so the contract holds both backends to identical semantics (lazy materialization, interrupted-turn close on load, contiguous-seq), expressed once over file bytes and once over rows.
-- **Metadata is out-of-log.** Format version, cwd, and lineage are storage concerns, not replayable conversation state, so they live in a `SessionHeader` owned by `dsh-session` and attached to a `Session` via a new readonly `session.header` — never in `SessionEventMap`, never reaching `deriveMessages()`. The alternative (a merge-extensible `session/meta` event as log line 0) was rejected: an in-log event would ride along with a seeded/forked session for free, but metadata is not replayable state, so the explicit out-of-log header seam is the cleaner cost. (The header was originally split into an immutable `SessionHeader` plus a mutable `SessionSummary` whose union was `SessionMeta`; the mutable summary was later removed as dead state — see [Drop the mutable session summary](../simplification/2026-06-19-drop-mutable-session-summary.md).)
+- **File backend canonical, DB backend a proven drop-in.** `SessionEvent` maps 1:1 onto a row `(session_id, seq, type, time, data)` — `append` is INSERT (in a transaction asserting the contiguous-seq contract), `load` is SELECT … ORDER BY seq. `dsh-session-persistence-sqlite` is exactly this: a `SessionPersistence` subclass with no interface change (opencode runs this exact shape on SQLite/WAL), and it passes the same `runPersistenceContract` suite as the JSONL backend — so the contract holds both backends to identical semantics (lazy materialization, interrupted-turn close on load, contiguous-seq), expressed once over file bytes and once over rows. Its database carries a dedicated application id and monotonic schema version. A pristine file creates all tables and stamps both header values in one transaction; an unversioned file with any user-defined schema object or application identity, a foreign current-version identity, and every non-current version reject before journal-mode mutation.
+- **Metadata is out-of-log.** Format version, cwd, and lineage are storage concerns, not replayable conversation state, so they live in a `SessionHeader` owned by `dsh-session` and attached to a `Session` via a new readonly `session.header` — never in `SessionEventMap`, never reaching `deriveMessages()`. `createdAt` is non-negative safe-integer Unix epoch milliseconds: live creation and persistence registration reject fractional values, JSONL validates the decoded header, and SQLite stores it in a strict `INTEGER` column. The alternative (a merge-extensible `session/meta` event as log line 0) was rejected: an in-log event would ride along with a seeded/forked session for free, but metadata is not replayable state, so the explicit out-of-log header seam is the cleaner cost. (The header was originally split into an immutable `SessionHeader` plus a mutable `SessionSummary` whose union was `SessionMeta`; the mutable summary was later removed as dead state — see [Drop the mutable session summary](../simplification/2026-06-19-drop-mutable-session-summary.md).)
- **`ctx.agents.create()` and `ctx.agents.resume()` are async factories; resume additionally crosses the persistence boundary.** `ctx.agents.resume({ resumeSessionId })` awaits `ctx.sessionPersistence.load`, recreates the live session with the loaded events (so `lastTurnNumber`/`deriveMessages` continue), and registers the fresh agent under the exact resumed id. The agent-loop does NOT hard-inject `sessionPersistence` (that would pend non-persistent demos forever); `resume` rejects with a clear error when it is absent.
## Alternatives considered
-Each key choice above records its rejected alternative where the choice is stated: a **chunk-filtered canonical log** (Codex's `policy.rs` shape) — breaks the contiguous-seq contract; **truncating a crashed turn** — silently destroys a long autonomous run's real work; an **in-log `session/meta` event as line 0** — metadata is not replayable state; **hard-injecting `sessionPersistence` into the loop** — would pend non-persistent demos forever.
+Each key choice above records its rejected alternative where the choice is stated: a **chunk-filtered canonical log** (Codex's `policy.rs` shape) — breaks the contiguous-seq contract; **truncating a crashed turn** — silently destroys a long autonomous run's real work; an **in-log `session/meta` event as line 0** — metadata is not replayable state; **finite fractional `createdAt` values** — have no producer and diverge from integer Unix-millisecond storage and query columns; **adopting a non-pristine unversioned SQLite file** — can overwrite unrelated objects or identity; **hard-injecting `sessionPersistence` into the loop** — would pend non-persistent demos forever.
Format versioning: the header carries a `version`; `load` rejects any non-current version (no migration — the pre-release session format is pinned at `SESSION_FORMAT_VERSION = 0` and absorbs shape churn, per the AGENTS.md pre-release stance). Stated honestly: append-only + flush is robust to partial trailing writes (tolerated on load) but not to fsync-less power loss mid-line; a DB/WAL backend is the stronger option later.
## Consequences
-Two new packages and the metadata seam in `dsh-session` (`session.header`, the `create(id?, options?)` signature). Bought: durable resume/fork, a read/replay path, crash tolerance, and the foundation the ACP `session/load` ([ACP support](../feature/2026-06-14-acp-agent-client-protocol.md)) needs — all over the existing event-sourced log, with the backend swappable behind one interface. The reusable `runPersistenceContract` suite holds every backend to the same append-only, contiguous-seq, lazy-materialization, and serializability semantics. Persisting the full log also settles event fidelity: `assistant/chunk` remains verbatim.
+Two new packages and the metadata seam in `dsh-session` (`session.header`, the `create(id?, options?)` signature). Bought: durable resume/fork, a read/replay path, crash tolerance, and host-side session access over the existing event-sourced log, with the backend swappable behind one interface. The reusable `runPersistenceContract` suite holds every backend to the same append-only, contiguous-seq, lazy-materialization, integer-metadata, and serializability semantics. Persisting the full logical log also settles event fidelity: every `assistant/chunk` survives exactly even when JSONL packs several into one storage row. SQLite initialization either commits its complete owned schema and header identity or leaves no partial schema to strand on the next open.
diff --git a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md
new file mode 100644
index 0000000000..a6bd400a05
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md
@@ -0,0 +1,36 @@
+# Agent Note: 会话持久化作为基于现有 `SessionEvent` 的抽象服务
+
+Status: implemented
+
+[English](2026-06-14-session-persistence.md) | 中文
+
+## 问题
+
+会话此前仅存在于内存中。示例插件 `session-jsonl.ts`(在两个示例中逐字节重复)是只写的遥测:它缓冲 `session/event` 并追加 JSON 行,没有读取/回放路径,没有崩溃安全性(无 fsync、无原子写入、fire-and-forget 的 dispose 排空),没有列表功能,也没有格式版本控制。没有任何机制能将磁盘上的历史会话重新注入到活跃的 agent(智能体)中,因此持久恢复、持久 fork 以及宿主侧的会话浏览都无法实现。
+
+[事件溯源模型](2026-06-11-event-sourced-sessions.md)将仅追加日志作为唯一真源,并从中派生 LLM(大语言模型)历史。持久化必须忠实于这一设计:直接持久化现有的 `SessionEvent`,不引入需要来回转换的并行「持久化消息」类型。后端也必须可替换——当前用文件存储,以后用数据库存储——统一在一个接口之后。
+
+## 决策
+
+持久化是一个抽象的**能力 seam**([能力 seam](2026-06-13-capability-seams.md),`dsh-bash` 模板),而非循环或核心逻辑:
+
+1. **接口**(`dsh-session-persistence`,`ctx.sessionPersistence`):一个抽象的 `SessionPersistence` 服务,提供 `create`/`append`/`load`/`list`。其持久化单元就是现有的 `SessionEvent`(`{ type, seq, time, data }`),原样复用,无转换类型。
+2. **实现**(`dsh-session-persistence-jsonl`):每个会话一个仅追加的逻辑 JSONL 日志:先是一行 `SessionHeader`,随后是无损表示连续 `SessionEvent` 流的存储记录。符合条件的 `assistant/chunk` 增量连续段默认使用打包行;[带校验和的 Zstandard 帧](2026-07-19-zstandard-jsonl-session-logs.md)是默认物理编码,也可通过配置使用原始行。
+
+以下关键选择记录于此,因为它们是持久性的、有争议的、且出人意料的:
+
+- **规范的持久日志无损保留每个 `SessionEvent`,包括 `assistant/chunk`。** JSONL 存储可以将一段连续的增量事件编码为一条打包行,但 `load` 会重建精确的事件边界、序号与时间戳。`deriveMessages()` 跳过分片,而过滤分片的方案(Codex 的 `policy.rs`)很有吸引力,但 `seq = log.length` 以及加载验证 `events[i].seq === i` 要求*连续*的逻辑日志;过滤掉分片会留下空洞,同时破坏契约和恢复功能。基于分片过滤的投影可以作为派生视图在后续实现(带有自己的重新编号),但它不是规范日志。
+- **仅追加;崩溃的轮次被关闭,而非截断。** 已刷写的事件永不被重写。[语义检查点策略](../bug-fix/2026-07-21-semantic-session-checkpoints.md)会在模型分发前排空请求、在工具分发前排空已记录的顶层调用,并在步骤结束后排空完整的响应/结果批次;循环则排空最终轮次边界。由于一个被中断的轮次可能包含大量有效工作,`load` 保留其连续、可解析的事件,并为未应答的 assistant 调用追加按风险分类的错误结果、补一个缺失的 `step/end`,以及带 `{ kind: 'interrupted' }` 的 `turn/end`。合成的结果保证恢复后的提供方 transcript(文本记录)仍然有效。只有不完整的最后一条记录会被丢弃;在最后一个真实 `turn/end` 处或之前出现解析错误或序号间隙,属于数据损坏,会使该会话不可加载。
+- **文件后端为规范实现,数据库后端为经过验证的直接替换。** `SessionEvent` 1:1 映射到一行 `(session_id, seq, type, time, data)`:`append` 是 INSERT(在一个断言连续 seq 契约的事务中),`load` 是 SELECT … ORDER BY seq。`dsh-session-persistence-sqlite` 正是如此:一个 `SessionPersistence` 子类,接口无变化(opencode 在 SQLite/WAL 上运行的正是这个形状),且通过与 JSONL 后端相同的 `runPersistenceContract` 测试套件。该契约以相同的语义约束两个后端(惰性物化、加载时关闭中断轮次、连续 seq),一次表达在文件字节上,一次表达在数据库行上。其数据库拥有专用的 application id 与单调递增的 schema 版本。系统会在一个事务中为全新文件创建所有表并写入这两个 header 值;未版本化文件若带有任何用户定义的 schema 对象或应用标识、当前版本文件若带有外部应用标识,以及任何非当前版本文件,都会在修改日志模式之前被拒绝。
+- **元数据在日志之外。** 格式版本、cwd 和谱系是存储关注点,不是可回放的对话状态,因此它们存放在 `dsh-session` 拥有的 `SessionHeader` 中,并通过新的只读属性 `session.header` 附加到 `Session` 上——永远不进入 `SessionEventMap`,永远不到达 `deriveMessages()`。`createdAt` 是以 Unix epoch 毫秒表示的非负安全整数:运行时创建和持久化注册会拒绝小数值,JSONL 会验证解码后的 header,SQLite 则将其存入严格的 `INTEGER` 列。替代方案(一个可合并扩展的 `session/meta` 事件作为日志第 0 行)被否决:日志内事件会随 seed/fork 的会话免费携带,但元数据不是可回放状态,因此显式的日志外 header seam 是更干净的代价。(header 最初被拆分为不可变的 `SessionHeader` 加可变的 `SessionSummary`,二者的联合类型为 `SessionMeta`;可变 summary 后来因属于死状态而被移除——见 [移除可变会话摘要](../simplification/2026-06-19-drop-mutable-session-summary.md)。)
+- **`ctx.agents.create()` 和 `ctx.agents.resume()` 是异步工厂;恢复还跨越持久化边界。** `ctx.agents.resume({ resumeSessionId })` 等待 `ctx.sessionPersistence.load`,用加载的事件重建活跃会话(使 `lastTurnNumber`/`deriveMessages` 得以延续),并以原样恢复的 id 注册新 agent。agent loop(智能体循环)不会硬注入 `sessionPersistence`(那样会让非持久化的演示永远挂起);当它不存在时,`resume` 会以明确的错误拒绝。
+
+## 曾考虑的替代方案
+
+上述每个关键选择都在陈述处记录了被否决的替代方案:**过滤分片的规范日志**(Codex 的 `policy.rs` 形式)破坏连续 seq 契约;**截断崩溃的轮次**会静默销毁长时间自主运行中的真实工作;**日志内 `session/meta` 事件作为第 0 行**——元数据不是可回放状态;**有限的非整数 `createdAt` 值**没有生产方,且与整数 Unix 毫秒存储及查询列不一致;**接受非全新的未版本化 SQLite 文件**可能覆盖无关对象或应用标识;**将 `sessionPersistence` 硬注入循环**会让非持久化的演示永远挂起。
+
+格式版本控制:header 携带一个 `version`;`load` 拒绝任何非当前版本(不做迁移——预发布阶段的会话格式固定为 `SESSION_FORMAT_VERSION = 0` 并吸收形状变动,遵循 AGENTS.md 的预发布立场)。坦率地说:仅追加 + 刷写对部分尾部写入是健壮的(加载时容忍),但对行写入中途的无 fsync 断电不健壮;数据库/WAL 后端是后续更强的选项。
+
+## 后果
+
+新增两个包(package),以及 `dsh-session` 中的元数据 seam(`session.header`,`create(id?, options?)` 签名)。收益:持久恢复/fork、读取/回放路径、崩溃容忍,以及基于现有事件溯源日志的宿主侧会话访问,后端在一个接口之后可替换。可复用的 `runPersistenceContract` 测试套件以相同的仅追加、连续 seq、惰性物化、整数元数据与可序列化语义约束每个后端。持久化完整的逻辑日志还确定了事件保真度:即使 JSONL 将多个 `assistant/chunk` 打包到一条存储行中,每个事件也会精确保留。SQLite 初始化要么提交完整的自有 schema 与 header 标识,要么不留下任何会使下次打开受阻的部分 schema。
diff --git a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml
new file mode 100644
index 0000000000..530e207654
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.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
+2026-06-17-filesystem-capability-seam.md: fee0161e5e8397ac1d1c0e2850efad840c65d971
+2026-06-17-filesystem-capability-seam.zh.md: ee50b36d25315c3d8daed4502bc248977f9e6011
diff --git a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md
index 7fa2bde08c..fee0161e5e 100644
--- a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md
+++ b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-17-filesystem-capability-seam.zh.md)
+
## Problem
The harness has a concrete `bash` capability seam (`dsh-bash` / `dsh-bash-local` / `dsh-tool-bash`), but filesystem operations are about to be added as model-facing tools without an equivalent seam. If `read`, `write`, and `edit` directly use `node:fs`, the model-facing tool package will own filesystem execution policy, local path resolution, atomic write behavior, text decoding, symlink behavior, and edit semantics all at once.
@@ -30,7 +32,7 @@ The read-before-write/edit and observed-state policy is a fourth package, `@deep
The first backend is deliberately local-only: `dsh-fs-local` implements `ctx.fs` against the host filesystem. Future sibling backends can provide sandboxed, remote, virtual, or project-scoped filesystems behind the same interface.
-The first consumer is deliberately text-file-only: `dsh-tool-fs` exposes model-facing `read`, `write`, and `edit` tools for UTF-8 text files. Future consumers can add directory listing, search/glob, binary-safe operations, file watching, or higher-level project operations without changing the local backend package, as long as the needed capability exists on `ctx.fs`. Direct directory listing was later added by [Add direct directory listing to the filesystem seam](2026-07-03-filesystem-directory-listing-seam.md).
+The first consumer is deliberately text-file-only: `dsh-tool-fs` exposes model-facing `read`, `write`, and `edit` tools for UTF-8 text files. Future consumers can add directory listing, search/glob, binary-safe operations, file watching, or higher-level project operations without changing the local backend package, as long as the needed capability exists on `ctx.fs`. Direct directory listing was later added by [Add direct directory listing to the filesystem seam](../../archived/architecture/2026-07-03-filesystem-directory-listing-seam.md).
Filesystem permissions and sandboxing are not implied by this split. The local backend resolves relative paths from its configured base directory, but containment policy is a separate decision: either a stricter `ctx.fs` implementation enforces it, or a permission/sandbox plugin wraps `tools/execute` and vetoes calls before they reach the consumer.
@@ -93,7 +95,7 @@ Literal edit is a provider primitive (`editText`), not composed in `tool-fs` fro
The policy plugin, not `ctx.fs`, gates on prior observation: an `edit` requires a prior observation by the owner (else `FS_NOT_OBSERVED`), and the recorded version is passed to `editText` as the CAS basis. With the policy plugin absent, `ctx.fs` alone is a complete unconstrained seam (unconditional write/edit); the tool is never method-coupled to the policy.
-Filesystem contract failures are thrown as `FsError extends HarnessError`, and the tool registry converts them into `isError` tool results with structured `{ name, code }` metadata. `dsh-fs` owns this vocabulary rather than each tool inventing messages. The codes are `FS_NOT_FOUND`, `FS_NOT_TEXT`, `FS_STALE_VERSION`, `FS_NOT_OBSERVED`, `FS_NOT_REGULAR_FILE`, `FS_AMBIGUOUS_EDIT`, `FS_EDIT_NOT_FOUND`, and `FS_ABORTED`. (An earlier draft included `FS_PARTIAL_OBSERVATION`; freshness-based authorization has no partial/full distinction, so it was dropped. Directory-listing-specific codes were added later by [Add direct directory listing to the filesystem seam](2026-07-03-filesystem-directory-listing-seam.md).)
+Filesystem contract failures are thrown as `FsError extends HarnessError`, and the tool registry converts them into `isError` tool results with structured `{ name, code }` metadata. `dsh-fs` owns this vocabulary rather than each tool inventing messages. The codes are `FS_NOT_FOUND`, `FS_NOT_TEXT`, `FS_STALE_VERSION`, `FS_NOT_OBSERVED`, `FS_NOT_REGULAR_FILE`, `FS_AMBIGUOUS_EDIT`, `FS_EDIT_NOT_FOUND`, and `FS_ABORTED`. (An earlier draft included `FS_PARTIAL_OBSERVATION`; freshness-based authorization has no partial/full distinction, so it was dropped. Directory-listing-specific codes were added later by [Add direct directory listing to the filesystem seam](../../archived/architecture/2026-07-03-filesystem-directory-listing-seam.md).)
## Tool consumer behavior
diff --git a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md
new file mode 100644
index 0000000000..ee50b36d25
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md
@@ -0,0 +1,160 @@
+# Agent Note: 文件系统能力 seam——ctx.fs、本地后端与面向模型的文件系统工具
+
+Status: implemented
+
+[English](2026-06-17-filesystem-capability-seam.md) | 中文
+
+## 问题
+
+harness 已有一个具体的 `bash` 能力 seam(`dsh-bash` / `dsh-bash-local` / `dsh-tool-bash`),但文件系统操作即将作为面向模型的工具加入,却没有等价的 seam。如果 `read`、`write` 和 `edit` 直接使用 `node:fs`,面向模型的工具包将同时承担文件系统执行策略、本地路径解析、原子写入行为、文本解码、符号链接行为和编辑语义。
+
+这把三个独立变化的关注点耦合在了一起:
+
+1. 文件系统契约:插件可以请求哪些操作。
+2. 后端:当前是本地磁盘,未来可能是沙箱/远程/项目作用域的文件系统。
+3. 消费方接口:面向模型的 `read` / `write` / `edit` schema 与结果格式化。
+
+如果没有 `ctx.fs` 接口,将本地文件系统访问替换为沙箱或远程后端时,即使面向模型的契约应当保持稳定,工具 schema、演示和提示词引导也会被迫变动。这还使权限/沙箱边界更难推理:一个 `cwd` 选项看起来像沙箱,但除非有显式的后端或 `tools/execute` 策略强制隔离,否则它只是一个基础路径。
+
+我们需要文件系统工具在成为公开包(package)接口之前,以与 bash 相同的能力 seam 形态落地。
+
+## 决策
+
+文件系统访问是一个一等的能力 seam,遵循[能力 seam Agent Note](2026-06-13-capability-seams.md):
+
+1. `@deepseek-ai/dsh-fs`(`packages/fs/fs`)拥有抽象的 `ctx.fs` 服务、文件系统词汇类型,以及 `fs/*` 策略事件词汇。
+2. `@deepseek-ai/dsh-fs-local`(`packages/fs/fs-local`)提供第一个实现,以本地文件系统为后端。
+3. `@deepseek-ai/dsh-tool-fs`(`packages/fs/tool-fs`)通过 `ctx.fs` 提供面向模型的 `read`、`write` 和 `edit` 工具,是分发 `fs/*` 事件的执行器。
+
+消费方包仅依赖接口包,从不依赖 `dsh-fs-local`。需要不同后端的部署只需为 `ctx.fs` 加载不同的提供方,无需改动工具 schema 或面向模型的提示词引导。
+
+读后写/编辑与观测状态策略是第四个包 `@deepseek-ai/dsh-fs-policy`(`packages/fs/fs-policy`),通过 `fs/*` 事件门控贡献,而非挂在 `ctx.fs` 上;加载 `dsh-tool-fs` 的部署同时加载 `dsh-fs-policy` 以获得读后写/编辑能力。本 Agent Note 确立了由三个包构成的 seam;策略从提供方基类拆出的决策由 [拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) 做出,其以事件门控插件(而非方法服务)实现的方式由 [事件门控 Agent Note](2026-06-26-file-context-as-event-gate.md) 做出。本文已更新为描述最终落地的四包形态。
+
+第一个后端有意仅限本地:`dsh-fs-local` 基于宿主文件系统实现 `ctx.fs`。未来的兄弟后端可在同一接口之后提供沙箱、远程、虚拟或项目作用域的文件系统。
+
+第一个消费方有意仅限文本文件:`dsh-tool-fs` 暴露面向模型的 `read`、`write` 和 `edit` 工具,处理 UTF-8 文本文件。未来的消费方可以添加目录列表、搜索/glob、二进制安全操作、文件监视或更高层的项目操作,只要 `ctx.fs` 上存在所需能力,就无需改动本地后端包。直接目录列表后来由[为文件系统 seam 添加直接目录列举能力](../../archived/architecture/2026-07-03-filesystem-directory-listing-seam.md)添加。
+
+文件系统权限和沙箱并非此拆分所隐含。本地后端从其配置的基目录解析相对路径,但隔离策略是独立的决策:要么由更严格的 `ctx.fs` 实现强制执行,要么由权限/沙箱插件包装 `tools/execute` 并在调用到达消费方之前否决。
+
+读后写/编辑与观测状态属于 `dsh-fs-policy`,而非 `ctx.fs`。通过 `fs/*` 事件门控,策略按不透明 actor 记录版本,并提供可选的变更期望;提供方原子性地强制新鲜度。`dsh-tool-fs` 发出事件但不依赖策略。见[拆分文件系统 seam](../simplification/2026-06-26-fsspec-style-fs-seam.md)和[事件门控插件](2026-06-26-file-context-as-event-gate.md) Agent Note。
+
+## 包拓扑
+
+文件系统 seam 使用与 bash 三件套相同的依赖方向:
+
+```text
+@deepseek-ai/dsh-tool-fs --depends on--> @deepseek-ai/dsh-fs <--depends on-- @deepseek-ai/dsh-fs-local
+ consumer interface implementation
+```
+
+`@deepseek-ai/dsh-fs` 仅依赖 `cordis` 加上来自 `@deepseek-ai/dsh-llm` 的仓库级 `HarnessError` 基类。它声明 `ctx.fs` 键、抽象 `FileSystem` 服务、后端和消费方共享的词汇类型、文件系统错误词汇,以及 `fs/*` 策略事件词汇。它不持有观测状态存储,也不持有 owner 推导形态;事件传递一个不透明的 `object` actor,提供方从不读取它,`dsh-fs-policy` 插件在这些事件之上拥有 owner 推导形态和观测状态存储。
+
+`@deepseek-ai/dsh-fs-local` 依赖 `@deepseek-ai/dsh-fs` 和 `cordis`。它继承 `FileSystem`,将自身注册为 `ctx.fs`,拥有本地后端配置(如基目录),并包含所有直接的 `node:fs` / `node:path` 访问。它不持有观测状态存储——新鲜度是后端铸造、策略插件记录的版本令牌。
+
+`@deepseek-ai/dsh-tool-fs` 依赖 `@deepseek-ai/dsh-fs`、`@deepseek-ai/dsh-tools`、`@deepseek-ai/dsh-system-prompt` 和 `cordis`。它注册面向模型的工具和提示词段落。它禁止导入 `node:fs`、`node:path` 或 `@deepseek-ai/dsh-fs-local`;文件系统执行始终通过 `ctx.fs`。如果实现需要具体的 agent 或会话辅助类型,这些依赖属于 `tool-fs`;它们禁止回漏到 `dsh-fs` 中。
+
+根 `tool-fs` 插件通过组合各工具的注册辅助函数来注册完整的文件系统工具套件(`read`、`write` 和 `edit`)。它注入 `fs`,从不导入实现包。
+
+## `ctx.fs` 契约
+
+`@deepseek-ai/dsh-fs` 拥有一个语义文件系统服务。它比 `readFile` / `writeFile` 更高层,这样 `tool-fs` 就不必重新实现路径解析、版本管理、文本解码、二进制拒绝、分页、原子替换、符号链接行为或字面编辑语义。
+
+该接口涵盖以下语义操作:
+
+- 将模型/插件提供的路径解析为后端定义的目标。
+- 获取目标元数据而不读取文件内容。
+- 从目标读取有界的 UTF-8 文本页。
+- 创建或替换一个 UTF-8 文本文件。
+- 通过字面替换编辑一个已有的 UTF-8 文本文件。
+
+提供方 seam 还携带策略所依赖的新鲜度钩子——但观测状态存储和 owner 推导位于 `dsh-fs-policy` 插件中,而非 `ctx.fs` 上:
+
+- 后端为每个目标铸造一个不透明的 `version` 令牌(在 `stat` 以及每次读取/变更结果中)。
+- `writeText`/`editText` 接受一个可选的版本期望:省略它表示无条件的裸提供方变更;提供它则在后端的原子临界区内守护变更。
+- `dsh-fs-policy` 插件在 `fs/write-intent`/`fs/edit-intent` 上决定该期望,并在 `fs/observed` 上记录观测版本,以它从不透明事件 actor 推导出的 owner 为键(通常是 `exec.agent.session`)。
+
+授权基于版本新鲜度,而非完整/部分视图的区分:任何读取都会记录目标的版本,后续的写入/编辑只要文件仍处于该版本就被授权——因此对第 100-150 行的窗口化读取可以授权对第 120 行的编辑。观测状态存储是 `dsh-fs-policy` 内部的 `WeakMap>`;`dsh-fs` 不持有任何此类数据,并将 actor 视为不透明。(本 Agent Note 最初建模了一个带 `full`/`partial` 视图的 `FileState` 缓存放在 `ctx.fs` 上;拆分文件系统 seam 与事件门控两份 Agent Note 将其替换为此处描述的基于新鲜度的策略插件。)
+
+路径解析是显式的,允许异步。本地解析可能只做路径规范化,但沙箱/远程/项目作用域的后端可能需要 I/O 才能将用户提供的路径解析为稳定的目标标识。
+
+解析后的目标必须至少暴露三个概念:
+
+- 原始输入路径,用于诊断。
+- 不透明的 `targetKey`,用于陈旧守护和文件状态查找。本地后端可能使用类似 realpath 的键;远程后端可能使用工作区 URI 或文件 id。消费方禁止解析或假设它是本地绝对路径。
+- `displayPath`,用于面向模型/UI 的输出。根据后端不同,它可能是本地绝对路径、工作区相对路径或远程 URI。
+
+读取和变更结果必须包含不透明的文件 `version`。本地后端从 bigint stat 元数据(`dev`、`ino`、`size`、`mtimeNs` 和 `ctimeNs`)派生令牌,因此同大小重写和 inode 替换都会可靠地使消费方失效;远程后端可以使用 revision id 或类似 hash 的令牌。`dsh-fs-policy` 插件记录版本用于陈旧检查;消费方可以展示相关元数据但禁止解释版本令牌。
+
+提供方返回已解码的文本:`readText` 返回整个常规文本文件,`streamText` 为大文件流式传输相同的文本语义。两者负责常规文件检查;有界的行/输出处理不是它们的职责——行窗口化、带行号渲染和总行数统计位于执行器(`dsh-tool-fs`)中,执行器通过 `ctx.fs` 读取并渲染面向模型的窗口。提供方负责 UTF-8 解码和二进制/NUL 拒绝;它不知道行窗口或视图。
+
+观测状态记录不在 `ctx.fs` 上:成功读取后,执行器发出 `fs/observed`,`dsh-fs-policy` 插件为推导出的 owner 记录 `{ version }`。没有 `full`/`partial` 视图——任何窗口的读取都记录版本,新鲜度(而非视图完整性)授权后续的写入/编辑。
+
+全文件写入创建或替换 UTF-8 文本文件。后端在支持且有文档说明时可以创建父目录。已有的非常规目标被拒绝。`writeText` 接受一个可选期望:`createIfAbsent` 创建缺失的目标并拒绝已存在的(报 `FS_NOT_OBSERVED`,这是策略为未观测 owner 使用的路径);`replaceIfVersion` 仅在目标处于观测版本时替换,否则报 `FS_STALE_VERSION`;省略期望则为无条件的裸提供方创建或覆盖。策略插件根据 owner 的观测状态选择提供哪个期望。
+
+字面编辑是提供方原语(`editText`),而非在 `tool-fs` 中由读取加写入组合而成。字面匹配、重复匹配拒绝、CRLF 保留、二进制拒绝、可选的陈旧版本检查和原子读-改-写必须一起留在后端的变更临界区内。`editText` 接受相同的可选版本期望;陈旧检查在字面匹配之前运行,因此基于旧读取的编辑会报 `FS_STALE_VERSION`。远程后端可以将编辑实现为原生的 compare-and-edit 操作;消费方不强制本地风格的组合。
+
+策略插件(而非 `ctx.fs`)对先前观测进行门控:`edit` 要求 owner 有先前观测(否则报 `FS_NOT_OBSERVED`),记录的版本作为 CAS 基础传给 `editText`。在策略插件缺席时,`ctx.fs` 本身是一个完整的无约束 seam(无条件写入/编辑);工具从不与策略方法耦合。
+
+文件系统契约失败以 `FsError extends HarnessError` 抛出,工具注册表将其转换为带结构化 `{ name, code }` 元数据的 `isError` 工具结果。`dsh-fs` 拥有此词汇,而非由每个工具各自发明消息。错误码包括 `FS_NOT_FOUND`、`FS_NOT_TEXT`、`FS_STALE_VERSION`、`FS_NOT_OBSERVED`、`FS_NOT_REGULAR_FILE`、`FS_AMBIGUOUS_EDIT`、`FS_EDIT_NOT_FOUND` 和 `FS_ABORTED`。(早期草案包含 `FS_PARTIAL_OBSERVATION`;基于新鲜度的授权没有 partial/full 区分,因此已删除。目录列表相关的错误码后来由[为文件系统 seam 添加直接目录列举能力](../../archived/architecture/2026-07-03-filesystem-directory-listing-seam.md)添加。)
+
+## 工具消费方行为
+
+`@deepseek-ai/dsh-tool-fs` 是面向模型的消费方。它拥有工具名称、JSON Schema、模型边界的参数校验、提示词段落和结果格式化。它不拥有文件系统执行。
+
+第一个工具套件包含:
+
+- `read`:检查一个 UTF-8 文本文件并返回带行号的内容与分页引导。
+- `write`:创建或完全替换一个 UTF-8 文本文件。
+- `edit`:通过替换字面文本更新一个已有的 UTF-8 文本文件,默认要求唯一匹配,并允许显式的全部替换模式。
+
+每个工具遵循相同的执行形态:
+
+1. 校验并规范化模型参数。
+2. 调用相应的 `ctx.fs` 操作。
+3. 将结果格式化为面向模型的 `ContentBlock[]`。
+4. 让抛出的后端/工具错误流经 `ToolRegistry.execute()`,由其转换为 `isError` 工具结果。
+
+该包通过 `ctx.systemPrompt.section(...)` 注册提示词引导,通过 `ctx.tools.register(...)` 注册 schema。工具 schema 仍通过 `SystemPrompt.assemble()` 和 `ToolRegistry.schemas()` 流入正常的提示词组装路径;无需改动 agent loop(智能体循环)。
+
+工具包在后端变化时保持面向模型的契约稳定:本地后端和远程后端内部可能以不同方式解析路径,但 `read` / `write` / `edit` schema 不会仅因后端变化而改变。
+
+默认部署要求在用 `write` 或 `edit` 更新已有文件之前先 `read`。`tool-fs` 不通过检查是否运行过名为 `read` 的工具来实现这一点:它分发 `fs/write-intent`/`fs/edit-intent` 事件(将执行上下文作为不透明 actor 传递),`dsh-fs-policy` 插件推导 owner、对先前观测进行门控并提供版本期望。任何窗口化读取都能授权后续的写入/编辑,只要文件未变。用 `write` 创建新文件不要求先前观测。
+
+根插件通过组合各工具的注册辅助函数来注册完整套件。它注入 `fs`、`tools` 和 `systemPrompt`。
+
+## 测试
+
+测试遵循包边界,而不仅是用户可见的工具:`dsh-fs` 中的服务 seam;`dsh-fs-local` 中通过 `ctx.fs` 接口测试的真实文件系统行为(解析、符号链接、流式传输、二进制/UTF-8 拒绝、无条件和版本守护的写入、字面编辑语义、行尾保留、结构化 `FsError` 错误码);`dsh-tool-fs` 中基于真实本地提供方的消费方接口(只 mock 模型/时钟,从不 mock 协作者);以及通过 `ctx.tools.execute()` 在有和没有 `dsh-fs-policy` 的情况下进行集成测试,通过从磁盘回读文件来验证世界状态,既不信任规范值,也不信任渲染内容。观测状态/owner 推导策略在 `dsh-fs-policy` 中测试,不在此处。
+
+本仓库曾踩过的防御性模式类别被直接固定:
+
+- **原子写入临时文件安全。** 写入/编辑通过目标旁边一个私有随机 `0700` 目录中的独占 owner-only(`'wx'`、`0o600`)临时文件暂存,失败时清理,最后原子 rename——与 bash 溢出文件规则一致,因为可预测的 world-readable 临时路径招致符号链接竞争和信息泄露。测试断言权限,并断言已存在的临时路径不会被覆盖;此原语是 seam 的常设要求。
+- **通过符号链接的 `targetKey` 同一性。** 两个输入路径解析到同一 realpath 时共享一个观测状态条目:通过路径 A 的 `read` 满足通过符号链接路径 B 的 `edit` 的读后编辑守护,通过一个路径的陈旧写入可通过另一个路径检测到。
+- **并发/陈旧竞争。** 对同一目标的两个并发写入/编辑操作确定性地收敛——一个成功,另一个被 `FS_STALE_VERSION` 拒绝——成功的编辑刷新记录状态,使同一 owner 的下一次编辑可以继续。
+- **HMR(热模块替换)安全与 dispose(资源释放)。** dispose 后端的 fiber 会撤回 `ctx.fs` 提供方;后续的提供方以无继承状态启动。
+
+## 曾考虑的替代方案
+
+- **面向模型的工具直接基于 `node:fs`**:工具包将同时承担执行策略、路径解析、原子写入、文本解码和编辑语义,耦合问题部分所列的三个独立变化的关注点,且任何后端替换都会搅动 schema。
+- **单一合并包 `dsh-fs-tools`**:seam 之前的形态;以与 bash 相同的接口/实现/消费方拆分理由否决,且合并名称从未成为公开接口。
+- **观测状态放在 `ctx.fs` 上**:本 Agent Note 最初落地的形态;被 [拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) 和 [事件门控 Agent Note](2026-06-26-file-context-as-event-gate.md) 取代:沙箱/远程后端不应继承面向模型的观测策略,因此提供方只保留版本令牌和可选的版本守护变更。
+
+## 后果
+
+**`cwd` 可能被误认为沙箱。** 本地后端的基目录是解析默认值,而非自动的隔离边界。如果需要隔离,必须由后端契约或 `tools/execute` 上的权限/沙箱插件强制执行。
+
+**接口可能变得过于本地化。** 如果 `ctx.fs` 返回 `absolutePath` 之类的字段,远程、沙箱或虚拟后端会变得尴尬。契约应暴露显示元数据,而不要求消费方理解宿主路径。
+
+**接口可能变得过于薄。** 如果 `ctx.fs` 只镜像 `node:fs` 原语,`tool-fs` 将重新实现二进制检测、分页、原子写入和编辑语义,重新制造本 Agent Note 试图避免的耦合。
+
+**编辑语义天然易受竞争影响。** 字面编辑是读-改-写操作;守护手段是后端的原子变更临界区加上可选的版本期望,因此并发编辑确定性地收敛——一个赢,另一个得到 `FS_STALE_VERSION`。
+
+**观测状态不属于 `ctx.fs`。** 记录执行上下文看到了什么是工作流策略,而非原始文件系统 I/O。本 Agent Note 最初将其放在文件系统 seam 内部;拆分文件系统 seam Agent Note 随后确立了沙箱/远程后端不应继承面向模型的观测策略,并将其移入 `dsh-fs-policy` 插件。提供方 seam 只保留写入/编辑安全在存储层真正需要的东西——后端铸造的版本令牌和可选的版本守护变更——而策略插件拥有 owner 推导、观测状态和基于 `fs/*` 事件的读后编辑门控。
+
+**`resolve` 然后操作的形态每次调用多一次往返。** 每个工具可能先将路径解析为 `FsTarget`,再以单独的 `ctx.fs` 调用发起读取/写入/编辑。对本地后端来说这可以忽略(解析是内存中的路径规范化),但远程/沙箱后端可能将每步变成独立请求,使单次 `read` 变为两次网络往返。往返开销重要的后端可以在内部缓存或折叠解析,同时保持可观测契约不变。
+
+**观测状态持久化被推迟。** 观测状态存在于内存中(`dsh-fs-policy` 内部的 `WeakMap`),因此恢复的会话保守地要求文件在写入/编辑前重新读取,直到未来的会话事件或持久化机制使观测可回放。
+
+**错误码成为 seam 的一部分。** `FsError` 错误码使陈旧版本和观测失败可通过既有的结构化错误分类体系进行机器路由。代价是 `dsh-fs` 从 `dsh-llm` 导入共享的 `HarnessError` 基类;该依赖是有意为之且限于错误词汇。
+
+**包拆分的成本前置。** 三包拆分在只有一个后端时就增加了样板代码。这是有意为之:文件系统访问是可能的沙箱/远程边界,在面向模型的工具发布后再改包接口代价更高。
diff --git a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.i18n.yaml
new file mode 100644
index 0000000000..3b07958faa
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.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
+2026-06-18-agent-lifecycle-and-ownership-seams.md: f190b4ba2b7f22d29f473c8a2725401ff371488e
+2026-06-18-agent-lifecycle-and-ownership-seams.zh.md: dcaa319232baa8951a4f515abc6bce5611da5576
diff --git a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md
index 3d84c8074b..f190b4ba2b 100644
--- a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md
+++ b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-18-agent-lifecycle-and-ownership-seams.zh.md)
+
## Problem
Several ACP and tool-bash limitations were symptoms of the same missing seam: plugins could create or resume agents through `ctx.agents`, but they could not own and dispose one agent independently, and long-running bash tasks carried no stable owner in the executor itself. ACP aborted and awaited agents on disconnect but could not unregister just that session's agent; `session/cancel` could not cancel queued-but-not-yet-started work; and `tool-bash` kept task ownership in a plugin-local `Map`, so an HMR reload could make an old task look unowned.
@@ -16,7 +18,7 @@ A new `cancel()` verb on the `Agent` interface — the single public stop primit
### 2. `AgentHandle` async disposer
-`ctx.agents.create`/`resume` (and the `AgentFactory` interface) return `AgentHandle = { agent: Agent; dispose(): Promise }`. The disposer is a **consumer capability** — a registry observer holding only the bare `Agent` cannot tear it down. The caller fiber and registered factory provider are structural co-owners: caller unload enforces structured ownership, while provider unload must stop old instances whose scoped dependency surface resolves through that provider. All three paths reach the same memoized teardown: stop the loop, await its exit and idle flushes (true quiescence, not just the `disposed` status flip), detach the agent, detach its session, and unwind its scope. Each public ID becomes reusable when its exact registry entry detaches; there is no separate reservation-release phase. Config-created agents are already owned by the `AgentLoop` fiber (the handle is discarded). ACP holds each session's disposer in its `SessionRecord` and runs it on disconnect/teardown, so a bare client disconnect leaves no registered agent and no session-store entry — even when `session/load` races teardown (the just-resumed handle is disposed before the closed-guard throw).
+`ctx.agents.create`/`resume` (and the `AgentFactory` interface) return `AgentHandle = { agent: Agent; dispose(): Promise }`. The disposer is a **consumer capability** — a registry observer holding only the bare `Agent` cannot tear it down. The caller fiber and registered factory provider are structural co-owners: caller unload enforces structured ownership, while provider unload must stop old instances whose scoped dependency surface resolves through that provider. All three paths reach the same memoized teardown: stop the loop, await its exit and idle flushes (true quiescence, not just the `disposed` status flip), detach the agent, detach its session, and unwind its scope. Each public ID becomes reusable when its exact registry entry detaches; there is no separate reservation-release phase. Config-created agents are already owned by the `AgentLoop` fiber (the handle is discarded). ACP holds each fresh session's disposer in its `SessionRecord` and runs it on disconnect or plugin teardown, so a bare client disconnect leaves no registered agent and no session-store entry. A create that loses the close race disposes its unpublished handle.
**Teardown ORDER is load-bearing for durability**, and the implementation folds the session lifecycle into the agent's SINGLE composite cordis effect (`SessionStore.prepare`/`enter`/`announce`, replacing a sibling-effect split). A fiber unload disposes sibling effects concurrently (`Promise.all`), which would race removing the session store's append publication hooks against the loop's closing `session/flush` and drop the closing `turn/end`; inside one effect the disposers run as an ordered LIFO chain (loop stopped + `await agent.done` BEFORE the session detaches), so the loop's final flush is captured on BOTH the handle's `dispose()` and a fiber unload. The contained `agent/disposed` and `session/disposed` notifications cannot reject the chain or skip later teardown.
@@ -28,7 +30,7 @@ Background-task ownership moved from a `tool-bash` plugin-local `Map }`。释放器是一种**消费方能力**——仅持有裸 `Agent` 的注册表观察者无法将其拆除。调用方 fiber 和已注册的 factory 提供方是结构上的共同所有者:调用方卸载强制结构化所有权,而提供方卸载必须停止旧实例,因为其实例作用域的依赖 surface 通过该提供方解析。三条路径都会进入同一个 memoize 的拆除过程:停止循环、等待其退出与空闲刷写完成(完全停稳,而非仅把状态翻转为 `disposed`)、分离 agent、分离其会话,然后解除其 scope。每个公开 ID 在其精确注册表条目分离时变得可复用;不存在独立的保留释放阶段。由配置创建的 agent 已归 `AgentLoop` fiber 所有(handle 被丢弃)。ACP 在其 `SessionRecord` 中保存每个全新会话的释放器,并在断连或插件拆除时运行它,因此单纯的客户端断连不会留下已注册 agent 或会话存储条目。在与关闭的竞态中落败的创建流程会 dispose 其尚未发布的 handle。
+
+**拆除顺序对持久性至关重要**,实现将会话生命周期折叠进 agent 的单个复合 Cordis effect(`SessionStore.prepare`/`enter`/`announce`,取代兄弟 effect 拆分)。fiber 卸载会并发释放兄弟 effect(`Promise.all`),这会让会话存储的 append 发布钩子移除与循环关闭时的 `session/flush` 竞争,从而丢失关闭的 `turn/end`;在一个 effect 内,释放器作为有序的 LIFO 链运行(停止循环 + `await agent.done` 在会话分离之前),因此无论 handle 的 `dispose()` 还是 fiber 卸载,都会捕获循环的最终刷写。被隔离的 `agent/disposed` 和 `session/disposed` 通知无法拒绝该链或跳过后续拆除。
+
+### 3. Bash seam 中的所有者令牌
+
+后台任务所有权从 `tool-bash` 插件本地的 `Map` 移入执行器。`BashExecRequest` 新增可选的 `owner?: string`;解析后的 `BashExecSpec` 将其作为必需但可空的 `owner: string | undefined` 携带(被遗忘的 owner 是可见的 `undefined`,而非静默缺失的属性)。执行器把 token 存在任务上,并通过新的 `BashExecutor.ownerOf(id): string | undefined` seam 暴露它(不放在公开的 `BashTask` 上——只有一条读取路径,没有冗余 API)。`tool-bash` 完全删除其 `Map`:它在 `start` 时将 `exec.agent?.id`(共享的注册表/会话 id)盖章为 owner,`bash_output`/`bash_kill` 则以 `!== undefined` 语义把 `ctx.bash.ownerOf(id)` 与调用方 token 比较(空字符串 token 仍是真实 owner)。完成通知通过扫描 `ctx.get('agents')?.list()` 查找 `agent.id === ownerToken` 的存活 agent(经 `ctx.get` 读取——`onTaskDone` 运行在 bash fiber 这一外部 fiber 上,直接使用 `ctx.agents` proxy 会抛异常)。由于所有权现在存活在执行器的任务上(随 `dsh-bash` fiber dispose),它能跨越 `tool-bash` HMR 重载,关闭旧的 `XXX(tool-bash-owner-hmr)` 缺口。(`onTaskDone` 监听器仍受 `tool-bash` 的 `apply` effect 约束,因此落在重载间隙的完成仍会丢失一条通知——既有的重载间隙丢失——但所有权隔离本身已经不受 HMR 影响。)
+
+## 验证
+
+以下不变式已经成立,并由测试固定:
+
+- ACP 断连或插件拆除后,任何由桥接层拥有的会话都不留下已注册 agent 或会话存储条目,包括与连接关闭竞争的创建流程。
+- 已入队的提示词启动前执行 `session/cancel`,能阻止该提示词运行;后来接受的提示词仍是独立的已入队轮次。
+- `tool-bash` HMR 重载不会使另一个会话能够读取或终止已有的后台任务(所有权保留在执行器上)。
+- 既有的非 ACP 演示无需显式管理 handle 仍能工作;由配置创建的 agent 仍归 `AgentLoop` 插件 fiber 所有。
+
+## 会话所有者令牌在存活 agent 中唯一
+
+bash 所有者 token 比较依赖共享的 `Agent.id`/`SessionId` 在存活 agent 中唯一。并发的同 ID 操作可以都私下准备,但发布会依次进入会话和 agent;`SessionStore.enter()` 拒绝重复的存活会话 id,每个失败事务都回滚自己的私有状态。因此程序化调用方无法发布两个共享同一会话 token 的存活 agent。访问*策略*(token 比较)留在消费方 `tool-bash`;bash seam 只存储不透明的 `owner` 字符串且从不解释它——这是正确的接口/实现/消费方拆分。
+
+## 曾考虑的替代方案
+
+- **公开的 `BashTask.owner` 字段**而非 `BashExecutor.ownerOf(id)` seam:否决。一条读取路径即可,无需冗余 API。
+- **为 agent 的会话生命周期使用兄弟 Cordis effect**:否决。fiber 卸载时并发释放兄弟 effect(`Promise.all`),store 拥有的 append 发布钩子的移除与循环的关闭 `session/flush` 产生竞争;单一复合 effect 的有序 LIFO 链才能在两条释放路径上都捕获关闭的 `turn/end`。
+- **在 `cancel()` 之外另设一个仅中止步骤的 `abort()`**:最初发布过,后因无人使用而移除;`cancel()` 是唯一的公开停止原语(见[公开停止接口 Agent Note](../simplification/2026-06-20-public-agent-stop-surface.md))。
+
+## 后果
+
+本变更有意触及公开接口(`Agent`、`AgentFactory`、bash seam),而非作为 ACP 的局部补丁。同步 `Agent.send()` 的简洁易用性得以保留;异步生命周期路径是增量添加的,供需要它的所有者使用。
diff --git a/.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml
new file mode 100644
index 0000000000..4946fc219a
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-18-session-surface.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
+2026-06-18-session-surface.md: 80034881d0112076759a68737b5931c8ff659d15
+2026-06-18-session-surface.zh.md: 26a3119faf0b6988049a7599ea9551a8ae65d63d
diff --git a/.agents/notes/implemented/architecture/2026-06-18-session-surface.md b/.agents/notes/implemented/architecture/2026-06-18-session-surface.md
index f1297b1f05..80034881d0 100644
--- a/.agents/notes/implemented/architecture/2026-06-18-session-surface.md
+++ b/.agents/notes/implemented/architecture/2026-06-18-session-surface.md
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-18-session-surface.zh.md)
+
## Problem
The event log is authoritative, but history manipulation had no durable shared mechanism. Plugins such as compaction would otherwise rewrite derived requests through order-sensitive listeners, leave no provenance, and require repeated changes to `deriveMessages()`.
diff --git a/.agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md b/.agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md
new file mode 100644
index 0000000000..26a3119faf
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md
@@ -0,0 +1,73 @@
+# Agent Note: 会话 surface:事件日志上的有序投影
+
+Status: implemented
+
+[English](2026-06-18-session-surface.md) | 中文
+
+## 问题
+
+事件日志是权威数据源,但历史操纵此前没有持久化的共享机制。如果没有这样的机制,上下文压缩(context compaction)等插件只能通过顺序敏感的监听器改写派生请求,不留溯源信息,且每次新增操纵都要反复修改 `deriveMessages()`。
+
+## 决策
+
+新增一个 **surface**:事件 seq 的派生、缓存有序投影(即产出 LLM(大语言模型)消息的事件子集),通过事件日志中的 `surfaceOp` 标记维护。
+
+### `SessionEvent` 新增两个顶层字段
+
+每个 `SessionEvent` 获得两个可选字段(结构性元数据,与 `seq`/`time` 同级):
+
+- **`sourceEventSeqs?: number[]`**:作为溯源来源的事件 seq 编号(例如构成 `assistant/message` 的各 `assistant/chunk` 的 seq,或被压缩标记遮蔽的 surface 节点)。出现的 `[]` 只在 `assistant/message` 上有效,表示已知为空的提供方流;在该事件上省略字段表示旧数据或未记录的溯源。其他 surface 事件一旦出现此字段,就必须是非空列表。溯源是核心设计原则;没有它,replace-range 操作在回放时无法被验证。
+- **`surfaceOp?: SurfaceOp`**:该事件如何进入 surface。非 surface 事件不携带此字段。
+
+### SurfaceOp:两种操作
+
+```ts
+export type SurfaceOp =
+ | 'append' // normal tail append
+ | { op: 'replace'; start: number; end: number } // shadow [start, end] inclusive
+```
+
+1. **Append**:在尾部追加新事件的 seq。`user/message`、`assistant/message`、`tool/result`、`context/message`、`steering/message` 使用此操作。agent loop(智能体循环)在所有此类追加上传入 `surfaceOp: 'append'`,并在适用时记录 `sourceEventSeqs`:每个成功的 `assistant/message` 都记录完整的 `assistant/chunk` 来源集合(包括 `[]`),而 `tool/result` 记录其 `tool/call` 来源。
+
+2. **Replace**:移除从 `start` 到 `end`(两端包含)的条目,并在其位置插入新事件的 seq。`start` 和 `end` 都必须存在于当前 surface;`start === end` 表示替换单个条目。该事件的 `sourceEventSeqs` 必须包含所有被遮蔽的 surface seq。被遮蔽的事件仍留在日志中,但不再出现在 surface 上。
+
+### SurfaceManager:基于增量,而非全量重建
+
+一个 `Session` 拥有一个 `SurfaceManager`,后者维护事件 seq 的有序 `number[]`。管理器会在提交前校验每个种子或追加候选项而不应用它,然后只处理上次同步之后已经提交的事件,而不重新扫描整个日志。`Session.surface` 通过只读的 `SessionSurface` 契约暴露同一个管理器,因此接纳、派生历史、压缩与工作区上下文共享同一份增量状态。Replace 按数组位置找到两端都包含的端点,并把替换 seq splice 到该范围;不会用第二个管理器、链接对象或 seq 到节点的 map 来重复表达顺序。
+
+无新事件时增量处理为 O(1),有新事件到达时为 O(新事件数)。
+
+`deriveMessages()` 在存在 surface 标记时使用 surface,对没有标记的会话回退到既有的线性扫描(向后兼容)。
+
+### 持久化
+
+新字段作为顶层 JSON 属性序列化。JSONL 后端无需任何改动:`JSON.stringify`/`JSON.parse` 透明地保留一切。SQLite 后端的 `events` 表新增两个可空 TEXT 列(`source_event_seqs`、`surface_op`)。磁盘上的 `SCHEMA_VERSION` 递增以反映列集变化,并且按照预发布的 bump-and-reject 策略,由其他构建写入的数据库在打开时被拒绝而非迁移(没有需要升级的持久化用户数据)。会话格式 `version` 固定为 `SESSION_FORMAT_VERSION = 0`(「不稳定/预发布」立场):可选的 surface 字段被吸收而不递增版本号。
+
+### 崩溃恢复
+
+`repair.ts` 模块在崩溃后为孤立的工具调用合成 `tool/result` 闭合事件。这些闭合事件携带 `surfaceOp: 'append'` 和指向孤立 `tool/call` 事件的 `sourceEventSeqs`,确保重建的 surface 有效。
+
+### 不变式
+
+`Session` 在始终启用的 seed/append 边界校验 `sourceEventSeqs` 与 `surfaceOp`:只有 `assistant/message` 可以使用空的溯源列表;引用必须唯一、更早且已知;替换端点必须存在于 surface 顺序中;溯源必须覆盖每个被遮蔽的节点。这些是单记录接纳与存储投影规则,不是可选的不变式服务贡献。
+
+每个 surface 可达事件都必须携带 `surfaceOp`,否则它将从派生历史中消失。类型化的 `append` 重载对字面事件类型强制执行此规则;`append` 和种子构造函数中的运行时检查覆盖宽化联合类型和加载的日志。按照预发布格式策略,无效的种子被拒绝而非升级。
+
+## 曾考虑的替代方案
+
+- **逐插件的 `agent/request` 包装**(surface 之前的历史操纵模式):监听器排序脆弱、无法持久记录改动内容,且每种新操纵都迫使核心 `deriveMessages()` 再次修改。
+- **半开区间 `[start, endExclusive)` 的 replace 范围**:否决。端点由 surface 事件 seq 命名,单条目替换(`start === end`)在闭区间语义下读起来更自然。
+- **链接节点对象加 seq map**:否决。生产代码不读取前驱链接,唯一的后继用途就是数组中的下一个位置,而替换本来就需要线性 `indexOf` 查找。单个 seq 数组在保留相同渐进复杂度的同时,只留下一个需要校验的表示。
+- **脏标记后全量重建**替代增量处理:在会话生命周期内为 O(N²),每次单事件追加都要重新扫描所有先前事件。
+
+## 后果
+
+- **`packages/core/session`**:`surface.ts`(`SurfaceManager`)维护一个用于候选接纳和实时投影的有序 seq 数组;`SessionSurface` 是其只读公共视图。`SurfaceOp`/`SurfaceIntent` 与顶层会话事件字段记录条目如何加入它。`append()` 要求 surface 事件携带 `SurfaceIntent`,`deriveMessages()` 以遍历 surface 作为唯一派生路径,`repair.ts` 则发出 surface 感知的闭合事件。种子构造函数拒绝缺少 `surfaceOp` 标记的 surface 可达种子事件(见「不变式」一节)。
+- **`packages/core/agent-loop`**:所有 surface 可达的追加操作传入 surface 选项。收集分片 seq 用于 `assistant/message` 溯源;捕获 `tool/call` seq 用于 `tool/result` 溯源。
+- **`packages/session-persistence/session-persistence-sqlite`**:`events` 表新增两个可空 TEXT 列(`source_event_seqs`、`surface_op`);`SCHEMA_VERSION` 递增(bump-and-reject,无迁移)。
+- **`packages/session-persistence/session-persistence-jsonl`**:无需改动。
+- **`packages/session-persistence/session-persistence`**:抽象接口不变。
+
+Surface 是未来历史操纵的基础。压缩或 tool-result-prune 插件追加一个既有的消息产出事件类型(例如一条携带摘要的 `user/message`),附带 `surfaceOp: { op: 'replace', start, end }` 和覆盖被遮蔽条目的 `sourceEventSeqs`——新事件在 surface 上取代该范围的位置,而插件自身的 trace 事件(如 `compaction/start`、`compaction/end`)不进入 surface。回放以确定性方式保留该决策。
+
+一次 `tool/result` 替换只能改写当前的一个 `tool/result`,并且必须保留除 `content` 以外的每个数据字段。Session 接纳会与位置范围和溯源校验一起强制这条规则,不依赖可选的诊断插件。
diff --git a/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.i18n.yaml
new file mode 100644
index 0000000000..d8dae837fe
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.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
+2026-06-18-shared-persistence-write-coordinator.md: 4632351a6f39c44c9ba8af58d508d4665b9e9279
+2026-06-18-shared-persistence-write-coordinator.zh.md: 40a7144038ac0db4ca6cac651c0a3cef5de4afa9
diff --git a/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md
index 48b1bfa45f..4632351a6f 100644
--- a/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md
+++ b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-18-shared-persistence-write-coordinator.zh.md)
+
## Problem
`dsh-session-persistence-jsonl` and `dsh-session-persistence-sqlite` intentionally prove the same `SessionPersistence` contract over different storage media, but their write-path orchestration was duplicated: per-session state, `session/created` adoption, backend-specific prefix reads, write-behind control, per-id operation serialization, HMR seeding, and dispose drains. The pure seed-prefix collision and serializability guards had already moved into the seam package; the remaining orchestration was still correctness-heavy and received the same fixes twice. Only the storage primitives (write bytes vs. INSERT rows) differed.
@@ -21,7 +23,7 @@ The coordinator retires a session from `session/disposed`: it waits for the cont
Five required members plus an optional lifecycle hook form the only boundary between the coordinator and storage:
- `name` — backend label for the dispose-failure `AggregateError`.
-- `loadStored(id)` — read one stored prefix by id across every storage scope (every JSONL cwd bucket; SQLite's id is globally unique). Resume/load, non-mutating inspection, live adoption, and the create-collision probe share this lookup. The coordinator asserts the returned id and rejects a stored/live cwd mismatch before repair or state publication.
+- `loadStored(id)` — read one stored prefix by id across every storage scope (every JSONL project directory; SQLite's id is globally unique). Resume/load, non-mutating inspection, live adoption, and the create-collision probe share this lookup. The coordinator asserts the returned id and rejects a stored/live cwd mismatch before repair or state publication.
- `appendBatch(meta, events, isMaterialized)` — durably append a contiguous batch, lazily materializing the session ATOMICALLY when not yet materialized (the materialize-write and the first event batch must commit together — a crash between them must not leave a materialized-but-empty session; this is why there is no separate `materialize` hook).
- `commitRepair(meta, tornMarker, closers)` — make a crash repair durable: truncate the torn tail (iff `tornMarker !== undefined`) and append `closers`. **NOT required to be atomic** — JSONL legitimately truncates-then-appends in two fsync'd steps, SQLite does DELETE+INSERT in one transaction. Used by `load` (truncate + synthetic closers) and live-adoption (truncate only, `closers = []`).
- `list()` — list all stored metadata.
diff --git a/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.zh.md b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.zh.md
new file mode 100644
index 0000000000..40a7144038
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.zh.md
@@ -0,0 +1,47 @@
+# Agent Note: 共享持久化写入协调器
+
+Status: implemented
+
+[English](2026-06-18-shared-persistence-write-coordinator.md) | 中文
+
+## 问题
+
+`dsh-session-persistence-jsonl` 与 `dsh-session-persistence-sqlite` 有意在不同存储介质上证明同一份 `SessionPersistence` 契约,但它们重复实现了写入路径编排:每会话状态、`session/created` 接管、后端特定的前缀读取、write-behind 控制、按 id 串行执行操作、HMR(热模块替换)种子注入与 dispose(资源释放)排空。纯粹的种子前缀碰撞检查与可序列化守卫已迁入 seam 包;剩余的编排仍然对正确性要求很高,且同样的修复被应用了两次。唯一的差异在于存储原语(写字节 vs. INSERT 行)。
+
+## 决策
+
+将一个后端无关的 `PersistenceCoordinator` 提取到 `dsh-session-persistence` 中。协调器统一拥有编排逻辑;每个第一方后端组合一个协调器实例(`new PersistenceCoordinator(ctx, this)`),实现一个小型 `PersistenceBackend` 钩子接口,并将其有状态的公开方法(`create`/`append`/`load`/`inspect`)委托给协调器。由后端拥有的元数据与修订版本列举会绕过协调器。
+
+组合,而非继承。协调器是后端持有的具体类,不是后端继承的基类。本 Agent Note 的风险——「协调器不得让非常规后端与继承层级作斗争」——由此规避:后端只暴露钩子,无法触及协调器的私有编排状态。第三方后端仍然可以完全不使用协调器、直接实现抽象服务,包括供读模型使用、不修改状态的 `inspect` 契约。
+
+协调器为每个确切的存活 `Session` 持有一个控制器;该控制器统合初始化、待处理事件与共享 flush promise。每个 `session/event` 都会立即启动排空,而 `session/flush` 只观察完全停稳,不会发起常规写入路径。[flush 控制器简化](../simplification/2026-07-23-collapse-persistence-flush-state.md)定义该生命周期。
+
+协调器通过 `session/disposed` 退役会话:它等待控制器完成初始化和当前 flush,串行执行最后一次排空,且仅在成功后才移除控制器与其拥有的每 id 状态。失败时保持控制器可被找到,以供后端 teardown(拆除)重试。每个 id 的已结算链尾仅在其仍是当前链尾时才移除自身,因此旧操作完成后不会抹除同一 id 的新操作。后端 teardown 会注销写入路径监听器、flush 每个剩余的控制器、等待所有按 id 串行化的操作,最后关闭后端。
+
+### 钩子接口(`PersistenceBackend`)
+
+五个必需成员加一个可选的生命周期钩子,构成协调器与存储之间唯一的边界:
+
+- `name`——后端标签,用于 dispose 失败时的 `AggregateError`。
+- `loadStored(id)`——按 id 跨所有存储范围读取一个已存储前缀(JSONL 的所有项目目录;SQLite 的 id 全局唯一)。恢复/加载、不修改状态的检查、存活会话接管与创建碰撞探测共用此查找。协调器会断言返回的 id,并在修复或发布状态之前拒绝已存储记录与存活会话的 cwd 不匹配。
+- `appendBatch(meta, events, isMaterialized)`——持久追加一个连续批次,在尚未物化时原子地惰性物化会话(物化写入与首批事件必须一起提交——崩溃不得留下一个已物化但为空的会话;这就是为什么没有单独的 `materialize` 钩子)。
+- `commitRepair(meta, tornMarker, closers)`——使崩溃修复持久化:截断损坏的尾部(当且仅当 `tornMarker !== undefined`)并追加 `closers`。**不要求原子性**——JSONL 合理地分两步 fsync(先截断再追加),SQLite 在一个事务中完成 DELETE+INSERT。用于 `load`(截断 + 合成 closers)和 live-adoption(仅截断,`closers = []`)。
+- `list()`——列出所有已存储的元数据。
+- `close?()`——可选的生命周期清理(SQLite 关闭 db 句柄;JSONL 省略),在 dispose effect 中于排空至完全停稳之后被 await,因此 close 失败不会掩盖排空错误。
+
+### 不透明的 torn marker
+
+保持 seam 整洁的唯一设计选择:崩溃修复中「损坏尾部在哪里」的 token 对协调器是不透明的。协调器计算合成 closers(它拥有来自 `dsh-session` 的 `interruptedTurnClosers`),但它只测试 `tornMarker !== undefined` 并将值原样传回 `commitRepair`——从不检视其内容。每个后端选择自己的 marker 类型:JSONL 携带要截断到的字节偏移,以及从不完整最终帧中解码出的任何完整事件;SQLite 则携带要从其开始删除的 seq。协调器因此既不了解字节长度,也不了解帧恢复状态。
+
+## 测试
+
+共享的 `runPersistenceContract`(公开 API 契约)为每个后端运行,并证明在 `load` 执行恢复之前,`inspect` 会保持被中断的日志与修订版本不变。`runCoordinatorContract`(`tests/coordinator-contract.ts`)通过内存参考实现、JSONL 与 SQLite 覆盖接管、HMR、碰撞、会话与后端 dispose 排空,以及崩溃尾部修复。协调器专属测试覆盖立即执行的后续批次、存活控制器清理、同 id 链尾竞态、排空失败重试与关闭顺序。各后端自身的测试规格只保留存储机制。每个真实后端都有一个经由协调器的崩溃尾部修复测试,以覆盖不透明 marker 分支,因为契约中的崩溃用例会产生合成 closers,却不会产生 torn marker。
+
+## 曾考虑的替代方案
+
+- **后端继承的基类**——否决,改用组合:后端只暴露钩子,无法触及协调器的私有编排状态,且第三方后端仍可完全不使用协调器、直接实现抽象服务。
+- **更宽的钩子面**——每个候选钩子都被折叠掉:没有限定存储范围的实时查找,因为 `loadStored` 加上协调器的 cwd 检查即可维持碰撞边界;没有存储定位器泛型,因为经验证的 JSONL 元数据可还原其路径,而 SQLite 已按 id 绑定;没有单独的 `materialize` 钩子,因为首批事件必须与物化原子提交;没有单独的创建碰撞探测,因为它就是 `loadStored(id) !== undefined`;`list()` 也不经由协调器透传,因为列举不需要任何编排。
+
+## 后果
+
+协调器增加了一层间接、一个不透明的 torn marker 和脱离会话生命周期的退役任务,但将此前每个后端重复的、对正确性要求很高的编排逻辑集中到一处。会话 dispose 仍是仅观察事件,因此会话所有者不会等待持久化退役;协调器会收容失败、在存活控制器中保留待处理事件,并以后端 teardown 为完全停稳边界。其钩子面保持窄小:标识校验、接管、碰撞检查与不修改状态的检查共用 `loadStored`;物化保持在 `appendBatch` 内原子完成;列举绕过协调器。读模型使用 `inspect` 而非 `load`,因此观察已持久化但仍开放的轮次时,不会因提交中断 closers 而与新的存活所有者产生竞态。新后端只需实现存储原语,而无需复制立即写入生命周期。
diff --git a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml
new file mode 100644
index 0000000000..04f7dcb1a3
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.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
+2026-06-20-branded-ids.md: 7c0b7ca89418e8312ec728223dac519f70edc3ed
+2026-06-20-branded-ids.zh.md: 8b41ad3c3c85690fb03b20a208f8460a1614477b
diff --git a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md
index e7a3110fce..7c0b7ca894 100644
--- a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md
+++ b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-20-branded-ids.zh.md)
+
## Problem
The harness brands `CallId` (`packages/llm/llm/src/brand.ts`) and the shared agent/session `SessionId` (`packages/core/session/src/types.ts`) using the `Branded = string & { readonly [BRAND]: B }` machinery (owned by the type-only `@deepseek-ai/dsh-brand` package at `packages/util/brand/` — see its [README](../../../../packages/util/brand/README.md)) and a zero-cost cast factory per type. `dsh-brand` also states the governing policy: *"Branding is for ids that cross package boundaries and could plausibly be confused; not every string needs a brand."* That policy is right; the problem is that it is only half-applied. Two gaps let a structurally-identical-but-semantically-wrong string slip through the type checker today.
@@ -10,7 +12,7 @@ The harness brands `CallId` (`packages/llm/llm/src/brand.ts`) and the shared age
The bash **owner token** is the related sub-case: `BashExecRequest.owner?: string` and `BashExecSpec.owner: string | undefined` (`packages/bash/bash/src/types.ts`) are documented as a deliberately *opaque* isolation key, but in every live caller the value IS the owning agent's shared `Agent.id`/`SessionId` (`callerToken = (exec) => exec.agent?.id` in `packages/bash/tool-bash/src/index.ts`) wearing a different seam-local name. It is compared for access control (`owner !== callerToken(exec)`), so a mismatched-but-well-typed string here is a cross-session isolation bug the type system currently cannot catch. This is the shared id alias covered by the [unified agent/session identity decision](../simplification/2026-06-20-unify-agent-and-session-id.md).
-**Gap 2 — brand erosion at the seams of the *already-branded* IDs.** Even `CallId` and `SessionId` decay back to bare `string` at exactly the places confusion is most likely: registry/store key types and public method params. Representative sites include the session store, the agent registry (both keyed by the shared `SessionId`), `ToolPresenter`'s call-id map, ACP's session-id records and loading set, and the persistence coordinator. A brand that is dropped at a collection key buys nothing on lookups — the value of the existing brands is partly unrealized.
+**Gap 2 — brand erosion at the seams of the *already-branded* IDs.** Even `CallId` and `SessionId` decay back to bare `string` at exactly the places confusion is most likely: registry/store key types and public method params. Representative sites include the session store, the agent registry (both keyed by the shared `SessionId`), tool-presentation call-id maps, ACP's session records, and the persistence coordinator. A brand that is dropped at a collection key buys nothing on lookups — the value of the existing brands is partly unrealized.
## Decision
diff --git a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md
new file mode 100644
index 0000000000..8b41ad3c3c
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md
@@ -0,0 +1,69 @@
+# Agent Note: 在所有应有之处使用 branded ID
+
+Status: implemented
+
+[English](2026-06-20-branded-ids.md) | 中文
+
+## 问题
+
+harness 使用 `Branded = string & { readonly [BRAND]: B }` 机制,为 `CallId`(`packages/llm/llm/src/brand.ts`)和 agent/会话共享的 `SessionId`(`packages/core/session/src/types.ts`)做 brand 处理;该机制由纯类型包(package) `@deepseek-ai/dsh-brand` 拥有,位于 `packages/util/brand/`,见其 [README](../../../../packages/util/brand/README.md),并为每个类型提供零开销的 cast 工厂。`dsh-brand` 还声明了治理策略:*「Branding 用于跨包边界且可能被混淆的 id;不是每个 string 都需要 brand。」* 这条策略是正确的;问题在于它只落实了一半。两处缺口使得结构相同但语义错误的 string 今天仍能通过类型检查器。
+
+**缺口 1:bash seam 中未 brand 的跨边界 ID。** 后台 task id 是普通 `string`:`BashTask.id: string`(`packages/bash/bash/src/types.ts`),作为 `string` 贯穿整个执行器 seam(`packages/bash/bash/src/index.ts` 中的 `BashExecutor.get`/`ownerOf`/`readOutput`/`kill(id: string)`),再由面向模型的工具以 `string` 校验并传递(`validateTaskId`、`assertTaskAccess`、`packages/bash/tool-bash/src/index.ts` 中 `task_id` 的 schema 参数)。它由每执行器计数器生成——`packages/bash/bash-local/src/index.ts` 中的 `` `bash-${this.nextTaskId++}` ``——其形状与 `SessionId` 的默认值**完全相同,都是 `name-N`**(`packages/core/session/src/index.ts` 中的 `` `session-${++counter}` ``)。bash task id 和会话 id 在调用点轻易就能互换,而编译器毫无反应。这是用户询问的核心案例,并且它是面向模型的 id(模型会把 `task_id` 传回 `bash_output`/`bash_kill`),所以该混淆可由不受信任的输入触达。
+
+bash **owner token** 是相关的子情形:`BashExecRequest.owner?: string` 和 `BashExecSpec.owner: string | undefined`(`packages/bash/bash/src/types.ts`)被文档描述为刻意*不透明*的隔离键,但在所有实际调用方中,该值就是所属 agent(智能体)共享的 `Agent.id`/`SessionId`(`callerToken = (exec) => exec.agent?.id`,位于 `packages/bash/tool-bash/src/index.ts`),只是披着另一个 seam 本地名称。它被用于访问控制比较(`owner !== callerToken(exec)`),因此一个不匹配但类型正确的 string 在此处就是跨会话隔离 bug,而当前类型系统无法捕获。这正是[统一 agent/session 标识决策](../simplification/2026-06-20-unify-agent-and-session-id.md)覆盖的共享 id 别名。
+
+**缺口 2:*已经 brand* 的 ID 在 seam 处被侵蚀。** 就连 `CallId` 和 `SessionId` 也恰好在最容易混淆的地方退化为裸 `string`:注册表/store 键类型和公开方法参数。代表性位置包括会话存储、agent 注册表(二者都以共享的 `SessionId` 为键)、工具展示层的 call-id map、ACP 的会话记录,以及持久化协调器。在集合键处丢弃 brand,会让既有 brand 在查找时毫无价值;它们的价值只实现了一部分。
+
+## 决策
+
+纯类型变更。Brand 是零开销 cast;运行时行为、序列化、比较和协议格式(wire format)均不变。工作分三部分,全部遵循既有的「不是每个 string 都需要」策略。
+
+- **为 bash task id 加 brand。** 在 `packages/bash/bash/src/types.ts`(*拥有*该 id 的包)中添加 `BashTaskId = Branded<'BashTaskId'>` 及其同名工厂,从 `@deepseek-ai/dsh-brand` 导入 `Branded`,方式与 `SessionId` 完全一致。brand 原语位于无依赖的 `dsh-brand` 工具包中,正是为了让 `dsh-bash` 仅依赖它就能为自己的 id 加 brand,而无需引入 `dsh-llm`(或 `dsh-session`)来获取 `Branded`。将其贯穿 `BashTask.id`、`BashExecutor` seam 方法(`get`/`ownerOf`/`readOutput`/`kill`)、`dsh-bash-local` 中的生成点(在创建时对计数器输出做一次 brand),以及 `dsh-tool-bash` 的校验/访问面(`validateTaskId` 返回 `BashTaskId`;`task_id` 在模型 string 到达的工具边界处被 brand)。
+
+- **铸造独立的 `OwnerToken` brand。** 在 `packages/bash/bash/src/types.ts` 中添加 `OwnerToken = Branded<'OwnerToken'>`;将 `BashExecRequest.owner` / `BashExecSpec.owner` / `BashExecutor.ownerOf` 的类型标注为 `OwnerToken | undefined`。`dsh-tool-bash` 消费方在边界处将 agent 共享的 `id`(`SessionId`)cast 为 `OwnerToken`——这是两套词汇唯一交汇的地方。bash seam 从不导入 `dsh-session`。(理由见下一节。)
+
+- **阻止 brand 侵蚀。** 将既有 brand 传播到缺口 2 列出的 `Map` 键类型和公开方法参数中:`Map`、`Map`、`get(id: SessionId)`、`Map`、ACP 的 `SessionId` surface、协调器的 `Map`。这是 diff 中机械量最大的部分,也是让*既有* brand 在查找处真正发挥作用(而不仅仅标注在结构体字段上)的关键。
+
+示意形状(工厂模式与已有的三个 brand 完全一致):
+
+```ts ignore-check
+import type { Branded } from '@deepseek-ai/dsh-brand'
+
+/** A background bash task handle (generated `bash-N` by the local executor). */
+export type BashTaskId = Branded<'BashTaskId'>
+export function BashTaskId(id: string): BashTaskId {
+ return id as BashTaskId
+}
+
+/** A bash task's opaque isolation key — the consumer's owner identity, NOT the bash seam's. */
+export type OwnerToken = Branded<'OwnerToken'>
+export function OwnerToken(id: string): OwnerToken {
+ return id as OwnerToken
+}
+```
+
+## 曾考虑的替代方案
+
+### 为什么不把 `owner` 类型标注为 `SessionId`?
+
+显而易见的捷径是直接把 `owner` 类型标注为 `SessionId`——它确实*总是*一个会话 id。我们否决这个方案。bash 执行器 seam 是能力 seam(接口 `dsh-bash`、实现 `dsh-bash-local`、消费方 `dsh-tool-bash`),其 owner token 被*明确记录为刻意不透明*:执行器「从不解释它(seam 中没有访问策略——那是消费方的职责)」(`packages/bash/bash/src/types.ts`)。把 seam 字段类型标注为 `SessionId`,会把 `dsh-session` 的词汇引入一个不应知道 owner token *含义*的包——这会让通用执行后端耦合会话模型,并违背不透明 token 的设计。取代 `dsh-bash-local` 的沙箱或远程执行器不应继承会话依赖。独立的 `OwnerToken` brand 使 seam 保持解耦:`dsh-bash` 只知道「owner 是某种带 brand 的不透明 token」,而已经决定访问策略的 `dsh-tool-bash` 消费方,是把其 `SessionId` cast 为 `OwnerToken` 的唯一边界。该 brand 仍带来安全收益(不能把 `BashTaskId` 或裸 string 传到 owner 位置),且不引入耦合。
+
+## 不在范围内 / 可能的扩展
+
+遵循「不是每个 string 都需要 brand」的策略,刻意保持窄范围。以下每项都是合理的未来 brand 候选,附带推迟理由而非承诺:
+
+- **`ModelId`**(`GenerateOptions.model`,`LlmService` 适配器注册表的键):一个真正的跨包查找键(config → agent → llm → 适配器);合理的下一个 brand,仅为控制本 Agent Note 的影响范围而暂不纳入。
+- **`ToolName`**(`ToolRegistry` 的键):由作者定义、人类可读,且很少与其他 id 混淆;最弱的候选,可能不值得加 brand。
+- **`ErrorCode`**(`HarnessError.code`):一个封闭词汇(`ABORTED`、`NO_ADAPTER`……),不是逐实例的 id;如果要做,string 字面量联合类型比 brand 更合适。
+- **数值序号**:轮次号、步骤号和事件 `seq` 是 `number` 而非 `string`,`Branded` 不适用;可以用并行的 `number & { readonly [BRAND]: B }` 变体来 brand 它们,但它们是位置序号、很少跨边界传递,收益较低。
+- **带校验的构造**:brand 工厂是纯 cast,无运行时检查,且每个边界(ACP `sessionId`、提供方签发的 `call.id`、`dsh-llm-deepseek` 中的空字符串回退)今天都信任裸 string。一个在边界处对格式错误的输入抛异常的 `SessionId.parse()` / `isValid()` 配套工具确实是缺口,但它是*运行时行为*变更,有自己的设计问题(什么算「格式错误」?失败时怎么办?),应在独立 Agent Note 中处理,不应捆绑进这次纯类型变更。
+
+## 验证
+
+已落地的不变式:`BashTaskId` 和 `OwnerToken` 定义在 `dsh-bash` 中,并端到端贯穿执行器 seam、`dsh-bash-local` 生成点与 `dsh-tool-bash` 面向模型的 surface,且 `dsh-bash` 未添加对 `dsh-session` 的依赖;没有任何以范围内 brand id(`CallId`/`SessionId`/`BashTaskId`)为键的集合使用裸 `string`;公开方法参数和导出签名保留 brand;每个原始 string 进入的边界(提供方 call id、ACP 会话 id、模型提供的 `task_id`)都通过 cast 工厂构造 brand,而不是散落的 `as` cast。
+
+## 后果
+
+- **两个接口面的机械性改动。** 传播 brand 涉及 bash seam(接口 + 实现 + 消费方)以及 ACP 会话 id 接口和持久化协调器。改动面广但严重度低:遗漏的位置是编译错误而非静默 bug。变更可观察地为纯类型变更——无快照或 e2e 行为差异。它与[统一 agent/会话标识决策](../simplification/2026-06-20-unify-agent-and-session-id.md)相邻,因为二者都触及会话 id / owner-token 边界;`OwnerToken` 出于上述解耦理由仍与统一后的 id 保持独立。
+- **Brand 不做校验。** Brand 是混淆防护,不是正确性证明:一个*错误的* 会话 id 只要仍是合法的 string,就和以前一样能通过类型检查器。本 Agent Note 不关闭这个缺口(见「不在范围内」)——它只阻止这类*类别*错误:传入错误*种类*的 id。
+- **「在哪里停下」仍是判断题。** 为 `BashTaskId` 加 brand 但不为 `ToolName` 加,为 `OwnerToken` 加但不为 `ModelId` 加,是对哪些 string「可能被混淆」的品味判断。合理的评审者可能想要更多或更少;`brand.ts` 中的策略是裁决依据,本 Agent Note 倾向于面向模型或用于访问控制的 id。
diff --git a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml
new file mode 100644
index 0000000000..db80fbcfa9
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write
+2026-06-20-generic-long-running-tool-runtime.md: 313d687b49da0d08b0ec321bcb655b642f7a5af3
+2026-06-20-generic-long-running-tool-runtime.zh.md: 6be129b7b16ff01d73dc94f7ce6d299ee2c10e55
diff --git a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md
index f3fe6373ea..313d687b49 100644
--- a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md
+++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md
@@ -2,6 +2,8 @@
Status: implemented
+English | [中文](2026-06-20-generic-long-running-tool-runtime.zh.md)
+
## Problem
Background bash originally combined two responsibilities: the bash executor ran processes and also managed task ids, ownership, incremental reads, cancellation, completion listeners, and model-facing control tools. Adding background subagents required the same lifecycle and interaction contract. Implementing that contract independently for every long-running capability would duplicate isolation, cleanup, notification, and prompt behavior while teaching the model a different collect-and-stop protocol for each producer.
@@ -17,7 +19,7 @@ The `tasks/` package group owns background-task semantics:
Long-running tools are producers. `dsh-tool-bash` adapts a `BashProcess` into incremental output and process cancellation; `dsh-tool-subagent` adapts a child run into final output and child disposal. The execution seams remain independent of sessions and the task registry.
-`TaskService` is a concrete, process-local service. TODO(task-service-backend): separate its public contract from the implementation when a second backend defines the required lifecycle; a systemd-backed runtime is one plausible driver, but this PR does not speculate about its durability, reconnect, ownership, or observation semantics.
+`TaskService` is the abstract seam in `@deepseek-ai/dsh-tasks`; the process-local registry is `LocalTaskService` in `@deepseek-ai/dsh-tasks-local` (the [task-registry seam Agent Note](2026-07-26-task-registry-seam.md) records that split).
## Runtime contract
@@ -67,7 +69,7 @@ A producer loaded without any control surface would let callers start work they
## Model-facing control surface
-`dsh-tool-tasks` registers three kind-independent tools with generic ACP cards:
+`dsh-tool-tasks` registers three kind-independent tools with generic UI cards:
- `task_output(task_id, wait?, timeout_ms?)` reads output and always appends `[status: ...]`. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Reads are non-blocking unless `wait: true`, whose timeout is defaulted and capped by plugin config. A wait timeout reports the still-running status and does not stop the task.
- `task_list()` returns caller-visible tasks as ` [] —