diff --git a/.agents/notes/archived/bug-fix/2026-08-10-web-favicon-dark-mode.i18n.yaml b/.agents/notes/archived/bug-fix/2026-08-10-web-favicon-dark-mode.i18n.yaml
new file mode 100644
index 0000000000..71c953a935
--- /dev/null
+++ b/.agents/notes/archived/bug-fix/2026-08-10-web-favicon-dark-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 .agents/notes/archived/bug-fix/2026-08-10-web-favicon-dark-mode.md
+2026-08-10-web-favicon-dark-mode.md: 22e1d063a72b177e0e5c11f4bcfcbc233a81fdfc
+2026-08-10-web-favicon-dark-mode.zh.md: dff90567337c6968220091c16c6cd55cf7dad2d1
diff --git a/.agents/notes/archived/bug-fix/2026-08-10-web-favicon-dark-mode.md b/.agents/notes/archived/bug-fix/2026-08-10-web-favicon-dark-mode.md
new file mode 100644
index 0000000000..22e1d063a7
--- /dev/null
+++ b/.agents/notes/archived/bug-fix/2026-08-10-web-favicon-dark-mode.md
@@ -0,0 +1,26 @@
+# Agent Note: Web favicon follows the color scheme
+
+Status: implemented
+Archived: 2026-08-10
+
+English | [中文](2026-08-10-web-favicon-dark-mode.zh.md)
+
+## Problem
+
+`apps/web/public/favicon.svg` paints the DeepSeek mark solid black (`fill="#000"`), and `index.html` declares only that single SVG icon. Under an OS or browser dark color scheme the tab strip is dark too, so the black mark is effectively invisible. Safari versions before 26 do not render SVG favicons, so their users get no tab icon in any scheme.
+
+## Decision
+
+The favicon stays one file and adapts through the browser's own color-scheme signal: `favicon.svg` embeds `@media (prefers-color-scheme: dark) { path { fill: #fff } }`, switching the mark to white under a dark scheme while the light scheme keeps black. `index.html` and `manifest.webmanifest` also declare a 32×32 PNG fallback (`favicon-32x32.png`, DeepSeek brand blue `#4D6BFE`) that Safari versions before 26 render and that stays visible on both light and dark tab strips, extending the [web-install-manifest decision](../feature/2026-08-06-web-install-manifest.md).
+
+The theme signal is the OS/browser scheme, not the GUI's in-app `dsh.theme` toggle: the favicon lives in browser chrome, whose background follows the browser scheme, so `prefers-color-scheme` is the correct semantic and needs no JavaScript. Known browser quirks — Chromium may not repaint the tab icon until reload after a scheme switch, and Safari versions before 26 ignore the SVG variant — are accepted and the PNG fallback covers the older-Safari case.
+
+## Alternatives considered
+
+- **A second `` pointing at a separate dark SVG.** Rejected: the same scheme semantics with two files to keep in sync, and no benefit over the in-file media query.
+- **A theme-presenter that swaps the icon href on `theme/change`.** Rejected: it would follow the in-app toggle rather than the browser scheme that actually colors the tab strip, and it adds client code and a presenter for a chrome asset.
+- **No PNG fallback.** Rejected: Safari versions before 26 never render SVG favicons, so the fallback is the only way those versions get a tab icon at all.
+
+## Consequences
+
+Light scheme still shows the black mark, dark scheme shows white, and Safari versions before 26 show the blue PNG in both. `apps/web/tests/pwa-manifest.e2e.ts` pins the PNG link and its order before the SVG, both manifest icons, the shipped PNG's format and dimensions, and the dark media query inside the shipped SVG. The Chromium repaint quirk remains a browser behavior the app cannot fix.
diff --git a/.agents/notes/archived/bug-fix/2026-08-10-web-favicon-dark-mode.zh.md b/.agents/notes/archived/bug-fix/2026-08-10-web-favicon-dark-mode.zh.md
new file mode 100644
index 0000000000..dff9056733
--- /dev/null
+++ b/.agents/notes/archived/bug-fix/2026-08-10-web-favicon-dark-mode.zh.md
@@ -0,0 +1,26 @@
+# Agent Note: 网页图标随配色方案切换
+
+Status: implemented
+Archived: 2026-08-10
+
+[English](2026-08-10-web-favicon-dark-mode.md) | 中文
+
+## 问题
+
+`apps/web/public/favicon.svg` 把 DeepSeek 图标绘制为纯黑色(`fill="#000"`),而 `index.html` 只声明了这一个 SVG 图标。当操作系统或浏览器处于暗色配色方案时,标签栏同样是深色,黑色图标实际上不可见。Safari 26 之前的版本不渲染 SVG favicon,因此这些版本的 Safari 用户无论何种配色方案都看不到标签页图标。
+
+## 决策
+
+favicon 保持单一文件,并通过浏览器自身的配色方案信号自适应:`favicon.svg` 内嵌 `@media (prefers-color-scheme: dark) { path { fill: #fff } }`,在暗色方案下把图标切换为白色,浅色方案保持黑色。`index.html` 与 `manifest.webmanifest` 同时声明 32×32 PNG 兜底(`favicon-32x32.png`,DeepSeek 品牌蓝 `#4D6BFE`),Safari 26 之前的版本会渲染该 PNG,且它在浅色与深色标签栏上都清晰可见;这是对 [Web 安装 manifest 决策](../feature/2026-08-06-web-install-manifest.md) 的扩展。
+
+主题信号取操作系统/浏览器方案,而不是 GUI 应用内 `dsh.theme` 开关:favicon 位于浏览器 chrome 中,其背景跟随浏览器方案,因此 `prefers-color-scheme` 是正确语义,无需任何 JavaScript。已知的浏览器怪癖——Chromium 在切换方案后可能要到刷新页面才重绘标签图标,Safari 26 之前的版本忽略 SVG 变体——均被接受,旧版 Safari 场景由 PNG 兜底覆盖。
+
+## 曾考虑的替代方案
+
+- **新增指向独立暗色 SVG 的第二个 ``。** 不予采纳:语义相同却要多维护一个文件,相比文件内媒体查询没有任何收益。
+- **由主题 presenter 在 `theme/change` 时替换图标 href。** 不予采纳:它会跟随应用内开关,而不是真正决定标签栏颜色的浏览器方案,并且为一个 chrome 资源引入客户端代码和 presenter。
+- **不提供 PNG 兜底。** 不予采纳:Safari 26 之前的版本从不渲染 SVG favicon,兜底是这些版本获得标签图标的唯一途径。
+
+## 后果
+
+浅色方案仍显示黑色图标,暗色方案显示白色,Safari 26 之前的版本两种方案都显示蓝色 PNG。`apps/web/tests/pwa-manifest.e2e.ts` 固定断言 PNG 链接及其位于 SVG 之前的顺序、manifest 中的两个图标、交付 PNG 的格式与尺寸,以及交付 SVG 内部的暗色媒体查询。Chromium 的重绘怪癖仍是浏览器行为,应用无法修复。
diff --git a/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.i18n.yaml b/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.i18n.yaml
new file mode 100644
index 0000000000..d07841d540
--- /dev/null
+++ b/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.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/archived/feature/2026-08-08-dsh-run-headless-command.md
+2026-08-08-dsh-run-headless-command.md: ce9cff965192357022c49655983fe6ff8d554b9f
+2026-08-08-dsh-run-headless-command.zh.md: 0484c069ed6365235e4616542a4fb3d5ceb2d880
diff --git a/.agents/notes/implemented/feature/2026-08-08-dsh-run-headless-command.md b/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.md
similarity index 84%
rename from .agents/notes/implemented/feature/2026-08-08-dsh-run-headless-command.md
rename to .agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.md
index ed095f4077..ce9cff9651 100644
--- a/.agents/notes/implemented/feature/2026-08-08-dsh-run-headless-command.md
+++ b/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.md
@@ -1,9 +1,12 @@
# Agent Note: `dsh run` owns one-shot headless execution
Status: implemented
+Archived: 2026-08-10
English | [中文](2026-08-08-dsh-run-headless-command.zh.md)
+> **Superseded command grammar.** [Apps now own their command lines](../architecture/2026-08-06-app-owned-command-line.md): the headless startup row parses the task from `dsh --profile headless `, and the launcher no longer has a `run` invocation or patches task text into rows. This note remains the rejected launcher-owned design context; the direct execution and completion contract it selected remains current in [headless is a direct core entry point](../architecture/2026-08-09-headless-direct-core-entry-point.md).
+
## Problem
Generic profile boot and one-shot task execution have different lifecycle contracts. A root grammar that accepts optional task text makes one argv shape mean either a long-lived process or a terminating task according to a plugin row discovered only after composition. It also exposes a profile implementation detail as the primary user command and gives custom profiles no explicit one-shot entry.
diff --git a/.agents/notes/implemented/feature/2026-08-08-dsh-run-headless-command.zh.md b/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.zh.md
similarity index 85%
rename from .agents/notes/implemented/feature/2026-08-08-dsh-run-headless-command.zh.md
rename to .agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.zh.md
index 89d54e3557..0484c069ed 100644
--- a/.agents/notes/implemented/feature/2026-08-08-dsh-run-headless-command.zh.md
+++ b/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.zh.md
@@ -1,9 +1,12 @@
# Agent Note: `dsh run` 负责一次性 headless 执行
Status: implemented
+Archived: 2026-08-10
[English](2026-08-08-dsh-run-headless-command.md) | 中文
+> **命令语法已被取代。** [应用现在持有自己的命令行](../architecture/2026-08-06-app-owned-command-line.md):headless 启动行从 `dsh --profile headless ` 解析任务,启动器不再包含 `run` 调用,也不再把任务文本 patch 进配置行。本笔记保留被否决的启动器持有设计背景;它选定的直接执行与完成约定仍由 [headless 是直接 core 入口](../architecture/2026-08-09-headless-direct-core-entry-point.md)持有。
+
## 问题
通用 profile 启动与一次性任务执行具有不同的生命周期约定。若根语法接受可选任务文本,同一种 argv 形态会表示常驻进程或终止式任务,具体含义取决于组合完成后才发现的插件配置行。它还会把 profile 实现细节暴露成主要用户命令,并使自定义 profile 缺少明确的一次性入口。
diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json
index 638d87373d..a8377c346f 100644
--- a/.agents/notes/archived/manifest.json
+++ b/.agents/notes/archived/manifest.json
@@ -94,6 +94,9 @@
"bug-fix/2026-08-03-tui-long-session-render-costs.i18n.yaml": "sha256:f65f7bf8fc84c7a1f022ee393c8d969c06d9bde8bed3a0206de86fb35b246ac6",
"bug-fix/2026-08-03-tui-long-session-render-costs.md": "sha256:6ecf2ef831f527f361ade18a882d79bc6eccf15cc676d05728e7753f41cde051",
"bug-fix/2026-08-03-tui-long-session-render-costs.zh.md": "sha256:5f44e707b332e13fa06d625212173ea055c1c3c0aee60888435a0ff099ec6037",
+ "bug-fix/2026-08-10-web-favicon-dark-mode.i18n.yaml": "sha256:859c4399f9a017a68ba89552fdafa05e73c0599d94cee9551c84ea5b749a14f3",
+ "bug-fix/2026-08-10-web-favicon-dark-mode.md": "sha256:4d17e247abd76ae3aed5fb4e075fd66a2838292f89f7021c82a79fe37ed905e6",
+ "bug-fix/2026-08-10-web-favicon-dark-mode.zh.md": "sha256:7bbff8a3b7061c127afcc75cd2a8043b02a999b78c0180edd8f7e4807fcfe71d",
"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",
@@ -253,6 +256,9 @@
"feature/2026-07-31-web-cards-toolrow.i18n.yaml": "sha256:f9a6ab72a77934cdcc02167c7313f08d7e9925362017b34bed7ad56c8c70fbaa",
"feature/2026-07-31-web-cards-toolrow.md": "sha256:5058f7cec4497d1cb0a5c8e77b88fddacac6eead034f3edec88e8514919b8a3e",
"feature/2026-07-31-web-cards-toolrow.zh.md": "sha256:ba84ef2e1be61211ab5ba6950b78ede3d3a979f252bc068d3e04e2c025f7bc03",
+ "feature/2026-08-08-dsh-run-headless-command.i18n.yaml": "sha256:1c2b4c5b61b9263b6267275d6fc69faeaad3cc887f0728a7ed4172d817af812b",
+ "feature/2026-08-08-dsh-run-headless-command.md": "sha256:7695fe7fd322377d5986f14e35f13337f4cd376405c758218a81230f6d182d1c",
+ "feature/2026-08-08-dsh-run-headless-command.zh.md": "sha256:113c14a36c64d2facc8ae46f37c7aa76359d8cacb9c18fcba26a723f15d036fb",
"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",
@@ -295,12 +301,24 @@
"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-23-personal-staging-maintenance-skills.i18n.yaml": "sha256:cbd5c32f2997713339699f4c12876b1950895fcb21594927c7509e3cb4c5b8d5",
+ "process/2026-07-23-personal-staging-maintenance-skills.md": "sha256:a1ef960ddc47c8bc14578d8432ed0ac4272e31178937780989e4e3b3e3f4d49f",
+ "process/2026-07-23-personal-staging-maintenance-skills.zh.md": "sha256:c0f3fcf0914af88cafcc7e094a2131bd1f764003defc6eaa446b743319e73ec1",
"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",
"process/2026-07-27-wine-windows-gates-experiment.i18n.yaml": "sha256:6f4cbc12ee9cddbb297bf7e138ccabcd204f66898a0f7411b1633f03d5a9eab5",
"process/2026-07-27-wine-windows-gates-experiment.md": "sha256:8d37dcdab058098c7de3da1de00ce61bef92bbc8d6ee71add959474c6fb3e936",
"process/2026-07-27-wine-windows-gates-experiment.zh.md": "sha256:77fbf04df36af09e55007a93bd6b22d08ff99869efe8de3e97dac5b4701e0a9e",
+ "process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml": "sha256:e7d072856cd3df3b717d94647be4ea05db087b114df4b11d68a171677b94c970",
+ "process/2026-07-31-installer-adopts-existing-checkout.md": "sha256:d97e39d07bde30d534417a406fc6c56ee6765dbc7b671888d17ef2cac9a7c6dc",
+ "process/2026-07-31-installer-adopts-existing-checkout.zh.md": "sha256:ab445493c1aacf3f8dc091bdbd765ad56c551ae613e0d8156610f0c4614a7431",
+ "process/2026-08-04-forward-only-pr-issue-status.i18n.yaml": "sha256:af23e203a66a95674154899410e2f420d1d0685dbf856c24cfccdaa547a17925",
+ "process/2026-08-04-forward-only-pr-issue-status.md": "sha256:2d31077da47d95ab3ddf64d5efc6b1b8fb7c7709d39aca4a825ef9e9d382d501",
+ "process/2026-08-04-forward-only-pr-issue-status.zh.md": "sha256:b61f865b7a8a0ac901250a3edbb92ea73177067c4c25448c7088925c2caeccd7",
+ "process/2026-08-08-review-driven-issue-lifecycle-triggers.i18n.yaml": "sha256:4c28c59d3fc323e7cd01eff31f1fe759834719c5bede1e82b39f868970bf856d",
+ "process/2026-08-08-review-driven-issue-lifecycle-triggers.md": "sha256:1b0514de5d030170e91e12e4d6ba788a9247f840e82700faa385a1c0c76ab857",
+ "process/2026-08-08-review-driven-issue-lifecycle-triggers.zh.md": "sha256:028d78d61f603d8bac64c4cce20b393a78f8e029d3bb4976e79a47ecaefa6032",
"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",
diff --git a/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.i18n.yaml b/.agents/notes/archived/process/2026-07-23-personal-staging-maintenance-skills.i18n.yaml
similarity index 66%
rename from .agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.i18n.yaml
rename to .agents/notes/archived/process/2026-07-23-personal-staging-maintenance-skills.i18n.yaml
index eb11cc1b92..ecf2ec52a1 100644
--- a/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.i18n.yaml
+++ b/.agents/notes/archived/process/2026-07-23-personal-staging-maintenance-skills.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.md
-2026-07-23-personal-staging-maintenance-skills.md: a7ccc5b1e0f13e880c58a93d2e4c2cd4f06e2a93
-2026-07-23-personal-staging-maintenance-skills.zh.md: 8593291f510352429cbb29679e7d033589312e41
+2026-07-23-personal-staging-maintenance-skills.md: 86654be1afe986107e7dae4440b497a266636012
+2026-07-23-personal-staging-maintenance-skills.zh.md: 7593a2e8cafc93df02c9418c94f84bbd3f154a0c
diff --git a/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.md b/.agents/notes/archived/process/2026-07-23-personal-staging-maintenance-skills.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.md
rename to .agents/notes/archived/process/2026-07-23-personal-staging-maintenance-skills.md
index a7ccc5b1e0..86654be1af 100644
--- a/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.md
+++ b/.agents/notes/archived/process/2026-07-23-personal-staging-maintenance-skills.md
@@ -1,6 +1,7 @@
# Agent Note: Personal staging maintenance skills
Status: implemented
+Archived: 2026-08-10
English | [中文](2026-07-23-personal-staging-maintenance-skills.zh.md)
diff --git a/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.zh.md b/.agents/notes/archived/process/2026-07-23-personal-staging-maintenance-skills.zh.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.zh.md
rename to .agents/notes/archived/process/2026-07-23-personal-staging-maintenance-skills.zh.md
index 8593291f51..7593a2e8ca 100644
--- a/.agents/notes/implemented/process/2026-07-23-personal-staging-maintenance-skills.zh.md
+++ b/.agents/notes/archived/process/2026-07-23-personal-staging-maintenance-skills.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 个人集成分支维护 skill
Status: implemented
+Archived: 2026-08-10
[English](2026-07-23-personal-staging-maintenance-skills.md) | 中文
diff --git a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml b/.agents/notes/archived/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml
similarity index 66%
rename from .agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml
rename to .agents/notes/archived/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml
index 1c58887b2b..c71f556ed2 100644
--- a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml
+++ b/.agents/notes/archived/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.md
-2026-07-31-installer-adopts-existing-checkout.md: 1d45a14273095610e3ed8047da2ce9f4ca95cbb6
-2026-07-31-installer-adopts-existing-checkout.zh.md: 971bc3b389c341b314872b8e45ab20ebd2ed5b2c
+2026-07-31-installer-adopts-existing-checkout.md: 08eec47a1cf1254409df93ab83ddf503b4d7d171
+2026-07-31-installer-adopts-existing-checkout.zh.md: dc3cc5ee8dbbe287860ad02f4bdba66afb4fbeaf
diff --git a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.md b/.agents/notes/archived/process/2026-07-31-installer-adopts-existing-checkout.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.md
rename to .agents/notes/archived/process/2026-07-31-installer-adopts-existing-checkout.md
index 1d45a14273..08eec47a1c 100644
--- a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.md
+++ b/.agents/notes/archived/process/2026-07-31-installer-adopts-existing-checkout.md
@@ -1,6 +1,7 @@
# Agent Note: the installer adopts an existing checkout into the managed layout
Status: implemented
+Archived: 2026-08-10
English | [中文](2026-07-31-installer-adopts-existing-checkout.zh.md)
diff --git a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.zh.md b/.agents/notes/archived/process/2026-07-31-installer-adopts-existing-checkout.zh.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.zh.md
rename to .agents/notes/archived/process/2026-07-31-installer-adopts-existing-checkout.zh.md
index 971bc3b389..dc3cc5ee8d 100644
--- a/.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.zh.md
+++ b/.agents/notes/archived/process/2026-07-31-installer-adopts-existing-checkout.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 安装器把已有检出接管进受管布局
Status: implemented
+Archived: 2026-08-10
[English](2026-07-31-installer-adopts-existing-checkout.md) | 中文
diff --git a/.agents/notes/implemented/process/2026-08-04-forward-only-pr-issue-status.i18n.yaml b/.agents/notes/archived/process/2026-08-04-forward-only-pr-issue-status.i18n.yaml
similarity index 68%
rename from .agents/notes/implemented/process/2026-08-04-forward-only-pr-issue-status.i18n.yaml
rename to .agents/notes/archived/process/2026-08-04-forward-only-pr-issue-status.i18n.yaml
index b8e885d109..a7df92883f 100644
--- a/.agents/notes/implemented/process/2026-08-04-forward-only-pr-issue-status.i18n.yaml
+++ b/.agents/notes/archived/process/2026-08-04-forward-only-pr-issue-status.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-04-forward-only-pr-issue-status.md
-2026-08-04-forward-only-pr-issue-status.md: dd567707bc7fccd0a631943ab3ffd2838a7f2f76
-2026-08-04-forward-only-pr-issue-status.zh.md: f7fee58d6afb812f97569ae4d86c3d6504f35752
+2026-08-04-forward-only-pr-issue-status.md: 56004a39ce52c77429574f481d9945cdc4936d30
+2026-08-04-forward-only-pr-issue-status.zh.md: ee85319842d3245bdfab9668de0a42ab29597fac
diff --git a/.agents/notes/implemented/process/2026-08-04-forward-only-pr-issue-status.md b/.agents/notes/archived/process/2026-08-04-forward-only-pr-issue-status.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-08-04-forward-only-pr-issue-status.md
rename to .agents/notes/archived/process/2026-08-04-forward-only-pr-issue-status.md
index dd567707bc..56004a39ce 100644
--- a/.agents/notes/implemented/process/2026-08-04-forward-only-pr-issue-status.md
+++ b/.agents/notes/archived/process/2026-08-04-forward-only-pr-issue-status.md
@@ -1,6 +1,7 @@
# Agent Note: Forward-only PR-to-Issue status projection
Status: implemented
+Archived: 2026-08-10
English | [中文](2026-08-04-forward-only-pr-issue-status.zh.md)
diff --git a/.agents/notes/implemented/process/2026-08-04-forward-only-pr-issue-status.zh.md b/.agents/notes/archived/process/2026-08-04-forward-only-pr-issue-status.zh.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-08-04-forward-only-pr-issue-status.zh.md
rename to .agents/notes/archived/process/2026-08-04-forward-only-pr-issue-status.zh.md
index f7fee58d6a..ee85319842 100644
--- a/.agents/notes/implemented/process/2026-08-04-forward-only-pr-issue-status.zh.md
+++ b/.agents/notes/archived/process/2026-08-04-forward-only-pr-issue-status.zh.md
@@ -1,6 +1,7 @@
# Agent Note: PR 到 Issue 的状态仅向前投射
Status: implemented
+Archived: 2026-08-10
[English](2026-08-04-forward-only-pr-issue-status.md) | 中文
diff --git a/.agents/notes/implemented/process/2026-08-08-review-driven-issue-lifecycle-triggers.i18n.yaml b/.agents/notes/archived/process/2026-08-08-review-driven-issue-lifecycle-triggers.i18n.yaml
similarity index 66%
rename from .agents/notes/implemented/process/2026-08-08-review-driven-issue-lifecycle-triggers.i18n.yaml
rename to .agents/notes/archived/process/2026-08-08-review-driven-issue-lifecycle-triggers.i18n.yaml
index a82d54640c..4c3a8c8db5 100644
--- a/.agents/notes/implemented/process/2026-08-08-review-driven-issue-lifecycle-triggers.i18n.yaml
+++ b/.agents/notes/archived/process/2026-08-08-review-driven-issue-lifecycle-triggers.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-review-driven-issue-lifecycle-triggers.md
-2026-08-08-review-driven-issue-lifecycle-triggers.md: 8a2d48ee23da4c20bb832ae0109e2ea9912dac83
-2026-08-08-review-driven-issue-lifecycle-triggers.zh.md: 004739ff471815b0fe12e111eba0ec7aaaef9507
+2026-08-08-review-driven-issue-lifecycle-triggers.md: 444927968912d93f473e27ae8576e8371b9c287c
+2026-08-08-review-driven-issue-lifecycle-triggers.zh.md: 6e00e2a936b6421824743e779756011fcd4a1c9e
diff --git a/.agents/notes/implemented/process/2026-08-08-review-driven-issue-lifecycle-triggers.md b/.agents/notes/archived/process/2026-08-08-review-driven-issue-lifecycle-triggers.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-08-08-review-driven-issue-lifecycle-triggers.md
rename to .agents/notes/archived/process/2026-08-08-review-driven-issue-lifecycle-triggers.md
index 8a2d48ee23..4449279689 100644
--- a/.agents/notes/implemented/process/2026-08-08-review-driven-issue-lifecycle-triggers.md
+++ b/.agents/notes/archived/process/2026-08-08-review-driven-issue-lifecycle-triggers.md
@@ -1,6 +1,7 @@
# Agent Note: Review-driven Issue lifecycle triggers
Status: implemented
+Archived: 2026-08-10
English | [中文](2026-08-08-review-driven-issue-lifecycle-triggers.zh.md)
diff --git a/.agents/notes/implemented/process/2026-08-08-review-driven-issue-lifecycle-triggers.zh.md b/.agents/notes/archived/process/2026-08-08-review-driven-issue-lifecycle-triggers.zh.md
similarity index 99%
rename from .agents/notes/implemented/process/2026-08-08-review-driven-issue-lifecycle-triggers.zh.md
rename to .agents/notes/archived/process/2026-08-08-review-driven-issue-lifecycle-triggers.zh.md
index 004739ff47..6e00e2a936 100644
--- a/.agents/notes/implemented/process/2026-08-08-review-driven-issue-lifecycle-triggers.zh.md
+++ b/.agents/notes/archived/process/2026-08-08-review-driven-issue-lifecycle-triggers.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 由评审驱动的 Issue 生命周期触发器
Status: implemented
+Archived: 2026-08-10
[English](2026-08-08-review-driven-issue-lifecycle-triggers.md) | 中文
diff --git a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml
index 21c6e32e11..9e32647a60 100644
--- a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md
-2026-06-21-mandatory-app-attribution-headers.md: 12482ca80d19e5cd1e62b8860865db6cccea61ab
-2026-06-21-mandatory-app-attribution-headers.zh.md: 43f356887948b88d34ba41d46ed1cd2f1c89a1e2
+2026-06-21-mandatory-app-attribution-headers.md: 39050a53ec76e8c5a6cac4d8e31fa15b992c406e
+2026-06-21-mandatory-app-attribution-headers.zh.md: bfd6aa2540022f68cf9f69ccc2c12b3bc0978960
diff --git a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md
index 12482ca80d..39050a53ec 100644
--- a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md
+++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md
@@ -32,7 +32,7 @@ The provider-neutral identity is owned by `dsh-llm` (`packages/llm/llm/src/attri
- product token for `User-Agent`: `deepseek-harness` (continuity with the pre-Agent Note wire value and the repo/org identity)
- version: read from the owning package's manifest via `createRequire`, never a hand-copied constant
-- app URL: `https://github.com/deepseek-ai/deepseek-harness-sdk` - the planned public home, which must exist before release
+- app URL: `https://github.com/deepseek-ai/deepseek-harness` - the repository home
The default is mandatory and non-empty. White-label deployments pass their own `AppIdentity` to `attributionHeaders(identity)` - the override hook is the function parameter, with no deployment config plumbing until a consumer needs it - and omission falls back to the harness default rather than suppressing attribution. There is no per-request API for the model, user prompt, session id, cwd, user email, API key owner, or local machine identity to influence these fields.
@@ -77,8 +77,6 @@ The landed contract:
**Providers see that traffic comes from the harness.** That is the point, but it means deployments that previously blended into generic SDK traffic become identifiable. Mitigation: send only static public product data and let forks/white-label deployments pass their own `AppIdentity`.
-**The app URL points at a repository that does not exist yet.** `deepseek-ai/deepseek-harness-sdk` is the planned public home; until it is created the URL is a dangling promise that blocks release.
-
**Header support differs by client library.** The hand-rolled adapter sets headers directly; the pi-ai-backed adapter depends on pi-ai continuing to honor `StreamOptions.headers` (merged last over provider defaults). The wire-level mock-server tests are the guard: if a pi-ai upgrade stops delivering the header, the suite goes red. This is useful pressure on the abstraction: a provider adapter that cannot set mandatory headers cannot fully implement the harness LLM contract.
**OpenRouter rankings do not benefit yet.** `User-Agent` is the correct baseline for provider-neutral HTTP identity, but it will not create OpenRouter app pages or rankings because OpenRouter requires `HTTP-Referer` for that product feature. That is deliberate: public app marketplace participation is a separate product decision, not a prerequisite for mandatory request attribution.
diff --git a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md
index 43f3568879..bfd6aa2540 100644
--- a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md
+++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md
@@ -32,7 +32,7 @@ OpenRouter 应用归属刻意未实现。`HTTP-Referer`、`X-OpenRouter-Title`
- `User-Agent` 的产品 token:`deepseek-harness`(与 Agent Note 之前的线路值及仓库/组织身份保持连续性)
- 版本:通过 `createRequire` 从所属包的 manifest(元数据清单)读取,绝不手动复制常量
-- 应用 URL:`https://github.com/deepseek-ai/deepseek-harness-sdk`——计划中的公开主页,且必须在发布前实际存在
+- 应用 URL:`https://github.com/deepseek-ai/deepseek-harness`——仓库主页
默认值是强制的且非空。白标部署通过向 `attributionHeaders(identity)` 传入自己的 `AppIdentity` 来覆盖——覆盖钩子就是函数参数,在有消费方需要之前不做部署配置管道——省略时回退到 harness 默认值而非抑制归属。没有逐请求 API 允许模型、用户提示词、会话 id、cwd、用户邮箱、API key 所有者或本地机器身份影响这些字段。
@@ -77,8 +77,6 @@ OpenRouter 应用归属刻意未实现。`HTTP-Referer`、`X-OpenRouter-Title`
**提供方看到流量来自 harness。** 这正是目的,但意味着此前混在通用 SDK 流量中的部署变得可识别。缓解措施:仅发送静态公开产品数据,并允许 fork/白标部署传入自己的 `AppIdentity`。
-**应用 URL 指向一个尚不存在的仓库。** `deepseek-ai/deepseek-harness-sdk` 是计划中的公开主页;在它创建之前,该 URL 是一个阻塞发布的悬空承诺。
-
**不同客户端库的头部支持有差异。** 手写适配器直接设置头部;基于 pi-ai 的适配器依赖 pi-ai 继续尊重 `StreamOptions.headers`(最后合并覆盖提供方默认值)。线路级 mock 服务器测试是守卫:如果 pi-ai 升级后不再投递该头部,套件会变红。这对抽象施加了有益的压力:一个无法设置强制头部的提供方适配器不能完整实现 harness 的 LLM 约定。
**OpenRouter 排名尚未受益。** `User-Agent` 是提供方无关的 HTTP 身份的正确基线,但它不会创建 OpenRouter 应用页面或排名,因为 OpenRouter 要求 `HTTP-Referer` 来实现该产品功能。这是有意为之:公开应用市场参与是一个独立的产品决策,不是强制请求归属的前提。
diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
index 8bef07d042..a48de33ce2 100644
--- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
-2026-07-10-single-file-executable-sdk-runtime-distribution.md: a45678c9bb5fcae340ff7134687890879f56c630
-2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: f1fccc508471356dd6434da0e126ed38f15ed3ba
+2026-07-10-single-file-executable-sdk-runtime-distribution.md: c2b6d9ff1825915e39738bf8f782c302ecfc1d0d
+2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: d7758a77083e07b1d2cac99ae2be3f15e6edd2dc
diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
index a45678c9bb..c2b6d9ff18 100644
--- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
+++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
@@ -34,27 +34,27 @@ Config discovery has two channels and fails loudly when both are missing: the `D
### Plugin resolution: the VFS holds a real package tree, the closure manifest IS the deploy root
-Inside the exe's VFS sits a **real package tree in build-artifact form** (each package's `lib/` plus a real `node_modules`); the Loader resolves plugin names through standard dynamic `import()`: bare specifiers resolve upward along `node_modules` from the Loader's position inside the VFS, and land inside the VFS naturally. The closed set needs no allowlist code — the set is whatever the VFS has installed, and importing a name outside the set fails.
+Inside the exe's VFS sits a **real package tree in build-artifact form** (each package's `lib/` plus a real `node_modules`). The packaged JSON-RPC entry supplies its installed harness base to app-boot's root Include: relative plugin specifiers resolve from the external configuration directory, while bare package names resolve from the VFS, so a configuration inside another Node project cannot shadow the packaged plugin set. The ordinary development bin leaves bare packages configuration-owned. Bare specifiers in the packaged entry resolve upward along `node_modules` from the entry's position inside the VFS and land inside the VFS naturally. The closed set needs no allowlist code — the set is whatever the VFS has installed, and importing a name outside the set fails.
The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json) (`dsh-jsonrpc-agent-pkg`, a pnpm workspace member and a zero-code pure dependency manifest) — the unified source of truth for "which plugins the exe ships" and "what the Python runtime distributes". Adding a plugin to the exe = adding one dependency line to the manifest and repackaging. [`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) traverses every workspace package covered by that manifest and requires every non-optional workspace peer at the runtime root, reporting the complete referencing-package → missing-peer chain; `pnpm run hygiene`, CI static, and the single-exe build run it before packaging. Deploy also packs by each package's `files`, so the shared chunks tsdown splits out must be covered by `files`.
### Build pipeline and artifacts
-[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → stage the target `node-pty` addon → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg--` land in `dist-exe/` and are copied back into the runtime directory. Linux installs build `pty.node` from source, so the builder copies it from the root install into the staged closure because legacy deploy omits that side-effect directory; macOS uses its target prebuild and emits the required `-spawn-helper` beside the executable. CI treats these products as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted yields a zero-symlink file tree (most stable for the pkg VFS, physically guaranteeing a single cordis instance); disabling automatic peer installation keeps unpublished package names from triggering registry resolution; link-workspace-packages points the closure at workspace/vendor sources.
+[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → restore any direct workspace package that legacy deploy hoisted back under the source manifest's `node_modules`, omitting its package-local dependency tree and rejecting any remaining manifest gap → replace every staged dependency symlink with its target bytes, remove package-manager `.bin` links, and fail if any symlink remains → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → stage the target `node-pty` addon → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg--` land in `dist-exe/` and are copied back into the runtime directory. Linux installs build `pty.node` from source, so the builder copies it from the root install into the staged closure because legacy deploy omits that side-effect directory; macOS uses its target prebuild and emits the required `-spawn-helper` beside the executable. CI treats these products as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry.
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml), triggered explicitly only — `workflow_dispatch`, or the `build-exe` label on a pull request; native builds on the three platforms linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached; macOS ad-hoc signing is handled by pkg. Each leg drives a mock SSE model through the SDK with the default config and a custom `cordis.yml`, drives the exe directly over NDJSON JSON-RPC, verifies the JSONL and final response, and installs release-shaped wheels into a clean venv without `runtime_bin`; Linux additionally inspects GLIBC requirements and runs in a manylinux 2.28 container. A full three-target run retains four artifacts, each containing one release file: the platform-independent SDK wheel and three native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts only `python-vX.Y.Z` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and three native runtime wheels, then a single serialized job checks and publishes all four to the project PyPI registry. Windows is a non-goal.
### Python SDK distribution: two carriers, exe for production, node for development
-The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds the checked-in default `runtime/cordis.yml`, the build-injected platform exe and optional helper, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions.
+The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds the checked-in default `runtime/cordis.yml`, the build-injected platform exe and optional helper, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions.
-[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative stable `X.Y.Z` from the repository root `package.json` and stages both packages at that version, with the SDK depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. An optional `python-vX.Y.Z` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe, and the macOS wheel also contains its architecture-matched helper. Runtime wheels use one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra helpers, and unsupported platforms.
+[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative stable `X.Y.Z` from the repository root `package.json` and stages both packages at that version, with `deepseek-harness-sdk` depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. An optional `python-vX.Y.Z` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe, and the macOS wheel also contains its architecture-matched helper. Runtime wheels use one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra helpers, and unsupported platforms.
The exe's "must be explicitly configured" hard semantic is unchanged; the zero-config experience is restored by the wrapper: when the caller gave no `cordis`, named no explicit runtime, and the environment has no `DSH_CORDIS_CONFIG`, the client explicitly injects the checked-in default `cordis.yml` (agent-core + preloaded llm-deepseek + JSONL persistence + bash-local + the `dsh-jsonrpc` serving entry, with `!!js` environment-variable fallbacks) via `DSH_CORDIS_CONFIG`.
### Naming lineage
-`@deepseek-ai/dsh-jsonrpc-demo` (the package) → `dsh-jsonrpc-agent` (the bin) → `dsh-jsonrpc-agent-pkg` (the closure manifest; no scope prefix, deliberately sidestepping the constraints' package-shape rules for `@deepseek-ai/dsh-*`) → `dsh-jsonrpc-agent-pkg--` (the exe artifacts). The wire `serverInfo.name` stays `deepseek-harness-sdk-runtime` (a protocol-stable value); the Python dist names are `deepseek-harness` / `deepseek-harness-runtime-bin`.
+`@deepseek-ai/dsh-jsonrpc-demo` (the package) → `dsh-jsonrpc-agent` (the bin) → `dsh-jsonrpc-agent-pkg` (the closure manifest; no scope prefix, deliberately sidestepping the constraints' package-shape rules for `@deepseek-ai/dsh-*`) → `dsh-jsonrpc-agent-pkg--` (the exe artifacts). The wire `serverInfo.name` stays `deepseek-harness-sdk-runtime` (a protocol-stable value); the Python distribution names are `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`, while the import modules remain `deepseek_harness` / `deepseek_harness_runtime`.
## Disposition of worker-style plugins
@@ -62,7 +62,7 @@ The exe's "must be explicitly configured" hard semantic is unchanged; the zero-c
## Testing
-The verification surface has three tiers. Mechanism tier: the measured conclusions for the `--sea` chain are embedded in the Decision sections (ESM dynamic import inside the VFS, single cordis instance, fail-loud config chain, `node:sqlite`, macOS ad-hoc signing runs). SDK tier: the complete keyless pytest suite covers the client protocol against a fake runtime peer, subprocess cleanup, absolute cwd propagation, dual-carrier launch, and carrier resolution; root CI runs it on Python 3.10. End-to-end tier: every platform build completes a turn against a mock endpoint through the default SDK path, a custom config, and the direct binary protocol, with final text and JSONL checked. The custom config additionally drives `run_code` and a zero-agent `workflow` through their real worker files inside the packaged VFS. The same build leg runs a committed executable-specific snapshot through the Python SDK: a keyless scripted model mounts a Cordis plugin that registers a tool, invokes that tool from `run_code`, runs a direct spawn subagent and a workflow that starts a second spawn child, then unmounts the plugin. The fixture explicitly disables its unused bundled Bash and local skill discovery so its tool set does not depend on repository-external state, and the comparison normalizes opaque message IDs in the SDK result and notification stream plus the parent and two child JSONL logs. This harness stays separate from ACP's `pnpm run test:snapshot` because the protocols and build artifacts differ. The platform wheel is then installed in a clean venv and run without `runtime_bin`.
+The verification surface has three tiers. Mechanism tier: the measured conclusions for the `--sea` chain are embedded in the Decision sections (ESM dynamic import inside the VFS, single cordis instance, fail-loud config chain, `node:sqlite`, macOS ad-hoc signing runs). SDK tier: the complete keyless pytest suite covers the client protocol against a fake runtime peer, subprocess cleanup, absolute cwd propagation, dual-carrier launch, and carrier resolution; root CI runs it on Python 3.10. End-to-end tier: every platform build completes a turn against a mock endpoint through the default SDK path, a custom config, the checked-in standalone minimal composition, and the direct binary protocol, with final text and JSONL checked. The minimal run asserts its exact system prompt and two-tool catalog, retains Bash state across calls, and invokes the editor. The custom config additionally drives `run_code` and a zero-agent `workflow` through their real worker files inside the packaged VFS. The same build leg runs a committed executable-specific snapshot through the Python SDK: a keyless scripted model mounts a Cordis plugin that registers a tool, invokes that tool from `run_code`, runs a direct spawn subagent and a workflow that starts a second spawn child, then unmounts the plugin. The fixture explicitly disables its unused bundled Bash and local skill discovery so its tool set does not depend on repository-external state, and the comparison normalizes opaque message IDs in the SDK result and notification stream plus the parent and two child JSONL logs. This harness stays separate from ACP's `pnpm run test:snapshot` because the protocols and build artifacts differ. The platform wheel is then installed in a clean venv and run without `runtime_bin`.
Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disposes immediately, so a short-lived pipe aborts an in-flight turn — pipe-driven runs must keep stdin open until the turn ends.
diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
index f1fccc5084..d7758a7708 100644
--- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
@@ -34,27 +34,27 @@ exe 使用 [@yao-pkg/pkg](https://github.com/yao-pkg/pkg)(vercel/pkg 归档后
### 插件解析:VFS 装载真实包树,闭包 manifest(元数据清单)就是部署根目录
-exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真实 `node_modules`)。loader 通过标准动态 `import()` 解析插件名:裸包名从 VFS 内 loader 所在位置沿 `node_modules` 向上解析,自然落在 VFS 内。封闭集不需要白名单代码——VFS 中安装了什么,集合中就有什么;`import()` 集合外的名称会失败。
+exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真实 `node_modules`)。打包专用 JSON-RPC 入口会向 app-boot 的根 Include 提供自身已安装 harness 的基准位置:相对插件说明符从外部配置目录解析,裸包名则从 VFS 解析,因此位于另一个 Node 项目内的配置无法遮蔽已打包的插件集合。普通开发 bin 仍由配置项目提供裸包。打包入口中的裸包名从该入口在 VFS 内的位置沿 `node_modules` 向上解析,自然落在 VFS 内。封闭集不需要白名单代码——VFS 中安装了什么,集合中就有什么;`import()` 集合外的名称会失败。
部署根目录是 [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json)(`dsh-jsonrpc-agent-pkg`,pnpm 工作区成员、零代码纯依赖 manifest),也是「exe 安装哪些插件」与「Python 运行时分发什么」的统一真源。向 exe 添加插件,就是在 manifest 中增加一行依赖后重新打包。[`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) 遍历该 manifest 覆盖的全部工作区包,要求每个非可选的工作区对等依赖(peer dependency)都显式列在运行时根目录,并报告“引用包 → 缺失对等依赖”的完整链路;`pnpm run hygiene`、CI 静态检查与 single-exe 构建都会在打包前运行该门禁。部署还会依据各包的 `files` 字段打包,因此 tsdown 拆出的共享分片必须被 `files` 覆盖。
### 构建管线与产物
-[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`;`assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg--` 写入 `dist-exe/`,并拷回运行时目录。Linux 安装会从源码构建 `pty.node`,而 `--legacy` 部署会省略该副作用目录,因此构建器会把它从根安装目录复制到暂存闭包;macOS 使用对应目标的预构建产物,并在可执行文件旁生成所需的 `-spawn-helper`。CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 产出无符号链接的文件树(对 pkg VFS 最稳定,并从物理上保证只有一个 Cordis 实例);关闭对等依赖自动安装可避免未发布包名触发注册表解析;`link-workspace-packages` 让闭包指向工作区/vendor 源码。
+[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 恢复被 legacy deploy 提升回源 manifest 的 `node_modules` 下的任何直接工作区包,同时省略其包内依赖树,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的每个符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js`;`assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg--` 写入 `dist-exe/`,并拷回运行时目录。Linux 安装会从源码构建 `pty.node`,而 `--legacy` 部署会省略该副作用目录,因此构建器会把它从根安装目录复制到暂存闭包;macOS 使用对应目标的预构建产物,并在可执行文件旁生成所需的 `-spawn-helper`。CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml),且只允许显式触发:手动派发 `workflow_dispatch`,或给 PR(Pull Request)添加 `build-exe` 标签。linux-x64、linux-arm64(`ubuntu-24.04-arm`)和 macos-arm64 三个平台分别进行原生构建,并缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都使用 mock SSE(Server-Sent Events)模型,分别通过默认配置和自定义 `cordis.yml` 驱动 SDK,再通过 NDJSON JSON-RPC 直接驱动 exe,校验 JSONL 与最终响应;最后把发布形态的 wheel 包安装到干净的 venv 中,并在不传 `runtime_bin` 的情况下运行。Linux 还会检查 GLIBC 依赖,并在 manylinux 2.28 容器中运行。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-vX.Y.Z` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。
### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发
-Python SDK 位于 [`python/`](../../../../python/README.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 与可选 helper,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**;`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。
+Python SDK 位于 [`python/`](../../../../python/README.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 与可选 helper,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**;`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。
-[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的稳定版本 `X.Y.Z`,以该版本暂存两个包,并让 SDK 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。可选的 `python-vX.Y.Z` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe,macOS wheel 包还包含与其架构匹配的 helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 或 `py3-none-macosx_11_0_arm64` 三种标签之一;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、helper 缺失或多余,以及不支持的平台。
+[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的稳定版本 `X.Y.Z`,以该版本暂存两个包,并让 `deepseek-harness-sdk` 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。可选的 `python-vX.Y.Z` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe,macOS wheel 包还包含与其架构匹配的 helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 或 `py3-none-macosx_11_0_arm64` 三种标签之一;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、helper 缺失或多余,以及不支持的平台。
exe「必须显式配置」的硬语义不变;零配置体验由包装层恢复:调用方没有提供 `cordis`、没有显式指定运行时,且环境中没有 `DSH_CORDIS_CONFIG` 时,客户端将检入的默认 `cordis.yml`(`agent-core` + 预载的 `llm-deepseek` + JSONL 持久化 + `bash-local` + `dsh-jsonrpc` 对外服务条目,并通过 `!!js` 使用环境变量兜底)显式注入 `DSH_CORDIS_CONFIG`。
### 命名血统
-`@deepseek-ai/dsh-jsonrpc-demo`(包)→ `dsh-jsonrpc-agent`(`bin`)→ `dsh-jsonrpc-agent-pkg`(闭包 manifest;没有作用域前缀,刻意避开 `constraints` 对 `@deepseek-ai/dsh-*` 的包形状规则)→ `dsh-jsonrpc-agent-pkg--`(exe 产物)。协议字段 `serverInfo.name` 保持为 `deepseek-harness-sdk-runtime`(协议稳定值);Python 分发名为 `deepseek-harness` / `deepseek-harness-runtime-bin`。
+`@deepseek-ai/dsh-jsonrpc-demo`(包)→ `dsh-jsonrpc-agent`(`bin`)→ `dsh-jsonrpc-agent-pkg`(闭包 manifest;没有作用域前缀,刻意避开 `constraints` 对 `@deepseek-ai/dsh-*` 的包形状规则)→ `dsh-jsonrpc-agent-pkg--`(exe 产物)。协议字段 `serverInfo.name` 保持为 `deepseek-harness-sdk-runtime`(协议稳定值);Python 分发包名为 `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`,导入模块名仍为 `deepseek_harness` / `deepseek_harness_runtime`。
## 工作线程插件
@@ -62,7 +62,7 @@ exe 内支持 `dsh-workflow-workerthread` 与 `dsh-code-runtime-worker`。两个
## 测试
-验证面分三层。机制层:`--sea` 链路的实测结论内嵌在「决策」各节(VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都通过默认 SDK 路径、自定义配置和直接二进制协议,对 mock 端点完成一个轮次,并校验最终文本与 JSONL。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。同一构建任务还会经 Python SDK 运行一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个直接 spawn 的 subagent 和一个会通过 spawn 启动第二个 subagent 的工作流,随后卸载该插件。该 fixture(测试前置数据)会显式禁用组合包中未使用的 Bash 和本地 skill(技能)发现,使其工具集不依赖仓库外部状态;比较时会规范化以下各处的不透明消息 ID:SDK 结果与通知流,以及父会话和两个子会话的 JSONL 日志。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。随后把平台 wheel 包安装进干净的 venv,并在不传 `runtime_bin` 的情况下运行。
+验证面分三层。机制层:`--sea` 链路的实测结论内嵌在「决策」各节(VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都通过默认 SDK 路径、自定义配置、仓库内置的独立 minimal 组合和直接二进制协议,对 mock 端点完成一个轮次,并校验最终文本与 JSONL。minimal 运行会断言其精确系统提示词与双工具目录,跨调用保留 Bash 状态,并调用编辑器。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。同一构建任务还会经 Python SDK 运行一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个直接 spawn 的 subagent 和一个会通过 spawn 启动第二个 subagent 的工作流,随后卸载该插件。该 fixture(测试前置数据)会显式禁用组合包中未使用的 Bash 和本地 skill(技能)发现,使其工具集不依赖仓库外部状态;比较时会规范化以下各处的不透明消息 ID:SDK 结果与通知流,以及父会话和两个子会话的 JSONL 日志。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。随后把平台 wheel 包安装进干净的 venv,并在不传 `runtime_bin` 的情况下运行。
手工驱动注意:`bin` 将 stdin EOF 视为「客户端已离开」并立即 dispose,短命管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml
index bc2d26325d..3d370c1e37 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
-2026-07-19-gui-layering-and-rpc-protocol.md: 514deb890d4e08d465db869669078473d32fb215
-2026-07-19-gui-layering-and-rpc-protocol.zh.md: f6fa71e3dac25f48b2ad4744a0cc695417528b34
+2026-07-19-gui-layering-and-rpc-protocol.md: da96ae97f2a2d64aeef7794bd82ccbd86602b1ad
+2026-07-19-gui-layering-and-rpc-protocol.zh.md: 36dc7391bc3f9bb0d5105fea14a2763d0b7159a1
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
index 514deb890d..da96ae97f2 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
@@ -10,7 +10,7 @@ English | [中文](2026-07-19-gui-layering-and-rpc-protocol.zh.md)
We need a UI integration layer. Beyond the existing ACP/stdio baseline, more product clients are coming — Web (server), Electron, and others. We call them Clients and want the following capabilities:
-- One `dsh` process supporting both `dsh web` (serve) and `dsh run` (headless) — one process, two modes (a design reservation)
+- One `dsh` process supporting both `dsh web` (serve) and `dsh --profile headless` (headless) — one process, two modes (a design reservation)
- Launching inside Electron with the same Web technologies as `dsh web`
That demands a stable layered responsibility model in the engineering codebase, so future clients plug in cleanly.
@@ -31,7 +31,7 @@ Directories layer as follows:
- **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dsh.client` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
- `apps/` holds the externally exported applications, assembled from Client / Host mixtures.
- `apps/web` (`dsh-frontend`) is the vite application: a thin `main.ts` over the shell surface exported by `dsh-client-web`.
- - `apps/cli` (`@deepseek-ai/dsh`) dispatches commands: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh run` = [a direct core Agent/Session entry point](2026-08-09-headless-direct-core-entry-point.md), with zero Host, HTTP, or browser layer.
+ - `apps/cli` (`@deepseek-ai/dsh`) dispatches commands: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh --profile headless` = [a direct core Agent/Session entry point](2026-08-09-headless-direct-core-entry-point.md), with zero Host, HTTP, or browser layer.
- A future Electron application reuses the same web client packages over an IPC fetch carrier.
```
@@ -79,7 +79,7 @@ Packages under `packages/host/*` and `packages/client/*` **must carry the direct
2. **Write an assembly module under `apps/`**: `startHost()` + a client subclass + the application's private signal/print/exit semantics; a mixture never becomes a package — assembly is written in the app.
3. **Import `dsh-host-webserver` only if you need HTTP carriage**, otherwise zero ports.
-The two existing applications preserve the division: the Web application mounts Host, carrier, and browser composition, while `dsh run` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem and mount directly via `ctx.plugin(entry-point plugin)` without fetch.
+The two existing applications preserve the division: the Web application mounts Host, carrier, and browser composition, while `dsh --profile headless` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem and mount directly via `ctx.plugin(entry-point plugin)` without fetch.
## Message protocol
@@ -215,7 +215,7 @@ All four quadrant full forms pass through `onEnvelope`; the base implementation
| Subclass | Package | doFetch | Purpose |
|---|---|---|---|
-| `InProcessApiClient` | apiproxy itself | the injected `{ fetch }` handler | **The isomorphic point**: `new InProcessApiClient(toFetchHandler(api))` never touches the network yet runs the real wire serialization/zod/SSE framing; carrier tests and callers can exercise the protocol without opening a port, while product `dsh run` drives core directly |
+| `InProcessApiClient` | apiproxy itself | the injected `{ fetch }` handler | **The isomorphic point**: `new InProcessApiClient(toFetchHandler(api))` never touches the network yet runs the real wire serialization/zod/SSE framing; carrier tests and callers can exercise the protocol without opening a port, while product `dsh --profile headless` drives core directly |
| `WebApiClient` | dsh-client-connection | `globalThis.fetch` uplink + one same-origin WebSocket downlink per logical stream | the browser client; physical boundary in the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) |
| `FixtureApiClient` | dsh-client-connection | unused (protocol-layer override) | serverless UI development (`?fixture`): overrides the `callUnary`/`openMux`/`openHost`/`respond` virtuals and is itself the fake server (frame rpcIds minted by it, semantics self-consistent) |
| IPC bridge subclass (hypothetical example — no such shell exists) | an Electron shell | IPC serialization round trip | would swap only doFetch; contract and base class unchanged |
diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md
index f6fa71e3da..36dc7391bc 100644
--- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md
@@ -9,7 +9,7 @@ Status: implemented
## Problem
需要提供 UI 对接层,除已有 ACP(Agent Client Protocol)/stdio 基线外,还需要 Web(server)、Electron 等其他产品客户端。我们把它们统一称为 Client。希望具备以下能力:
-- 一个 `dsh` 进程同时支持 `dsh web`(启动)和 `dsh run`(headless),一个进程两种模式(设计预留)
+- 一个 `dsh` 进程同时支持 `dsh web`(启动)和 `dsh --profile headless`(headless),一个进程两种模式(设计预留)
- 在 Electron 中使用与 `dsh web` 相同的 Web 技术启动
那么当前的工程代码需要稳定的分层职责模型,便于以后接入各类 client。
@@ -29,7 +29,7 @@ Status: implemented
- **fetch 到达插件包**(`ui-layout`、`ui-sidebar`、`ui-conversation`、`ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dsh.client` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布(tsdown 闭包工厂 bundle)。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。
- `apps/` 作为对外导出的应用入口,可以由 Client / Host 混合组装。
- `apps/web`(`dsh-frontend`)是 vite 应用:`dsh-client-web` 导出的壳表面之上的一层薄 `main.ts`。
- - `apps/cli`(`@deepseek-ai/dsh`)分发命令:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist;`dsh run` = [直接使用核心 Agent/Session 的入口](2026-08-09-headless-direct-core-entry-point.md),不含 Host、HTTP 或浏览器层。
+ - `apps/cli`(`@deepseek-ai/dsh`)分发命令:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist;`dsh --profile headless` = [直接使用核心 Agent/Session 的入口](2026-08-09-headless-direct-core-entry-point.md),不含 Host、HTTP 或浏览器层。
- 将来的 Electron 应用经由 IPC fetch 载体复用同一套 web client 包。
```
@@ -77,7 +77,7 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
2. **在 `apps/` 下写拼装模块**:`startHost()` + 客户端子类 + 该应用私有的信号/打印/退出语义;混合体不建包,拼装写在 app 里。
3. **需要 HTTP 承载才 import `dsh-host-webserver`**,否则零端口。
-现有两个应用保持这一区分:Web 应用挂载 Host、载体与浏览器组合,而 `dsh run` 挂载直接使用核心服务的 runner,不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(入口插件)` 挂载,不使用 fetch。
+现有两个应用保持这一区分:Web 应用挂载 Host、载体与浏览器组合,而 `dsh --profile headless` 挂载直接使用核心服务的 runner,不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(入口插件)` 挂载,不使用 fetch。
## 消息协议
@@ -213,7 +213,7 @@ export type ResponseValue =
| 子类 | 所在包 | doFetch | 用途 |
|---|---|---|---|
-| `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**:`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧;载体测试与调用方可以在不打开端口的情况下运行这套协议,而产品 `dsh run` 直接驱动 core |
+| `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**:`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧;载体测试与调用方可以在不打开端口的情况下运行这套协议,而产品 `dsh --profile headless` 直接驱动 core |
| `WebApiClient` | dsh-client-connection | `globalThis.fetch` 上行 + 每逻辑流一条同源 WebSocket 下行 | 浏览器客户端;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.md) |
| `FixtureApiClient` | dsh-client-connection | 不用(协议层覆写) | 无 server 的 UI 开发(`?fixture`):覆写 `callUnary`/`openMux`/`openHost`/`respond` 虚方法,自己就是假 server(帧 rpcId 由它 mint,语义自洽) |
| IPC 桥子类(假想示例——尚无此形态) | Electron 壳 | IPC 序列化往返 | 只需换 doFetch,约定/基类零改 |
diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml
index 327eeef2fb..98319ccd48 100644
--- a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md
-2026-07-25-web-client-session-scope-and-provide-channel.md: ff411e1b387dcd83b04f8c02d5dde3c70a815932
-2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 593e194654bb3552ad3cac4534847fba11cf3694
+2026-07-25-web-client-session-scope-and-provide-channel.md: f371b93ccf6cb3ba10cbdb73baa670cbf889f393
+2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 78440131c4e09c9a458009fbe5a2a34a707a481c
diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md
index ff411e1b38..f371b93ccf 100644
--- a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md
+++ b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md
@@ -93,7 +93,7 @@ Slot scope is the closed set `root | session-maybe | session`:
- `session-maybe` follows the current session with ADOPTION identity (the only behavior — there is no hold-identity-forever mode): an incarnation born session-less keeps its React instance across the arrival of the FIRST session (the blank shell adopts it — no remount, the DOM survives), and from then on behaves exactly like a strict session entry — switching to a different session remounts, and dropping back to no-session remounts into a fresh blank incarnation that will adopt again. Component-local per-session state therefore clears by construction; state that must survive a switch belongs in session-bound sources (machine, store, hooks). With no session, `sessionId`, the results of `useSession`/`useInput`, and `inputActions` may all be absent. The unkeyed root `SessionMaybeProvider` drives these updates by subscribing to the runtime's atomic `currentProvide` projection — selection moves and provider-roster changes publish through the same source, so a roster change under a stable current id republishes the mounted bundle instead of stranding entries on an obsolete hook/prop schema — while `SessionMaybeProvideInfo` uses the static key map to retain the complete hook/prop shape even with no session; the per-entry adoption bookkeeping (incarnation-counter key) lives in the renderer's `SessionMaybeEntry`.
- `session` guarantees that `sessionId`, every hook source, and every prop exist; each strict entry's error boundary is keyed by `sessionId`, so switching sessions recreates that entry and its session store.
-`conversation` is the resident `session-maybe` shell: `ConversationRoot`, HeroShell, the Workspace picker, the root-owned scrollport and composer stack, and the overlay chain's fallback frame retain their React instances across the no-session → blank-session switch. Two strict entries fill fixed regions without reparenting that tree: `conversation.session.header` carries breadcrumb/tabs/actions above the scrollport, while `conversation.session` carries the view ring and draft mirror inside it; both share the same session-scoped chat store. The composer bar (`conversation.composer.bar`) is itself `session-maybe`: with no session it renders inert (machine faces absent, `disabled` owner prop), and the same instance — textarea included — goes live when a session appears; the remaining input slots stay strict `session` and dispatch nothing until then. The blank → engaging/active transition never rebuilds the InputBar on a phase flip.
+`conversation` is the resident `session-maybe` shell: `ConversationRoot`, HeroShell, the Workspace picker, the root-owned scrollport and composer stack, and the overlay chain's fallback frame retain their React instances across the no-session → blank-session switch. Two strict entries fill fixed regions without reparenting that tree: `conversation.session.header` carries breadcrumb/tabs/actions above the scrollport, while `conversation.session` carries the view ring and draft mirror inside it; both share the same session-scoped chat store. The composer bar (`conversation.composer.bar`) is itself `session-maybe`: with no session its machine faces and message actions are inert, while the whole dashed card opens the existing Workspace picker by pointer and its read-only textarea does the same through Enter or Space. The same instance — textarea included — goes live when a session appears; the remaining input slots stay strict `session` and dispatch nothing until then. The blank → engaging/active transition never rebuilds the InputBar on a phase flip.
- The runtime's first built-in entry: the `'session'` hook — `useSession` itself rides the same mechanism, no special-casing.
- Concurrent discipline: the render plane reads only from the hooks compartment (uSES consistency guarantee); props-compartment callbacks are used only in event-handler space; descriptor resolution is render-safe (idempotent caching, with prune reaping residue from abandoned renders).
@@ -132,5 +132,5 @@ Slot scope is the closed set `root | session-maybe | session`:
- Plugins gain session context isomorphic to the host's: per-session state hangs on the actx and mounts/tears down in one piece with the scope fiber, making leaks structurally impossible; two-session isolation is structurally guaranteed by the scope filter.
- The client object layer converges to a wire mirror: session identity, lifecycle, and capability adjudication all defer to the host entity — the input system (the next layer) always faces a session with a real Agent, and providers like slash/skill uniformly address by sessionId directly.
- Blank-session governance takes zero dedicated mechanisms: state rides one derived bit, visibility rides the unified list projection (only the current blank shows, as `New Session`), reclamation rides lazy persistence's existing contract (evaporation on restart), and the ordinary ceiling rides same-Workspace reuse.
-- The cost: the id→ctx handoff discipline and provide's Concurrent discipline are conventions rather than type-enforced, pinned by review and tests; fully disabled input while no workspace is picked is an experience cost the product surface accepts (the price of the single state axis).
+- The cost: the id→ctx handoff discipline and provide's Concurrent discipline are conventions rather than type-enforced, pinned by review and tests. The single state axis still withholds machine faces until a Session exists; the resident card routes activation to the Workspace picker during that interval ([decision](../feature/2026-08-07-workspace-picker-composer-entry.md)).
- Known gaps: approval/question recovery across prune (TODO); model selection returns in live-mutation shape (the host `selectModel` trio is ready-made, its client consumer not yet built).
diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md
index 593e194654..78440131c4 100644
--- a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md
@@ -93,7 +93,7 @@ slot scope 是闭集 `root | session-maybe | session`:
- `session-maybe` 以**收养(adoption)身份语义**跟随 current session(唯一行为——不存在「永久保持实例」模式):空态出生的化身在**第一个** session 到来时保持 React 实例(空壳收养它——不重挂,DOM 存活);此后行为与严格 session entry 完全一致——切到不同 session 重挂,跌回无 session 也重挂为崭新的空态化身(之后再次收养)。因此组件本地的 per-session 状态**由构造保证**随切换清零;需要活过切换的状态必须住 session 绑定的源(machine、store、hooks)。无 session 时 `sessionId`、`useSession`/`useInput` 的选择结果及 `inputActions` 均可缺省。根部无 key 的 `SessionMaybeProvider` 通过订阅 runtime 的原子 `currentProvide` 投影驱动这条更新——选择移动和提供方名册变化经同一 source 发布,current id 不变时的名册变化也会重发已挂载 bundle,而不是把 entry 困在过期的钩子/prop 形状上——`SessionMaybeProvideInfo` 靠静态键表在无 session 时仍保留完整钩子/prop 形状;逐 entry 的收养记账(化身计数 key)住在 renderer 的 `SessionMaybeEntry`。
- `session` 保证 `sessionId`、所有钩子 source 与 props 均存在;每个严格 entry 的错误边界以 `sessionId` 为 key,切换 session 会重建该 entry 及其 session store。
-`conversation` 是 `session-maybe` 的常驻外壳:`ConversationRoot`、HeroShell、Workspace picker、root 持有的 scrollport 与 composer stack,以及 overlay chain 的 fallback 外框,在无 session → blank session 的切换中保持 React 实例。两个严格 session entry 只填入固定区域,不改变该树的父级:`conversation.session.header` 在 scrollport 上方承载 breadcrumb/tab/action,`conversation.session` 在其内部承载 view ring 与 draft mirror;二者共享同一个 session scope chat store。composer bar(`conversation.composer.bar`)本身即为 `session-maybe`:无 session 时以惰性态渲染(machine face 缺席、`disabled` owner prop),session 出现后同一实例(含 textarea)转为 live;其余输入 slot 保持严格 `session`,在此之前不分发任何条目。blank → engaging/active 的 InputBar 不因 phase 翻转而重建。
+`conversation` 是 `session-maybe` 的常驻外壳:`ConversationRoot`、HeroShell、Workspace picker、root 持有的 scrollport 与 composer stack,以及 overlay chain 的 fallback 外框,在无 session → blank session 的切换中保持 React 实例。两个严格 session entry 只填入固定区域,不改变该树的父级:`conversation.session.header` 在 scrollport 上方承载 breadcrumb/tab/action,`conversation.session` 在其内部承载 view ring 与 draft mirror;二者共享同一个 session scope chat store。composer bar(`conversation.composer.bar`)本身即为 `session-maybe`:无 session 时,其 machine face 和消息操作保持惰性,整张虚线卡片可经指针打开现有 Workspace picker,只读 textarea 也可通过 Enter 或 Space 打开。session 出现后同一实例(含 textarea)转为 live;其余输入 slot 保持严格 `session`,在此之前不分发任何条目。blank → engaging/active 的 InputBar 不因 phase 翻转而重建。
- 运行时内建第一条:`'session'` 钩子——`useSession` 本身走同一机制,无特判。
- Concurrent 纪律:渲染平面只从 hooks 格读(uSES 一致性保证);props 格回调只在事件 handler 空间用;描述符解析 render-safe(幂等缓存、废弃渲染残留由 prune 收尸)。
@@ -132,5 +132,5 @@ slot scope 是闭集 `root | session-maybe | session`:
- 插件获得与 host 同构的会话上下文:逐会话状态挂 actx、随 scope fiber 一次拆装,泄漏结构性不可能;双会话隔离由 scope filter 结构性保证。
- client 对象层收敛为 wire 镜像:会话身份、生命周期、能力判别全部以 host 实体为准——输入体系(下一层)面对的永远是「有真 Agent 的会话」,slash/skill 等提供方一律以 sessionId 直接寻址。
- 空会话治理零专用机制:状态靠一个派生位,可见性靠统一列表投影(仅 current blank 以 `New Session` 展示),回收靠 lazy persistence 的既有约定(重启蒸发),常规上限靠同 Workspace 复用。
-- 代价:id→ctx 换乘纪律、provide 的 Concurrent 纪律都是约定而非类型强制,靠 review 与测试钉住;「未选 workspace」期间输入全禁是产品面接受的体验代价(单一状态轴换来的)。
+- 代价:id→ctx 换乘纪律、provide 的 Concurrent 纪律都是约定而非类型强制,靠 review 与测试钉住。单一状态轴仍会在 Session 存在前隐藏 machine face;这段时间内,常驻卡片会把激活操作转到 Workspace picker([决策](../feature/2026-08-07-workspace-picker-composer-entry.md))。
- 已知欠账:approval/question 跨 prune 恢复(TODO);模型选择以 live-mutation 形状回归(host `selectModel` 三件套现成,其 client 消费方尚未构建)。
diff --git a/.agents/notes/implemented/architecture/2026-07-28-experimental-plugin-package-group.md b/.agents/notes/implemented/architecture/2026-07-28-experimental-plugin-package-group.md
deleted file mode 100644
index 1ebae5dbb1..0000000000
--- a/.agents/notes/implemented/architecture/2026-07-28-experimental-plugin-package-group.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Agent Note: Experimental and internal package group
-
-Status: implemented
-
-English | [中文](2026-07-28-experimental-plugin-package-group.zh.md)
-
-## Problem
-
-The [package hierarchy](../../../../packages/README.md) groups plugins by product role, but it cannot distinguish release packages from prototypes or internal-only packages. The team needs an obvious shared place for useful work that is not part of the official release.
-
-## Decision
-
-The subtree rules in [`packages/experimental/AGENTS.md`](../../../../packages/experimental/AGENTS.md) make `packages/experimental//` the required home for Cordis plugin packages whose whole public contract is experimental or internal-only. Package names remain `@deepseek-ai/dsh-`.
-
-The group is the team's in-repository place to share engineering and product-manager prototypes: members can discover, run, review, and extend one another's work against the real plugin graph without implying product support.
-
-Official releases exclude this directory. A package enters a release only after moving to its product-role group; release packages cannot take runtime dependencies on packages here. Examples may use them, while any other runtime dependent also belongs here. Tests may use them as development dependencies.
-
-Experimental packages carry no stability, compatibility, migration, or support promise: they may change APIs, configuration, or data, or disappear without deprecation or migration. Internal-only packages may define narrower internal contracts but make no public release promise. Neither status relaxes engineering, security, documentation, lifecycle, testing, or snapshot requirements.
-
-The pending `@deepseek-ai/dsh-tui-session-changes` `/diff` viewer and `/btw` plugin are examples governed by this rule. Promotion into an official release requires explicit review of the public contract, limitations, test evidence, and a named owner accepting stable-package obligations.
-
-## Alternatives considered
-
-**Keep experimental and internal-only packages in product-role groups with README labels.** Labels are easy to miss and cannot enforce dependency boundaries.
-
-**Treat every package as experimental until the first tagged release.** This provides no durable incubation boundary.
-
-**Develop prototypes and internal packages elsewhere.** This loses the real plugin graph, examples, snapshots, and lifecycle checks needed to evaluate them.
-
-## Consequences
-
-The path makes release exclusion and dependency blast radius visible while retaining the real plugin graph for team sharing. It gives up product-role colocation and creates path churn on promotion, while the npm name remains stable. The subtree rules, repository [current-owner/current-need rule](../../../../packages/AGENTS.md), and unchanged engineering gates limit junk-drawer growth. Because official release tooling does not yet exist, contributor policy enforces the exclusion; when such tooling is added, the directory is its required exclusion boundary.
diff --git a/.agents/notes/implemented/architecture/2026-07-28-experimental-plugin-package-group.zh.md b/.agents/notes/implemented/architecture/2026-07-28-experimental-plugin-package-group.zh.md
deleted file mode 100644
index 2d09451c50..0000000000
--- a/.agents/notes/implemented/architecture/2026-07-28-experimental-plugin-package-group.zh.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Agent Note: 实验性与内部专用包分组
-
-Status: implemented
-
-[English](2026-07-28-experimental-plugin-package-group.md) | 中文
-
-## 问题
-
-[包层级结构](../../../../packages/README.md)按产品角色对插件分组,但无法区分发布包、原型和内部专用包。团队需要一个明确的共享位置,存放不属于官方发布版本的有价值成果。
-
-## 决策
-
-[`packages/experimental/AGENTS.md`](../../../../packages/experimental/AGENTS.md) 中的子树规则要求所有公开约定整体处于实验状态或仅限内部使用的 Cordis 插件包位于 `packages/experimental//`。包名仍为 `@deepseek-ai/dsh-`。
-
-该分组供团队在仓库内共享工程人员和产品经理制作的原型:成员可以基于真实插件图发现、运行、评审并扩展彼此的原型,但这不代表产品会提供支持。
-
-官方发布版本不包含此目录。包只有移入对应的产品角色分组后才会纳入发布版本;发布包不得在运行时依赖此处的包。示例可以使用这些包;其他任何运行时依赖方也必须位于此处。测试可以将它们用作开发依赖。
-
-实验性包不提供稳定性、兼容性、迁移或支持保证:其 API、配置或数据可以变更,包也可以移除,均不提供弃用期或迁移路径。内部专用包可以定义范围更窄的内部约定,但不作公开发布承诺。无论哪种状态,都不降低仓库对工程、安全、文档、生命周期、测试或快照的要求。
-
-尚待完成的 `@deepseek-ai/dsh-tui-session-changes` `/diff` 查看器和 `/btw` 插件都受这项规则约束。将包提升为稳定包并纳入官方发布版本,需要明确评审其公开约定、限制和测试证据,并指定一名愿意承担稳定包义务的负责人。
-
-## 考虑过的替代方案
-
-**将实验性和内部专用包留在产品角色分组中,并用 README 标注。** 标注容易被忽略,也无法强制执行依赖边界。
-
-**首个带标签的版本发布前,将所有包都视为实验性。** 这无法提供持久的孵化边界。
-
-**在其他位置开发原型和内部专用包。** 这会失去评估它们所需的真实插件图、示例、快照和生命周期检查。
-
-## 后果
-
-该路径明确标示不纳入发布版本的包及其依赖影响范围,同时保留供团队共享成果的真实插件图。代价是这些包无法与同产品角色的包共置,提升并纳入发布版本时还会产生路径变动,但 npm 包名保持稳定。子树规则、仓库已有的[「必须有当前负责人和实际需求」规则](../../../../packages/AGENTS.md)以及保持不变的工程门禁,可限制该分组无序膨胀。由于官方发布工具尚不存在,目前由贡献者政策执行这项排除规则;添加发布工具后,必须以该目录为排除边界。
diff --git a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml
index 8f17ecd480..92744d0ead 100644
--- a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md
-2026-07-29-dsh-source-launch-tsx-esm.md: ed22e51d59a25db130b3760ce484c116bade4348
-2026-07-29-dsh-source-launch-tsx-esm.zh.md: bdd549092eb30f7749c8f7561068daafe3548b28
+2026-07-29-dsh-source-launch-tsx-esm.md: b2428602a780f2880f0f803ba59b16a76b39790e
+2026-07-29-dsh-source-launch-tsx-esm.zh.md: 866bc8886725788e6e619f9507f5c8ccab63f82d
diff --git a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md
index ed22e51d59..b2428602a7 100644
--- a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md
+++ b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md
@@ -14,11 +14,11 @@ Startup latency also mattered: the off-thread `module.register()` hooks worker s
## Decision
-The `dsh` TUI, Web, and headless source launches run `node --import tsx/esm`: tsx's ESM-only hook owns both TypeScript transformation and tsconfig `paths` projection. `bin/dsh`, the root `dsh`/`demo:tui`/`demo:web` scripts, and the Code Mode TUI overlay use the same vector; `bin/dsh` references the hook and tsconfig by absolute checkout paths (bare `tsx/esm` does not resolve from an arbitrary cwd) and pins `TSX_TSCONFIG_PATH` to the root tsconfig. The CJS hook stays off because the CLI source graph is ESM-only; measured TUI time-to-banner is ~0.7s versus ~1.1s under the full tsx default and ~0.75s under the removed native chain.
+The `dsh` TUI, Web, and headless source launches run `node --import tsx/esm`: tsx's ESM-only hook owns both TypeScript transformation and tsconfig `paths` projection. The root `dsh` script completes the repository build, then uses that vector from the repository root. The CJS hook stays off because the CLI source graph is ESM-only; measured runtime launch to the TUI banner is ~0.7s versus ~1.1s under the full tsx default and ~0.75s under the removed native chain.
`scripts/tspath-loader.ts` and `apps/cli/src/tsconfig-paths-loader.ts` are deleted. With them went the loader's runtime rule of mapping a workspace import only for declared runtime dependencies — tsx applies the `paths` map unconditionally. Declaration completeness now rests on the static gates alone: `verify-cordis-config` for configured bare plugins, and workspace constraints for manifests. (That runtime rule found real bugs: `dsh-plan-mode` and `dsh-tool-tasks` imported `@deepseek-ai/dsh-llm` while declaring it only in devDependencies; since fixed.)
-The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`apps/cli/tests/source-launch.compat.spec.ts`): a keyless piped-stdio launch of the exact production vector asserting the non-zero-exit TTY refusal. Any future Node change to module hooks or TypeScript handling turns this gate red instead of breaking developers' `pnpm dsh`.
+The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`apps/cli/tests/source-launch.compat.spec.ts`): a keyless piped-stdio launch of the exact production runtime vector asserting the non-zero-exit TTY refusal. Any future Node change to module hooks or TypeScript handling turns this gate red instead of breaking developers' `pnpm dsh`.
## Alternatives considered
@@ -35,4 +35,4 @@ The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`
- One launch vector across the whole engines range, including future Node lines that change native TypeScript support; the smoke gate enforces it per matrix line.
- TypeScript transformation is delegated to tsx/esbuild again, reversing the prior note's goal of proving Node-native transformation; that goal is unreachable while vendored sources use non-erasable syntax and Node ships no transform mode.
- The runtime declared-dependency enforcement in source launches is gone; undeclared workspace imports now surface only through static gates or built-mode resolution failures.
-- Startup improves ~0.4s over the full tsx default (`demo:headless` now aliases the same `dsh run` source launch; ACP keeps `--import tsx` because its graph was not audited for CJS-hook dependence and its launch latency is not on the interactive path).
+- Runtime launch improves ~0.4s over the full tsx default; ACP keeps `--import tsx` because its graph was not audited for CJS-hook dependence and its launch latency is not on the interactive path.
diff --git a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md
index bdd549092e..866bc88867 100644
--- a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md
@@ -14,11 +14,11 @@ Status: implemented
## 决策
-`dsh` 的 TUI、Web 与无头源码启动运行 `node --import tsx/esm`:由 tsx 的 ESM-only 钩子同时负责 TypeScript 转换与 tsconfig `paths` 投影。`bin/dsh`、根目录的 `dsh`/`demo:tui`/`demo:web` 脚本以及 Code Mode TUI overlay 使用同一向量;`bin/dsh` 以 checkout 的绝对路径引用钩子与 tsconfig(裸的 `tsx/esm` 无法从任意 cwd 解析),并将 `TSX_TSCONFIG_PATH` 固定到根 tsconfig。CJS 钩子保持关闭,因为 CLI 源码图是纯 ESM;实测 TUI 到 banner 约 0.7s,对比完整 tsx 默认形态约 1.1s、已移除的原生链约 0.75s。
+`dsh` 的 TUI、Web 与无头源码启动运行 `node --import tsx/esm`:由 tsx 的 ESM-only 钩子同时负责 TypeScript 转换与 tsconfig `paths` 投影。根目录的 `dsh` 脚本先完成仓库构建,然后从仓库根目录使用同一启动方式。CJS 钩子保持关闭,因为 CLI(命令行界面)源码图是纯 ESM;实测运行时启动至 TUI banner 耗时约 0.7s,对比完整 tsx 默认形态约 1.1s、已移除的原生链约 0.75s。
`scripts/tspath-loader.ts` 与 `apps/cli/src/tsconfig-paths-loader.ts` 已删除。随之消失的还有该 loader「仅为已声明运行时依赖映射 workspace import」的运行时规则——tsx 无条件应用 `paths` 映射。声明完整性现在仅由静态门禁保障:配置的裸插件走 `verify-cordis-config`,manifest(元数据清单)走 workspace constraints。(该运行时规则确实发现过真实缺陷:`dsh-plan-mode` 与 `dsh-tool-tasks` 导入 `@deepseek-ai/dsh-llm` 却只声明在 devDependencies;后已修复。)
-node-compat CI 矩阵(Node 22.19 与 26)新增 `dsh-source-launch-smoke`(`apps/cli/tests/source-launch.compat.spec.ts`):以精确的生产启动向量做 keyless 管道 stdio 启动,断言非零退出的 TTY 拒绝。未来 Node 对模块钩子或 TypeScript 处理的任何改动都会让该门禁变红,而不是破坏开发者的 `pnpm dsh`。
+node-compat CI 矩阵(Node 22.19 与 26)新增 `dsh-source-launch-smoke`(`apps/cli/tests/source-launch.compat.spec.ts`):以精确的生产运行时启动向量做 keyless 管道 stdio 启动,断言非零退出的 TTY 拒绝。未来 Node 对模块钩子或 TypeScript 处理的任何改动都会让该门禁变红,而不是破坏开发者的 `pnpm dsh`。
## 备选方案
@@ -35,4 +35,4 @@ node-compat CI 矩阵(Node 22.19 与 26)新增 `dsh-source-launch-smoke`(`
- 整个 engines 范围(包括未来改变原生 TypeScript 支持的 Node 版本线)只有一个启动向量;冒烟门禁按矩阵行强制执行。
- TypeScript 转换重新委托给 tsx/esbuild,逆转了前一篇 Agent Note「证明 Node 原生转换可用」的目标;在 vendor 源码使用不可擦除语法且 Node 不再提供 transform 模式的情况下,该目标不可达。
- 源码启动中的运行时依赖声明强制不复存在;未声明的 workspace import 现在只能通过静态门禁或构建模式的解析失败暴露。
-- 启动相比完整 tsx 默认形态快约 0.4s(`demo:headless` 现为同一条 `dsh run` 源码启动命令的别名;ACP 保留 `--import tsx`,因为它的依赖图尚未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上)。
+- 运行时启动相比完整 tsx 默认形态快约 0.4s;ACP(Agent Client Protocol)保留 `--import tsx`,因为它的依赖图尚未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上。
diff --git a/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.i18n.yaml
index c50bf478ad..f5c1e4cad3 100644
--- a/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-29-package-regrouping.md
-2026-07-29-package-regrouping.md: 3c37bce05bacd6af800a76ac93fb691b896a6772
-2026-07-29-package-regrouping.zh.md: 68903ff1fad6a975c4445fe8971c8fe0dd40117f
+2026-07-29-package-regrouping.md: 30fc45a122263350b4a2ad1998850f631c20f9b8
+2026-07-29-package-regrouping.zh.md: a3a9a11ec71b7f894dcea7c733eb39a80b71ac50
diff --git a/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md b/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md
index 3c37bce05b..30fc45a122 100644
--- a/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md
+++ b/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.md
@@ -58,7 +58,7 @@ The moves landed as pure `git mv` moves, so rename detection carries the history
A group move did not touch: npm names, imports, `cordis.yml` configs, snapshot fixtures, the `pnpm-workspace.yaml`/`tsdown` globs (both `packages/*/*`), or the Python runtime manifest — all reference packages by npm name.
-`client/` and `host/` were out of scope and are unchanged. The `experimental/` group proposal (PR #844) is orthogonal — a release-boundary container, not a clustering decision.
+`client/` and `host/` were out of scope and are unchanged.
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.zh.md b/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.zh.md
index 68903ff1fa..a3a9a11ec7 100644
--- a/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.zh.md
@@ -58,7 +58,7 @@ Status: implemented
组移动未触及:npm 包名、import、`cordis.yml` 配置、快照 fixture(测试前置数据)、`pnpm-workspace.yaml` 与 `tsdown` 的 glob(都是 `packages/*/*`),以及 Python 运行时 manifest(元数据清单)——它们全部按 npm 包名引用包。
-`client/` 与 `host/` 不在本次范围内,保持不变。`experimental/` 组提案(PR #844)与本案正交:它是发布边界容器,不是聚类决策。
+`client/` 与 `host/` 不在本次范围内,保持不变。
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.md b/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.md
deleted file mode 100644
index 38e7356d4a..0000000000
--- a/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# Agent Note: Package-manager-native repository cache
-
-Status: implemented
-
-English | [中文](2026-07-30-package-manager-native-repository-cache.zh.md)
-
-## Problem
-
-A standalone Harness app cannot rely on a developer-owned SDK project to declare and install repository dependencies. Loading a configured GitHub repository therefore needs a persistent fetch, preparation, and cache boundary, but implementing Git transport, hosted-source syntax, package preparation, and a content store inside DSH would duplicate a package manager. Requiring a separately installed package manager would make a config-only feature depend on host setup.
-
-The cache also needs an update identity. A mutable branch name cannot both remain permanently cached and reflect later commits without an independent refresh protocol.
-
-## Decision
-
-Vendored `@cordisjs/plugin-loader/repository` exports `RepositoryCache`, a generic Node-only package helper with no DSH plugin-format knowledge. Keeping it on a subpath prevents browser consumers of the Loader's main entry from traversing Node filesystem and child-process imports. The caller supplies a package-manager-native source specifier and a cache root. DSH-specific callers own accepted source syntax, path selection, and the cache-root location; the [SDK project dependency workflow](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md#external-cordis-plugin-installation) remains a separate path owned by the developer project's selected package manager.
-
-The Loader carries an exact runtime dependency on `pnpm@11.7.0` and invokes that package's JavaScript entry with the current Node executable. It never discovers a global executable or delegates through Corepack. Each cache miss creates an isolated project with one dependency named `repository`; pnpm owns Git/GitHub resolution, fetching, its content-addressed store, dependency installation, and lifecycle scripts in the repository's dependency graph.
-
-The isolated workspace sets `dangerouslyAllowAllBuilds: true`. A configured repository and its dependency graph are trusted executable code: lifecycle scripts may run before DSH reads any declared assets. The child receives ordinary host process state needed by Git and pnpm, but ambient credential-shaped (`KEY`, `PASSWORD`, `SECRET`, `TOKEN`) variables are removed. No OAuth, token forwarding, or private-repository authentication contract is added.
-
-The SHA-256 of the exact specifier names the cache entry. Concurrent same-process requests share one task. Installation occurs in a sibling temporary directory; only a successful install with a package directory and marker is atomically renamed into the final key. Failed staging is removed, and a competing process's already-published valid entry wins. A later process validates the marker and package directory before returning the stable `node_modules/repository` path.
-
-An identical specifier permanently reuses its published entry. The caller changes the ref or another part of the specifier to request a new generation; the cache does not poll remotes, reinterpret mutable refs, expire entries, or garbage-collect old generations.
-
-## Alternatives considered
-
-**Implement GitHub download, archive extraction, preparation, and caching directly.** Rejected under the [dependency policy](../process/2026-07-26-dependencies-over-hand-rolling.md): pnpm already owns hosted Git syntax, Git execution, lifecycle policy, and a shared content store. A second resolver would add more code while still needing package semantics.
-
-**Require `pnpm` on `PATH` or invoke Corepack.** Rejected because changing one app config must be sufficient on every supported installation. Pinning and shipping the CLI also makes the preparation policy reviewable and independent of the host's package-manager version.
-
-**Resolve a branch or tag again on every startup.** Rejected because it turns startup into a network refresh, changes code without a config diff, and makes rollback depend on remote state. Explicit ref changes preserve auditability even when a user deliberately chooses a mutable ref.
-
-**Disable repository lifecycle scripts.** Rejected because common plugin repositories need a declarative `prepare` step to validate and package their plugin subdirectory. The trust boundary is explicit configuration of executable source, not an incomplete illusion that only static files can run.
-
-**Introduce a Cordis repository service.** Rejected because cache lookup has no runtime contribution registry or provider variation. A small helper lets the later host own Cordis lifecycle and HMR without adding a service contract prematurely.
-
-## Consequences
-
-- Standalone apps carry pnpm's approximately 18.6 MB unpacked runtime instead of requiring a global tool or owning a Git/package implementation.
-- A repository author may use ordinary package preparation, and a malicious configured repository or dependency can execute code with the scrubbed child environment and the user's filesystem authority.
-- Exact specifiers make startup deterministic after the first successful install; changing cached code requires a config/ref change.
-- Failed installs leave no published cache entry and may be retried. Published corruption fails loud instead of silently reinstalling under the same identity.
-- Cache generations consume disk until a future explicit cache-management policy removes them.
-
-## Testing
-
-`packages/boot/app-boot/tests/repository-cache.spec.ts` covers same-process single-flight, cross-instance cache reuse, exact-specifier separation, failed-stage cleanup and retry, and boundary validation. Its real local-Git case invokes the bundled pnpm, runs the fixture repository's `prepare` script, and reads the prepared file from the installed cache entry without network access.
diff --git a/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.zh.md b/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.zh.md
deleted file mode 100644
index 6833eeb427..0000000000
--- a/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.zh.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# Agent Note: 包管理器原生仓库缓存
-
-Status: implemented
-
-[English](2026-07-30-package-manager-native-repository-cache.md) | 中文
-
-## 问题
-
-独立运行的 Harness 应用不能依赖开发者自有的 SDK 工程来声明并安装仓库依赖。因此,加载配置中的 GitHub 仓库需要一道持久的获取、准备与缓存边界;但如果在 DSH 内实现 Git 传输、托管来源语法、包准备流程和内容存储,就会重复实现包管理器。若要求用户另行安装包管理器,则只需修改配置即可使用的功能还会依赖宿主环境的额外配置。
-
-缓存还需要明确更新标识。若没有独立的刷新协议,可变分支名无法既永久缓存,又反映后续 commit。
-
-## 决策
-
-vendor 中的 `@cordisjs/plugin-loader/repository` 导出 `RepositoryCache`:一个不包含 DSH 插件格式知识、仅限 Node 使用的通用包辅助工具。把它保留在子路径上,可以避免 Loader 主入口的浏览器消费方在解析依赖时遍历到 Node 文件系统和子进程 import。调用方提供包管理器原生的来源 specifier 和缓存根目录。DSH 专属调用方负责规定可接受的来源语法、路径选择与缓存根目录位置;[SDK 工程依赖工作流](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md#external-cordis-plugin-installation)仍是另一条路径,由开发者工程选定的包管理器负责。
-
-Loader 将 `pnpm@11.7.0` 作为固定版本的运行时依赖,并使用当前 Node 可执行文件调用该包的 JavaScript 入口。它绝不探测全局可执行文件,也不经 Corepack 调用。每次缓存未命中都会创建一个隔离工程,其中只有一个名为 `repository` 的依赖;Git 与 GitHub 来源的解析和获取、pnpm 自身的内容寻址 store、依赖安装,以及仓库依赖图中的生命周期脚本均由 pnpm 负责。
-
-隔离工作区设置 `dangerouslyAllowAllBuilds: true`。用户配置的仓库及其依赖图都属于受信任的可执行代码:DSH 读取任何已声明资产之前,生命周期脚本就可能运行。子进程会收到 Git 与 pnpm 所需的常规宿主进程状态,但会移除环境中名称形似凭据(`KEY`、`PASSWORD`、`SECRET`、`TOKEN`)的变量。该机制不新增 OAuth、token 转发或私有仓库认证约定。
-
-缓存项以精确 specifier 的 SHA-256 命名。同一进程内针对相同 specifier 的并发请求共享一项任务。安装在同级临时目录中进行;只有安装成功且存在包目录和标记时,系统才会把暂存目录原子重命名为最终键对应的目录。失败的暂存目录会被删除;如果另一进程已发布有效项,则以该项为准。后续进程会先校验标记与包目录,再返回稳定的 `node_modules/repository` 路径。
-
-相同的 specifier 会永久复用已发布项。调用方通过修改 ref 或 specifier 的其他部分来请求新的缓存代次;缓存不会轮询远端、重新解释可变 ref、让条目过期,也不会垃圾回收旧代次。
-
-## 曾考虑的替代方案
-
-**直接实现 GitHub 下载、归档解压、准备与缓存。** 根据[依赖政策](../process/2026-07-26-dependencies-over-hand-rolling.md)不予采纳:pnpm 已负责托管 Git 语法、Git 执行、生命周期政策和共享内容存储。第二套解析器会增加更多代码,却仍需实现包语义。
-
-**要求 `pnpm` 位于 `PATH` 上,或调用 Corepack。** 不予采纳:在每种受支持的安装形态中,只修改一份应用配置就必须足以启用该功能。固定并随应用分发 CLI(命令行界面)还能使准备政策可供评审,并与宿主的包管理器版本无关。
-
-**每次启动都重新解析分支或 tag。** 不予采纳:这会把启动变成网络刷新,在配置 diff 未变化时更改代码,并让回滚依赖远端状态。即使用户有意选择可变 ref,显式修改 ref 仍能保持可审计性。
-
-**禁用仓库生命周期脚本。** 不予采纳:常见插件仓库需要声明式 `prepare` 步骤来校验并打包插件子目录。信任边界是显式配置可执行来源,而不是营造一种不完整的假象,仿佛只有静态文件能够运行。
-
-**引入 Cordis 仓库服务。** 不予采纳:缓存查找没有运行时贡献注册表,也不存在提供方变体。小型 helper 让后续宿主负责 Cordis 生命周期与 HMR(热模块替换),无需过早新增服务约定。
-
-## 后果
-
-- 独立应用随附 pnpm 约 18.6 MB 的解压后运行时,不要求全局工具,也无需自行实现 Git 与包处理。
-- 仓库作者可以使用常规包准备流程;恶意的已配置仓库或依赖可以在经过上述清理的子进程环境中,以用户的文件系统权限执行代码。
-- 精确 specifier 使首次安装成功后的启动具有确定性;更改缓存代码必须修改配置或 ref。
-- 安装失败不会留下已发布缓存项,可以再次重试。已发布缓存损坏时会明确报错,而不会在同一标识下静默重装。
-- 缓存代次会持续占用磁盘,直到未来有明确的缓存管理政策将其移除。
-
-## 测试
-
-`packages/boot/app-boot/tests/repository-cache.spec.ts` 覆盖同进程 single-flight、跨实例缓存复用、精确 specifier 隔离、失败暂存清理与重试,以及边界校验。其真实本地 Git 用例会调用随附的 pnpm,运行 fixture(测试前置数据)仓库的 `prepare` 脚本,并在不访问网络的情况下,从已安装缓存项中读取准备后的文件。
diff --git a/.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.md b/.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.md
deleted file mode 100644
index c66ee111eb..0000000000
--- a/.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# Agent Note: Static repository Plugin format
-
-Status: implemented
-
-English | [中文](2026-07-30-static-repository-plugin-format.zh.md)
-
-## Problem
-
-A repository that already contains reusable skills or an MCP server declaration should be usable by standalone Harness applications without becoming a Harness SDK project or rewriting its existing layout. Popular repositories must be able to add one `.dsh-plugin` directory while keeping their current skills and `.mcp.json` elsewhere in the tree. These portable static contributions still need to reuse the existing skill and MCP lifecycle owners when the same trusted package also carries native Cordis code.
-
-The [package-manager-native repository cache](2026-07-30-package-manager-native-repository-cache.md) prepares an exact package source but intentionally knows nothing about DSH formats. This layer therefore needs a package-manager-compatible authoring format, a deterministic prepared artifact, and a Cordis composition that stays transactional under Loader disposal and replacement.
-
-## Decision
-
-`@deepseek-ai/dsh-repository-plugin` owns the static contribution subformat inside a `.dsh-plugin` package: skill roots and one common `.mcp.json`. Its package metadata uses `package.json#dsh.skills` for relative skill-root paths and `package.json#dsh.mcpServers` for the relative MCP document path. Each path may leave `.dsh-plugin` to reuse repository content but must remain beneath the directory containing that `.dsh-plugin`; a nested selectable Plugin therefore owns the adjacent subtree above its package without gaining access to unrelated host paths. The package may additionally declare the explicit code entry owned by the [trusted repository package decision](2026-08-08-trusted-repository-package-code.md), and at least one code or static contribution is required.
-
-The `.dsh-plugin` package declares the published `@deepseek-ai/dsh-repository-plugin` package as a development dependency and a non-empty `scripts.prepack` that invokes its `dsh-plugin-prepare` executable. During Git installation, pnpm installs that dependency from the selected package's own manifest; `prepack` runs after dependency installation and before pnpm packs a selected subdirectory, including a Plugin nested inside another package-manager workspace. The package may build its code first. The helper validates metadata and source types, strictly parses `.mcp.json`, copies static assets into `dsh-plugin-assets`, and writes `dsh-plugin.mjs`; the source loader revalidates the installed package's helper-bearing lifecycle metadata before importing that wrapper. A static-only package still receives an import-free wrapper containing its normalized manifest, service-derived `inject` list, and delegation to the `dsh-repository-plugin` Loader builtin. The dependency and workspace-isolation rationale is in the [Git source preparation repair](../bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md).
-
-Loading the DSH package registers that builtin as an effect. A generated wrapper mounts the builtin as its child with `import.meta.url`, so all contributions belong to the wrapper fiber and disappear on Loader removal or rollback. The builtin revalidates the prepared manifest and path containment before reading assets. It composes the existing implementations rather than registering skills or MCP tools itself.
-
-Each prepared skill set mounts `dsh-skill-local` with a unique `repository:` provider name, only the copied custom roots, and watching disabled. `dsh-skill-local` therefore gains two general configuration fields: `providerName` and `includeDefaultRoots`. Their defaults preserve its existing single local provider; repository instances set a distinct name and exclude project/user roots so multiple instances neither collide nor duplicate host-local discovery.
-
-Each `.mcp.json` server becomes one existing `dsh-mcp-client` child. The adapter accepts the common root `{ "mcpServers": ... }`; stdio definitions allow only optional `type: "stdio"`, `command`, `args`, and `env`, while HTTP definitions allow only `type: "http"`, `url`, and `headers`. Exact `${NAME}` process-environment references expand at runtime, after cache preparation; missing names fail Plugin load. HTTP maps to the client's Streamable HTTP transport, and stdio uses the prepared package directory as `cwd`. The existing client alone owns connection attempts, failure logging, remote tool synchronization, tool calls, and disconnects. Repository instances enable strict startup, so an initial connection, discovery, or tool-registration failure rejects the repository Loader generation; non-strict standalone clients retain the logged successful-plugin/no-tools behavior.
-
-Unknown MCP fields reject. This intentionally excludes OAuth, `auth` objects, `CLAUDE_PLUGIN_ROOT`, and a broader Claude compatibility contract. Commands, hooks, agents, rules, and other foreign manifest conventions are not inferred from static repository layout; DSH-native behavior uses the explicit trusted Cordis entry. Repository subdirectory selection and GitHub source configuration belong to the [standalone app integration](../feature/2026-07-30-config-only-repository-plugins.md), not this static adapter.
-
-## Alternatives considered
-
-**Discover an entry from `main`, `exports`, or repository layout.** Rejected because static assets do not imply that a package's ordinary entry is a Cordis Plugin. Trusted code loading is explicit through `dsh.entry` and remains outside this static adapter's ownership.
-
-**Teach generated wrappers to implement skills and MCP directly.** Rejected because copied runtime code would drift from `dsh-skill-local` and `dsh-mcp-client`, especially their provider invalidation, tool synchronization, failure, and teardown contracts.
-
-**Import Harness packages from each generated wrapper.** Rejected because repository packages should not resolve or version the application's internal dependency graph. A Loader builtin supplies one app-owned implementation and keeps generated wrappers import-free.
-
-**Watch prepared repository assets.** Rejected because an exact repository cache generation is immutable. Ref, subdirectory, or configuration changes select a new generation; a second watcher would create an unowned refresh identity.
-
-**Make every MCP connect failure a Loader update failure.** Rejected because optional standalone MCP clients deliberately contain startup failures and expose no tools. The MCP client instead owns an explicit strict-startup option, which repository adapters enable for their declared servers.
-
-## Consequences
-
-- Existing skill/MCP repositories can add a small `.dsh-plugin/package.json` without relocating their assets or adopting an SDK project.
-- Prepared static output is deterministic glue, while an optional `dsh.entry` and the configured repository lifecycle remain trusted executable package-manager input rather than a sandbox.
-- Multiple repository Plugins coexist through provider names and ordinary MCP server-name uniqueness; duplicate names fail through their existing registries and participate in Loader rollback.
-- Cached source edits do not appear live. Another exact source/ref/path/config selection is required.
-- Adding another portable static contribution kind requires an explicit format and DSH-owned runtime consumer; DSH-native behavior uses the separate explicit code entry.
-
-## Testing
-
-Focused tests prepare skills and MCP metadata, prove a static-only wrapper contains no imports, reject Work IQ-style OAuth fields, map Expo-style HTTP and DataJunction-style stdio plus environment values, and exercise missing variables. A real Loader test mounts a generated wrapper through the registered builtin, reads its skill through `ctx.skills`, removes the Loader entry, and observes provider cleanup. The CI built-entry acceptance invokes `dsh run` with a GitHub source pinned to the pull request head and observes the copied skill alongside the trusted code and MCP proofs owned by the superseding decision.
diff --git a/.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.zh.md b/.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.zh.md
deleted file mode 100644
index c85aaf44d9..0000000000
--- a/.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.zh.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# Agent Note: 静态 repository Plugin 格式
-
-状态:已实现
-
-[English](2026-07-30-static-repository-plugin-format.md) | 中文
-
-## 问题
-
-一个已经包含可复用 skills 或 MCP server 声明的仓库,应当能被独立 Harness 应用使用,而不必先变成 Harness SDK 项目,也不应被迫改写现有布局。常见仓库只需新增一个 `.dsh-plugin` 目录,同时仍可把原有 skills 与 `.mcp.json` 放在仓库其他位置。当同一个受信任包还携带原生 Cordis 代码时,这些可移植静态贡献仍需复用现有的 skill 与 MCP 生命周期所有者。
-
-[Package-manager-native repository cache](2026-07-30-package-manager-native-repository-cache.md) 会准备一个精确 package source,但有意不了解任何 DSH 格式。因此本层需要一种兼容 package manager 的创作格式、确定性的已准备产物,以及在 Loader dispose 和替换期间仍保持事务性的 Cordis 组合。
-
-## 决策
-
-`@deepseek-ai/dsh-repository-plugin` 负责 `.dsh-plugin` 包内的静态贡献子格式:skill 根和一个通用 `.mcp.json`。其包元数据使用 `package.json#dsh.skills` 声明相对 skill 根路径,使用 `package.json#dsh.mcpServers` 声明相对 MCP 文档路径。每条路径都可以离开 `.dsh-plugin` 以复用仓库内容,但必须留在包含该 `.dsh-plugin` 的目录之下;因此,一个嵌套且可选择的插件可以拥有其包上方相邻的子树,却不能访问无关宿主路径。该包还可以声明由[受信任 repository 包决策](2026-08-08-trusted-repository-package-code.md)负责的显式代码入口,并且至少需要一种代码或静态贡献。
-
-`.dsh-plugin` 包将已发布的 `@deepseek-ai/dsh-repository-plugin` 包声明为开发依赖,并声明非空 `scripts.prepack` 来调用其 `dsh-plugin-prepare` 可执行文件。在 Git 安装期间,pnpm 会按所选包自身的 manifest(元数据清单)安装该依赖;`prepack` 会在依赖安装后、pnpm 打包选定子目录前运行,即使插件嵌套在另一个包管理器工作区内也不例外。包可以先构建其代码。该辅助程序会校验元数据与源码类型,严格解析 `.mcp.json`,把静态资源复制到 `dsh-plugin-assets`,并写入 `dsh-plugin.mjs`;源码 loader 会在导入该包装层前重新校验已安装包的生命周期元数据是否包含辅助命令。仅含静态贡献的包仍会获得无 import 包装层,其中包含规范化 manifest、由服务派生的 `inject` 列表,以及对 `dsh-repository-plugin` Loader builtin 的委托。依赖与 workspace 隔离的设计依据见[Git 源准备修复](../bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md)。
-
-加载 DSH package 会以 effect 方式注册该 builtin。生成的包装模块使用 `import.meta.url` 把 builtin 挂载为自己的子级,因此所有贡献都归属于包装 fiber,并在 Loader 移除或回滚时消失。Builtin 会在读取资源前重新校验已准备 manifest 与路径包含关系。它只组合现有实现,而不自行注册 skills 或 MCP 工具。
-
-每份已准备 skill 集合都会挂载 `dsh-skill-local`,使用唯一的 `repository:` 提供方名称、仅包含复制后的自定义根,并禁用监视。因此 `dsh-skill-local` 新增两个通用配置字段:`providerName` 和 `includeDefaultRoots`。默认值保持原有单一本地提供方行为;repository 实例设置不同名称并排除项目/用户根,使多个实例既不冲突,也不会重复宿主本地发现。
-
-`.mcp.json` 中的每个 server 都变成一个现有 `dsh-mcp-client` 子级。适配层接受通用根对象 `{ "mcpServers": ... }`;stdio 定义只允许可选的 `type: "stdio"`、`command`、`args` 与 `env`,HTTP 定义只允许 `type: "http"`、`url` 与 `headers`。严格的 `${NAME}` 进程环境变量引用在运行时、cache 准备之后展开;缺失变量会使 Plugin 加载失败。HTTP 映射到 client 的 Streamable HTTP transport,stdio 使用已准备 package 目录作为 `cwd`。只有现有 client 负责连接尝试、失败日志、远端工具同步、工具调用和断开。Repository 实例会启用严格启动,因此初始连接、发现或工具注册失败会拒绝 repository Loader generation;非严格的独立 client 则保留“记录日志、Plugin 成功但不注册工具”的行为。
-
-未知 MCP 字段会被拒绝。这里有意排除 OAuth、`auth` 对象、`CLAUDE_PLUGIN_ROOT` 和更广泛的 Claude 兼容约定。命令、hook、agent(智能体)、规则和其他外来 manifest 约定不会从静态 repository 布局中推断出来;DSH 原生行为使用显式的受信任 Cordis 入口。Repository 子目录选择与 GitHub 源配置属于[独立应用集成](../feature/2026-07-30-config-only-repository-plugins.md),而不是本静态适配器。
-
-## 考虑过的替代方案
-
-**从 `main`、`exports` 或 repository 布局中发现入口。** 拒绝,因为静态资源并不表示包的普通入口就是 Cordis 插件。受信任代码通过 `dsh.entry` 显式加载,不属于该静态适配器的职责。
-
-**让生成包装模块直接实现 skills 和 MCP。** 拒绝,因为复制的运行时代码会与 `dsh-skill-local` 和 `dsh-mcp-client` 漂移,尤其是提供方失效、工具同步、失败和 teardown 约定。
-
-**让每个生成包装模块 import Harness package。** 拒绝,因为 repository package 不应解析或锁定应用的内部依赖图。Loader builtin 提供一份由 app 所有的实现,并让生成包装模块保持无 import。
-
-**监视已准备 repository 资源。** 拒绝,因为一个精确 repository cache generation 是不可变的。Ref、子目录或配置变化会选择新 generation;第二套 watcher 会创造一套没有所有者的刷新身份。
-
-**把每次 MCP 连接失败都当作 Loader 更新失败。** 拒绝,因为可选的独立 MCP client 会有意收束启动失败,并且不暴露工具。MCP client 改为自行提供显式的严格启动选项,由 repository 适配器为其声明的 server 启用。
-
-## 后果
-
-- 现有 skill/MCP 仓库可以新增一个很小的 `.dsh-plugin/package.json`,无需移动资源或采用 SDK 项目。
-- 已准备的静态输出是确定性胶水;可选的 `dsh.entry` 和已配置的 repository 生命周期仍是受信任的可执行包管理器输入,而非沙箱。
-- 多个 repository Plugin 通过提供方名称和普通 MCP server-name 唯一性共存;重复名称经现有 registry 失败,并参与 Loader 回滚。
-- Cache 内的源码编辑不会实时出现;必须选择另一个精确 source/ref/path/config。
-- 新增可移植静态贡献类型必须提供显式格式和 DSH 自有运行时消费方;DSH 原生行为使用独立的显式代码入口。
-
-## 测试
-
-聚焦测试会准备 skill 与 MCP 元数据,证明仅含静态贡献的包装模块不含 import,拒绝 Work IQ 风格的 OAuth 字段,映射 Expo 风格 HTTP 与 DataJunction 风格 stdio 及环境变量,并覆盖缺失变量。真实 Loader 测试通过已注册 builtin 挂载生成包装模块,经 `ctx.skills` 读取其 skill,移除 Loader 条目并观察提供方清理。CI 构建入口验收会使用锁定到 PR(Pull Request)head 的 GitHub 源调用 `dsh run`,并观察已复制的 skill,以及由取代本决策的新决策所负责的受信任代码与 MCP 验证证据。
diff --git a/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.i18n.yaml
index f9e1b3c024..29f0dd7d88 100644
--- a/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md
-2026-08-03-per-session-agent-presets.md: c39117ab0de001650a95f98ccf3e42f3a5034c92
-2026-08-03-per-session-agent-presets.zh.md: 5e98a2865013a355c134317ded8a4f2ddaccf42c
+2026-08-03-per-session-agent-presets.md: 5a82f0220058c10892b819a83499c817aa9be6ad
+2026-08-03-per-session-agent-presets.zh.md: 0fb3330014eb703c854f70c9e5ed552256651c31
diff --git a/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md
index c39117ab0d..5a82f02200 100644
--- a/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md
+++ b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md
@@ -18,7 +18,7 @@ Composition splits into two planes, decided by what must be shared rather than b
| Plane | Instances | Contents |
|---|---|---|
-| Host | one | The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), cross-session facilities (persistence, query, projections, storage, settings, credentials, telemetry), and the web host |
+| Host | one | The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), cross-session facilities (persistence, query, projections, storage, settings, credentials, telemetry), the subagent providers those facilities resolve, and the web host |
| Agent | one per session | What a single agent contributes to those registries: tool plugins, persona and prompt sections, compaction policy |
Model routing stays out of presets. `installAgentLlmTarget` is already the per-agent seam for provider, model, and reasoning effort, and an LLM adapter mounted inside a preset would never be resolved by `agent-loop`, which lives in the host plane.
@@ -55,7 +55,7 @@ Which preset an unnamed session gets is a user setting (`agent-presets.default`)
**Authoring a preset is an RPC, and a privileged one.** A composition is a file, but "edit it on the filesystem" is not a browser affordance, so the roster gained `read`/`write`/`remove` beside `select`. Those three are loopback-pinned: a composition names the plugins a session runs, so reading one is reconnaissance and writing one is arbitrary capability. `list` and `select` deliberately stay ordinary. The roster carries ids and trust only, and a LAN client's picker needs it; and choosing a preset looked like escalation — one of them mounts the toolset that edits the live runtime — but `session.create` already takes an `agentPreset`, so pinning only the switch would have left the same capability one method over. The capability is not the preset's to grant either: the deployment's own default already carries `bash` and the filesystem tools, so any caller that may start a session at all can already run commands as this process. Containment is a property of the id (`[a-z0-9][a-z0-9-]*`), checked before it becomes a directory name rather than by inspecting the joined path afterwards; the text is parsed with the loader's own schema and dialect, so a save cannot leave a file no session could load. Shipped presets are refused for writes and deletes, because the deployment's copy is what a broken local preset is compared against — which also makes "duplicate, then edit" the authoring path rather than an afterthought.
-**A service with a consumer outside the agent plane cannot move into a preset.** The aggressive split moved the `subagents` registry and its spawn/fork backends into the delegation group's entry-local realm, and `dsh web` then failed to boot: `dsh-host-apiproxy` is a HOST row that injects `subagents` to answer the browser's cross-session queries (`listChildren`, `followup`), so it waited forever for a service only sessions now provided. A per-session copy is wrong twice over — a provider name registers once, so the second session would have collided anyway. The registry and its backends are host-plane; the preset contributes the delegation TOOLS, which resolve the host registry. `workflows` stays entry-local because nothing outside an agent reads it. Grepping injectors is what should have caught this and did not: the search has to include the host packages, not just the agent-plane ones.
+**A service with a consumer outside the agent plane cannot move into a preset.** The aggressive split moved the `subagents` registry and its spawn/fork backends into the delegation group's entry-local realm, and `dsh web` then failed to boot: `dsh-host-apiproxy` is a HOST row that injects `subagents` to answer the browser's cross-session queries (`listChildren`, `followup`), so it waited forever for a service only sessions now provided. A per-session copy is wrong twice over — a provider name registers once, so the second session would have collided anyway. The registry and every shared backend, including the [fixed Codex and Claude Code product providers](2026-08-10-product-subagent-providers-in-shared-host.md), are host-plane; a preset contributes whichever delegation TOOLS its agent should see, and those tools resolve the host registry. `workflows` stays entry-local because nothing outside an agent reads it. Grepping injectors is what should have caught this and did not: the search has to include the host packages, not just the agent-plane ones.
**A real-composition test that disables a host row cannot audit that row.** The web composition test disabled `api-gateway` — the api-proxy itself — as a row with side effects, which is exactly the row whose pending injection would have named the break. It now boots with the api-proxy enabled and the browse directory picker substituted, so the boot audit covers the whole host-plane injection graph; only the port, the asset tree, and the telemetry exporter stay off.
@@ -78,3 +78,7 @@ Which preset an unnamed session gets is a user setting (`agent-presets.default`)
**Make the agent's scope key the preset.** Sessions on one preset would share a layer for free, but per-agent registrations — `installAgentLlmTarget`, per-agent tool restrictions — would then collide across sessions.
**Run each preset as a child process.** [`subagent-dsh-sdk`](../../../../packages/subagent/subagent-dsh-sdk/README.md) already proves a full child harness works, and isolation would be absolute. It also means proxying streaming, approvals, and projections per session, which is a transport project rather than a composition one.
+
+**Give product subagents global enable settings and a separate settings page.** The process-wide value would compete with the preset as owner of model-visible tools and could not express two sessions using different compositions. Product providers stay host-side, while ordinary preset rows independently expose Codex and Claude Code tools.
+
+**Ship one preset for every Codex and Claude Code combination.** Four identities duplicate the full preset composition to represent two independent rows. A copied preset can enable either row directly, so combination presets add roster and maintenance cost without adding a user result.
diff --git a/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.zh.md b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.zh.md
index 5e98a28650..0fb3330014 100644
--- a/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.zh.md
@@ -18,7 +18,7 @@ Status: implemented
| 平面 | 实例数 | 内容 |
|---|---|---|
-| 宿主 | 一份 | 注册表本身(`tools`、`systemPrompt`、`agents`、`agent-loop`、`sessions`)、跨会话设施(持久化、查询、投影、存储、设置、凭据、遥测),以及 web 宿主 |
+| 宿主 | 一份 | 注册表本身(`tools`、`systemPrompt`、`agents`、`agent-loop`、`sessions`)、跨会话设施(持久化、查询、投影、存储、设置、凭据、遥测)、这些设施所解析的 subagent provider,以及 web 宿主 |
| agent | 每会话一份 | 单个 agent 对这些注册表的贡献:工具插件、人设与提示词段落、压缩策略 |
模型路由不进 preset。`installAgentLlmTarget` 已经是 provider、model 与 reasoning effort 的按 agent 可替换点;而挂在 preset 内部的 LLM 适配器永远不会被 `agent-loop` 解析到,因为后者位于宿主平面。
@@ -56,7 +56,7 @@ Status: implemented
**创作 preset 是一次 RPC,而且是特权 RPC。** 组装是一个文件,但“去文件系统里改它”并不是浏览器能提供的操作,因此名单在 `select` 之外新增了 `read`/`write`/`remove`。这三者被固定在环回地址:组装指明了一个会话所运行的插件,因此读取它是侦察,写入它是任意能力。`list` 与 `select` 刻意保持为普通方法。名单只携带 id 与信任级别,而局域网客户端的选择器需要它;至于选择本身,它看起来像提权——其中一个 preset 会挂载可编辑活动运行时的工具集——但 `session.create` 本就接受 `agentPreset`,只固定切换会把同一能力留在隔壁一个方法上。这份能力也不由 preset 授予:部署自带的默认 preset 本就带着 `bash` 与文件系统工具,因此任何被允许开启会话的调用方,早已能以本进程的身份执行命令。约束是 id 自身的性质(`[a-z0-9][a-z0-9-]*`),在它成为目录名之前就检查,而不是事后再去审视拼接出的路径;文本使用 loader 自身的 schema 与方言解析,因此保存不会留下任何会话都无法加载的文件。随部署提供的 preset 拒绝写入与删除,因为部署自带的那一份正是用来对照有问题的本地 preset 的——这也让“先复制、再编辑”成为创作路径本身,而非事后补充。
-**在 agent 平面之外还有消费方的服务,不能搬进 preset。** 激进拆分把 `subagents` 注册表连同 spawn/fork 后端一起搬进了 delegation 组的 entry-local realm,于是 `dsh web` 直接起不来:`dsh-host-apiproxy` 是宿主行,它注入 `subagents` 来回答浏览器的跨会话查询(`listChildren`、`followup`),因而永远等待一个此刻只有会话才提供的服务。按会话各一份在两个层面上都是错的——provider 名只能注册一次,第二个会话本来也会相撞。注册表与后端属于宿主平面;preset 贡献的是委派**工具**,它们解析宿主注册表。`workflows` 保持 entry-local,因为 agent 之外没有任何东西读它。本该拦下它的是「检索注入方」这一步,而它没拦住:检索必须覆盖宿主包,而不只是 agent 平面的包。
+**在 agent 平面之外还有消费方的服务,不能搬进 preset。** 激进拆分把 `subagents` 注册表连同 spawn/fork 后端一起搬进了 delegation 组的 entry-local realm,于是 `dsh web` 直接起不来:`dsh-host-apiproxy` 是宿主行,它注入 `subagents` 来回答浏览器的跨会话查询(`listChildren`、`followup`),因而永远等待一个此刻只有会话才提供的服务。按会话各一份在两个层面上都是错的——provider 名只能注册一次,第二个会话本来也会相撞。注册表与所有共享后端,包括[固定的 Codex 与 Claude Code 产品 provider](2026-08-10-product-subagent-providers-in-shared-host.md),都属于宿主平面;preset 只贡献自己的 agent 应看见的委派**工具**,这些工具解析宿主注册表。`workflows` 保持 entry-local,因为 agent 之外没有任何东西读它。本该拦下它的是「检索注入方」这一步,而它没拦住:检索必须覆盖宿主包,而不只是 agent 平面的包。
**真实组装测试若禁用了某个宿主行,就无法审计该行。** web 组装测试把 `api-gateway`——也就是 api-proxy 本身——当作「有外部副作用的行」禁用了,而它恰恰是那个会以 pending 注入点名此次断裂的行。现在它在启用 api-proxy、并替换为 browse 目录选择器的前提下引导,启动审计因此覆盖整个宿主平面的注入图;只有端口、资源目录与遥测导出器仍然关闭。
@@ -79,3 +79,7 @@ Status: implemented
**把 agent 的 scope 键设为 preset。** 同一 preset 上的会话就能免费共享一层,但按 agent 的注册——`installAgentLlmTarget`、按 agent 的工具限制——会跨会话相撞。
**把每个 preset 作为子进程运行。** [`subagent-dsh-sdk`](../../../../packages/subagent/subagent-dsh-sdk/README.md) 已经证明完整的子 harness 可行,隔离性也会是绝对的。但这同时意味着要按会话代理流式输出、审批与投影,那是一个传输层项目,而非组装问题。
+
+**给产品 subagent 增加全局启用设置与独立设置页。** 进程级值会与 preset 争夺模型可见工具的所有权,也无法表达两个会话使用不同组装。产品 provider 留在宿主,普通 preset 行分别暴露 Codex 与 Claude Code 工具。
+
+**为 Codex 与 Claude Code 的每种组合交付一份 preset。** 四个身份会复制完整 preset 组装,只为表示两条独立行。复制后的 preset 已能直接启用任一行,因此组合 preset 只增加名单与维护成本,不增加用户结果。
diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml
index 938e802716..c583d6f373 100644
--- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
-2026-08-05-profile-plugin-bundles.md: 2924b3cb445064fd47d82bcc94ec8d77ded5721b
-2026-08-05-profile-plugin-bundles.zh.md: b2287034010bcac1048bb385b2266f1bc75921da
+2026-08-05-profile-plugin-bundles.md: 54626e3f48a2ba7db19813e6e883f0e77499d0e2
+2026-08-05-profile-plugin-bundles.zh.md: 357e0f63d4eba0f0985c9e14aad54595c7b41c77
diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
index 2924b3cb44..54626e3f48 100644
--- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
+++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
@@ -10,11 +10,9 @@ The `dsh` launcher hardcoded its compositions: `base.cordis.yml` + `web.cordis.y
## Decision
-Everything becomes a **profile**: a directory `$DSH_HOME/profiles/` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the two manifest kinds live under distinct `dsh.profile` / `dsh.bundle` keys so a package.json states which role it plays. The tree composes over an empty root by applying each bundle's patch in `dsh.profile.bundles` order, then the user layer, then `--patch` overlays, then flag patches — one `applyEntryPatches` call, identical for boot, flag derivation, and `--dump-config`.
+Everything becomes a **profile**: a directory `$DSH_HOME/profiles/` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the two manifest kinds live under distinct `dsh.profile` / `dsh.bundle` keys so a package.json states which role it plays. The tree composes over an empty root by applying each bundle's patch in `dsh.profile.bundles` order, then the user layer and `--patch` overlays — one `applyEntryPatches` call shared by boot and `--dump-config`. App invocation values later moved from launcher-derived patches to startup services in the [app-owned command-line decision](2026-08-06-app-owned-command-line.md).
-The shipped bundles are `@deepseek-ai/dsh-base` (shared core rows), `@deepseek-ai/dsh-web-app` (browser Host rows and Web runtime glue), and `@deepseek-ai/dsh-headless` (a direct one-shot runner over base, without web-app). `dsh web` is the Web-flag alias for `--profile web`; `dsh run [--profile ] "task"` owns one-shot execution and defaults to the headless profile; generic `dsh --profile ` boots without a task. Patch overlays use `--patch`. `dsh plugin --profile ` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract.
-
-The [`dsh run` command decision](../feature/2026-08-08-dsh-run-headless-command.md) owns the one-shot grammar; this note owns the profile composition it selects.
+The shipped bundles are `@deepseek-ai/dsh-base` (shared core rows), `@deepseek-ai/dsh-web-app` (browser Host rows and Web runtime glue), and `@deepseek-ai/dsh-headless` (a direct one-shot runner over base, without web-app). Generic `dsh --profile ` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, while headless owns its task positional. Patch overlays use launcher-owned `--patch`. `dsh plugin --profile ` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract.
Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory — so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them — while bare plugin names in patch rows resolve through the profile directory's Node parent-walk into the maintained flat fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch).
@@ -24,7 +22,7 @@ Two supporting refactors: the webserver's built-in static dist serving became th
- **Dependency-scan plus partial `patchOrder`** (the original sketch): scanning `dependencies` for bundles and ordering unlisted ones alphabetically has two sources of truth and an implicit tie-break; one explicit ordered `dsh.profile.bundles` list is smaller and fully deterministic. A raw `pnpm add` inside the profile installs a library without activating any patch — explicit, no spooky scan.
- **`link:` entries for in-box bundles**: pnpm cannot version, install, or update a `link:` into the installation, it embeds a machine path in a user file, and it breaks when the installation moves. The two-anchor resolution plus healed symlink fallback gives the same guarantee ("bundles come from the installation") without ceremony.
-- **A pre-boot `context` module in the bundle manifest** for boot-time values (dist path, flag facts): rejected in favor of pure plugins — the glue is ordinary rows the launcher patches, so the composition stays fully dumpable and the manifest stays data-only. The launcher-owned `ctx.headlessIo` host hook is the one host-provided slot, and it is provided in `boot()`'s `prepare` hook, before any config-tree entry mounts.
+- **A pre-boot `context` module in the bundle manifest** for boot-time values (dist path, flag facts): rejected in favor of pure plugins — the glue is ordinary rows and app-owned startup services, so the composition stays fully dumpable and the manifest stays data-only. The launcher-owned `ctx.headlessIo` host hook is the one host-provided slot, and it is provided in `boot()`'s `prepare` hook, before any config-tree entry mounts.
- **Transitive bundle auto-application**: only direct `dsh.profile.bundles` entries contribute layers; a meta-bundle wanting to re-export another bundle's patch must do so explicitly in its own patch file.
## Consequences
diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md
index b228703401..357e0f63d4 100644
--- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md
@@ -10,11 +10,9 @@ Status: implemented
## Decision
-一切都变成 **profile**:即目录 `$DSH_HOME/profiles/`,其中包含一个 `package.json`(pnpm 管理的树外插件 `dependencies`,加上 profile manifest(元数据清单)`dsh.profile` 及其有序的 `bundles` 层列表)和一份用户 `cordis.patch.yml`。**组合包**(bundle)是声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;两种 manifest 分别位于互不相同的 `dsh.profile` / `dsh.bundle` 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 `dsh.profile.bundles` 顺序应用每个组合包的 patch,然后是用户层,然后是 `--patch` overlay,最后是 flag patch——全部收敛为一次 `applyEntryPatches` 调用,启动、flag 派生与 `--dump-config` 使用完全相同的路径。
+一切都变成 **profile**:即目录 `$DSH_HOME/profiles/`,其中包含一个 `package.json`(pnpm 管理的树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和一份用户 `cordis.patch.yml`。**组合包**(bundle)是声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;两种 manifest 分别位于互不相同的 `dsh.profile` / `dsh.bundle` 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 `dsh.profile.bundles` 顺序应用每个组合包的 patch,然后是用户层与 `--patch` overlay——启动与 `--dump-config` 共享同一条 `applyEntryPatches` 路径。随后,[应用持有命令行的决策](2026-08-06-app-owned-command-line.md)又把调用期取值从启动器派生的 patch 迁移到了启动服务。
-随附的组合包是 `@deepseek-ai/dsh-base`(共享核心配置行)、`@deepseek-ai/dsh-web-app`(浏览器 Host 配置行与 Web 运行时粘合层)和 `@deepseek-ai/dsh-headless`(直接叠加在 base 上且不含 web-app 的一次性 runner)。`dsh web` 是携带 Web flag 家族的 `--profile web` 别名;`dsh run [--profile ] "task"` 负责一次性执行,默认使用 headless profile;通用的 `dsh --profile ` 启动 profile 而不携带任务。patch overlay 使用 `--patch`。`dsh plugin --profile ` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](2026-08-09-headless-direct-core-entry-point.md)负责 headless 组合约定。
-
-[`dsh run` 命令决策](../feature/2026-08-08-dsh-run-headless-command.md)负责一次性语法;本 Agent Note 负责该语法所选择的 profile 组合。
+随附的组合包是 `@deepseek-ai/dsh-base`(共享核心配置行)、`@deepseek-ai/dsh-web-app`(浏览器 Host 配置行与 Web 运行时粘合层)和 `@deepseek-ai/dsh-headless`(直接叠加在 base 上且不含 web-app 的一次性 runner)。通用的 `dsh --profile ` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 则持有任务位置参数。patch overlay 使用启动器持有的 `--patch`。`dsh plugin --profile ` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](2026-08-09-headless-direct-core-entry-point.md)负责 headless 组合约定。
解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 `$DSH_HOME/profiles/node_modules`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。
@@ -24,7 +22,7 @@ Status: implemented
- **依赖扫描加部分 `patchOrder`**(最初的草案):扫描 `dependencies` 找出组合包、未列出者按字母序排列,会产生两个真源和一条隐式决胜规则;一份显式有序的 `dsh.profile.bundles` 列表更小、完全确定。在 profile 内直接 `pnpm add` 只会安装一个库,不激活任何 patch——行为显式,没有暗中扫描。
- **内置组合包使用 `link:` 条目**:pnpm 无法对指向安装目录的 `link:` 做版本管理、安装或更新,它会把机器路径嵌进用户文件,并且在安装目录移动后失效。双锚点解析加上每次启动修复的符号链接回退提供了同样的保证(「组合包来自安装目录」),且没有这些繁文缛节。
-- **在组合包 manifest 中放一个启动前 `context` 模块**承载启动期取值(dist 路径、flag 事实):否决,改用纯插件——粘合逻辑就是启动器 patch 的普通配置行,因此组合始终可完整 dump,manifest 保持纯数据。启动器持有的 `ctx.headlessIo` 宿主钩子是唯一由宿主提供的 slot,且在任何配置树条目挂载之前,于 `boot()` 的 `prepare` 钩子中提供。
+- **在组合包 manifest 中放一个启动前 `context` 模块**承载启动期取值(dist 路径、flag 事实):否决,改用纯插件——粘合逻辑就是普通配置行和由应用持有的启动服务,因此组合始终可完整 dump,manifest 保持纯数据。启动器持有的 `ctx.headlessIo` 宿主钩子是唯一由宿主提供的 slot,且在任何配置树条目挂载之前,于 `boot()` 的 `prepare` 钩子中提供。
- **组合包的传递式自动应用**:只有直接列在 `dsh.profile.bundles` 中的条目才贡献层;想重新导出另一个组合包 patch 的元组合包,必须在自己的 patch 文件中显式完成。
## Consequences
diff --git a/.agents/notes/implemented/feature/2026-08-08-dsh-run-headless-command.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.i18n.yaml
similarity index 58%
rename from .agents/notes/implemented/feature/2026-08-08-dsh-run-headless-command.i18n.yaml
rename to .agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.i18n.yaml
index 730f57e681..15abf1380b 100644
--- a/.agents/notes/implemented/feature/2026-08-08-dsh-run-headless-command.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.i18n.yaml
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-08-dsh-run-headless-command.md
-2026-08-08-dsh-run-headless-command.md: ed095f4077a23e51bffb647d24eed19ba09e11ed
-2026-08-08-dsh-run-headless-command.zh.md: 89d54e35573f14786e05d648f2b42891ca27a043
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.md
+2026-08-06-app-owned-command-line.md: 4a05cac5ed7f44fb55c2d4498bf28a43befdb073
+2026-08-06-app-owned-command-line.zh.md: 86a37f416d17c4615152b29d73f171803f24c4c3
diff --git a/.agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.md b/.agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.md
new file mode 100644
index 0000000000..4a05cac5ed
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.md
@@ -0,0 +1,51 @@
+# Agent Note: Apps own their command line through `ctx.cmdlineArgs`
+
+Status: implemented
+
+English | [中文](2026-08-06-app-owned-command-line.zh.md)
+
+## Problem
+
+After profiles, compositions were installable but their command lines were not. `apps/cli` still declared the Web flag family (`--host`, `--port`, `--dev`, `--workspace-root`, `--trusted-host`) and the one-shot task positional, then derived patches for row ids it hardcoded (`webserver`, `api-gateway`, `connection`, `web-runtime`). An out-of-tree app such as [turtle-ui](https://github.com/deepseek-harness/turtle-ui) could contribute rows but had no way to accept a flag: `dsh --profile tui --resume ` had nowhere to be parsed, and `dsh --profile web --help` printed the launcher's help rather than the web app's.
+
+## Decision
+
+The launcher parses only what it owns — `--profile`, `--patch`, the config dumps — and hands **everything after its own flags** to the booted tree verbatim. The split is positional: the first token the launcher does not recognize starts the app's arguments (commander's `passThroughOptions` + `allowUnknownOption` + `helpOption(false)`). A bare `dsh -h`, which has no app to hand the flag to, still prints the launcher's own help.
+
+The new `@deepseek-ai/dsh-cmdline` package owns the handoff. A launcher calls `provideCmdline(ctx, host)` before any entry mounts, providing `ctx.cmdlineArgs` (whose whole interface is `get(): readonly string[]`) and `ctx.appExit`. Any ordinary app plugin may inject `cmdlineArgs`, call `parseCmdline(ctx, program, plan)` with its own commander program, and provide the returned value as an app-owned service. Its Loader row carries no launcher marker or special kind, and the launcher does not inspect the composition for an owner. Multiple plugins may read the same immutable snapshot; a profile with no reader ignores its app arguments. Rows configured from a provider inject its service and read direct lazy config expressions (`port: !!js ctx.webStartup.port ?? 3080`), so a flag beats the value written beside it and nothing is written back into any row.
+
+The boot mounts the composition once. Cordis holds each row until its injections are active; Loader then interpolates that row's `!!js` against the injection-ready plugin context immediately before activation. Include keeps nested row expressions raw until their target row reaches this point. `--help` leaves the provider's service absent, so dependent rows never activate, and a live patch reload interpolates again against the service that remains active, so a served port cannot be silently reset.
+
+The shipped apps moved their flags into their bundles: `dsh-web-app` owns the Web family (and enables the `client-hmr` row it now ships disabled, for `--dev`), and `dsh-headless` owns the task positional and rejects a missing task as a usage error. `apps/cli/src/web.ts` is gone; `runProfile` no longer knows any flag-target row id. Out of tree, turtle-ui gained `--resume ` / `--session ` the same way, which is the design's real validation: an installed plugin added a flag with no launcher change.
+
+Two further consequences. Loader mounts sibling rows concurrently, so one row can activate while another still mounts or while the whole boot is rolling back; the Web bundle therefore publishes its URL only after its own Loader tree settles. The Web bundle's runtime plugin owns the harness-source prompt section too, so `dsh web` and `dsh --profile web` boot identically without Web-specific launcher setup.
+
+## Why Loader owns the ordering
+
+Four framework facts shape the mechanism:
+
+- **A profile's rows arrive inside the root include's `patches` option.** Include is an entry-tree owner, so its static entry-config resolver interpolates Include's own options while preserving nested `!!js` nodes for their target rows instead of recursively evaluating them in the Include context.
+- **Cordis activates a fiber only after all declared injections are active.** Immediately before each activation, Cordis runs the `internal/config` waterfall against the fiber's own context; Loader's listener interpolates the raw config after Cordis snapshots its injected services.
+- **Provider replacement and HMR must preserve the same contract.** Fiber reactivation re-runs the waterfall, HMR carries the raw config to the replacement fiber, and a pending row accepts option changes without prematurely evaluating expressions against absent services.
+- **A row cannot be inserted from inside a mounting plugin** — `tree.create` returns a prefixed id it then fails to resolve — so a conditional row ships `disabled: true` and an active row enables it (`dsh web --dev` and its reload chain). Enablement is an in-memory Loader override rather than an options rewrite, so Include reapplication cannot silently disable it. The Web bundle also starts client discovery only after enabling the optional row, ensuring the first browser graph already contains its HMR receiver.
+
+This leaves dependency ordering in Cordis activation and Loader interpolation, which own it. Rows keep their `inject` and config, Loader mounts the composition once, and the launcher only provides argv and process-lifecycle services.
+
+## Alternatives considered
+
+- **Writing the resolved values into each row** (a config update per row, plus a patch layer handed back to the launcher so a reload could not undo it): it worked, but it meant patches travelling from an app to the launcher and back, two mechanisms for one fact, and a recycle whose correctness depended on Loader restart internals. The maintainer rejected the round trip; the service the rows read replaced all of it.
+- **Releasing rows by clearing their `inject`**: it worked in isolation and failed on the real web tree, because clearing `inject` is exactly what loses the plugin's static injections. The failure is silent until a plugin reads a service it declared.
+- **Launcher-managed two-pass mounting**: it can make a provider active before readers are applied, but duplicates the composition, makes ordering a launcher concern, and conceals the Loader defect that nested expressions were evaluated in the include context rather than the target row's injected context.
+- **The launcher running each bundle's command function before boot** (no Cordis involvement): strictly earlier than "boot, then help", but it makes app startup a second plugin protocol outside the tree. An ordinary `cmdlineArgs`-injected provider keeps one protocol and remains dumpable and patchable.
+- **A launcher-enforced command-line owner**: rejecting zero or multiple readers would arbitrate overlaps such as `-h`, but `get()` is an immutable read and normal composition may need several app-owned services. Plugins therefore share the snapshot and own any parser interaction through ordinary composition.
+- **`instanceof CommanderError`**: an out-of-tree plugin brings its own commander copy, so the class identity differs and a printed `--help` was rethrown as a fatal load failure. Commander's control-flow errors are detected structurally instead.
+
+## Consequences
+
+- An app's flags, help text, and usage errors live with the rows they configure; adding a flag to an installed plugin needs no launcher change.
+- The launcher still recognizes the headless runner for one-shot process lifetime and the telemetry row for its environment switch; neither path interprets app arguments.
+- `--help` leaves every row that depends on the provider's service pending and requests bounded exit; unrelated rows may activate concurrently before teardown.
+- An app-owned service has no statically declared provider: a bundle shipping consumer rows without that provider fails at settlement with pending entries naming the service, not at load.
+- A user patch that replaces a row's whole `config` drops its expressions, and with them the flag's precedence for that row.
+- Launcher flags must precede app arguments; a first app argument equal to `web` or `plugin` selects that subcommand instead, `-V`/`--version` remains launcher-owned before that boundary, and the launcher's parser consumes one `--`, so a literal `--` for the app needs `-- --`.
+- `--dump-config` never runs app command-line providers, so it prints the composition before any app argument is resolved and rejects an invocation that carries app arguments.
diff --git a/.agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.zh.md b/.agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.zh.md
new file mode 100644
index 0000000000..86a37f416d
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.zh.md
@@ -0,0 +1,51 @@
+# Agent Note: 应用通过 `ctx.cmdlineArgs` 持有自己的命令行
+
+Status: implemented
+
+[English](2026-08-06-app-owned-command-line.md) | 中文
+
+## 问题
+
+profile 落地之后,组合可以安装,命令行却不能。`apps/cli` 仍然声明着 Web flag 家族(`--host`、`--port`、`--dev`、`--workspace-root`、`--trusted-host`)和一次性任务位置参数,再为自己硬编码的行 id(`webserver`、`api-gateway`、`connection`、`web-runtime`)派生 patch。像 [turtle-ui](https://github.com/deepseek-harness/turtle-ui) 这样的树外应用能贡献行,却无处接受一个 flag:`dsh --profile tui --resume ` 没有地方可供解析,而 `dsh --profile web --help` 打印的是启动器的 help,而不是 web 应用的 help。
+
+## 决策
+
+启动器只解析属于自己的部分(`--profile`、`--patch`、配置 dump),并把**自己 flag 之后的一切**原样交给引导起来的配置树。切分按位置进行:启动器不认识的第一个 token 就是应用参数的起点(依靠 commander 的 `passThroughOptions` + `allowUnknownOption` + `helpOption(false)`)。裸的 `dsh -h` 没有可交付的应用,仍然打印启动器自己的 help。
+
+新包 `@deepseek-ai/dsh-cmdline` 持有这次交接。启动器在任何条目挂载之前调用 `provideCmdline(ctx, host)`,提供 `ctx.cmdlineArgs`(其全部接口就是 `get(): readonly string[]`)与 `ctx.appExit`。任何普通应用插件都可以注入 `cmdlineArgs`,用自己的 commander program 调用 `parseCmdline(ctx, program, plan)`,再把返回值作为应用自有服务提供出去。它的 Loader 行不携带启动器标记或特殊类型,启动器也不会检查组合中的所有者。多个插件可以读取同一份不可变快照;没有读取方的 profile 会忽略自己的应用参数。由提供方配置的行注入其服务,并在惰性配置表达式中直接读取它(`port: !!js ctx.webStartup.port ?? 3080`),因此 flag 胜过写在它旁边的值,也没有任何东西被写回任何一行。
+
+boot 只挂载一次整套组合。Cordis 让每一行等待其注入激活;Loader 随后在激活前一刻,基于已注入就绪的插件上下文插值该行的 `!!js`。Include 会保留嵌套的行表达式,直到目标行到达这一时点。`--help` 会让提供方服务保持缺失,因此依赖行永不激活;活动 patch 重载会针对仍然在线的服务再次插值,所以已经服务中的端口不会被悄悄重置。
+
+已交付的各应用把自己的 flag 搬进了组合包:`dsh-web-app` 持有 Web 家族(并为 `--dev` 启用它如今以禁用状态交付的 `client-hmr` 行),`dsh-headless` 持有任务位置参数,缺少任务时按用法错误拒绝。`apps/cli/src/web.ts` 已删除;`runProfile` 不再知道任何 flag 目标行 id。在树外,turtle-ui 以同样的方式获得了 `--resume ` / `--session `,这才是这套设计的真正验证:一个已安装的插件加上了一个 flag,启动器毫无改动。
+
+还有两条后果。Loader 会并发挂载兄弟行,因此一行可能已经激活,而另一行仍在挂载,或整次 boot 正在回滚;所以 Web 组合包只会在自身的 Loader 配置树结算后公布 URL。另外,Web 组合包的运行时插件也持有 harness 源码提示词段,因此 `dsh web` 与 `dsh --profile web` 无需 Web 专用启动器设置即可按完全相同的方式启动。
+
+## 为什么由 Loader 持有顺序
+
+四条框架事实塑造了这套机制:
+
+- **profile 的各行位于根 include 的 `patches` 选项内部。** Include 是条目树所有者,因此它的静态条目配置解析器会插值 Include 自身的选项,同时为目标行保留嵌套的 `!!js` 节点,而不是在 Include 上下文中递归求值。
+- **Cordis 只在所有声明的注入都已激活后才激活 fiber。** 每次激活前一刻,Cordis 会基于 fiber 自身上下文运行 `internal/config` waterfall;Cordis 快照注入服务之后,Loader 的监听器再插值原始配置。
+- **提供方替换与 HMR 必须保持相同契约。** fiber 重新激活时会重跑 waterfall,HMR 会把原始配置带给替换 fiber,而待处理行可以接受选项变更,不会针对缺失服务提前求值表达式。
+- **不能从正在挂载的插件内部插入一行**——`tree.create` 返回一个带前缀的 id,随后它自己解析不出来——因此条件性的行以 `disabled: true` 交付,再由活跃行启用(`dsh web --dev` 及其重载链路)。启用采用 Loader 的内存覆盖而非改写选项,因此 Include 重新应用配置时不会悄然将其禁用。Web 组合包还会在启用可选行之后才启动客户端发现,确保首份浏览器图中已经包含 HMR 接收端。
+
+这样,依赖顺序仍由负责它的 Cordis 激活与 Loader 插值流程处理。各行保留自己的 `inject` 和配置,Loader 只挂载一次组合,启动器只提供 argv 与进程生命周期服务。
+
+## 曾考虑的替代方案
+
+- **把解析出的取值写进每一行**(逐行一次配置更新,外加交还给启动器的一层 patch,使重载无法撤销它):它能工作,但这意味着 patch 在应用与启动器之间来回传递、同一件事有两套机制,以及一套其正确性依赖 Loader 重启内部细节的回收重建。维护者否决了这次往返;供各行读取的服务取代了这一切。
+- **通过清空行的 `inject` 来放行**:孤立测试可行,在真实 web 树上失败,因为清空 `inject` 恰恰会丢失插件的静态注入。在插件真的去读它声明过的服务之前,这个失败是静默的。
+- **由启动器管理两趟挂载**:它可以让提供方先于读取行激活,但会重复组合、把顺序变成启动器职责,还掩盖了 Loader 的缺陷——嵌套表达式在 include 上下文而不是目标行的注入上下文中求值。
+- **由启动器在 boot 之前运行每个组合包的命令函数**(完全不经过 Cordis):严格早于「先 boot 再 help」,但这会让应用启动成为配置树之外的第二套插件协议。使用注入 `cmdlineArgs` 的普通提供方只保留一套协议,并且仍可 dump、可 patch。
+- **由启动器强制指定命令行所有者**:拒绝零个或多个读取方可以裁决 `-h` 等重叠项,但 `get()` 是不可变读取,普通组合也可能需要多个应用自有服务。因此插件共享该快照,并通过普通组合持有各自解析器的交互。
+- **`instanceof CommanderError`**:树外插件会带来自己的一份 commander 副本,类身份因此不同,已经打印出来的 `--help` 会被重新抛成致命的加载失败。改为按结构识别 commander 的控制流错误。
+
+## 后果
+
+- 应用的 flag、help 文本和用法错误与它们所配置的行放在一起;给已安装的插件加一个 flag 不需要改动启动器。
+- 启动器仍会识别 headless runner 以管理一次性进程生命周期,并识别 telemetry 行以应用环境开关;两条路径都不解析应用参数。
+- `--help` 会让所有依赖提供方服务的行保持待处理并请求有边界的退出;无关行可能在拆除前并发激活。
+- 应用自有服务没有静态声明的提供方:交付了消费行却缺少对应提供方的组合包会在结算时失败,报出指向该服务的待处理条目,而不是在加载时失败。
+- 用户 patch 若整体替换某行的 `config`,会连同其中的表达式一起丢掉,该行上 flag 的优先级也随之消失。
+- 启动器的 flag 必须写在应用参数之前;如果应用的第一个参数恰好等于 `web` 或 `plugin`,会选择对应的子命令;`-V`/`--version` 在该边界之前仍归启动器持有;而且启动器的解析器会消耗掉一个 `--`,因此要给应用传一个字面量 `--` 需要写成 `-- --`。
+- `--dump-config` 从不运行应用命令行提供方,因此它在任何应用参数被解析之前打印组合,并拒绝携带应用参数的调用。
diff --git a/.agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.md b/.agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.md
deleted file mode 100644
index 387479b3b3..0000000000
--- a/.agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# Agent Note: Trusted repository packages load Cordis code
-
-Status: implemented
-
-English | [中文](2026-08-08-trusted-repository-package-code.zh.md)
-
-## Problem
-
-The standalone repository format already installs a selected Git package and runs its dependency and lifecycle code with host authority, but it exposed only copied skills and MCP metadata to DSH. Forbidding a Cordis entry did not create a security boundary: package installation remained trusted executable code while the restriction prevented the package from contributing the Plugin behavior that the Harness architecture is designed to compose.
-
-A repository author also needs to keep an ordinary TypeScript npm package shape. Requiring publication to npm, pre-generated JavaScript in Git, or a DSH-owned TypeScript compiler would make a Git source less capable than the same package installed through a developer-owned SDK project. The first model request must observe any MCP tools that this package starts; background-only initial discovery makes a successful installation nondeterministic at the application boundary.
-
-## Decision
-
-A configured repository package is trusted code. Its `.dsh-plugin/package.json` may declare `dsh.entry` as a relative path to a compiled ESM Cordis Plugin inside that package, alongside or instead of `dsh.skills` and `dsh.mcpServers`. At least one contribution is required. The entry may use namespace exports or a default export and retains ordinary Cordis semantics for `name`, `inject`, `Config`, registrations, startup failure, and effect-scoped teardown.
-
-The package owns its npm dependencies and build toolchain. It declares the published `@deepseek-ai/dsh-repository-plugin` package to obtain the `dsh-plugin-prepare` executable. `scripts.prepack` is a non-empty package-authored command that must invoke that dependency-provided helper, but it may first run `tsc`, `tsdown`, or any other build. DSH neither injects the helper, parses the shell program, nor compiles repository source. The helper validates the metadata after the preceding build, requires the configured entry to resolve to a file within `.dsh-plugin`, validates and copies declared static assets, and writes the prepared `dsh-plugin.mjs` wrapper. The installed package must retain a `prepack` declaration containing that helper command; a missing dependency, wrapper, or build output fails before a cache generation becomes usable.
-
-The generated wrapper first mounts the DSH-owned static runtime for skills and MCP definitions, then dynamically imports and unwraps the explicit entry and mounts it as a child. The wrapper statically declares dependencies implied by the prepared manifest; an entry module's additional `inject` is discovered only when mounted and must already be available in the host composition. Both children must reach Cordis `ACTIVE`; an unsatisfied `inject` or startup exception rejects the repository Loader transaction instead of committing an inert generation. Loader removal, failed replacement, and parent disposal unwind the entry, skill providers, MCP clients, and their effects together.
-
-`dsh-mcp-client` resolves its initial connection and tool synchronization promise as part of Plugin application. Its entry is an `async function`, not an ordinary function returning a Promise: Cordis identifies prototype-bearing ordinary functions as constructors and does not treat a constructor's returned Promise as startup work. A valid server's tools therefore exist before its parent repository wrapper activates and before a one-shot application starts its first model request. Its `failOnStartupError` config preserves optional standalone servers by default while letting repository adapters require their declared servers. Repository-translated MCP clients enable that mode, so initial connection, discovery, or tool-registration failure rejects the candidate generation and rollback still closes the transport.
-
-## Trust boundary
-
-Exact refs, source containment, credential-shaped environment scrubbing, prepared manifests, and immutable cache keys protect identity and composition integrity; they do not sandbox executable package input. Repository lifecycle scripts, transitive npm dependencies, the compiled entry, and spawned MCP servers can exercise the authority available to the DSH process and the Cordis services they receive. Users must therefore trust the selected repository and should pin immutable refs and grant Git only the narrow read credential needed for acquisition.
-
-Model-visible behavior remains governed by the owning DSH seam. A repository entry may register tools, prompt sections, policies, commands, agents, or other effects, but anything reaching a model request still needs the corresponding logged DSH representation and lifecycle cleanup. The repository format grants code loading; it does not weaken those service contracts.
-
-## Alternatives considered
-
-**Keep code forbidden while allowing arbitrary package lifecycles.** Rejected because installation already executes trusted repository code, so the restriction added no isolation and forced Plugin authors to publish or maintain a second integration path.
-
-**Have DSH compile repository TypeScript.** Rejected because compiler choice, module layout, generated chunks, native dependencies, and package metadata belong to the npm package. Running the package's declared build preserves the same boundary as other Git dependencies.
-
-**Import `main`, `exports`, or another discovered entry implicitly.** Rejected because an npm package may contain utilities or an MCP executable that is not a Cordis Plugin. The explicit `dsh.entry` field makes code activation reviewable and lets preparation validate the packed path.
-
-**Add a closed manifest field for every future DSH contribution.** Rejected as the universal extension mechanism. Skills and common MCP files retain useful portable static adapters, while DSH-native behavior composes through the existing Cordis Plugin and service contracts.
-
-## Consequences
-
-- A TypeScript DSH Plugin can live in a GitHub repository, install ordinary npm dependencies, compile during `prepack`, and run without publishing the Plugin package to npm.
-- Static-only repository packages remain valid and retain import-free wrappers; adding `dsh.entry` opts that package into runtime code import.
-- A package build, dependency install, entry import, unmet service, or Plugin startup failure prevents the candidate generation from replacing the last good configuration.
-- Initial MCP synchronization can lengthen application startup by the MCP SDK's per-request timeout, and a repository-declared server that is unavailable or cannot publish its complete tool generation prevents that candidate generation from activating.
-- Repository code receives host authority, so source review and immutable pinning are operational security requirements rather than optional hardening.
-
-## Testing
-
-Repository-format tests prepare and mount default-export code entries through the real Loader, observe an entry-owned service, remove the Loader row, and observe cleanup; they also retain skill/MCP preparation, containment, damaged-package, pending-service, and rollback coverage. MCP lifecycle tests require `apply` to settle only after initial tool publication, preserve opt-in contained startup failure, and prove strict connection or tool-registration rejection still closes the client.
-
-The Node 24 consumer acceptance uses the actual built `dsh run` command with a fresh DSH home and an authenticated private GitHub source pinned to the pull request's exact head SHA. The test packs the current repository Plugin build with the same private-field removal and workspace-dependency pinning used for publication, serves its packument and tarball from a job-local npm registry, and directs the Git package's ordinary scoped npm resolution there. That repository package obtains `dsh-plugin-prepare` from the simulated published dependency, installs its other pinned runtime and development dependencies, type-checks and bundles TypeScript during `prepack`, prepares a skill plus a stdio MCP server and `dsh.entry`, exposes the skill and MCP schema in the first real model request, executes the MCP tool, and lets the compiled Cordis entry append a second marker to the result observed in the following request. Registry and cache assertions require npm resolution to reach the simulated publication, source files to be absent from the packed installation, and both built modules, their installed dependency, copied assets, and generated wrapper to be present.
diff --git a/.agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.zh.md b/.agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.zh.md
deleted file mode 100644
index ecc325c3dd..0000000000
--- a/.agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.zh.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# Agent Note: 受信任 repository 包加载 Cordis 代码
-
-状态:已实现
-
-[English](2026-08-08-trusted-repository-package-code.md) | 中文
-
-## 问题
-
-独立 repository 格式已经会安装选定的 Git 包,并以宿主权限运行其依赖和生命周期代码,但它向 DSH 暴露的只有复制后的 skill(技能)和 MCP 元数据。禁止 Cordis 入口并未建立安全边界:包安装过程仍会执行受信任代码,而这项限制却阻止包贡献 Harness 架构本就用于组合的插件行为。
-
-仓库作者还需要保持普通 TypeScript NPM 包的结构。如果要求发布到 NPM、把预生成的 JavaScript 签入 Git,或使用 DSH 自有的 TypeScript 编译器,Git 源的能力就会弱于通过开发者自有 SDK 项目安装的同一个包。首个模型请求必须看到该包启动的所有 MCP 工具;仅在后台进行初始发现,会让一次成功安装在应用边界上具有不确定性。
-
-## 决策
-
-已配置的 repository 包是受信任代码。其 `.dsh-plugin/package.json` 可以连同 `dsh.skills` 和 `dsh.mcpServers` 声明 `dsh.entry`,也可以用它取代二者;`dsh.entry` 是指向该包内已编译 ESM Cordis 插件的相对路径。至少需要一种贡献。入口可以使用 namespace 导出或 default export,并沿用 Cordis 对 `name`、`inject`、`Config`、注册、启动失败和 effect 作用域清理的常规语义。
-
-包自行负责其 NPM 依赖和构建工具链。它声明已发布的 `@deepseek-ai/dsh-repository-plugin` 包以取得 `dsh-plugin-prepare` 可执行文件。`scripts.prepack` 是由包作者编写的非空命令,必须调用该依赖提供的辅助程序,但可以先运行 `tsc`、`tsdown` 或其他任意构建。DSH 不会注入辅助程序,也不会解析该 shell 程序或编译 repository 源码。辅助程序会在前序构建之后校验元数据,要求已配置入口解析到 `.dsh-plugin` 内的文件,校验并复制已声明的静态资源,再写入已准备的 `dsh-plugin.mjs` 包装层。已安装包必须保留包含该辅助命令的 `prepack` 声明;依赖、包装层或构建输出缺失会在缓存 generation 可用前导致失败。
-
-生成的包装层先挂载 DSH 自有的静态运行时来处理 skill 和 MCP 定义,再动态导入显式入口、解包其导出并将其挂载为子级。包装层会静态声明已准备 manifest(元数据清单)所隐含的依赖;入口模块的额外 `inject` 只有在挂载时才会被发现,并且此时必须已存在于宿主组合中。两个子级都必须进入 Cordis `ACTIVE`;无法满足的 `inject` 或启动异常会拒绝 repository Loader 事务,而不会提交未激活的 generation。Loader 移除、替换失败和父级 dispose(资源释放)会一并撤销入口、skill 提供方、MCP client 及其 effect。
-
-`dsh-mcp-client` 会在插件应用期间完成其初始连接和工具同步 promise。其入口必须是 `async function`,而不是返回 Promise 的普通函数:Cordis 会把带 prototype 的普通函数识别为 constructor,不会把 constructor 返回的 Promise 当作启动工作。因此,有效 server 的工具会在父级 repository 包装层激活前、一次性应用发起首个模型请求前就已存在。其 `failOnStartupError` 配置默认保留独立可选 server 的行为,同时允许 repository adapter 要求已声明 server 必须可用。Repository 转换出的 MCP client 会启用该模式,因此初始连接、发现或工具注册失败会拒绝候选 generation,回滚仍会关闭 transport。
-
-## 信任边界
-
-精确 ref、源路径包含约束、清除名称符合凭据模式的环境变量、已准备的 manifest 和不可变缓存键,可以保护身份与组合完整性;它们不会为可执行包输入提供沙箱隔离。Repository 生命周期脚本、传递性 NPM 依赖、已编译入口和 spawn 的 MCP server 可以行使 DSH 进程可用的权限,以及它们所获 Cordis 服务授予的权限。因此,用户必须信任所选仓库,应当固定不可变 ref,并只授予 Git 获取源码所需的最小只读凭据。
-
-模型可见行为仍由所属 DSH seam 管理。repository 入口可以注册工具、提示词段落、策略、命令、agent(智能体)或其他 effect,但任何进入模型请求的内容仍须具有对应的 DSH 日志表示和生命周期清理。repository 格式授予代码加载能力;它不会削弱这些服务约定。
-
-## 考虑过的替代方案
-
-**继续禁止代码,但允许任意包生命周期。** 拒绝,因为安装过程本就执行受信任的 repository 代码,所以该限制没有提供隔离,反而迫使插件作者发布或维护第二条集成路径。
-
-**由 DSH 编译 repository TypeScript。** 拒绝,因为编译器选择、模块布局、生成分片、原生依赖和包元数据属于 NPM 包。运行包所声明的构建,可以保持与其他 Git 依赖相同的边界。
-
-**隐式导入 `main`、`exports` 或其他发现的入口。** 拒绝,因为 NPM 包可能包含并非 Cordis 插件的实用工具或 MCP 可执行文件。显式 `dsh.entry` 字段使代码激活可供评审,并让准备阶段校验打包后的路径。
-
-**为未来每种 DSH 贡献添加封闭 manifest 字段。** 不采用它作为通用扩展机制。skill 和通用 MCP 文件仍保留有用的可移植静态适配器;DSH 原生行为则通过现有 Cordis 插件与服务约定组合。
-
-## 后果
-
-- TypeScript DSH 插件可以存放在 GitHub 仓库中,安装普通 NPM 依赖,在 `prepack` 期间完成编译,并在无需把插件包发布到 NPM 的情况下运行。
-- 仅含静态贡献的 repository 包仍然有效,并保留无 import 包装层;添加 `dsh.entry` 会使该包选择启用运行时代码导入。
-- 包构建、依赖安装、入口导入、所需服务未满足或插件启动失败,都会阻止候选 generation 替换最后一个可用配置。
-- 初始 MCP 同步可能因 MCP SDK 的单次请求超时而延长应用启动时间;repository 声明的 server 不可用或无法发布完整工具 generation 时,该候选 generation 无法激活。
-- Repository 代码获得宿主权限,因此源码评审和锁定不可变 ref 是运行安全要求,而不是可选加固措施。
-
-## 测试
-
-repository 格式测试通过真实 Loader 准备并挂载使用 default export 的代码入口,观察入口自有服务,移除 Loader 配置项,再观察清理;测试还保留针对 skill/MCP 准备、路径包含约束、包损坏、等待服务和回滚的覆盖。MCP 生命周期测试要求 `apply` 只在初始工具发布后完成,保留可选择启用的启动失败收束行为,并证明严格连接拒绝或工具注册拒绝仍会关闭 client。
-
-Node 24 消费方验收使用实际构建的 `dsh run` 命令、全新 DSH 主目录,以及锁定到 PR(Pull Request)的精确 head SHA 且经过认证的私有 GitHub 源。测试会采用发布时相同的移除 `private` 字段和固定 workspace 依赖版本流程,对当前 repository 插件构建进行打包;再由作业本地 NPM 注册表提供其 `packument` 与 tarball,并把 Git 包的常规 scoped NPM 解析指向该注册表。该 repository 包从模拟发布的依赖取得 `dsh-plugin-prepare`,安装其他固定版本的运行时依赖与开发依赖,在 `prepack` 期间对 TypeScript 进行类型检查和打包,准备一个 skill、一个 stdio MCP server 及 `dsh.entry`,在首个真实模型请求中暴露 skill 与 MCP schema,执行 MCP 工具,并让已编译 Cordis 入口向结果追加第二个标记,供后续请求观察。注册表与缓存断言要求 NPM 解析必须命中模拟发布,打包安装中不存在源码文件,同时必须存在两个已构建模块、其已安装依赖、复制资源和生成包装层。
diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml
index b76951347b..42600f1c56 100644
--- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md
-2026-08-09-client-conversation-node-assembly.md: 16a39539064644e5467f701789a7e2ef1f7ff172
-2026-08-09-client-conversation-node-assembly.zh.md: 0e0fbdf8f3320393022528e6e3fe2cf0d492a1d3
+2026-08-09-client-conversation-node-assembly.md: f6cd7ea94d485d92fd4ea178751c08bd01ecb4b2
+2026-08-09-client-conversation-node-assembly.zh.md: e550b7870a611ec625d7c2a738bf71947825ea58
diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md
index 16a3953906..f6cd7ea94d 100644
--- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md
+++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md
@@ -148,7 +148,7 @@ The Assembler verifies `node.key === context.key` and `node.target === target`.
`current` lets a Definition distinguish "never materialized" from "already materialized and now hidden." Assistant retry and Turn Error suppression use it to avoid illegal Node withdrawal.
-A Definition may branch by target to construct different data, while matching, Context identity, and State remain target-neutral. This change registers only the `chat` builder; Trajectory remains on its independent `session-history` fold until it gains a registered target.
+A Definition owns at most one view target; state-only Definitions omit both `target` and `buildViewNode()`. Chat and Trajectory register separate business Definitions even when they recognize the same durable Event family, while the shared Assembler supplies the same matching, replay, Location, and publication mechanics to both targets.
#### No generic `end()`
@@ -328,7 +328,9 @@ When business logic deliberately changes a materialized Node to hidden, it leave
The concrete Tool renderer remains governed by the [`ui-tool ownership decision`](2026-08-08-client-tool-presentation-ownership.md). Tool Definition supplies recursive root/subcall data, and `ui-tool` dispatches concrete presentation by the Tool-name keyed slot.
-Trajectory has no registered target and does not consume the Chat Builder's legacy slice. Its activated `SessionHistoryInspection` keeps an independent history fold, while the ordinary Session snapshot no longer runs a second transcript fold. The Chat Builder retains its legacy slice for StatsLine and the top-level public compatibility fields; a future Trajectory migration does not change the Event Definition, Context, Reader, or Location contracts.
+Trajectory registers its own target and business Definitions against the same Assembler and Session event window as Chat. Its target builder preserves the stage-oriented read model without consuming the Chat Builder's legacy slice or running an independent history fold. The Chat Builder retains its legacy slice for StatsLine and the top-level public compatibility fields; target-specific Definitions do not change the shared Context, Reader, or Location contracts.
+
+The target-specific Trajectory Definitions, retained stage model, Steering adaptation, complexity bounds, and presentation hot paths are owned by the [Trajectory Context assembly decision](2026-08-11-trajectory-conversation-context-assembly.md).
## Runtime and render path
@@ -339,20 +341,17 @@ Session Event window
-> Context matches + State + Location
-> Definition.buildLocationData(step -> turn)
-> StepLocation.data / TurnLocation.data
- -> Definition.buildViewNode(target = chat)
- -> ChatSnapshotBuilder
- -> order[] + keyed Node store + Location index + timeline
- -> ChatView
- -> ChatNodeSeat(key)
- -> conversation.chat.node(entryKey = node.kind, hookContext = key)
- -> slot-level useTurnData(businessKey)
+ -> Definition.buildViewNode() for its declared target
+ -> target View Builder
+ -> chat: ChatSnapshotBuilder -> ChatView -> keyed ChatNodeSeat
+ -> trajectory: TrajectorySnapshotBuilder -> stages/layout/table
```
## Verification
Runtime tests pin Definition lifecycle registration, exact-ID append, update-before-start collection followed by forward replay after start, prepend identity, Reader window-gap repair, transitive dependencies, Location closure, Step→Turn data phase order, Location data replacement, publication cadence, illegal withdrawal, and per-target Builders.
-Conversation tests cover every built-in Definition, Assistant Step data, Turn Tail and Deliverables Turn data, Chat ordering and structural sharing, selector isolation, Assistant and Tool running-to-settled identity, nested Code Dispatch, steering, Compaction, Retry, interruption, load-older anchoring, and slot dispatch.
+Conversation tests cover every built-in Chat Definition, Assistant Step data, Turn Tail and Deliverables Turn data, Chat ordering and structural sharing, selector isolation, Assistant and Tool running-to-settled identity, nested Code Dispatch, steering, Compaction, Retry, interruption, load-older anchoring, and slot dispatch. Trajectory tests cover its independently registered Message, Assistant, Tool, Compaction, Request-header, and boundary Definitions together with the preserved stage-oriented view model.
Slot type/runtime tests pin required parent-provided common inject, the `hookContext` type, Hook isolation across Node contexts, stable factory/Hook identity, and the absence of business-renderer rerenders for unrelated Session publications. Existing entry-owned Observable Hook tests continue to pin the path that does not use a contextual factory.
@@ -382,7 +381,7 @@ History-path tests cover complete replace, non-overlapping prepend, overlapping-
**Add generic `end()`, prepared, or window-reset lifecycles.** Rejected: businesses have different completion conditions, and a pagination gap is not a business lifecycle. Business Events update State, Location close triggers replay/build, and Reader dependencies own pagination invalidation.
-**Register separate Event Definitions for Chat and Trajectory.** Rejected: identity, State, and Location are target-neutral. `buildViewNode(target)` and each Builder express view differences; Trajectory's independent history fold remains until it registers its own Builder.
+**Reuse one Event Definition across Chat and Trajectory by branching in `buildViewNode(target)`.** Rejected: the views require different business State and intermediate records, so a shared Definition would make each package carry the other's conditions and payloads. Separate target-owned Definitions keep those choices local while sharing the Assembler's ingestion and lifecycle contracts.
**Add a generic layout model above final business Nodes.** Rejected: activity, tail candidacy, and layout enums would centralize current Chat business semantics in the engine again. Final Nodes carry renderer-required data directly and share only identity, ordering, and Location facts.
@@ -406,4 +405,4 @@ Steps and Turns become stable homes for cross-business aggregates. Turn Tail and
The cost is new Runtime contracts for Registry, Assembler, Location data, dependency replay, and per-target Builders, plus parent-owned common inject and per-occurrence `hookContext` in UI Slots. Definition authors must understand stable IDs, unique starts, forward replay, Step→Turn publication order, read-only Reader access, and the prohibition on Node withdrawal.
-`useTurnData()` does not revoke the standard `useSession` capability from session-scoped renderers, so this boundary relies on API guidance and tests rather than capability isolation. Registry changes remain low-frequency full rebuilds; the Chat Builder still maintains a legacy slice for StatsLine and the top-level public fields, Trajectory still owns an independent history fold, and built-in Definitions currently remain centralized in `ui-conversation`. These compatibility boundaries do not return business interpretation to Session.
+`useTurnData()` does not revoke the standard `useSession` capability from session-scoped renderers, so this boundary relies on API guidance and tests rather than capability isolation. Registry changes remain low-frequency full rebuilds; the Chat Builder still maintains a legacy slice for StatsLine and the top-level public fields, while Trajectory owns target-specific Definitions and a Builder over the shared Session window. Built-in Definitions remain in their respective UI packages, and these compatibility boundaries do not return business interpretation to Session.
diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md
index 0e0fbdf8f3..e550b7870a 100644
--- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md
@@ -148,7 +148,7 @@ Assembler 校验 Node `key === context.key` 且 Node `target === target`。业
`current` 让 Definition 区分“从未生成”与“已经生成后需要隐藏”。Assistant retry 和 Turn Error suppression 使用它避免非法的 Node 撤回。
-Definition 可以针对 target 分支构造不同 data,但匹配、Context identity 和 State 保持 target-neutral。本次只注册 `chat` builder;在拥有注册 target 之前,Trajectory 继续使用独立的 `session-history` fold。
+一个 Definition 最多拥有一个 view target;仅维护状态的 Definition 同时省略 `target` 与 `buildViewNode()`。即使 Chat 与 Trajectory 识别同一持久 Event 族,它们也分别注册自己的业务 Definition;共享 Assembler 则为两个 target 提供相同的匹配、replay、Location 与发布机制。
#### 不提供通用 `end()`
@@ -328,7 +328,9 @@ Assistant streaming 到 final、Tool running 到 settled 只更新同一个 Seat
具体 Tool renderer 仍由 [`ui-tool ownership decision`](2026-08-08-client-tool-presentation-ownership.md) 约束。Tool Definition 只交付递归 root/subcall data,`ui-tool` 再按 Tool name keyed slot 分发具体表现。
-Trajectory 尚未注册 target,也不消费 Chat Builder 的 legacy slice。它已激活的 `SessionHistoryInspection` 继续维护独立 history fold,而普通 Session snapshot 不再运行第二套 transcript fold。Chat Builder 为 StatsLine 和顶层公共兼容字段保留 legacy slice;未来迁移 Trajectory 不改变 Event Definition、Context、Reader 或 Location 契约。
+Trajectory 针对与 Chat 相同的 Assembler 和 Session 事件窗口注册自己的 target 与业务 Definition。它的 target builder 保留 stage-oriented read model,既不消费 Chat Builder 的 legacy slice,也不运行独立 history fold。Chat Builder 为 StatsLine 和顶层公共兼容字段保留 legacy slice;target 专属 Definition 不改变共享的 Context、Reader 或 Location 契约。
+
+target 专属 Trajectory Definition、保留的 stage model、Steering 适配、复杂度上界与表现层热点由 [Trajectory Context 组装决策](2026-08-11-trajectory-conversation-context-assembly.md)负责。
## Runtime and render path
@@ -339,20 +341,17 @@ Session Event window
-> Context matches + State + Location
-> Definition.buildLocationData(step -> turn)
-> StepLocation.data / TurnLocation.data
- -> Definition.buildViewNode(target = chat)
- -> ChatSnapshotBuilder
- -> order[] + keyed Node store + Location index + timeline
- -> ChatView
- -> ChatNodeSeat(key)
- -> conversation.chat.node(entryKey = node.kind, hookContext = key)
- -> slot-level useTurnData(businessKey)
+ -> Definition.buildViewNode() for its declared target
+ -> target View Builder
+ -> chat: ChatSnapshotBuilder -> ChatView -> keyed ChatNodeSeat
+ -> trajectory: TrajectorySnapshotBuilder -> stages/layout/table
```
## Verification
Runtime tests 固定 Definition 生命周期注册、exact-ID append、update-before-start 收集与 start 后正序 replay、prepend identity、Reader window-gap 修复、传递依赖、Location closure、Step→Turn data phase order、Location data replacement、publication cadence、非法撤回和 per-target Builder。
-Conversation tests 覆盖全部内建 Definition、Assistant Step data、Turn Tail 与 Deliverables Turn data、Chat 排序和结构共享、selector isolation、Assistant/Tool running-to-settled identity、nested Code Dispatch、steering、Compaction、Retry、interruption、load-older anchoring 和 slot dispatch。
+Conversation tests 覆盖全部内建 Chat Definition、Assistant Step data、Turn Tail 与 Deliverables Turn data、Chat 排序和结构共享、selector isolation、Assistant/Tool running-to-settled identity、nested Code Dispatch、steering、Compaction、Retry、interruption、load-older anchoring 和 slot dispatch。Trajectory tests 则覆盖它独立注册的 Message、Assistant、Tool、Compaction、Request-header 与 boundary Definition,以及继续保留的 stage-oriented view model。
Slot type/runtime tests 固定父注册必须提供声明的 common inject、`hookContext` 类型、不同 Node context 的 Hook 隔离、factory/Hook identity 稳定,以及无关 Session publication 不重渲染业务 renderer。原 entry-owned Observable Hook 测试继续固定未使用 contextual factory 的路径。
@@ -382,7 +381,7 @@ Assembled Web snapshot、GUI 和浏览器场景覆盖真实 plugin graph。浏
**增加通用 `end()`、prepared 或 window reset 生命周期。** 拒绝:不同业务完成条件不同,分页缺口也不是业务生命周期。业务 Event 更新 State,Location close 触发 replay/build,Reader dependency 负责补页失效。
-**为 Chat 与 Trajectory 注册两套 Event Definition。** 拒绝:identity、State 和 Location 与 target 无关。视图差异由 `buildViewNode(target)` 和各自 Builder 表达;Trajectory 在注册自己的 Builder 之前继续使用独立 history fold。
+**在同一个 Event Definition 内通过 `buildViewNode(target)` 为 Chat 与 Trajectory 分支。** 拒绝:两种视图需要不同的业务 State 与中间记录,共用 Definition 会迫使每个 package 携带另一边的条件与 payload。target 自有的 Definition 把这些选择留在本地,同时复用 Assembler 的摄入与生命周期契约。
**在最终业务 Node 上再叠一层通用 layout model。** 拒绝:activity、tail candidacy 和 layout enum 会把当前 Chat 的业务语义重新集中到引擎。最终 Node 直接携带 renderer 所需 data,只共享 identity、排序和 Location 事实。
@@ -406,4 +405,4 @@ Step/Turn 成为业务间共享聚合的稳定宿主。Turn Tail 和 Deliverable
代价是 Runtime 新增 Registry、Assembler、Location data、依赖重放和 per-target Builder 契约,UI Slots 也新增 parent-owned common inject 与 per-occurrence `hookContext`。Definition 作者必须理解稳定 ID、唯一 start、正序 replay、Step→Turn 发布顺序、只读 Reader 和 Node 不撤回规则。
-`useTurnData()` 不撤销 session-scoped renderer 的标准 `useSession`,因此该边界依靠 API 引导和测试,而不是能力隔离。Registry 变化仍是低频完整 rebuild;Chat Builder 继续为 StatsLine 和顶层公共字段维护 legacy slice,Trajectory 继续拥有独立 history fold,内建 Definitions 暂时集中在 `ui-conversation`。这些是兼容边界,不把业务解释权交还给 Session。
+`useTurnData()` 不撤销 session-scoped renderer 的标准 `useSession`,因此该边界依靠 API 引导和测试,而不是能力隔离。Registry 变化仍是低频完整 rebuild;Chat Builder 继续为 StatsLine 和顶层公共字段维护 legacy slice,Trajectory 则在共享 Session 窗口上拥有 target 专属 Definition 与 Builder。内建 Definition 分别留在所属 UI package;这些兼容边界不把业务解释权交还给 Session。
diff --git a/.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.i18n.yaml
index b851627050..a5e5d22982 100644
--- a/.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.md
-2026-08-09-headless-direct-core-entry-point.md: 49afe2993de7302adbedcdf9e8e2347d6424ee2a
-2026-08-09-headless-direct-core-entry-point.zh.md: 73c1cbe5ac777025f63f46751b1d5ccebbfe9676
+2026-08-09-headless-direct-core-entry-point.md: b705df2e6d88e096ee3ba50a6156b815dbd98b98
+2026-08-09-headless-direct-core-entry-point.zh.md: 439f4c21a2ce1741e8d483bc307508550da1bec7
diff --git a/.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.md b/.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.md
index 49afe2993d..b705df2e6d 100644
--- a/.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.md
+++ b/.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.md
@@ -20,11 +20,11 @@ The shipped `headless` profile contains `dsh-base` and `dsh-headless`. The headl
`loadProfile` recognizes the exact installation-owned headless tuple (`dsh-base`, `dsh-web-app`, `dsh-headless`) and normalizes it to the shipped headless template while preserving every other manifest field. Extra, missing, or reordered bundle lists are user-owned and remain untouched.
-This note owns the headless transport and completion contracts. [`dsh run` owns one-shot headless execution](../feature/2026-08-08-dsh-run-headless-command.md) owns the command grammar, [GUI layering and RPC protocol](2026-07-19-gui-layering-and-rpc-protocol.md) owns browser gateway boundaries, [web config-tree boot and transport layering](2026-07-24-web-config-tree-boot-and-transport-layering.md) owns the Web tree, and [the default model follows the picker](../feature/2026-08-07-default-model-follows-the-picker.md) owns persistence of the shared Agent default.
+This note owns the headless transport and completion contracts. [Apps own their command lines](2026-08-06-app-owned-command-line.md) owns the current `dsh --profile headless` grammar; the former [`dsh run` decision](../../archived/feature/2026-08-08-dsh-run-headless-command.md) records the superseded launcher-owned grammar, [GUI layering and RPC protocol](2026-07-19-gui-layering-and-rpc-protocol.md) owns browser gateway boundaries, [web config-tree boot and transport layering](2026-07-24-web-config-tree-boot-and-transport-layering.md) owns the Web tree, and [the default model follows the picker](../feature/2026-08-07-default-model-follows-the-picker.md) owns persistence of the shared Agent default.
## Verification
-Package tests use the real Session store and Agent registry around a scripted Agent factory to pin idle-to-idle aggregation, late asynchronous completion, terminal model diagnostics, other non-completed exits, direct failures, Loader-time disposal, and flush-before-exit ordering. The keyless assembled snapshots drive `dsh run` through a replayed tool round trip, record a `user/message` with `source.kind: 'user'`, and expose a terminal model failure on stderr. Built-bin acceptance reaches a mock provider through the published entry and requires final text on stdout, exit 0, and empty stderr. Config-dump acceptance excludes every Host, Web, and Client package from the shipped headless tree; PTY shutdown coverage requires no observation line and bounded disposal.
+Package tests use the real Session store and Agent registry around a scripted Agent factory to pin idle-to-idle aggregation, late asynchronous completion, terminal model diagnostics, other non-completed exits, direct failures, Loader-time disposal, and flush-before-exit ordering. The keyless assembled snapshots drive `dsh --profile headless` through a replayed tool round trip, record a `user/message` with `source.kind: 'user'`, and expose a terminal model failure on stderr. Built-bin acceptance reaches a mock provider through the published entry and requires final text on stdout, exit 0, and empty stderr. Config-dump acceptance excludes every Host, Web, and Client package from the shipped headless tree; PTY shutdown coverage requires no observation line and bounded disposal.
## Alternatives considered
@@ -39,6 +39,6 @@ Package tests use the real Session store and Agent registry around a scripted Ag
## Consequences
-`dsh run` provides a local Agent task rather than browser observation, Host APIs, or HTTP. Users who need those capabilities choose `dsh web`. Successful stderr is empty, completion follows durable flush, and the persisted Session remains available to later tooling. Its initial user message records `source.kind: 'user'` and therefore carries no ApiProxy `rpcId`.
+`dsh --profile headless` provides a local Agent task rather than browser observation, Host APIs, or HTTP. Users who need those capabilities choose `dsh web`. Successful stderr is empty, completion follows durable flush, and the persisted Session remains available to later tooling. Its initial user message records `source.kind: 'user'` and therefore carries no ApiProxy `rpcId`.
ApiProxy carrier coverage stays in the ApiProxy package. Custom one-shot profiles may include Host or Web bundles explicitly, while the shipped profile and the recognized installation-owned tuple are Web-free.
diff --git a/.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.zh.md b/.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.zh.md
index 73c1cbe5ac..439f4c21a2 100644
--- a/.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.zh.md
@@ -20,11 +20,11 @@ Status: implemented
`loadProfile` 识别安装过程拥有的精确 headless 元组(`dsh-base`、`dsh-web-app`、`dsh-headless`),将其规范化为随附的 headless 模板,并保留 manifest(元数据清单)的其他所有字段。带额外项、缺少项或顺序不同的组合包列表归用户所有,保持不变。
-本 Agent Note 负责 headless 的传输与完成约定。[`dsh run` 负责一次性 headless 执行](../feature/2026-08-08-dsh-run-headless-command.md)负责命令语法,[GUI 分层与 RPC 协议](2026-07-19-gui-layering-and-rpc-protocol.md)负责浏览器网关边界,[Web 配置树启动与传输分层](2026-07-24-web-config-tree-boot-and-transport-layering.md)负责 Web 插件树,[默认模型跟随选择器](../feature/2026-08-07-default-model-follows-the-picker.md)负责共享 Agent 默认值的持久化。
+本 Agent Note 负责 headless 的传输与完成约定。[应用持有自己的命令行](2026-08-06-app-owned-command-line.md)负责当前的 `dsh --profile headless` 语法;原 [`dsh run` 决策](../../archived/feature/2026-08-08-dsh-run-headless-command.md)记录已被取代的启动器持有语法,[GUI 分层与 RPC 协议](2026-07-19-gui-layering-and-rpc-protocol.md)负责浏览器网关边界,[Web 配置树启动与传输分层](2026-07-24-web-config-tree-boot-and-transport-layering.md)负责 Web 插件树,[默认模型跟随选择器](../feature/2026-08-07-default-model-follows-the-picker.md)负责共享 Agent 默认值的持久化。
## 验证
-包测试围绕脚本化 Agent 工厂使用真实的会话存储与 Agent 注册表,固定空闲态到空闲态的聚合、延迟异步完成、终止态模型诊断、其他未完成退出、直接失败、Loader 加载期间的 dispose(资源释放),以及退出前 flush 的顺序。组装后的无密钥快照通过回放的工具往返驱动 `dsh run`,记录一条带 `source.kind: 'user'` 的 `user/message`,并在 stderr 暴露终止态模型失败。构建后二进制验收通过已发布入口访问 mock 提供方,并要求最终文本出现在 stdout、退出状态为 0 且 stderr 为空。配置转储验收排除随附 headless 树中的所有 Host、Web 与 Client 包;PTY 关闭覆盖要求不出现观察行,并在有界时间内完成 dispose。
+包测试围绕脚本化 Agent 工厂使用真实的会话存储与 Agent 注册表,固定空闲态到空闲态的聚合、延迟异步完成、终止态模型诊断、其他未完成退出、直接失败、Loader 加载期间的 dispose(资源释放),以及退出前 flush 的顺序。组装后的无密钥快照通过回放的工具往返驱动 `dsh --profile headless`,记录一条带 `source.kind: 'user'` 的 `user/message`,并在 stderr 暴露终止态模型失败。构建后二进制验收通过已发布入口访问 mock 提供方,并要求最终文本出现在 stdout、退出状态为 0 且 stderr 为空。配置转储验收排除随附 headless 树中的所有 Host、Web 与 Client 包;PTY 关闭覆盖要求不出现观察行,并在有界时间内完成 dispose。
## 考虑过的替代方案
@@ -39,6 +39,6 @@ Status: implemented
## 后果
-`dsh run` 提供本地 Agent 任务,而不是浏览器观察、Host API 或 HTTP。需要这些能力的用户选择 `dsh web`。成功时 stderr 为空,完成结果在持久化 flush 后推导,持久化会话仍可供后续工具使用。初始用户消息记录 `source.kind: 'user'`,因此不携带 ApiProxy `rpcId`。
+`dsh --profile headless` 提供本地 Agent 任务,而不是浏览器观察、Host API 或 HTTP。需要这些能力的用户选择 `dsh web`。成功时 stderr 为空,完成结果在持久化 flush 后推导,持久化会话仍可供后续工具使用。初始用户消息记录 `source.kind: 'user'`,因此不携带 ApiProxy `rpcId`。
ApiProxy 载体覆盖保留在 ApiProxy 包中。自定义一次性 profile 可以显式包含 Host 或 Web 组合包;随附 profile 与可识别的安装过程所属元组均不含 Web。
diff --git a/.agents/notes/implemented/architecture/2026-08-09-layered-skill-registry.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-09-layered-skill-registry.i18n.yaml
index 22e5090312..22296f97a9 100644
--- a/.agents/notes/implemented/architecture/2026-08-09-layered-skill-registry.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-08-09-layered-skill-registry.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-09-layered-skill-registry.md
-2026-08-09-layered-skill-registry.md: 3f092cfb4b722e3dd51fa4dc46c620259eaffa39
-2026-08-09-layered-skill-registry.zh.md: 38b17329c8d46ee9bbd0863f3fae7cf6be39aa75
+2026-08-09-layered-skill-registry.md: 73897c3cb7e0055ff59221b7ea47c5d6ced06991
+2026-08-09-layered-skill-registry.zh.md: 655780d4ef154434d6debf478134d3293d6c564f
diff --git a/.agents/notes/implemented/architecture/2026-08-09-layered-skill-registry.md b/.agents/notes/implemented/architecture/2026-08-09-layered-skill-registry.md
index 3f092cfb4b..73897c3cb7 100644
--- a/.agents/notes/implemented/architecture/2026-08-09-layered-skill-registry.md
+++ b/.agents/notes/implemented/architecture/2026-08-09-layered-skill-registry.md
@@ -24,7 +24,7 @@ The composition moves with it: the web-app bundle re-enables the base `skill` re
**A deployment-level skill reaches every preset-composed session that mounts `tool-skill`.** The repository-plugin e2e's skill root and assertions are restored; the shipped-Web e2e proves the badge row (the same host-registration shape) merges into a standard-preset agent's catalog while the host view stays global-only.
-**Layer visibility and consumption stay separate choices.** A core-web agent can read the global layer in principle, but composes no `skill` tool — whether an agent has skills at all remains the preset's decision, made by mounting or omitting `tool-skill`.
+**Layer visibility and consumption stay separate choices.** A `minimal` agent can read the global layer in principle, but composes no `skill` tool — whether an agent has skills at all remains the preset's decision, made by mounting or omitting `tool-skill`.
**Provider options are still the borrowed caller object.** `SkillViewOptions` extends `SkillLookupOptions`; the registry consumes `scope` and providers read only their own contract from the same readonly object, preserving the existing borrow-identity guarantee.
diff --git a/.agents/notes/implemented/architecture/2026-08-09-layered-skill-registry.zh.md b/.agents/notes/implemented/architecture/2026-08-09-layered-skill-registry.zh.md
index 38b17329c8..655780d4ef 100644
--- a/.agents/notes/implemented/architecture/2026-08-09-layered-skill-registry.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-09-layered-skill-registry.zh.md
@@ -24,7 +24,7 @@ agent-preset stack 曾把整个 skill 能力——注册表、本地提供方和
**部署级 skill 会到达每个挂载 `tool-skill` 的 preset 会话。**repository-plugin e2e 的 skill 根目录与断言已恢复;shipped-Web e2e 证明 badge 行(同一种宿主注册形态)汇入 standard preset agent 的目录,而宿主视图保持仅全局。
-**层可见性与消费仍是两个独立选择。**core-web agent 原则上可读全局层,但不组合 `skill` 工具——agent 是否拥有 skill 依旧由 preset 通过挂载或省略 `tool-skill` 决定。
+**层可见性与消费仍是两个独立选择。** `minimal` agent 原则上可读全局层,但不组合 `skill` 工具——agent 是否拥有 skill 依旧由 preset 通过挂载或省略 `tool-skill` 决定。
**提供方选项仍是借用的调用方对象。**`SkillViewOptions` 扩展 `SkillLookupOptions`;注册表消费 `scope`,提供方只从同一个只读对象中读取自己的契约,保持既有的借用恒等保证。
diff --git a/.agents/notes/implemented/architecture/2026-07-28-experimental-plugin-package-group.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.i18n.yaml
similarity index 54%
rename from .agents/notes/implemented/architecture/2026-07-28-experimental-plugin-package-group.i18n.yaml
rename to .agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.i18n.yaml
index 43e4b3d605..da6e264f1b 100644
--- a/.agents/notes/implemented/architecture/2026-07-28-experimental-plugin-package-group.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.i18n.yaml
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-28-experimental-plugin-package-group.md
-2026-07-28-experimental-plugin-package-group.md: 1ebae5dbb16d4c966f94ffde69fb0cb9bc163d80
-2026-07-28-experimental-plugin-package-group.zh.md: 2d09451c5069a775906e5bc8748c334c29008164
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.md
+2026-08-10-host-plane-ownership-after-presets.md: 5b0a340e875005182a0e6cd0f880b34b14258fb2
+2026-08-10-host-plane-ownership-after-presets.zh.md: 4b1e04f924e656b0f6ad4d070a3b77ce0189c608
diff --git a/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.md b/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.md
new file mode 100644
index 0000000000..5b0a340e87
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.md
@@ -0,0 +1,45 @@
+# Agent Note: What stays host-plane once presets own the agent plane
+
+Status: implemented
+
+English | [中文](2026-08-10-host-plane-ownership-after-presets.zh.md)
+
+## Problem
+
+[Per-session agent presets](2026-08-03-per-session-agent-presets.md) moved every model-facing row onto the agent plane, and each later fix has been one reader that assumed the world before the move. `tasks` came back to the host because a preset row outside its realm resolved it; `goals` never left for the same reason; a child agent's `toolFilter` was repaired once every model-facing tool became an ancestor contribution rather than a global one ([child agents join their parent's preset](../bug-fix/2026-08-10-child-agents-join-their-parent-preset.md)).
+
+Two more readers were still on the wrong side of that line.
+
+`dsh-token-meter` was disabled on the host and mounted inside each preset's `compaction` realm. It takes no configuration, keys every fold by `Session`, and registers no tool or prompt section — but it owns the `tokenUsage`, `contextPressure`, and `contextBreakdown` projection units, and `sessionProjections` is a process-wide table with no scope layering. A unit registered from inside one preset therefore answers for every session: whether a `minimal` session showed a context meter depended on whether some *other* session had mounted `standard` since boot, and a process that only ever ran `minimal` showed none at all.
+
+Nothing named an agent that joined no preset. The join is a scope-parent link; without it the `tools`, `system-prompt`, and `skill` views resolve the empty global layer and the model receives nothing — no error, no empty catalog, just an agent that cannot act. That is how delegated subagents ran for as long as presets existed, and the same hole is open at every entry point that predates them.
+
+## Decision
+
+**The meter is host-plane.** `dsh-token-meter` returns to the host composition and leaves the presets' `isolate` map, so `compact-basic` and `tool-result-prune` resolve the one host instance from inside their realm. The presets keep the realm and the backend — what a preset chooses is whether its agent compacts, not whether its tokens are counted. This is the criterion `tasks` and `goals` are already read by, applied to a Service whose *projection* reach is what made preset ownership wrong: a unit whose empty value is indistinguishable from a real one cannot be per-composition while the table it registers into is per-process.
+
+**An unjoined agent is named twice, at two different points.** `AgentPresets` logs one warning per agent published with a scope chain of length one while a roster is configured. The invariant companion fails instead — and at `system-prompt/assemble`, not at publication, because an unjoined agent is legal until it addresses a model: `recompose` binds exactly such an agent as its first link, and prompt assembly is the only caller that supplies an agent scope, so a host assembly and a standing mount are both correctly out of range.
+
+Three limits stay open and are recorded where they bite rather than fixed here: projection key presence is not a per-session capability signal ([`dsh-session-projection`](../../../../packages/session/session-projection/README.md)); a superseded standing generation is never reclaimed, which the settings-page authoring flow turns into a per-save cost ([`dsh-agent-presets`](../../../../packages/preset/agent-presets/README.md)); and a temporary plugin mounted through `cordis_mount` belongs to the composition rather than the session that mounted it ([`dsh-tool-cordis`](../../../../packages/self-modification/tool-cordis/README.md)).
+
+## Testing
+
+`apps/cli/tests/web-agent-presets.e2e.ts` reads `ctx.get('tokenMeter')` on the booted Web composition before any preset in the file mounts — a preset-side meter sits behind an `isolate` realm and is invisible to `ctx.get`, so the read is an ownership assertion rather than a mount-order coincidence — then asserts a `minimal` session's snapshot carries all three units.
+
+`packages/preset/agent-presets/tests/mount.spec.ts` asserts the warning fires exactly once for a bare agent and not at all for a joined one. `tests/invariant.spec.ts` carries the negative control: an unjoined agent's assembly rejects, while a joined agent's assembly and a scopeless host assembly both pass.
+
+## Alternatives considered
+
+**Keep the meter in the preset and scope-layer the projection registry.** The precise fix, and much larger: `snapshot`, `checkpoint`, and the eager drive would each need a session→scope resolution that a cold read does not have without the api-proxy's `presenterScopeFor`. Rejected as disproportionate to one Service with no per-preset state at all; the general rule is documented on the registry instead.
+
+**Veto publication for an unjoined agent.** Loud beats silent, and the registry supports it — a synchronous `agent/created` listener that throws rolls the creation back. Rejected because composing an agent outside the roster is legal: `recompose` documents the bare agent it then binds, and the ACP bridge, the SDK server, and the headless bundle all create one today. A veto would convert a capability gap into an outage.
+
+**Check the join at `agent/created` in the companion too.** Rejected: publication cannot distinguish a missed join from an agent that will be bound later, so the check would reject a documented path. Prompt assembly can distinguish them.
+
+**Move `plan-mode` and `tool-todo` off the agent plane for the same projection reason.** Rejected: both are genuinely per-preset capabilities, and their units compute an empty value for a session that never uses them, which clients already read by value (`plan.active`, an empty list). Only a unit whose empty value is indistinguishable from a real one — the meter — forces host ownership.
+
+## Consequences
+
+The context meter becomes a per-session fact instead of a function of mount history. A preset can no longer opt out of token accounting; no shipped preset did, and `minimal` now says it drops auto-compaction rather than the accounting.
+
+The warning is advisory, so a deployment that adds a roster to the ACP or SDK-server entry points still starts agents with no tools — it just says so once per agent instead of silently. The invariant reaches only compositions that load `dsh-invariants`, which fences package tests and development hosts, not a shipped one.
diff --git a/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.zh.md b/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.zh.md
new file mode 100644
index 0000000000..4b1e04f924
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.zh.md
@@ -0,0 +1,45 @@
+# Agent Note: What stays host-plane once presets own the agent plane
+
+Status: implemented
+
+[English](2026-08-10-host-plane-ownership-after-presets.md) | 中文
+
+## Problem
+
+[逐会话 agent preset](2026-08-03-per-session-agent-presets.md) 把每一个面向模型的行搬上了 agent 平面,此后的每一处修复都是一个仍按搬迁之前的世界写成的读取点。`tasks` 因为 realm 之外的 preset 行要解析它而搬回宿主;`goals` 因为同样的理由从未离开;而当所有面向模型的工具都变成祖先贡献之后,子 agent 的 `toolFilter` 也已被修好([子 agent 加入父方 preset](../bug-fix/2026-08-10-child-agents-join-their-parent-preset.md))。
+
+还有两个读取点仍站在这条线的错误一侧。
+
+`dsh-token-meter` 在宿主侧被禁用,改挂进每个 preset 的 `compaction` realm。它不接受任何配置,每次折叠都以 `Session` 建键,也不注册工具或提示段——但它拥有 `tokenUsage`、`contextPressure` 与 `contextBreakdown` 三个投影单元,而 `sessionProjections` 是一张进程级、没有作用域分层的表。因此从某个 preset 内部注册的单元会替所有会话作答:一个 `minimal` 会话是否显示 context meter,取决于本次启动以来有没有**别的**会话挂过 `standard`;而只跑过 `minimal` 的进程根本不显示。
+
+没有加入任何 preset 的 agent 也无人指出。加入是一条 scope 父链链接;缺了它,`tools`、`system-prompt` 与 `skill` 的视图都解析到空的全局层,模型什么也收不到——不报错,也没有空目录可看,只是一个无法行动的 agent。被委派的子 agent 在 preset 存在的整段时间里都是这样运行的,而同一个洞在每一个早于 preset 的入口点上都开着。
+
+## Decision
+
+**meter 属于宿主平面。** `dsh-token-meter` 回到宿主组装,并离开各 preset 的 `isolate` 映射,于是 `compact-basic` 与 `tool-result-prune` 在自己的 realm 内部解析到那一份宿主实例。preset 保留 realm 与压缩后端——preset 选择的是它的 agent 是否压缩,而不是它的 token 是否被计。这正是 `tasks` 与 `goals` 已经采用的判据,只是这次适用于一个因**投影**触达面而不该归 preset 所有的 Service:当一个单元的空值与真实值无法区分时,只要它注册进的那张表是进程级的,它就不能是逐组装的。
+
+**未加入的 agent 在两个不同的点上被指出两次。** 在配置了名单的前提下,`AgentPresets` 对每个作用域链长度为一就发布的 agent 记录一条警告。invariant 配套则直接失败——并且发生在 `system-prompt/assemble` 而非发布时,因为一个未加入的 agent 在它对模型说话之前都是合法的:`recompose` 绑定的正是这样一个 agent 作为它的首次链接;而提示词组装是唯一会提供 agent 作用域的调用方,因此宿主组装与常驻挂载都正确地落在检查范围之外。
+
+有三处限制不在此处修复,而是记录在会咬到它们的地方:投影 key 是否存在不能当作逐会话的能力信号([`dsh-session-projection`](../../../../packages/session/session-projection/README.md));被替代的常驻代际永不回收,而设置页的编写流程把它变成每次保存的代价([`dsh-agent-presets`](../../../../packages/preset/agent-presets/README.md));通过 `cordis_mount` 挂上的临时插件属于组装而非挂载它的会话([`dsh-tool-cordis`](../../../../packages/self-modification/tool-cordis/README.md))。
+
+## Testing
+
+`apps/cli/tests/web-agent-presets.e2e.ts` 在本文件中任何 preset 挂载**之前**,于已启动的 Web 组装上读取 `ctx.get('tokenMeter')`——preset 侧的 meter 会待在 `isolate` realm 里,对 `ctx.get` 不可见,因此这次读取是一次所有权断言而不是挂载顺序的巧合——随后断言一个 `minimal` 会话的快照带齐三个单元。
+
+`packages/preset/agent-presets/tests/mount.spec.ts` 断言警告对裸 agent 恰好触发一次、对已加入的 agent 完全不触发。`tests/invariant.spec.ts` 承担负控:未加入 agent 的组装被拒绝,而已加入 agent 的组装与不带作用域的宿主组装都通过。
+
+## Alternatives considered
+
+**把 meter 留在 preset,改为给投影注册表分层。** 这是更精确的修法,代价也大得多:`snapshot`、`checkpoint` 与主动驱动都需要一次「会话 → 作用域」的解析,而冷读在没有 api-proxy 的 `presenterScopeFor` 时并不具备。相对于一个完全没有 per-preset 状态的 Service,这不成比例,因此改为把通则写在注册表上。
+
+**对未加入的 agent 否决发布。** 大声胜过静默,注册表也支持这么做——同步的 `agent/created` 监听器抛出会把创建整体回滚。否决的理由是:在名单之外组装 agent 是合法的——`recompose` 写明了它随后绑定的那个裸 agent,而 ACP 桥、SDK server 与 headless bundle 今天都会创建一个。否决会把能力缺口变成一次故障。
+
+**让配套也在 `agent/created` 处检查加入情况。** 否决:发布时分不清漏掉的加入与之后才会被绑定的 agent,因此该检查会拒绝一条已写明的路径。提示词组装分得清。
+
+**基于同样的投影理由,把 `plan-mode` 与 `tool-todo` 也搬离 agent 平面。** 否决:两者确实是逐 preset 的能力,且对从不使用它们的会话,其单元算出的就是空值,而客户端本来就按值读取(`plan.active`、空列表)。只有空值与真实值无法区分的单元——meter——才被迫归宿主所有。
+
+## Consequences
+
+context meter 成为逐会话的事实,而不再是挂载历史的函数。代价是 preset 不能再选择不做 token 记账;随附的 preset 没有一个这么做,`minimal` 现在也写明它放弃的是自动压缩而非记账。
+
+那条警告是建议性的,因此给 ACP 或 SDK server 入口加上名单的部署依然会启动没有工具的 agent——只是每个 agent 会说一次,而不再静默。invariant 只触达装载了 `dsh-invariants` 的组装,因此它把关的是包测试与开发宿主,不是随附宿主。
diff --git a/.agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.i18n.yaml
similarity index 56%
rename from .agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.i18n.yaml
rename to .agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.i18n.yaml
index 5985f18a06..20b1a4cfd5 100644
--- a/.agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.i18n.yaml
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.md
-2026-07-30-config-only-repository-plugins.md: 35327a30e03c51311f634e05ade209ab93ae0155
-2026-07-30-config-only-repository-plugins.zh.md: 5755045560da761b59f7c65e99d551f599c2b5b3
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md
+2026-08-10-message-feedback-sidecar.md: 780cbaa840fcac7bcfa799468bfd61f5b08715cb
+2026-08-10-message-feedback-sidecar.zh.md: 72ecc82717010f65d013418a45c3b38c6084aaf9
diff --git a/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md b/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md
new file mode 100644
index 0000000000..780cbaa840
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md
@@ -0,0 +1,43 @@
+# Agent Note: Lifecycle-bound message feedback sidecar
+
+Status: implemented
+
+English | [中文](2026-08-10-message-feedback-sidecar.zh.md)
+
+## Problem
+
+The existing `/feedback` command records an immutable Session-level `feedback/record` event. That event can release a pending telemetry prefix under `FEEDBACK_ONLY`, so it is the wrong authority for an editable positive/negative rating and optional note attached to one assistant message. Message feedback needs independent update and delete semantics without entering the canonical Session log, changing a projection, reaching the model surface, or implicitly consenting to telemetry.
+
+A sidecar keyed only by `SessionId` can outlive the log lifecycle it describes when an id is recreated with a different header identity. A Session-wide revision also makes unrelated message edits conflict, while plain storage-domain read/put has no cross-process compare-and-swap. Session disposal is only live-store detach, not durable deletion, and the current Session persistence seam exposes no deletion operation that could own a truthful cascade.
+
+## Decision
+
+`@deepseek-ai/dsh-message-feedback` owns the `ctx.messageFeedback` service and stores message feedback as one storage-domain sidecar row per Session. The sidecar is neither Session-log content nor a Session projection. It emits no `feedback/record` event and performs no telemetry handoff; the command-feedback and message-feedback contracts remain independent.
+
+Every usable row is bound to the inspected Session header identity `{createdAt, cwd}`, not merely its `SessionId`. A lifecycle mismatch is treated as absence: `list` returns no items, and `put` may replace the stale row with one bound to the current identity. An id reused with a different header identity therefore cannot inherit stale feedback. A fork receives its own Session identity and no sidecar copy: even when the fork seed contains the same assistant messages, feedback remains attached to the Session in which the human recorded it.
+
+`put` accepts a target only when `SessionPersistence.inspect()` observes a non-empty, append-origin `assistant/message` with that `MessageId`. Replacement-origin messages, empty usage-only assistant records, and non-assistant targets are rejected. Inspection is the cold-safe authority: it neither publishes or resumes an Agent nor commits cold-log repair merely to validate feedback. A cold `listSnapshots()` preflight classifies definite absence; inspection failure for a catalogued Session remains an infrastructure failure. A request in the narrow live-detach-to-header-materialization interval can therefore return `session-not-found`, and the caller retries after retirement materialization.
+
+Before `put` commits a sidecar row, it puts the target log behind a durability barrier. A matching live Session passes through the canonical `ctx.sessions.flush` checkpoint, then both live and cold paths are physically read from sequence zero through `SessionPersistence.readFrom`. The resulting observation's header identity and target are checked again. A missing flush participant, changed identity, vanished target, or physical-read failure prevents the sidecar write, so a committed feedback item never precedes the durable assistant message it references.
+
+Each message item carries its own opaque version plus Host-assigned `createdAt` and `updatedAt` timestamps. `put` compares the caller's `ifVersion` only with the addressed item, so editing one message does not invalidate another. The comparison is strict even when the desired value already matches, preventing a stale request from crossing an ABA value cycle; a conflict returns the authoritative current item so callers can reconcile without a second read. A matching-version no-op preserves the version and timestamps, while a material update preserves `createdAt`, replaces the version, and keeps `updatedAt` from moving backward. An already-absent delete is likewise successful. Versions are tokens for equality, not counters callers may order or synthesize.
+
+A per-Session mutation queue encloses lifecycle inspection, sidecar read, conflict evaluation, and whole-row write. This makes one service instance's mutations serial and preserves the per-message compare-and-swap contract inside one Host process. Plugin disposal closes admission, drains accepted queue work, and then closes the storage domain. The underlying storage-domain API provides no cross-process conditional write, so the implementation claims no cross-process linearizability or lost-update protection.
+
+`maxNoteBytes` is a required deployment choice and bounds the UTF-8 byte length of an optional note; the Web Host bundle sets it explicitly to `8192`. The package publishes the Host `messageFeedback.list`, `messageFeedback.put`, and `messageFeedback.delete` contract directly through `GatewayService` and `@Remote`. Client Remote aggregate mounting and UI remain separately owned and deferred; their later adapter stays a thin consumer of this Host contract.
+
+The service performs no fake deletion cascade. `session/disposed` and `host/session-removed` describe detach from live ownership, not durable Session deletion, and Session persistence currently has no delete surface. Sidecar rows can therefore remain after out-of-band log removal; a different `{createdAt, cwd}` prevents such an orphan from becoming feedback for a later Session that reuses the id.
+
+## Alternatives considered
+
+**Append edits to the Session log and derive a projection.** Rejected because editable UI metadata would become canonical conversation-adjacent history, forks would replay and inherit it, deletion would require tombstones, and reusing `feedback/record` would silently couple a message rating to telemetry consent.
+
+**Key feedback globally by `MessageId`, copy it on fork, or use one Session revision.** Rejected because message ids are meaningful only within a Session lifecycle, forked conversations need independent human judgments, and unrelated message mutations must not create false conflicts.
+
+**Extend `KvTable` with cross-process compare-and-swap in this change.** Rejected because the shipped storage-domain backends expose no common conditional-write primitive. A process-local queue matches the supported one-Host topology; a real multi-process guarantee requires a backend-level atomic contract and is separate work.
+
+**Delete feedback on Session disposal.** Rejected because disposal includes ordinary detach and rollback paths. Treating it as durable deletion would lose feedback while the Session log still exists; cleanup waits for a real Session deletion authority.
+
+## Consequences
+
+Message feedback is locally durable and independently editable without changing model-visible history or telemetry behavior. Concurrent callers in one Host receive per-message conflict detection and retry-safe outcomes, while deployments with multiple writers to the same storage root remain unsupported. A differing header identity treats a stale row as absent but does not reclaim it; a cloned log that retains the same `{createdAt, cwd}` is indistinguishable by this contract. The Host Remote contract is available now; client assembly and UI can remain thin consumers rather than taking ownership of persistence or concurrency semantics.
diff --git a/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.zh.md b/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.zh.md
new file mode 100644
index 0000000000..72ecc82717
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.zh.md
@@ -0,0 +1,43 @@
+# Agent Note: 绑定生命周期的消息反馈伴随记录
+
+Status: implemented
+
+[English](2026-08-10-message-feedback-sidecar.md) | 中文
+
+## 问题
+
+现有 `/feedback` 命令记录不可变的 Session 级 `feedback/record` 事件。在 `FEEDBACK_ONLY` 下,该事件可以释放待处理的遥测前缀,因此它不适合作为挂在单条 assistant 消息上的可编辑好评/差评与可选备注的权威来源。消息反馈需要独立的更新与删除语义,且不得进入权威 Session 日志、改变投影、到达模型接口,或隐式表示遥测同意。
+
+只按 `SessionId` 建索引的伴随记录可能在该 id 以不同 header 身份重建后,继续存活于其所描述的日志生命周期之外。Session 级 revision 还会让无关消息的编辑彼此冲突,而普通 storage-domain 读/写不提供跨进程 compare-and-swap。Session disposal 只是从 live store 脱离,并非持久删除;当前 Session 持久化 seam 也没有可拥有真实级联的删除操作。
+
+## 决策
+
+`@deepseek-ai/dsh-message-feedback` 拥有 `ctx.messageFeedback` 服务,并把消息反馈存为每个 Session 一条 storage-domain 伴随记录(sidecar)。该伴随记录既不是 Session 日志内容,也不是 Session 投影。它不发出 `feedback/record` 事件,也不执行遥测交接;command-feedback 与 message-feedback 契约保持独立。
+
+每条可用记录都绑定到经检查的 Session header 身份 `{createdAt, cwd}`,而不只是其 `SessionId`。生命周期不匹配按不存在处理:`list` 返回空条目,`put` 可以用绑定当前身份的新记录替换陈旧行。因此,以不同 header 身份复用的 id 不会继承陈旧反馈。fork 拥有自己的 Session 身份,且不复制伴随记录:即使 fork 种子包含相同的 assistant 消息,反馈仍只属于人类记录它的那个 Session。
+
+`put` 只接受由 `SessionPersistence.inspect()` 观测到的非空、append-origin `assistant/message`,且其 `MessageId` 必须与目标相同。replacement-origin 消息、仅承载 usage 的空 assistant 记录以及非 assistant 目标都会被拒绝。检查使用 cold-safe 权威路径:它不会仅为验证反馈而发布或恢复 Agent,也不会提交 cold 日志修复。cold 路径由 `listSnapshots()` 预检明确不存在;已进入目录的 Session 若检查失败,仍按基础设施故障处理。因此,请求若恰落在 live detach 到 header materialization 的极短窗口,可能返回 `session-not-found`,调用方在 retirement materialization 后重试。
+
+`put` 提交伴随记录前,会先让目标日志通过 durability barrier。身份匹配的 live Session 经过权威 `ctx.sessions.flush` checkpoint,随后 live 与 cold 路径都会通过 `SessionPersistence.readFrom` 从序列零做物理复读。之后再次校验所得观测的 header 身份与目标。缺少 flush 参与方、身份变化、目标消失或物理读取失败都会阻止伴随记录写入,因此已提交反馈绝不会先于它引用的持久 assistant 消息。
+
+每个消息条目都携带自己的 opaque version,以及 Host 分配的 `createdAt` 和 `updatedAt` 时间戳。`put` 只把调用方的 `ifVersion` 与目标条目比较,因此编辑一条消息不会使另一条消息失效。即使目标值已经相同,比较仍然严格执行,从而防止陈旧请求穿过 ABA 值循环;冲突会返回权威当前条目,调用方无需二次读取即可协调。携带匹配 version 的无变化请求会保留 version 与时间戳;实质更新保留 `createdAt`、替换 version,并保证 `updatedAt` 不倒退。删除已经不存在的条目也同样成功。version 是只能做相等比较的 token,不是调用方可以排序或自行合成的计数器。
+
+按 Session 划分的变更队列覆盖生命周期检查、伴随记录读取、冲突判断与整行写入。这使同一个服务实例的变更串行化,并在单个 Host 进程内保持逐消息 compare-and-swap 契约。Plugin disposal 会关闭接纳、排空已进入队列的工作,然后关闭 storage domain。底层 storage-domain API 不提供跨进程条件写,因此实现不承诺跨进程线性一致性或防止丢失更新。
+
+`maxNoteBytes` 是必填的部署选择,用于限制可选备注的 UTF-8 字节长度;Web Host bundle 将其显式设为 `8192`。该包通过 `GatewayService` 与 `@Remote` 直接发布 Host `messageFeedback.list`、`messageFeedback.put` 与 `messageFeedback.delete` 契约。客户端 Remote 聚合挂载与 UI 由各自边界负责并保持延后;后续适配层只是该 Host 契约的薄消费者。
+
+服务不伪造删除级联。`session/disposed` 与 `host/session-removed` 表示脱离 live ownership,而非持久删除,Session persistence 当前也没有删除接口。因此在带外移除日志后,伴随记录可能继续存在;不同的 `{createdAt, cwd}` 可阻止此类孤儿记录变成后来复用该 id 的 Session 反馈。
+
+## 考虑过的替代方案
+
+**把编辑追加到 Session 日志并派生投影。** 不予采纳,因为可编辑 UI 元数据会变成权威且邻近对话的历史,fork 会回放并继承它,删除需要 tombstone,而复用 `feedback/record` 会把消息评分与遥测同意静默耦合。
+
+**按全局 `MessageId` 建索引、在 fork 时复制,或使用一个 Session revision。** 不予采纳,因为消息 id 仅在某个 Session 生命周期内有意义,fork 后的对话需要独立的人类判断,而且无关消息的变更不应制造虚假冲突。
+
+**在本次变更中为 `KvTable` 扩展跨进程 compare-and-swap。** 不予采纳,因为出厂 storage-domain 后端没有共同的条件写原语。进程内队列符合受支持的单 Host 拓扑;真实的多进程保证需要后端级原子契约,属于独立工作。
+
+**在 Session disposal 时删除反馈。** 不予采纳,因为 disposal 包含普通 detach 与 rollback 路径。把它当成持久删除会在 Session 日志仍存在时丢失反馈;清理必须等待真正的 Session 删除权威。
+
+## 后果
+
+消息反馈在本地持久化并可独立编辑,且不改变模型可见历史或遥测行为。同一 Host 中的并发调用方获得逐消息冲突检测与可安全重试的结果;多个写入者共享同一存储根目录的部署仍不受支持。不同的 header 身份会让陈旧记录被视为不存在,但不会将其回收;本契约无法区分保留相同 `{createdAt, cwd}` 的克隆日志。Host Remote 契约现在可用;客户端组装与 UI 可以保持为薄消费者,而不接管持久化或并发语义。
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
new file mode 100644
index 0000000000..b696b46749
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.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-08-10-product-subagent-providers-in-shared-host.md
+2026-08-10-product-subagent-providers-in-shared-host.md: 33b6eb6cf7a6c19e9ea71cdb7dc8881e8052ef24
+2026-08-10-product-subagent-providers-in-shared-host.zh.md: fd78c7a3fee4e4ee30d27d87c752e1a23576fd85
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
new file mode 100644
index 0000000000..33b6eb6cf7
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
@@ -0,0 +1,41 @@
+# Agent Note: Product subagent providers live in the shared profile host
+
+Status: implemented
+
+English | [中文](2026-08-10-product-subagent-providers-in-shared-host.zh.md)
+
+## Problem
+
+The [Codex and Claude Code provider contracts](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md) were first shipped as independently installable packages that a deployment loaded beside the common subagent tool. Agent Presets later became the ordinary owner of one agent's model-visible tools, but a preset cannot safely own these product providers: `ctx.subagents` is a process registry, provider names are unique, and host consumers resolve the same registry across sessions. Requiring a person to edit both a Profile and a Preset would also make a generic preset row incomplete by itself.
+
+The placement decision must preserve two independent facts. Loading a provider must not start or authenticate a product, while enabling a tool must remain per preset so two sessions can expose different products. A global product switch, a provider instance per agent, or pre-enumerated combination presets would each create a second owner for one of those facts.
+
+## Decision
+
+Every shipped Profile loads the fixed `codex` and `claude-code` providers once through the base bundle's host plane. Loading either plugin only registers a dormant backend; the corresponding Codex or Claude process starts on the first actual delegation call. Agent Presets independently contribute ordinary `dsh-tool-subagent` rows for `subagent_codex` and `subagent_claude_code`, so a preset can expose neither tool, either one, or both without changing the provider registry.
+
+This decision supersedes only the opt-in composition placement recorded by the provider-contract note. That note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
+
+The providers use products already selected by the host environment. Codex starts `codex` from `PATH`; Claude Code resolves `claude` through the shared subprocess execution world and passes the exact path to the official SDK. Profile loading does not install a product, create product state, probe a version, test authentication, or add product-specific settings. Missing commands and product failures remain local to the attempted delegation.
+
+The current base dependency closure still includes the Claude Agent SDK's optional platform CLI payload even though production resolves the host `claude`. Removing that unused payload belongs to the separate product installation-closure follow-up; this placement decision neither installs it dynamically nor treats it as the production executable.
+
+## Verification
+
+The base Loader test proves both provider names register exactly once and no product process starts during Profile boot. Real Agent Preset composition covers none, Codex-only, Claude-only, and both tool sets, including generation isolation after an authored preset changes. Keyless ACP snapshots pin the model-visible tool schemas for one and both products, while provider tests separately prove native executable resolution, failure, cancellation, and process-tree quiescence.
+
+## Alternatives considered
+
+**Keep product providers opt-in at the Profile layer.** This preserves a smaller default dependency closure, but a copied or agent-authored Preset row is not usable unless the person also discovers and edits a second composition layer. It leaves the general Preset entry incomplete for these otherwise ordinary tools.
+
+**Store global or per-Profile product enable switches.** A process switch competes with the Preset as owner of model-visible tools and cannot express two sessions using different combinations. Availability and authentication are deployment facts, not another persisted product state.
+
+**Mount a provider inside every Agent Preset.** Provider names belong to a process registry, so the second session would collide with the first. Host consumers also need the registry independently of any one agent's lifetime.
+
+**Ship four product-combination presets.** Four identities duplicate complete compositions to represent two independent tool rows. Ordinary rows already express the full matrix without adding roster or maintenance state.
+
+## Consequences
+
+A user manages both products through the same Agent Preset authoring path as other plugins, and each new session receives exactly the tools its chosen preset contributes. Every Profile carries two dormant provider registrations, so unused products consume package and module-loading footprint but no product process, login, model call, or product home.
+
+The Host registry remains the single provider authority and each Preset remains the single model-tool authority. The trade-off is the current Claude SDK optional-payload installation cost, which stays explicitly deferred rather than being hidden behind another enable state or installer lifecycle.
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
new file mode 100644
index 0000000000..fd78c7a3fe
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
@@ -0,0 +1,41 @@
+# Agent Note: 产品 subagent 提供方位于共享 profile 宿主
+
+Status: implemented
+
+[English](2026-08-10-product-subagent-providers-in-shared-host.md) | 中文
+
+## 问题
+
+[Codex 与 Claude Code 提供方约定](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md)最初以可独立安装的包交付,由部署环境在通用 subagent 工具旁加载。Agent Preset 后来成为单个 agent(智能体)的模型可见工具的常规责任方,但 preset 不能安全地拥有这些产品提供方:`ctx.subagents` 是进程级注册表,提供方名称唯一,而宿主消费方会跨会话解析同一个注册表。如果要求用户同时编辑 Profile 和 Preset,也会使通用 preset 行本身不完整。
+
+归属决策必须同时保留两个彼此独立的事实:加载提供方不得启动产品,也不得对产品执行身份验证;而工具是否启用仍须按 preset 决定,这样两个会话才能暴露不同的产品。全局产品开关、按 agent 创建提供方实例或预先枚举的组合 preset,都会为其中一个事实另设第二责任方。
+
+## 决策
+
+每个随发行版交付的 Profile 都会通过 base 组合包的宿主平面,把固定的 `codex` 与 `claude-code` 提供方各加载一次。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 分别通过普通的 `dsh-tool-subagent` 行贡献 `subagent_codex` 与 `subagent_claude_code`,因此一个 preset 可以不暴露任何工具、只暴露其中一个或同时暴露两者,而无需更改提供方注册表。
+
+本决策仅取代提供方约定说明所记录的、原先由用户选择启用的组装位置。该说明仍负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)仍负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
+
+这些提供方使用宿主环境已经选定的产品。Codex 启动 `codex`,该命令从 `PATH` 解析;Claude Code 通过共享的子进程执行世界解析 `claude`,并把确切路径交给官方 SDK。加载 Profile 不会安装产品、创建产品状态、探测版本、测试身份验证,也不会新增产品专属设置。命令缺失和产品故障仍局限于发生问题的那次委派。
+
+当前 base 依赖闭包仍包含 Claude Agent SDK 的可选平台 CLI(命令行界面)载荷,尽管生产环境解析的是宿主提供的 `claude`。移除这份未使用载荷属于独立的产品安装闭包后续项;本归属决策既不会动态安装它,也不会将它当作生产可执行文件。
+
+## 验证
+
+base Loader 测试证明两个提供方名称都恰好注册一次,而且 Profile 启动期间不会启动产品进程。真实 Agent Preset 组装覆盖不暴露任何工具、仅暴露 Codex、仅暴露 Claude 和同时暴露两者这四种工具集合,也覆盖自行创作的 preset 发生改动后的代际隔离。无密钥 ACP(Agent Client Protocol)快照固定单个产品与两个产品同时启用时的模型可见工具 schema,提供方测试则另行证明原生可执行文件解析、失败、取消和进程树完全停稳。
+
+## 考虑过的替代方案
+
+**将产品提供方保留为 Profile 层的按需启用项。** 这样可缩小默认依赖闭包,但复制或由 agent 创作的 Preset 行无法直接使用,除非用户还发现并编辑第二个组装层。对于这些本来与其他工具无异的工具,通用 Preset 入口仍不完整。
+
+**存储全局或按 Profile 配置的产品启用开关。** 进程级开关会与 Preset 争夺模型可见工具的责任归属,也无法表示两个会话使用不同组合。可用性与身份验证属于部署事实,并非另一份需要持久化的产品状态。
+
+**在每个 Agent Preset 内挂载一个提供方。** 提供方名称属于进程级注册表,因此第二个会话会与第一个冲突。宿主消费方也需要独立于任何单个 agent 的生命周期使用该注册表。
+
+**交付四个产品组合 preset。** 四个身份会复制完整组装,只为表示两条独立的工具行。普通行已经能表达完整矩阵,无需新增名单或维护状态。
+
+## 后果
+
+用户通过与其他插件相同的 Agent Preset 创作路径管理两个产品,每个新会话只会获得其所选 preset 所贡献的工具。每个 Profile 都携带两个休眠的提供方注册,因此未使用的产品会产生包和模块加载开销,但不会启动产品进程、登录、调用模型或创建产品主目录。
+
+宿主注册表仍是提供方的唯一权威,每个 Preset 仍是模型工具的唯一权威。代价是当前 Claude SDK 可选载荷的安装成本继续被明确延期处理,而不会隐藏在另一种启用状态或安装程序生命周期之后。
diff --git a/.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.i18n.yaml
new file mode 100644
index 0000000000..ee249b18ea
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.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-08-10-session-log-version-mechanism.md
+2026-08-10-session-log-version-mechanism.md: 25eb1230a254219c827b1d2750dba367b113f9f7
+2026-08-10-session-log-version-mechanism.zh.md: c47670f2de77773c17c9595eff442bf7f1e8ec3e
diff --git a/.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md b/.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md
new file mode 100644
index 0000000000..25eb1230a2
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md
@@ -0,0 +1,30 @@
+# Agent Note: Session log versioning — one integer, an upgrade chain, and a per-event ignorable marker
+
+Status: implemented
+
+English | [中文](2026-08-10-session-log-version-mechanism.zh.md)
+
+## Problem
+
+Session logs must be upgradable after release, and the runtime that ships first is the floor for every later decision: whatever refusal and degradation behavior is missing from the first released reader can never be added to the copies users already run. Release issue #1901 required at minimum that an old runtime reading a newer session format reports "unsupported" instead of misreading it. The pre-change reader did the opposite on both axes: `assertVersion` rejected any version mismatch with one direction-blind message, and the JSONL decoder passed unknown event types through untouched, so reconstruction silently skipped them — resuming a gutted session with no diagnostic at all.
+
+## Decision
+
+**One monotonic integer, no major/minor split.** Whether a version step is auto-upgradable is a property of that step — expressed by whether its upgrader exists — not something a two-level numbering scheme should promise in advance (you rarely know at design time whether the next change will turn out "major"). This matches the SQLite backend's `SCHEMA_VERSION` precedent.
+
+**The writer decides bumps, not the reader.** A bump is required exactly when an old runtime could no longer handle a new log with full semantic correctness. "Parses without error" is not the bar: silently skipping content that shapes reconstruction is a wrong read. Only structural changes qualify — header shape, event envelope, core event semantics, the surface mechanism (`SurfaceEventType` set, `SurfaceOp` variants). When unsure, bump: a near-identity upgrader is almost free, a missed bump silently corrupts old readers.
+
+**Read rules by direction.** Equal version: read normally. Newer than the reader: refuse, name the direction ("written by a newer harness — upgrade"), and point at the raw log artifact so the user can still see the text (`SessionFormatUnsupportedError`, distinct from `SessionPersistenceCorruptionError` because nothing is damaged). Older than the reader: convert in memory through the chain of n→n+1 upgraders for viewing; persist the converted log only when the session is actually continued (atomic temp-file replace, original kept as backup). A step whose upgrader cannot be written is left empty, which cuts off every version at or below it — those degrade to raw-text viewing.
+
+**A per-event `ignorable` marker covers vocabulary growth, so ordinary event additions never bump the version.** The event vocabulary is decided by which plugins are mounted, which a single version integer cannot describe. A reader meeting an unrecognized event type refuses to interpret the log unless the event carries `ignorable: true` in its envelope. The default is *required*: forgetting the marker over-refuses a resumable session (an inconvenience), while a default of ignorable would make the same mistake silently resume a gutted one (a safety failure). The architecture makes this sound: model-visible content flows only through the three `surfaceOp`-marked surface event types plus the `request/header`/`request/context` folds, so the dangerous unknowns are exactly the non-surface events that change how the rest of the log is read (`session/end-seed` is the existing example).
+
+## Consequences
+
+What shipped in v0 (release 0812): direction-aware refusal with the raw-log path; the unknown-event guard against a generated known-vocabulary list (`KNOWN_SESSION_EVENT_TYPES`, emitted by `gen-persistence-catalog` from every `SessionEventMap` merge and kept fresh by `verify-persistence-catalog`); the `ignorable` envelope field accepted by seed validation, both backends (a dedicated SQLite column, `SCHEMA_VERSION` 15), and the BFF wire schema. The upgrader chain itself is deferred until the first real v0→v1 step exists to test it against; writers do not yet set `ignorable` (no producer needs it), so `Session.append` gains that surface with its first user. Until a registration surface exists, an out-of-repo plugin's events refuse resume under first-party readers — the pre-release stance accepts that, and the refusal is loud rather than silent. The unknown-type guard is read-side only: `appendCore` keeps rejecting retired legacy shapes but does not vocabulary-check new types, because an append-time refusal would stall a live session's durability mid-flight, which costs more than a loud refusal at the log's next load. The JSONL backend additionally refuses a foreign version from the raw header line before validating today's header shape or decoding any event row, so a structurally different future format still reports the upgrade direction instead of "corrupt"; SQLite gates whole-file structure through its own `SCHEMA_VERSION` pragma first.
+
+## Alternatives considered
+
+- **Major/minor versioning** — the "is it convertible" bit lives on each step's upgrader, and pre-committing it into a number shape invites wrong promises.
+- **Default-ignorable unknown events** — inverts the failure mode of a forgotten marker from visible over-refusal into silent corruption.
+- **Auto-migrating on view** — rewriting the artifact on open turns a read into a destructive write: a converter bug corrupts logs at browse time, and a same-directory older runtime loses access because a newer one merely looked.
+- **Per-plugin runtime registration of known event types** — would make the known set composition-dependent, so a leaner same-version composition would refuse logs a fuller one wrote. The generated repo-wide list keeps same-version reads uniform; out-of-repo plugin events are outside it by construction, and a registration surface for them is deferred until such a consumer exists.
diff --git a/.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.zh.md b/.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.zh.md
new file mode 100644
index 0000000000..c47670f2de
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.zh.md
@@ -0,0 +1,30 @@
+# Agent Note:Session log 版本机制:单调整数、升级器链、逐事件可忽略标记
+
+Status: implemented
+
+[English](2026-08-10-session-log-version-mechanism.md) | 中文
+
+## 问题
+
+Session log 在发布后必须能升级格式,而最先发布的运行时决定了此后一切的下限:第一个发布版的读取器缺少哪种拒绝和降级行为,用户手里已经装上的副本就永远补不上。发布 issue #1901 的最低要求是老运行时读到新 Session 格式时明确报不支持,而不是读错。改动前的读取器在两个方向上都做反了:`assertVersion` 对任何版本不匹配抛出同一条不区分方向的消息;JSONL 解码器把不认识的事件类型原样放行,重建时静默跳过,恢复出一个内容残缺的会话且没有任何诊断。
+
+## 决定
+
+**一个单调递增的整数,不分大小版本。**某一步能不能自动升级是那一步自己的属性,由它的升级器存在与否表达,不该由两级编号方案提前承诺(设计时很少能预知下一个变更算不算"大")。这与 SQLite 后端 `SCHEMA_VERSION` 的先例一致。
+
+**升不升版本由写入方决定,与读取方能力无关。**当且仅当老运行时无法在语义上完全正确地处理新日志时才必须升版本。"解析不报错"不是标准:静默跳过影响重建的内容就是读错。只有结构性变更够得上这条线:header 形状、事件信封、核心事件语义、surface 机制(`SurfaceEventType` 集合、`SurfaceOp` 变体)。拿不准就升:近似恒等的升级器几乎没有成本,漏升一次会让老读取器静默读坏。
+
+**读取规则按方向区分。**版本相等:正常读。比读取器新:拒绝,说明方向("由更新的 harness 写入,请升级"),并给出原始日志文件的路径,用户仍能看到文本(`SessionFormatUnsupportedError`,与 `SessionPersistenceCorruptionError` 区分,因为数据没有损坏)。比读取器旧:查看时经 n→n+1 升级器链在内存中逐级转换;只有会话真正被继续时才把转换落盘(临时文件原子替换,原文件留备份)。写不出升级器的那一步留空,这会切断该步及更早所有版本的升级路径,它们降级为只能看原文。
+
+**逐事件的 `ignorable` 标记吸收词汇表增长,普通的新增事件永远不用升版本。**事件词汇表由挂载了哪些插件决定,单个版本整数描述不了它。读取器遇到不认识的事件类型时拒绝解读日志,除非该事件的信封带 `ignorable: true`。默认为必需:忘写标记的后果是把一个本可恢复的会话拒绝过头(体验问题),而默认可忽略会让同样的疏忽静默恢复出残缺会话(安全事故)。架构保证了这条规则成立:模型可见内容只经三种带 `surfaceOp` 标记的 surface 事件加 `request/header`、`request/context` 折叠进入重建,危险的未知事件恰好是那些不进 surface 但改变日志其余部分解读方式的事件(`session/end-seed` 是现存例子)。
+
+## 影响
+
+v0(0812 发布)交付的内容:分方向的拒绝并带原始日志路径;基于生成的已知词汇清单(`KNOWN_SESSION_EVENT_TYPES`,由 `gen-persistence-catalog` 从所有 `SessionEventMap` 声明合并生成,`verify-persistence-catalog` 保证新鲜)的未知事件守卫;`ignorable` 信封字段被种子校验、两个后端(SQLite 专用列,`SCHEMA_VERSION` 升到 15)和 BFF 线上 schema 接受。升级器链本身推迟到第一个真实的 v0→v1 变更出现、有真实对象可测时再建;写入侧目前不写 `ignorable`(还没有生产者需要它),`Session.append` 的这一表面随第一个使用者一起落地。在注册表面出现之前,仓库外插件的事件在第一方读取器下无法恢复会话,预发布立场接受这一点,而且拒绝是显式的而非静默的。未知类型守卫只在读取侧生效:`appendCore` 继续拒绝已淘汰的 legacy 形状,但不对新类型做词汇检查,因为写入时拒绝会让活跃会话的持久化中途停摆,代价大于下次加载时的显式拒绝。JSONL 后端还会在校验当前 header 形状、解码任何事件行之前,直接从原始 header 行拒绝外来版本,因此结构完全不同的未来格式仍会报告升级方向而不是"损坏";SQLite 则先由自己的 `SCHEMA_VERSION` pragma 把关整个文件的结构。
+
+## 曾考虑的替代方案
+
+- **大小两级版本号**:能否转换这一位信息属于每一步的升级器,把它预先固化进编号形状会做出错误承诺。
+- **未知事件默认可忽略**:把忘写标记的后果从可见的过度拒绝反转成静默损坏。
+- **查看时自动迁移落盘**:打开即改写把读操作变成破坏性写操作,转换器的 bug 会在浏览时损坏日志,同目录的旧版本运行时也会因为新版本只是看了一眼就失去访问能力。
+- **插件运行时注册已知事件类型**:会让已知集依赖插件组合,同版本的精简组合会拒绝完整组合写出的日志。生成的全仓库清单保证同版本读取行为一致;仓库外插件的事件按构造就在清单之外,为它们提供注册表面推迟到真有这样的消费者时再做。
diff --git a/.agents/notes/implemented/architecture/2026-08-11-trajectory-conversation-context-assembly.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-11-trajectory-conversation-context-assembly.i18n.yaml
new file mode 100644
index 0000000000..6db3babb64
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-11-trajectory-conversation-context-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 .agents/notes/implemented/architecture/2026-08-11-trajectory-conversation-context-assembly.md
+2026-08-11-trajectory-conversation-context-assembly.md: 7d0aea2fc09f0f04bd5de923bee15c42a773489a
+2026-08-11-trajectory-conversation-context-assembly.zh.md: be3903bc62a8b1cc21bd9ddac450541ad4679ff0
diff --git a/.agents/notes/implemented/architecture/2026-08-11-trajectory-conversation-context-assembly.md b/.agents/notes/implemented/architecture/2026-08-11-trajectory-conversation-context-assembly.md
new file mode 100644
index 0000000000..7d0aea2fc0
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-11-trajectory-conversation-context-assembly.md
@@ -0,0 +1,105 @@
+# Agent Note: Trajectory assembly from registered Conversation Contexts
+
+Status: implemented
+
+English | [中文](2026-08-11-trajectory-conversation-context-assembly.zh.md)
+
+## Problem
+
+Trajectory maintained an independent Session History source and folded the complete loaded Event window into Assistant, Tool, message, Request-header, and Compaction state. Chat already assembled the same Event families through registered Conversation Definitions. The two paths duplicated business correlation and pagination behavior, and a Trajectory structural update copied or rescanned work proportional to the raw Event count even when one business object changed.
+
+Reusing Chat's final Nodes would not solve the ownership problem. Trajectory needs request lifecycles, running Assistant state, prompt inheritance, Tool schemas, timing records, and a stage-oriented read model that Chat does not consume. Sharing final Node payloads would couple both views to the union of their requirements.
+
+The migration also had to preserve durable steering classification. A `user/message` does not say whether it opened a Turn or was claimed from the `next-step` inbox, and an older page can supply the missing inbox predecessor or Location after the message has already materialized.
+
+## Decision
+
+Trajectory registers target-owned Conversation Definitions and a `trajectory` View Builder against the shared [`ConversationNodeAssembler`](2026-08-09-client-conversation-node-assembly.md). Session owns one contiguous Event window and publishes both Chat and Trajectory snapshots through `Session.views`; it does not run a second Trajectory history source or business fold.
+
+Each Definition belongs to one target. Chat and Trajectory may recognize the same durable Event family, but they keep separate State and final Node payloads. They share only the Assembler's exact-ID matching, ordered Matches, Location facts, Reader dependencies, publication scheduling, and replace/prepend/append lifecycle.
+
+The existing [Trajectory inspection ledger](../feature/2026-07-27-trajectory-inspection-ledger.md) remains the view model. The Trajectory Builder converts materialized target Nodes into its established `eventNodes`, Requests, Tool schemas, running calls, and Location map; layout, table virtualization, selection, Overview, and inspector behavior do not become generic Conversation contracts.
+
+### Business Definitions
+
+| Business | Context identity | State assembly | Trajectory contribution |
+|---|---|---|---|
+| `next-step` inbox | splice Event seq | Apply the splice to the nearest preceding inbox Context | State only; no visible Node |
+| User, steering, or injected message | message Event seq | Read the preceding inbox State and classify the durable message | Input or context Node |
+| Assistant and ordinary Request | `turn:step` | Fold `step/start`, chunks, final message, retry, and `step/end` | Final Assistant, partial Assistant, and Request |
+| Root Tool call | root call ID | Fold root call/result and nested Code Dispatch events into one call tree | Final or running Tool tree |
+| Compaction | compaction ID | Fold start, summary, end, and replacement checkpoint | Compaction Request |
+| Request header | header Event seq | Read the preceding header and retain effective prompt plus the actual change | Prompt and Tool-schema source |
+| Session and Turn boundaries | boundary Event seq | Retain closure time and error facts | Interrupted Compaction or failed ordinary Request |
+
+Every correlating Event must expose the same business ID directly. Code Dispatch uses `rootCallId`, Compaction uses its compaction ID, and ordinary Tool and retry events retain their protocol identities even when a specific Definition correlates by `turn:step`. Legacy records that lack the required correlation ID are ignored by that Definition rather than merged into an `undefined` Context or crashing the Session.
+
+Assistant chunks update only their `turn:step` Context. Content-bearing chunks request animation-frame publication; usage and finish chunks update State without forcing their own frame. A final message, retry, or boundary publishes immediately. Completed Assistant State retains assembled blocks, timing, usage, and retry facts rather than copying the raw chunk ledger into the target snapshot.
+
+### Steering from predecessor Contexts
+
+Trajectory reconstructs steering from durable inbox history, using the same identity rule as the [Chat steering decision](../feature/2026-08-04-web-context-source-and-steer-marks.md) without sharing Chat's final Node.
+
+Each `agent/inbox/spliced` Event targeting `next-step` starts an invisible Context identified by its Event seq. Its `start()` reads the nearest earlier inbox Context, applies the splice, and stores the pending identities plus the cumulative set of claimed message IDs. A later user-origin `user/message` reads the nearest earlier inbox Context: a claimed ID produces a Steering Node, while every other user-origin message produces an ordinary User Node.
+
+A Reader miss while older history remains records a window-gap dependency. When prepend supplies the missing predecessor, the Assembler replays the affected inbox chain and message Contexts in forward Event order. Historical page direction therefore cannot permanently misclassify a message.
+
+The message Event's Location places steering in the owning Step. If the loaded history window lacks enough boundary Events to resolve that Location, layout uses the following Assistant step as the positional fallback. A running Request marker follows leading steering input in the same Step, so the marker denotes the model Request caused by that input rather than appearing before it.
+
+### Window paths and complexity
+
+Let `E` be the loaded raw Event count, `P` one newly prepended page, `D` the number of Trajectory Definitions, `C` the number of materialized Trajectory Context contributions, and `Mᵣ` the total Matches held by Contexts invalidated by a prepend. `D` is a small registered set; streaming chunks aggregate into one Assistant Context, so `C` is normally much smaller than `E`.
+
+| Path | Context work | Target snapshot work | Result |
+|---|---|---|---|
+| Initial tail or reconnect replace | Match the loaded window in `O(E × D)` and build State in forward Event order | Build and order `C` contributions | A full replace remains proportional to the loaded window |
+| Older-page prepend | Match only fresh Events and replay only Contexts whose Match, Location, or Reader answer changed, in `O(P × D + Mᵣ)` | Rebuild the stage snapshot from `C` contributions | Business folding does not restart over all `E` Events |
+| Live append | Match in `O(D)`, locate the keyed Context in `O(1)`, and update only that State | Replace a same-anchor contribution in `O(1)` before snapshot assembly | Business correlation is independent of loaded Event history |
+
+The Builder stores contributions by Context key and keeps a key-to-position index. A content update with the same anchor replaces one contribution in place; a new contribution or anchor change rebuilds and sorts contribution order. Snapshot assembly then walks `C` contributions, indexes Request headers and Tool schemas with Maps, and handles Compaction boundaries and Turn errors with linear cursors or indexes.
+
+Final Event and Request ordering keeps a publication's current upper bound at `O(C log C)`. The migration removes repeated reverse lookups and the old raw-history refold, but it does not claim end-to-end `O(1)` publication. Chat retains its existing keyed snapshot behavior and complexity; adding the Trajectory target does not make Chat scan Trajectory Contexts or Nodes.
+
+### Independent presentation hot paths
+
+The Context migration and the following presentation optimizations solve different costs. These reductions preserve the existing view model and are theoretical from call counts and asymptotic behavior; this decision does not claim benchmark measurements.
+
+| Hot path | Retained behavior | Expected reduction |
+|---|---|---|
+| Markdown summaries | Layout retains source Markdown; each stable Table record memoizes its displayed summary by content, while Detail parses only the selected record | A one-record append reparses the changed visible record instead of every Markdown record |
+| Search text | `TrajectorySearchIndex` linearly checks stable Record IDs and source signatures, but normalizes Markdown only for changed records and commits updates in three-second batches | Signature comparison remains `O(C)`; expensive normalization follows the changed-record count, and continuous frame updates collapse into one batch per interval |
+| Timeline tooltip | Timing text is computed after the delayed tooltip opens | A render with no open tooltip performs no per-span label formatting |
+| Following Assistant lookup | One reverse pass records the next Assistant for every input position | The former repeated forward lookup falls from worst-case `O(C²)` to `O(C)` |
+| Group duration | Fixed decimal grouping replaces `toLocaleString('en-US')` for the invariant English numeric shape | Complexity remains linear in Groups, but the Intl formatter leaves the repeated render path |
+
+Display memoization and search indexing stay separate. Search must include off-screen records and may lag live changes by the throttle interval; Table rendering must update the visible changed record immediately and must not inherit the index's commit cadence.
+
+## Alternatives considered
+
+**Keep the independent Session History fold and optimize it locally.** Rejected: caches could reduce selected hot paths, but Trajectory would still own a second Event window, pagination repair, request inspection fold, and business-correlation implementation beside Chat.
+
+**Reuse Chat Definitions and branch on a `target` argument in `buildViewNode()`.** Rejected: Trajectory needs different State and intermediate records, not only another React renderer. One Definition would carry both views' payloads and conditionals and would invalidate unrelated target data when either view changed.
+
+**Create a Trajectory-specific Assembler.** Rejected: exact-ID routing, update-before-start collection, prepend replay, Location repair, Reader dependencies, and publication cadence are not Trajectory-specific. A second engine would recreate the lifecycle duplication this change removes.
+
+**Add generic Surface, rewind, fanout, or settled lifecycle concepts.** Rejected: the current durable Event stream does not require a generic Surface branch, and Session or Turn boundaries are target business inputs rather than a reason to fan out one Event over every historical Context. Completion remains business State interpreted with Location closure.
+
+**Replace the Trajectory stages with generic Conversation Nodes.** Rejected: stages organize requests, timing, schemas, and table layout for one view. Making them engine contracts would constrain a future plain Session-log view and return view-specific composition to Client Runtime.
+
+**Share one Markdown cache between display and search.** Rejected: display is immediate and viewport-bound, while search covers the complete loaded record set and intentionally batches updates. A shared cache would couple correctness and scheduling across unrelated consumers.
+
+## Verification
+
+Runtime tests pin target registration, exact-ID append, update-before-start replay, prepend identity, Reader window-gap repair, Location replay, and isolation between Chat and Trajectory snapshots.
+
+Trajectory Definition and Builder tests pin Assistant streaming and interruption, nested Tool calls and parallel interruption, Compaction and prompt inheritance, Steering classification and Step placement, Request marker order, stable contribution replacement, and prepend expansion. Table, layout, Timeline, and search tests pin deferred Markdown work, throttled index updates, tooltip-time formatting, and stable search results across append and prepend.
+
+## Consequences
+
+Trajectory business assembly now scales with the changed page or keyed Context instead of restarting from the complete raw Event window. Target-owned Definitions can evolve independently from Chat while retaining one Session window and one set of lifecycle rules. Steering becomes a first-class Trajectory record at its actual Step position without adding steering-specific state to Session.
+
+The retained stage-oriented Builder still performs work proportional to materialized Trajectory contributions and may sort on publication. The search index still performs a light linear signature pass when its input layout changes. These costs are explicit target-view work, not hidden full Event refolding.
+
+Definition authors must provide stable protocol identities. Old Events without a required ID can disappear from the affected Trajectory business view, which is preferable to joining unrelated records or failing history load; producers that require faithful display must log the identity.
+
+The [Conversation assembly decision](2026-08-09-client-conversation-node-assembly.md) remains the authority for the generic Context, Reader, Location, and publication contracts. The [Trajectory ledger decision](../feature/2026-07-27-trajectory-inspection-ledger.md) remains the authority for table hierarchy, virtualization, inspector, and interaction behavior. This Note owns how Trajectory adapts those two decisions and why the adaptation does not share final Nodes with Chat.
diff --git a/.agents/notes/implemented/architecture/2026-08-11-trajectory-conversation-context-assembly.zh.md b/.agents/notes/implemented/architecture/2026-08-11-trajectory-conversation-context-assembly.zh.md
new file mode 100644
index 0000000000..be3903bc62
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-08-11-trajectory-conversation-context-assembly.zh.md
@@ -0,0 +1,105 @@
+# Agent Note: Trajectory 基于注册式 Conversation Context 组装数据
+
+Status: implemented
+
+[English](2026-08-11-trajectory-conversation-context-assembly.md) | 中文
+
+## 问题
+
+Trajectory 曾维护独立的 Session History 数据源,并把完整的已加载 Event 窗口折叠为 Assistant、Tool、消息、Request header 和 Compaction 状态。Chat 已经通过注册式 Conversation Definition 组装相同的 Event 族。两条链路重复实现业务关联与分页行为;即使只改变一个业务对象,Trajectory 的结构更新仍会复制或重新扫描与原始 Event 数量成正比的数据。
+
+复用 Chat 的最终 Node 无法解决职责问题。Trajectory 需要请求生命周期、运行中 Assistant 状态、提示词继承、Tool schema、计时记录和 stage-oriented read model,而 Chat 不消费这些数据。共享最终 Node payload 会让两个视图都依赖双方需求的并集。
+
+本次迁移还必须保留持久 steering(中途引导)分类。`user/message` 本身不说明它是开启了一个 Turn,还是从 `next-step` inbox 被领取;更早页面还可能在消息已经物化后,才补齐缺失的 inbox 前驱或 Location。
+
+## 决策
+
+Trajectory 针对共享的 [`ConversationNodeAssembler`](2026-08-09-client-conversation-node-assembly.md) 注册 target 自有的 Conversation Definition 和 `trajectory` View Builder。Session 只维护一份连续 Event 窗口,并通过 `Session.views` 发布 Chat 与 Trajectory 快照;它不再运行第二套 Trajectory history source 或业务 fold。
+
+每个 Definition 只属于一个 target。Chat 与 Trajectory 可以识别同一持久 Event 族,但分别维护自己的 State 和最终 Node payload。它们只共享 Assembler 的精确 ID 匹配、有序 Match、Location 事实、Reader 依赖、发布调度,以及 replace/prepend/append 生命周期。
+
+既有的 [Trajectory 检查记录表](../feature/2026-07-27-trajectory-inspection-ledger.md)继续作为视图模型。Trajectory Builder 把已物化的 target Node 转换为原有的 `eventNodes`、Requests、Tool schema、运行中调用和 Location map;layout、表格虚拟化、选择、Overview 与检查器行为不会成为通用 Conversation 约定。
+
+### 业务 Definition
+
+| 业务 | Context 标识 | State 组装方式 | Trajectory contribution |
+|---|---|---|---|
+| `next-step` inbox | splice Event seq | 把 splice 应用到最近的前序 inbox Context | 只维护状态,不产生可见 Node |
+| 用户、steering 或注入消息 | message Event seq | 读取前序 inbox State,并对持久消息分类 | Input 或 context Node |
+| Assistant 与普通 Request | `turn:step` | 折叠 `step/start`、chunk、最终消息、retry 和 `step/end` | 最终 Assistant、partial Assistant 与 Request |
+| 根 Tool call | root call ID | 把根 call/result 与嵌套 Code Dispatch Event 折叠为一棵调用树 | 最终或运行中的 Tool tree |
+| Compaction | compaction ID | 折叠 start、summary、end 和 replacement checkpoint | Compaction Request |
+| Request header | header Event seq | 读取前一个 header,保留生效提示词及真实变化 | Prompt 与 Tool-schema 来源 |
+| Session 与 Turn 边界 | boundary Event seq | 保留关闭时间和错误事实 | 被中断的 Compaction 或失败的普通 Request |
+
+每个关联 Event 都必须直接提供相同的业务 ID。Code Dispatch 使用 `rootCallId`,Compaction 使用 compaction ID;即使某个 Definition 按 `turn:step` 关联,普通 Tool 与 retry Event 仍保留各自的协议标识。缺少必要关联 ID 的旧记录由该 Definition 忽略,不会合入 `undefined` Context,也不会导致 Session 崩溃。
+
+Assistant chunk 只更新对应的 `turn:step` Context。带内容的 chunk 请求 animation-frame 发布;usage 与 finish chunk 更新 State,但不单独强制刷新一帧。最终消息、retry 或边界立即发布。已完成 Assistant State 只保留组装后的 block、计时、usage 与 retry 事实,不会把原始 chunk ledger 复制进 target snapshot。
+
+### 通过前序 Context 恢复 steering
+
+Trajectory 从持久 inbox 历史恢复 steering,使用与 [Chat steering 决策](../feature/2026-08-04-web-context-source-and-steer-marks.md)相同的标识规则,但不共享 Chat 的最终 Node。
+
+每条目标为 `next-step` 的 `agent/inbox/spliced` Event 都会启动一个以 Event seq 标识的不可见 Context。它的 `start()` 读取最近的前序 inbox Context,应用 splice,并存储待处理标识以及累计的已领取 message ID 集合。后续用户来源的 `user/message` 读取最近的前序 inbox Context:已领取的 ID 生成 Steering Node,其余用户来源消息生成普通 User Node。
+
+仍有更早历史时,Reader miss 会记录 window-gap 依赖。prepend 补齐缺失的前驱后,Assembler 按 Event 正序重放受影响的 inbox chain 与 message Context。因此,历史分页方向不会永久错误分类消息。
+
+消息 Event 的 Location 会把 steering 放进所属 Step。如果已加载历史窗口缺少足够的边界 Event,无法解析该 Location,layout 就以后续 Assistant step 作为位置回退。同一个 Step 中,运行中 Request 标记排在前置 steering 输入之后,因此该标记表示由这条输入触发的模型 Request,而不会出现在输入前面。
+
+### 窗口链路与复杂度
+
+记 `E` 为已加载原始 Event 数,`P` 为一次新 prepend 的页面,`D` 为 Trajectory Definition 数,`C` 为已物化的 Trajectory Context contribution 数,`Mᵣ` 为一次 prepend 使其失效的 Context 所持有的 Match 总数。`D` 是较小的注册集合;流式 chunk 会聚合到同一个 Assistant Context,因此通常 `C` 明显小于 `E`。
+
+| 链路 | Context 工作量 | Target snapshot 工作量 | 结果 |
+|---|---|---|---|
+| 初始尾页或重连 replace | 以 `O(E × D)` 匹配已加载窗口,并按 Event 正序构造 State | 构造并排序 `C` 个 contribution | 完整 replace 仍与已加载窗口成正比 |
+| 更早页面 prepend | 只匹配新 Event,并只重放 Match、Location 或 Reader 答案发生变化的 Context,成本为 `O(P × D + Mᵣ)` | 从 `C` 个 contribution 重建 stage snapshot | 业务 fold 不会从头重跑全部 `E` 个 Event |
+| 实时 append | 以 `O(D)` 匹配,以 `O(1)` 找到 keyed Context,并只更新对应 State | snapshot 组装前,以 `O(1)` 替换 anchor 未变的 contribution | 业务关联成本与已加载 Event 历史无关 |
+
+Builder 按 Context key 保存 contribution,并维护 key-to-position index。anchor 相同的内容更新会原位替换一个 contribution;新增 contribution 或 anchor 变化才会重建并排序 contribution 顺序。随后,snapshot assembly 遍历 `C` 个 contribution,用 Map 索引 Request header 与 Tool schema,并以线性游标或索引处理 Compaction boundary 与 Turn error。
+
+最终 Event 和 Request 排序使单次发布的当前上界保持为 `O(C log C)`。本次迁移移除了重复反向查找和旧的原始历史 refold,但不声称端到端发布达到 `O(1)`。Chat 保持既有 keyed snapshot 行为与复杂度;增加 Trajectory target 不会让 Chat 扫描 Trajectory Context 或 Node。
+
+### 独立的表现层热点优化
+
+Context 迁移与下列表现层优化解决的是不同成本。这些优化保留既有视图模型;收益来自调用次数和渐进复杂度推算,本决策不声称存在 benchmark 实测结果。
+
+| 热点 | 保留的行为 | 预期减少的工作 |
+|---|---|---|
+| Markdown 摘要 | Layout 只保留源 Markdown;每个稳定 Table record 按内容 memo 展示摘要,Detail 只解析当前选中记录 | 单条 record append 只重解析发生变化的可见记录,而非全部 Markdown record |
+| 搜索文本 | `TrajectorySearchIndex` 仍线性核对稳定 Record ID 与来源签名,但只为变化的 record 标准化 Markdown,并以三秒批次提交更新 | 签名比较仍为 `O(C)`;昂贵标准化只随变化 record 数量增长,持续 frame update 每个时间窗合并成一个批次 |
+| Timeline tooltip | 延迟 Tooltip 打开后才计算计时文案 | 没有打开 Tooltip 的 render 不执行逐 span label 格式化 |
+| 后继 Assistant 查找 | 一次反向遍历为每个输入位置记录后续 Assistant | 原先重复向前查找的最坏复杂度从 `O(C²)` 降为 `O(C)` |
+| Group duration | 以固定十进制分组替代固定英文数字形态下的 `toLocaleString('en-US')` | 复杂度仍与 Group 数线性相关,但重复 render 路径不再调用 Intl formatter |
+
+展示 memo 与搜索索引彼此独立。搜索必须覆盖屏幕外 record,并允许实时变化延迟一个 throttle 周期;Table 必须立即更新发生变化的可见 record,不能继承索引的提交节奏。
+
+## 考虑过的替代方案
+
+**保留独立 Session History fold,只做局部优化。** 不予采纳:缓存可以降低部分热点,但 Trajectory 仍会在 Chat 之外拥有第二套 Event 窗口、分页修复、request inspection fold 与业务关联实现。
+
+**复用 Chat Definition,并在 `buildViewNode()` 中按 `target` 分支。** 不予采纳:Trajectory 需要不同的 State 与中间 record,不只是另一套 React renderer。单一 Definition 会携带两个视图的 payload 与条件,并在任一视图变化时让无关 target 数据失效。
+
+**创建 Trajectory 专属 Assembler。** 不予采纳:精确 ID 路由、先 update 后 start 的收集、prepend replay、Location 修复、Reader 依赖与发布节奏都不是 Trajectory 特有行为。第二套引擎会重新制造本次改造要消除的生命周期重复。
+
+**增加通用 Surface、rewind、fanout 或 settled 生命周期。** 不予采纳:当前持久 Event stream 不需要通用 Surface branch;Session 或 Turn boundary 是 target 业务输入,不构成把一个 Event fanout 到全部历史 Context 的理由。完成条件仍由业务 State 结合 Location closure 判断。
+
+**用通用 Conversation Node 替换 Trajectory stage。** 不予采纳:stage 为单一视图组织 Request、计时、schema 和表格 layout。把它变成引擎约定会限制未来的朴素 Session-log 视图,并把视图专属组合重新放回 Client Runtime。
+
+**在展示与搜索之间共享一套 Markdown cache。** 不予采纳:展示要求立即更新且受 viewport 约束,搜索则覆盖全部已加载 record,并有意批量提交更新。共享 cache 会把两个无关消费方的正确性与调度节奏耦合起来。
+
+## 验证
+
+Runtime 测试固定 target 注册、精确 ID append、先 update 后 start 的 replay、prepend identity、Reader window-gap 修复、Location replay,以及 Chat 与 Trajectory snapshot 隔离。
+
+Trajectory Definition 与 Builder 测试固定 Assistant streaming 与 interruption、嵌套 Tool call 和并行 interruption、Compaction 与 prompt 继承、Steering 分类和 Step 位置、Request 标记顺序、稳定 contribution 替换与 prepend 扩展。Table、layout、Timeline 与搜索测试固定延迟 Markdown 工作、节流索引更新、Tooltip 展示时格式化,以及 append/prepend 期间稳定的搜索结果。
+
+## 后果
+
+Trajectory 业务组装的成本随变化页面或 keyed Context 增长,不再从完整原始 Event 窗口重新开始。target 自有 Definition 可以独立于 Chat 演进,同时继续共享一份 Session 窗口和一套生命周期规则。steering 会在实际所属 Step 位置成为一等 Trajectory record,不需要向 Session 增加 steering 专属状态。
+
+保留的 stage-oriented Builder 仍会执行与已物化 Trajectory contribution 数量成正比的工作,并可能在发布时排序。输入 layout 变化时,搜索索引仍会执行一次轻量线性签名检查。这些成本是显式的 target view 工作,不是隐藏的完整 Event refold。
+
+Definition 作者必须提供稳定的协议标识。缺少必要 ID 的旧 Event 可能不会出现在受影响的 Trajectory 业务视图中;与合并无关记录或让历史加载失败相比,这是更安全的退化方式。要求完整展示的生产方必须记录该标识。
+
+[Conversation assembly 决策](2026-08-09-client-conversation-node-assembly.md)继续作为通用 Context、Reader、Location 与发布约定的真源。[Trajectory ledger 决策](../feature/2026-07-27-trajectory-inspection-ledger.md)继续负责表格层级、虚拟化、检查器和交互行为。本 Note 负责说明 Trajectory 如何适配这两项决策,以及为何该适配不与 Chat 共享最终 Node。
diff --git a/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.i18n.yaml
index 34f0f1457e..e4ff6a9cfa 100644
--- a/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.i18n.yaml
+++ b/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.md
-2026-08-03-cli-signal-shutdown-escalation.md: 55917400fac2728d13dc2cdd799a7e234b6ed661
-2026-08-03-cli-signal-shutdown-escalation.zh.md: c7897a8d77e8c2ebad43cec4e12170b04c837350
+2026-08-03-cli-signal-shutdown-escalation.md: 173d06482cd8a1fcbb985763cc313e3f9b170bc6
+2026-08-03-cli-signal-shutdown-escalation.zh.md: efa52524199906cf636cb2b55cb4249857dc1348
diff --git a/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.md b/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.md
index 55917400fa..173d06482c 100644
--- a/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.md
+++ b/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.md
@@ -6,7 +6,7 @@ English | [中文](2026-08-03-cli-signal-shutdown-escalation.zh.md)
## Problem
-The default telemetry mount added SIGINT/SIGTERM handlers to `dsh web` and the headless command (now `dsh run`) so process exit could drain the Cordis tree instead of dropping queued telemetry. Each handler used a one-way boolean latch and exited only after `ctx.fiber.dispose()` settled. Headless normal completion also awaited that disposal without a bound.
+The default telemetry mount added SIGINT/SIGTERM handlers to `dsh web` and the headless command (now `dsh --profile headless`) so process exit could drain the Cordis tree instead of dropping queued telemetry. Each handler used a one-way boolean latch and exited only after `ctx.fiber.dispose()` settled. Headless normal completion also awaited that disposal without a bound.
A user then reproduced the headless command hanging immediately after the observation URL and ignoring repeated `Ctrl+C`; `DSH_TELEMETRY_DISABLED=1` removed the hang, while a standalone Node handler in the same Linux sandbox received SIGINT. This isolated the pending disposer to telemetry rather than terminal signal forwarding. OTel's `BatchLogRecordProcessor.shutdown()` awaits `exporter.forceFlush()` before the `exportTimeoutMillis`-bounded completion promise, and the OTLP exporter's `forceFlush()` waits directly on its in-flight HTTP Promise. A proxy/sandbox connection that never obtains a socket can therefore leave provider shutdown pending despite both configured SDK timeouts.
diff --git a/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md b/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md
index c7897a8d77..efa5252419 100644
--- a/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md
+++ b/.agents/notes/implemented/bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md
@@ -6,7 +6,7 @@ Status: implemented
## 问题
-默认挂载遥测后,`dsh web` 与 headless 命令(现为 `dsh run`)新增了 SIGINT/SIGTERM 处理器,使进程退出时可以排空 Cordis 插件树,而不是丢弃排队中的遥测数据。每个处理器都使用单向布尔闩锁(latch),并且只有在 `ctx.fiber.dispose()` 结算后才退出。headless 正常完成时同样会无界等待整棵树执行 dispose(资源释放)。
+默认挂载遥测后,`dsh web` 与 headless 命令(现为 `dsh --profile headless`)新增了 SIGINT/SIGTERM 处理器,使进程退出时可以排空 Cordis 插件树,而不是丢弃排队中的遥测数据。每个处理器都使用单向布尔闩锁(latch),并且只有在 `ctx.fiber.dispose()` 结算后才退出。headless 正常完成时同样会无界等待整棵树执行 dispose(资源释放)。
随后有用户复现,headless 命令在打印观察 URL 后立即卡死,重复按 `Ctrl+C` 也没有反应;设置 `DSH_TELEMETRY_DISABLED=1` 后不再卡死,而同一 Linux 沙箱中的独立 Node 信号处理器能够收到 SIGINT。这将待结算的 disposer 定位到遥测,而非终端信号转发。OTel 的 `BatchLogRecordProcessor.shutdown()` 会先等待 `exporter.forceFlush()`,再进入受 `exportTimeoutMillis` 限制的完成 promise;OTLP 导出器的 `forceFlush()` 则直接等待正在进行的 HTTP Promise。因此,代理/沙箱连接始终无法取得 socket 时,即使已经配置两项 SDK 超时,也会让提供方关闭一直待结算。
diff --git a/.agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.i18n.yaml
deleted file mode 100644
index 86e63238fa..0000000000
--- a/.agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.i18n.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
-# side as of the last confirmed-consistent state. Both languages carry equal authority;
-# after editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md
-2026-08-08-npm-backed-git-repository-plugin-preparation.md: 958b932f82f4da3cf63aa911260411855e514409
-2026-08-08-npm-backed-git-repository-plugin-preparation.zh.md: d2256e0eae303c371371b9b5ba1967105aa61834
diff --git a/.agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md b/.agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md
deleted file mode 100644
index 958b932f82..0000000000
--- a/.agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Agent Note: npm-backed preparation makes GitHub repository Plugins self-contained
-
-Status: implemented
-
-English | [中文](2026-08-08-npm-backed-git-repository-plugin-preparation.zh.md)
-
-## Problem
-
-The repository Plugin authoring contract requires `scripts.prepack` to invoke `dsh-plugin-prepare`. Supplying that executable from the running DSH installation made a source package appear valid even when its own manifest could not obtain the helper. It therefore did not prove the behavior users need after `@deepseek-ai/dsh-repository-plugin` is published: an ordinary Git-hosted npm package must be installable and preparable from only its declared dependencies.
-
-A selectable `.dsh-plugin` inside a pnpm workspace has a second isolation requirement. pnpm prepares a Git-hosted package by running the repository's preferred package manager before packing the selected subdirectory. A nested `pnpm install` can join the containing workspace; when the root lockfile does not list `.dsh-plugin` as an importer, pnpm can report success without installing dependencies declared only by that package. Its TypeScript build or prepare command then fails, or a pre-generated artifact hides the missing dependency.
-
-The checked-in headless fixture mounts an already prepared wrapper. It proves runtime composition, not GitHub acquisition, npm resolution, or package-owned preparation.
-
-## Decision
-
-The `.dsh-plugin` package declares `@deepseek-ai/dsh-repository-plugin` as an ordinary development dependency and invokes its published `dsh-plugin-prepare` executable from `scripts.prepack`. The package may declare any other build and runtime dependencies and run arbitrary compilation before the helper. The repository Plugin package marks its Cordis and DSH peers optional so a helper-only development install resolves only the helper's actual `zod` runtime dependency; an application composition still supplies the peers used by the package's Cordis entry.
-
-DSH does not materialize or prepend a prepare executable. `RepositoryCache` supplies only a transaction-owned `pnpm` wrapper: the outer install runs the pinned pnpm entry directly, while pnpm's hard-coded Git-package `pnpm install` reinvokes the same entry with `--ignore-workspace`. The selected package therefore owns dependency resolution even beneath another pnpm lockfile, and normal package-manager lifecycle `PATH` construction exposes `node_modules/.bin/dsh-plugin-prepare`. The temporary pnpm wrapper disappears after the child settles. The repository remains trusted package-manager input: all dependency and lifecycle code executes under the existing trust contract.
-
-The Node 24 consumer lane passes an exact source derived from the pull request head repository and SHA. It uses the existing private DeepSeek Harness repository rather than creating another repository per run. A job-scoped Git configuration gives the read-only job token access to that exact private source and rewrites pnpm's SSH fallback to authenticated HTTPS.
-
-The built-entry acceptance also creates an in-process npm registry. It stages the current built `@deepseek-ai/dsh-repository-plugin` as a publication artifact by removing `private`, replacing workspace protocols with the release version, and packing the declared files. The registry serves the resulting packument and tarball, while a job-local npm config directs only the `@deepseek-ai` scope to it. The real built `dsh run` child then fetches the exact Git source; that package resolves the helper through npm, type-checks and bundles a TypeScript Cordis entry and MCP server, prepares the adjacent skill, and loads all three contributions. A deliberately failing host `PATH` command proves the lifecycle selected the dependency-local executable. The acceptance also requires registry resolution and inspects the immutable prepared cache, so restoring a host-injected helper cannot satisfy it.
-
-## Alternatives considered
-
-**Inject `dsh-plugin-prepare` from the running DSH installation.** Rejected because it lets an incomplete repository manifest pass and tests a host-only path that npm consumers cannot reproduce.
-
-**Publish the source fixture itself to npm.** Rejected because the product contract is specifically that the DSH Plugin remains Git-hosted; only the reusable preparation helper is an npm dependency.
-
-**Create a new private GitHub repository in every CI run.** Rejected because the pull request repository at its exact head SHA is already a real authenticated private Git remote. Per-run repository mutation would add credentials, cleanup, and eventual-consistency failure modes without changing the acquisition path.
-
-**Prepare after `RepositoryCache` installs the selected package.** Rejected because pnpm's packed subdirectory no longer contains sibling source assets referenced by paths such as `../skills`; preparation must happen before packlist.
-
-**Clone GitHub repositories in DSH and bypass pnpm's Git fetcher.** Rejected because it would duplicate ref resolution, subdirectory selection, dependency installation, packlist behavior, and cache integrity already owned by the pinned package manager.
-
-## Consequences
-
-- A repository author can commit a `.dsh-plugin` package, TypeScript source, skills, and MCP definitions to GitHub without publishing that Plugin package to npm. The package must declare the published preparation dependency.
-- Private GitHub sources use the host's standard Git authentication. CI proves that path with a temporary read-only configuration rather than persistent runner credentials.
-- `prepack`, not `prepare`, is part of the authoring format. It may contain arbitrary package-owned build steps but must invoke the dependency-provided helper; missing dependency or lifecycle metadata fails before a cache generation is usable.
-- A selected package in a pnpm repository installs from its own manifest rather than an enclosing workspace. It cannot rely on workspace-only hoisting; ordinary registry and relative `file:` dependencies remain package-owned inputs.
-- Exact source strings identify immutable cache generations; a changed ref or source configuration selects another generation.
-- Package dependencies, compilation, preparation, and the trusted `dsh.entry` contribution remain owned by the repository package and the [trusted-code decision](../architecture/2026-08-08-trusted-repository-package-code.md).
-
-## Testing
-
-`packages/boot/app-boot/tests/repository-cache.spec.ts` runs a package excluded from its source repository's root pnpm lockfile through a local Git subpath and requires relative `file:` dependencies to provide both its build command and `dsh-plugin-prepare`; it also proves that visible environment survives while credential-shaped variables are scrubbed. `packages/self-modification/repository-plugin/tests/repository-plugin.spec.ts` pins helper-bearing `prepack` metadata and preparation output. `examples/headless-agent/tests/keyless-smoke.e2e.ts` keeps the checked-in prepared fixture on that source contract. `apps/cli/tests/github-repository-plugin.built.e2e.ts` is the product acceptance: simulated published helper package, job-local npm registry, fresh DSH home, exact authenticated private GitHub source, actual built `dsh run`, package-owned TypeScript build, real MCP execution, code-entry transformation, mock LLM request observation, and prepared cache inspection.
diff --git a/.agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.zh.md b/.agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.zh.md
deleted file mode 100644
index d2256e0eae..0000000000
--- a/.agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.zh.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Agent Note: 基于 NPM 的准备机制使 GitHub repository 插件自包含
-
-状态:已实现
-
-[English](2026-08-08-npm-backed-git-repository-plugin-preparation.md) | 中文
-
-## 问题
-
-repository 插件创作约定要求 `scripts.prepack` 调用 `dsh-plugin-prepare`。如果由正在运行的 DSH 安装提供该可执行文件,即使源包自身的 manifest(元数据清单)无法取得辅助程序,它也会显得有效。因此,这并未证明 `@deepseek-ai/dsh-repository-plugin` 发布后用户所需的行为:普通 Git 托管 NPM 包必须只依靠自身声明的依赖即可安装和准备。
-
-pnpm workspace 内可选择的 `.dsh-plugin` 还有另一项隔离要求。pnpm 会在打包所选子目录前运行仓库首选的包管理器,以准备 Git 托管包。嵌套的 `pnpm install` 可能加入外层 workspace;当根 lockfile 未把 `.dsh-plugin` 列为 importer 时,pnpm 可能报告成功,却未安装仅由该包声明的依赖。随后,其 TypeScript 构建或准备命令会失败;也可能因为存在预生成产物,依赖缺失被掩盖。
-
-签入仓库的 headless fixture(测试前置数据)挂载的是已准备好的包装层。它证明运行时组合,而不证明 GitHub 获取、NPM 解析或包自有准备。
-
-## 决策
-
-`.dsh-plugin` 包将已发布的 `@deepseek-ai/dsh-repository-plugin` 声明为普通开发依赖,并在 `scripts.prepack` 中调用其已发布的 `dsh-plugin-prepare` 可执行文件。该包可以声明其他任意构建依赖与运行时依赖,并在辅助程序前执行任意编译。repository 插件包把 Cordis 与 DSH 对等依赖(peer dependency)标为可选,因此仅为使用辅助程序而进行的开发安装只会解析辅助程序实际依赖的 `zod` 运行时依赖;应用组合仍会提供该包 Cordis 入口所使用的对等依赖。
-
-DSH 不会生成准备阶段可执行文件,也不会将其前置到 `PATH`。`RepositoryCache` 只提供一个由事务持有的 `pnpm` 包装脚本:外层安装直接运行锁定的 pnpm 入口,而 pnpm 为 Git 包硬编码的 `pnpm install` 会以 `--ignore-workspace` 重新调用同一入口。因此,即使位于另一个 pnpm lockfile 之下,所选包仍自行负责依赖解析,正常的包管理器生命周期 `PATH` 构造会暴露 `node_modules/.bin/dsh-plugin-prepare`。临时 pnpm 包装脚本会在子进程结算后消失。repository 仍是受信任的包管理器输入:所有依赖与生命周期代码都按既有信任约定执行。
-
-Node 24 消费方 CI 任务会传入从 PR(Pull Request)head 仓库与 SHA 派生的精确源。它复用现有私有 DeepSeek Harness 仓库,而不会为每次运行新建仓库。作业作用域的 Git 配置允许只读作业 token 访问该精确私有源,并把 pnpm 的 SSH 回退改写为已认证 HTTPS。
-
-构建入口验收还会创建一个进程内 NPM 注册表。它通过移除 `private`、将 workspace protocol 替换为发布版本并打包声明的文件,把当前已构建的 `@deepseek-ai/dsh-repository-plugin` 暂存为发布产物。注册表会提供由此生成的 `packument` 与 tarball,作业本地 NPM 配置则只把 `@deepseek-ai` scope 指向它。实际构建的 `dsh run` 子进程随后获取精确 Git 源;该包通过 NPM 解析辅助程序,对 TypeScript Cordis 入口和 MCP server 进行类型检查与打包,准备相邻的 skill(技能),并加载全部三类贡献。一个刻意设为失败的宿主 `PATH` 命令可以证明,该生命周期选中的是依赖内的可执行文件。验收还要求经过注册表解析并检查不可变的已准备缓存,因此恢复宿主注入的辅助程序也无法通过。
-
-## 考虑过的替代方案
-
-**从正在运行的 DSH 安装注入 `dsh-plugin-prepare`。** 拒绝,因为这会让 manifest 不完整的 repository 包通过,并测试 NPM 消费方无法复现的纯宿主路径。
-
-**把源 fixture 本身发布到 NPM。** 拒绝,因为产品约定明确要求 DSH 插件仍托管在 Git;只有可复用的准备辅助程序是 NPM 依赖。
-
-**在每次 CI 运行中创建新的私有 GitHub 仓库。** 拒绝,因为 PR 仓库的精确 head SHA 已是经过认证的真实私有 Git remote。每次运行的仓库变更会增加凭据、清理和最终一致性失败模式,却不改变获取路径。
-
-**在 `RepositoryCache` 安装所选包后再准备。** 拒绝,因为 pnpm 打包后的子目录不再包含 `../skills` 等路径所引用的同仓库相邻资源;准备必须在生成 packlist 前完成。
-
-**在 DSH 中克隆 GitHub 仓库并绕过 pnpm 的 Git 获取器。** 拒绝,因为这会重复实现已由锁定包管理器负责的 ref 解析、子目录选择、依赖安装、packlist 行为和缓存完整性。
-
-## 后果
-
-- 仓库作者可以把 `.dsh-plugin` 包、TypeScript 源码、skill 与 MCP 定义提交到 GitHub,而无需把该插件包发布到 NPM。该包必须声明已发布的准备依赖。
-- 私有 GitHub 源使用宿主的标准 Git 认证。CI 使用临时的只读配置而非运行器上的持久凭据来验证该路径。
-- 创作格式使用 `prepack` 而不是 `prepare`。其中可以包含任意包自有构建步骤,但必须调用依赖提供的辅助程序;依赖或生命周期元数据缺失时,会在缓存 generation 可用前失败。
-- pnpm 仓库中的所选包按自身 manifest 安装,而不继承外层 workspace。它不能依赖仅由 workspace 提升而可见的包;普通注册表依赖和相对 `file:` 依赖仍是包自有输入。
-- 精确源字符串标识不可变缓存 generation;改变 ref 或源配置会选择另一个 generation。
-- 包依赖、编译、准备和受信任的 `dsh.entry` 贡献仍由 repository 包和[受信任代码决策](../architecture/2026-08-08-trusted-repository-package-code.md)负责。
-
-## 测试
-
-`packages/boot/app-boot/tests/repository-cache.spec.ts` 会通过本地 Git 子路径运行一个未列入源仓库根 pnpm lockfile 的包,并要求相对 `file:` 依赖同时提供构建命令与 `dsh-plugin-prepare`;该测试还证明可见环境变量得以保留,而名称符合凭据模式的变量会被清除。`packages/self-modification/repository-plugin/tests/repository-plugin.spec.ts` 锁定包含辅助命令的 `prepack` 元数据与准备输出。`examples/headless-agent/tests/keyless-smoke.e2e.ts` 使签入仓库的已准备 fixture 继续符合该源格式约定。`apps/cli/tests/github-repository-plugin.built.e2e.ts` 是产品验收测试:模拟发布的辅助程序包、作业本地 NPM 注册表、全新 DSH 主目录、精确且经过认证的私有 GitHub 源、实际构建的 `dsh run`、包自有 TypeScript 构建、真实 MCP 执行、代码入口转换、mock LLM(大语言模型)请求观测,以及已准备缓存检查。
diff --git a/.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml
similarity index 55%
rename from .agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.i18n.yaml
rename to .agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml
index ff9e0f5bbc..68f399bab8 100644
--- a/.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.i18n.yaml
+++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.md
-2026-07-30-static-repository-plugin-format.md: c66ee111eb0cac9e0d6c54581855ffc18efc8611
-2026-07-30-static-repository-plugin-format.zh.md: c85aaf44d96098eb1ccb45456cce1a15e7408fc5
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md
+2026-08-10-minimal-preset-owns-rl-composition.md: 002cad0827e969b322997821dc978db85e2955f3
+2026-08-10-minimal-preset-owns-rl-composition.zh.md: e957b57395c68b336695bdae07ea15a54ca1ea4e
diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md
new file mode 100644
index 0000000000..002cad0827
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md
@@ -0,0 +1,39 @@
+# Agent Note: The minimal preset owns the complete RL agent composition
+
+Status: implemented
+
+English | [中文](2026-08-10-minimal-preset-owns-rl-composition.zh.md)
+
+## Problem
+
+The Web surface offered two owners for the Claude SWE-compatible RL agent: a process-wide `core-web.cordis.yml` patch and the per-session `minimal` preset. Once [agent presets](../architecture/2026-08-03-per-session-agent-presets.md) became the agent-composition boundary, the preset's scoped `deployment:persona` shadowed the overlay's corrected global persona with stale coding-agent text. The overlay test mounted no preset, while the preset test booted without the overlay, so neither exercised the composition users selected.
+
+The split also hid other drift. The preset mounted one-shot Bash rather than the [persistent Bash](../feature/2026-07-29-persistent-bash-str-replace-editor.md) used by the RL harness and omitted the RL compaction policy. Keeping both owners makes every future prompt, tool, and policy change a cross-product.
+
+## Decision
+
+The shipped Web `minimal` preset is the sole Web owner of the RL agent composition. It declares an entry-local PTY registry and local backend, persistent `bash` with the RL environment description and 300-second timeout, `str_replace_editor`, and an entry-local compaction backend. Tool presentation remains a deployment choice. The compaction policy keeps the RL threshold, absolute retention, generation cap, and retry count; model capacity comes from routed adapter metadata because `contextWindow` is no longer a compact-basic config field. The editor accepts no `requireAbsolutePath` setting because absolute paths are its unconditional contract.
+
+The preset persona is exactly `You are a helpful software engineer assistant.` and sets `complete: true`. A complete `PromptSection` participates in ordinary assembly so tools, contexts, variables, and cooperative listeners still resolve; after the `system-prompt/assemble` waterfall, the prompt registry restores a detached copy of that section as the sole system-prompt section. Multiple effective complete sections reject assembly. This final registry constraint prevents harness identity, Web orientation, tool guidance, or an assembly listener from appending prompt text.
+
+The process-wide `core-web.cordis.yml` patch is absent. Browser UI, workspace attachment, persistence, filesystem, subprocess, sandbox, permission, model routing, and other cross-session services remain host-owned. Selecting `minimal` changes one agent's model-facing composition without changing other sessions in the Web process.
+
+## Verification
+
+System-prompt and persona package tests prove final complete-section enforcement, including waterfall mutation and duplicate rejection. The shipped-preset composition test asserts the exact prompt, Bash description, absolute editor schema, and two-tool catalog under the default native presentation. The keyless Web replay sends a real request through a `minimal` agent while global identity, Web surface text, and a test section are registered, then executes two persistent Bash calls to prove environment and cwd state survive and executes the editor through an absolute path.
+
+The standalone [`minimal.cordis.yml`](../../../../examples/jsonrpc-agent/minimal.cordis.yml) mirrors the same prompt, tools, timeouts, and compaction policy for the bundled JSON-RPC runtime. Its keyless SDK replay asserts the assembled system prompt and two-tool catalog, executes persistent Bash across calls, and exercises the editor; the Python SDK tutorial provides the runnable entry point.
+
+## Alternatives considered
+
+**Keep `core-web.cordis.yml` as a compatibility patch.** Rejected because a process patch and a session preset are two independent owners for one agent contract; precedence makes either one capable of silently undoing the other.
+
+**Disable every known prompt contributor in the preset.** Rejected because host rows are process-wide and new contributors would reopen the prompt. A final complete-section constraint expresses the negative guarantee at the registry that assembles the prompt.
+
+**Filter sections only with a prepended waterfall listener.** Rejected because another prepended wrapper can run outside it and append after the filter. Enforcement after the complete waterfall has stable final authority.
+
+**Mount PTY services on the Web host.** Rejected because only the minimal agent consumes them. An entry-local `pty` realm gives the services the same lifetime and scope as their sole consumer without publishing a process-global service from a preset.
+
+## Consequences
+
+The RL prompt is fixed rather than environment-overridable. The Web preset and standalone JSON-RPC example state the same contract for their respective launch surfaces. The model sees only persistent `bash` and `str_replace_editor`; shell state is per agent and disappears with that agent. The preset pays for its own PTY and compaction service instances, while other presets pay nothing for them. The local persistent-shell backend requires the supported POSIX terminal substrate, so this preset is not a Windows agent surface.
diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md
new file mode 100644
index 0000000000..e957b57395
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md
@@ -0,0 +1,39 @@
+# Agent Note: minimal preset 拥有完整的 RL agent 组合
+
+Status: implemented
+
+[English](2026-08-10-minimal-preset-owns-rl-composition.md) | 中文
+
+## 问题
+
+Web surface 同时由两个位置定义与 Claude SWE 兼容的 RL agent(智能体):进程级 `core-web.cordis.yml` patch,以及逐会话的 `minimal` preset。[agent preset](../architecture/2026-08-03-per-session-agent-presets.md) 成为 agent 组合边界后,preset 中带作用域的 `deployment:persona` 会用陈旧的 coding-agent 文本遮蔽 overlay 修正过的全局 persona。overlay 测试没有挂载 preset,而 preset 测试启动时没有 overlay,因此两者都没有覆盖用户实际选择的组合。
+
+这种拆分还掩盖了其他偏差。preset 挂载了一次性 Bash,而不是 RL harness 使用的[持久 Bash](../feature/2026-07-29-persistent-bash-str-replace-editor.md),并且遗漏了 RL 压缩(compaction)策略。保留两个所有者,会使今后每次修改提示词、工具或策略时都必须验证二者的交叉组合。
+
+## 决策
+
+随附的 Web `minimal` preset 是 RL agent 组合在 Web 中的唯一所有者。它声明 entry 本地的 PTY 注册表与本地后端、带 RL 环境描述且超时为 300 秒的持久 `bash`、`str_replace_editor`,以及 entry 本地的压缩后端。工具呈现仍由部署选择。压缩策略保留 RL 的阈值、绝对保留量、生成上限和重试次数;模型容量来自经路由选定的适配器元数据,因为 `contextWindow` 已不再是 compact-basic 的配置字段。编辑器不接受 `requireAbsolutePath` 设置,因为要求绝对路径是它的无条件约定。
+
+preset persona 恰好是 `You are a helpful software engineer assistant.`,并设置 `complete: true`。complete `PromptSection` 参与常规组装,因此工具、上下文、变量和协作式监听器仍会解析;`system-prompt/assemble` waterfall(瀑布式事件)结束后,提示词注册表会将该段落的独立副本恢复为唯一的系统提示词段落。存在多个有效 complete 段时,组装会被拒绝。这项最终注册表约束可防止 harness 身份、Web 定位、工具引导或组装监听器追加提示词文本。
+
+进程级 `core-web.cordis.yml` patch 不再存在。浏览器 UI、workspace 附加、持久化、文件系统、子进程、沙箱、权限、模型路由及其他跨会话服务仍由宿主持有。选择 `minimal` 只会改变一个 agent 面向模型的组合,不会改变 Web 进程中的其他会话。
+
+## 验证
+
+系统提示词与 persona 包测试证明了 complete 段的最终约束,包括 waterfall 修改与重复项拒绝。交付 preset 组合测试在默认原生呈现下断言精确的提示词、Bash 描述、要求绝对路径的编辑器 schema 和双工具目录。无密钥 Web 回放通过 `minimal` agent 发送一个真实请求,同时注册全局身份、Web surface 文本和一个测试段落;随后执行两次持久 Bash 调用,证明环境与 cwd 状态能够保留,并通过绝对路径执行编辑器。
+
+独立的 [`minimal.cordis.yml`](../../../../examples/jsonrpc-agent/minimal.cordis.yml) 为内置 JSON-RPC 运行时复现相同的提示词、工具、超时和压缩策略。其无密钥 SDK 回放会断言组装后的系统提示词与双工具目录,跨调用执行持久 Bash,并使用编辑器;Python SDK 教程提供可运行的入口。
+
+## 考虑过的替代方案
+
+**将 `core-web.cordis.yml` 保留为兼容 patch。** 被拒绝,因为进程 patch 与会话 preset 是同一 agent 约定的两个独立所有者;优先级会使任意一方都能静默撤销另一方的配置。
+
+**在 preset 中禁用每个已知的提示词贡献方。** 被拒绝,因为宿主行属于整个进程,新的贡献方也会重新开放提示词。由组装提示词的注册表实施最终 complete 段约束,才能表达这项否定保证。
+
+**仅使用前置 waterfall 监听器筛选段落。** 被拒绝,因为另一个前置包装层可以在该监听器外执行,并在筛选后追加内容。在整个 waterfall 结束后实施约束,才能稳定拥有最终决定权。
+
+**在 Web 宿主上挂载 PTY 服务。** 被拒绝,因为只有 minimal agent 消费这些服务。entry 本地的 `pty` realm 与唯一消费方具有相同的生命周期和作用域,无需由 preset 发布进程级全局服务。
+
+## 后果
+
+RL 提示词固定不变,不能通过环境覆盖。Web preset 与独立 JSON-RPC 示例分别在各自的启动界面声明相同的约定。模型只看到持久 `bash` 与 `str_replace_editor`;shell 状态按 agent 隔离,并随该 agent 一并消失。preset 为自身的 PTY 与压缩服务实例承担开销,其他 preset 无需承担。持久 shell 的本地后端需要受支持的 POSIX 终端基础环境,因此该 preset 不适用于 Windows agent surface。
diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.i18n.yaml
new file mode 100644
index 0000000000..612916a290
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-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 .agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md
+2026-08-10-subagent-empty-terminal-message-output.md: 693013f6810005ce02b08bd82f1f6a18511c40fb
+2026-08-10-subagent-empty-terminal-message-output.zh.md: 64d61af21f838ef3f515db8af116cbdd74e96179
diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md b/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md
new file mode 100644
index 0000000000..693013f681
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md
@@ -0,0 +1,31 @@
+# Agent Note: One selection rule keeps subagent output past an empty terminal message
+
+Status: implemented
+
+English | [中文](2026-08-10-subagent-empty-terminal-message-output.zh.md)
+
+## Problem
+
+The agent loop appends an empty-content `assistant/message` when a `max-tokens` step assembled only tool-call blocks because `BlockAssembler.blocks()` drops truncated tool calls; the message records usage only. Three consumers selected the child's output independently and treated that usage record as output. The in-process driver's `readResult` and the continuable Activation's `subagent/end` capture selected the last `assistant/message` without filtering, while the SDK backend's observer let any `assistant/message` take precedence over accumulated text. In a multi-step turn cut off at max-tokens, the final empty message caused the real partial answer to be omitted from `SubagentResult.output`, the tool result, telemetry, and `subagent/end.lastAssistantMessage`. The in-process driver also lacked a streamed-text fallback, so a cancelled child whose only text existed in `assistant/chunk` events reported `[]`.
+
+## Decision
+
+`dsh-subagent` owns one canonical selection rule in `src/assistant-output.ts`: select the last non-empty assistant message; without one, select the accumulated `text-delta` stream; ignore empty-content messages. The incremental `AssistantOutputFold` implements the rule through `push(event)` for session-event transports, `pushText(text)` for chunk-only transports, and `collect()` for selection. `finalAssistantOutput(events)` applies it to a complete event suffix for the in-process `readResult` and Activation capture. The SDK backend folds notification events; the ACP backend exposes no complete assistant messages and folds raw chunk text. `SubagentResult.output` defines the result contract, and `subagent/end.lastAssistantMessage` uses the same rule. When a child produces neither form of output, the lifecycle field is absent rather than an empty array for both one-shot and continuable runs. A `max-tokens` or `aborted` result retains its actual stop reason.
+
+The foreground delegation tool uses the same selection. A non-`completed` result remains an `isError` tool result, but its message appends the child's partial text after the stop-reason headline so the parent model receives both the failure and available output.
+
+## Verification
+
+The keyless SDK backend test uses `FAKE_EMPTY_MESSAGE` to emit a usage-only terminal message. The `subagent-max-tokens-partial` ACP snapshot records a child that streams text and a tool call, ends at a tool-only max-tokens step with an empty usage message in its durable log, and returns the partial text through the parent's errored tool result. Unit coverage checks empty terminal messages, cancellation, message ordering, textless non-empty messages, and exclusion of tool-result content.
+
+## Alternatives considered
+
+**Fix each consumer in place without a shared helper.** Rejected: three independent selections had diverged, while observers of one run must agree on its output.
+
+**Stop the loop from appending the empty message.** Rejected: the message records usage and preserves the step in the durable log ("model-visible ⟺ logged"); changing session events to address output selection would affect every replay and projection consumer.
+
+**Treat empty-content messages as an error.** Rejected: the streamed text is the child's real partial answer, and the stop reason already tells the consumer the turn was cut short.
+
+## Consequences
+
+Multi-step children cut off at max-tokens report their earlier text; cancelled in-process children retain text streamed before the abort; one-shot and continuable `subagent/end` events agree with `SubagentResult.output`. A message whose content is non-empty but textless, such as reasoning-only content, is selected instead of streamed text because the rule tests content length rather than text presence. A non-empty message is also selected instead of text streamed after it: a child cancelled while streaming a later step reports its earlier complete message, while the stop reason records the truncation.
diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.zh.md b/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.zh.md
new file mode 100644
index 0000000000..64d61af21f
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.zh.md
@@ -0,0 +1,31 @@
+# Agent Note: 用同一条选取规则在空终止消息后保留子代理输出
+
+Status: implemented
+
+[English](2026-08-10-subagent-empty-terminal-message-output.md) | 中文
+
+## 问题
+
+当 `max-tokens` 步骤只组装了工具调用块时,agent loop(智能体循环)会追加一条空内容的 `assistant/message`,因为 `BlockAssembler.blocks()` 会丢弃被截断的工具调用;这条消息仅记录 usage。三个消费方独立选取子 agent 的输出,并把这条 usage 记录当成输出。进程内驱动的 `readResult` 与 continuable Activation 的 `subagent/end` capture 不加过滤地选取最后一条 `assistant/message`,SDK 后端的观察器则让任何 `assistant/message` 优先于累积的文本。在被 max-tokens 截断的多步轮次中,最后那条空消息导致 `SubagentResult.output`、工具结果、遥测与 `subagent/end.lastAssistantMessage` 都漏掉真实的部分回答。进程内驱动也没有流式文本兜底,因此被取消的子 agent 若其唯一文本只存在于 `assistant/chunk` 事件中,也会报告 `[]`。
+
+## 决策
+
+`dsh-subagent` 在 `src/assistant-output.ts` 中拥有唯一的规范选取规则:选取最后一条非空 assistant 消息;没有时选取累积的 `text-delta` 流;忽略空内容消息。增量的 `AssistantOutputFold` 通过 `push(event)` 处理会话事件传输,通过 `pushText(text)` 处理仅分片传输,并通过 `collect()` 完成选取。`finalAssistantOutput(events)` 把规则应用于完整的事件后缀,供进程内 `readResult` 与 Activation capture 使用。SDK 后端折叠通知事件;ACP 后端不暴露完整的 assistant 消息,而是折叠原始分片文本。`SubagentResult.output` 定义结果约定,`subagent/end.lastAssistantMessage` 使用同一规则。子 agent 不产生这两种输出中的任何一种时,一次性与 continuable 运行的生命周期字段都会缺省,而不是空数组。`max-tokens` 或 `aborted` 结果保留实际的终止原因。
+
+前台委派工具使用同一选取规则。非 `completed` 的结果仍是 `isError` 工具结果,但其消息会在终止原因标题之后附上子 agent 的部分文本,让父模型同时接收失败信息与已有输出。
+
+## 验证
+
+无密钥 SDK 后端测试使用 `FAKE_EMPTY_MESSAGE` 发出一条仅记录 usage 的终止消息。`subagent-max-tokens-partial` ACP 快照记录一个子 agent:它流式输出文本与一次工具调用,结束于仅含工具调用的 max-tokens 步骤,持久化日志中含一条空的 usage 消息,并通过父侧的错误工具结果返回部分文本。单元覆盖检查空终止消息、取消、消息顺序、不含文本的非空消息,以及排除工具结果内容。
+
+## 考虑过的替代方案
+
+**各消费方就地修复、不抽共享辅助函数。** 之所以否决:三处独立选取已发生分歧,而同一次运行的观察方必须对其输出达成一致。
+
+**让 loop 不再追加空消息。** 之所以否决:这条消息记录 usage,并在持久化日志中保留该步骤("model-visible ⟺ logged");为处理输出选取而改动会话事件,会影响所有 replay 与 projection 消费方。
+
+**把空内容消息视为错误。** 之所以否决:流式文本才是子代理真实的部分回答,且终止原因已经告诉消费方轮次被截断。
+
+## 后果
+
+被 max-tokens 截断的多步子 agent 会报告其更早的文本;被取消的进程内子 agent 保留中止前已流式的文本;一次性与 continuable 的 `subagent/end` 事件同 `SubagentResult.output` 一致。内容非空但不含文本的消息(例如仅含 reasoning 的内容)仍然优先于流式文本,因为规则检查内容长度,而不是文本是否存在。非空消息同样优先于其后才流式出的文本:子 agent 在流式输出后续步骤时被取消,报告的是更早那条完整消息,终止原因则记录该截断。
diff --git a/.agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-11-preset-card-description-clamp.i18n.yaml
similarity index 55%
rename from .agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.i18n.yaml
rename to .agents/notes/implemented/bug-fix/2026-08-11-preset-card-description-clamp.i18n.yaml
index 9d49d8b1c2..21b6957cd2 100644
--- a/.agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.i18n.yaml
+++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-card-description-clamp.i18n.yaml
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.md
-2026-08-08-trusted-repository-package-code.md: 387479b3b36a8bc5e145641ae40802b3090ced70
-2026-08-08-trusted-repository-package-code.zh.md: ecc325c3dd0a9e823f1411c3a809c30ada486289
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-11-preset-card-description-clamp.md
+2026-08-11-preset-card-description-clamp.md: 16ebf371d5af7c9e54fcc37819696b380856d5cb
+2026-08-11-preset-card-description-clamp.zh.md: 5b7a18f41e4b3e8acd681a001f6826b19ca7026d
diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-card-description-clamp.md b/.agents/notes/implemented/bug-fix/2026-08-11-preset-card-description-clamp.md
new file mode 100644
index 0000000000..16ebf371d5
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-card-description-clamp.md
@@ -0,0 +1,43 @@
+# Agent Note: Preset cards clamp their description instead of sizing the roster
+
+Status: implemented
+
+English | [中文](2026-08-11-preset-card-description-clamp.zh.md)
+
+## Problem
+
+A preset publishes its own `description`, of any length, and the settings section renders the roster as a card grid. The description had a `min-height` and no upper bound, while the grid sizes rows with `grid-auto-rows: 1fr` — which makes every implicit row the same height, not just the row holding the tall card. One long description therefore set the height of the whole roster: with a 250-character description in the custom group, all four cards measured 421px and the short-description cards filled with blank space.
+
+The description is also the field that tells presets apart, so hiding it is not an option; the card has to bound it and still make the whole text reachable.
+
+## Decision
+
+The description clamps to four lines and offers the rest through the shared `Tooltip`, attached only while the element actually overflows (`scrollHeight > clientHeight`, re-measured through a ResizeObserver because the settings pane width follows the window). This mirrors the chat stats line, which clamps to one line on the same measure-then-attach rule.
+
+Card height stays derived rather than fixed. With the description bounded, `grid-auto-rows: 1fr` already equalizes the grid, and a card carrying the broken-preset reason or a revealed path still sizes itself — a pixel height would clip both.
+
+Three smaller decisions ride along:
+
+- `.cardId` takes the card's free space with `margin-top: auto`, and the description no longer grows. A flex-stretched box leaves the clamp height and the box height disagreeing; sizing the clamped box by content alone keeps the behavior independent of that interaction.
+- The description carries `title=""`. An empty `title` means the element has no advisory information and the lookup stops there, so the card body's native tooltip does not climb to the description and a cut-off description answers with one bubble instead of two.
+- `Tooltip` gains an optional `maxWidth`. Its default half-viewport cap renders a description as a slab wider than the settings dialog it belongs to, spilling across the application behind it.
+- `Tooltip` also flips a `top` or `bottom` bubble to the other side when the viewport has no room for it, which its horizontal-only clamp previously left unhandled. Custom presets sit at the bottom of the roster and carry the longest descriptions, so the common case put a tall bubble under an anchor low on the page. The flip only moves into a side that genuinely fits, so an anchor with room on neither side keeps the requested placement rather than oscillating; sliding the bubble vertically instead would cover the text being read.
+
+A roster row that failed its shape check is badged `Failed to load` (`加载失败`) rather than `Broken` (`已损坏`). Discovery sets `broken` when the composition file is missing, unreadable, or malformed — most often a file the user just edited or deleted — so a damage claim overstates what was observed, and the verbatim reason under the badge already names the file and the fix.
+
+## Alternatives considered
+
+- **A fixed card height.** It states the intent directly but clips the two rows whose height legitimately varies: the broken-preset reason and the revealed preset directory.
+- **The native `title` attribute carrying the full description.** No measurement and no component, but a roughly one-second delay, operating-system styling, and it takes over the card's `set as default` hint across most of the card's area.
+- **Attaching the tooltip unconditionally.** It drops the ResizeObserver, at the cost of answering a hover over a short description with a bubble repeating what is already on the card.
+- **Expanding the clamp on hover.** It shows the text in place, and moves the grid under the pointer.
+
+## Consequences
+
+The section owns a small measured component and the shared primitive owns one more optional prop. In exchange, no card's height follows the longest description anywhere in the roster, and the whole description stays in the accessibility tree because the clamp is CSS rather than truncated text.
+
+The `title=""` suppression is pinned by a DOM assertion, not by observing the native tooltip: a browser tooltip is drawn outside the page and cannot be captured. If a browser ever resumes climbing past an empty `title`, the fallback is to drop the card body's `title` — its content is already in the body's `aria-label`.
+
+## Testing
+
+Package tests cover the three measurement outcomes (cut off, fitting, and a runtime without `ResizeObserver`) and the tooltip width cap. The web e2e goldens replay unchanged except `damaged.expected.md`, re-recorded for the badge copy.
diff --git a/.agents/notes/implemented/bug-fix/2026-08-11-preset-card-description-clamp.zh.md b/.agents/notes/implemented/bug-fix/2026-08-11-preset-card-description-clamp.zh.md
new file mode 100644
index 0000000000..5b7a18f41e
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-11-preset-card-description-clamp.zh.md
@@ -0,0 +1,43 @@
+# Agent Note: 预设卡片截断自身描述,而不是由描述决定整份名单的高度
+
+Status: implemented
+
+[English](2026-08-11-preset-card-description-clamp.md) | 中文
+
+## 问题
+
+preset 自行发布 `description`,长度不限,而设置分区把名单渲染为卡片网格。描述只有 `min-height` 没有上限,网格则以 `grid-auto-rows: 1fr` 排布行——该取值让每一个隐式行等高,而不只是承载高卡片的那一行。因此一条长描述决定了整份名单的高度:自定义组里放入一条 250 字的描述后,四张卡片全部量得 421px,短描述卡片被大片空白填满。
+
+描述同时又是区分各个 preset 的字段,因此不能藏起来;卡片必须既给它设上限,又让全文仍然可达。
+
+## 决定
+
+描述截断为四行,其余内容通过共享的 `Tooltip` 呈现,且仅在元素确实溢出时才挂载(`scrollHeight > clientHeight`,并经 ResizeObserver 重新测量,因为设置面板宽度跟随窗口)。这与聊天统计行一致:它按同样的「先测量再挂载」规则截断为一行。
+
+卡片高度仍是推导得出而非固定。描述有了上限之后,`grid-auto-rows: 1fr` 本身就让网格等高,而承载损坏原因或已展示目录的卡片仍能按自身内容定高——写死像素高度会把两者一并裁掉。
+
+随之而来三个更小的决定:
+
+- `.cardId` 以 `margin-top: auto` 吃掉卡片的空余空间,描述不再拉伸。被 flex 拉伸的盒子会让截断高度与盒子高度不一致;让截断盒子只按内容定高,行为便不依赖这层交互。
+- 描述带有 `title=""`。空 `title` 表示该元素没有提示信息,查找就此停止,因此卡片主体的原生 tooltip 不会向上找到描述,被裁切的描述只回应一个气泡而不是两个。
+- `Tooltip` 新增可选的 `maxWidth`。它默认的半视口上限会把描述渲染成比所属设置弹窗还宽的一整块,溢出到背后的应用界面上。
+- `Tooltip` 同时在视口放不下时把 `top` 或 `bottom` 气泡翻到另一侧,此前它只做水平收敛。自定义 preset 位于名单末尾、又恰恰承载最长的描述,因此常见情形正是让一个高气泡挂在页面靠下的锚点之下。翻转只会移向确实放得下的一侧,两侧都放不下时保持请求的位置而不来回摆动;改为垂直滑动则会盖住正在阅读的文本。
+
+形状检查未通过的名单行,徽记从 `Broken`(`已损坏`)改为 `Failed to load`(`加载失败`)。discovery 在组装文件缺失、读不出或格式错误时置位 `broken`——最常见的是用户刚编辑或删除的文件——因此断言损坏超出了观察到的事实,而徽记下方原样展示的原因本就点名了文件与修法。
+
+## 备选方案
+
+- **写死卡片高度。** 它直接表达了意图,却会裁掉两处高度本就可变的行:损坏预设的原因行和已展示的预设目录。
+- **用原生 `title` 属性承载完整描述。** 无需测量也无需组件,代价是约一秒的延迟、操作系统的样式,以及在卡片大部分区域内顶替掉「设为默认」的提示。
+- **无条件挂载 tooltip。** 省掉 ResizeObserver,代价是把鼠标停在短描述上时,弹出一个重复卡片已有内容的气泡。
+- **hover 时展开截断。** 它就地展示文本,同时让网格在指针下方发生位移。
+
+## 后果
+
+分区多了一个带测量的小组件,共享基元多了一个可选 prop。换来的是:任何卡片的高度都不再跟随名单中最长的那条描述;而且截断由 CSS 完成而非截短文本,完整描述始终留在无障碍树中。
+
+`title=""` 的抑制作用由一条 DOM 断言钉住,而非通过观察原生 tooltip:浏览器 tooltip 画在页面之外,无法被捕获。若某个浏览器日后重新越过空 `title` 继续向上查找,退路是去掉卡片主体的 `title`——它的内容已经在主体的 `aria-label` 里。
+
+## 测试
+
+包内测试覆盖三种测量结果(被裁切、放得下、运行时没有 `ResizeObserver`)以及 tooltip 的宽度上限。web e2e golden 除 `damaged.expected.md` 按徽记文案重录外,其余原样回放通过。
diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml b/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml
index c1ba01b255..ea386ac60b 100644
--- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-06-approval-seam.md
-2026-07-06-approval-seam.md: 7c830d93f19a40ab193cfebabca854882ab68d62
-2026-07-06-approval-seam.zh.md: 9dedfddadc23b0da44b28e8750508653ee20bb83
+2026-07-06-approval-seam.md: 8aa9986139dae77e08c166b72545bfa688a389e0
+2026-07-06-approval-seam.zh.md: ef4ccf5fd2b54888a648737866ff6f5fe1678882
diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.md b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md
index 7c830d93f1..8aa9986139 100644
--- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.md
+++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md
@@ -123,7 +123,7 @@ Costs and accepted limits:
- **Who decides whether a call asks in the first place?** Policy producers: a hook returning `permissionDecision: ask`, any `tools/pre-execute` listener, or the sandbox escalation gate. The seam and the bridge only route and answer; neither injects its own judgment about what deserves a prompt.
- **What happens when the user dismisses the prompt, or the turn aborts mid-ask?** Dismissal maps to `cancelled` with its own deny text. An already-aborted signal settles `cancelled` without dispatching; an abort during the ask discards the late answer. When both audit appends commit, either path records one pair, never two.
- **What if the client answers with an option the harness never offered?** Any selection other than the offered `allow_once` maps to `rejected` — an unknown optionId from a non-conforming client can never grant.
-- **How do subagents' approvals route?** An agent no answerer owns delegates through the whole waterfall and fails closed — in-process subagents are deliberately unanswerable. A `'never'` parent seeds that override into each in-process child's log ([decision](2026-07-25-subagent-policy-inheritance.md)), so the child is told up front instead of asking into the empty waterfall. `subagent-acp`'s child-side auto-answer is separate; routing a child's asks to the parent controller is deferred (§ Deferred).
+- **How do subagents' approvals route?** They do not: delegation pins every in-process child to `'never'` ([approvals-pinned decision](2026-08-10-subagent-approval-pinned-never.md)), so each child ask resolves `rejected` before any answerer and the child is told up front through its runtime context. `subagent-acp`'s child-side auto-answer is separate; routing a child's asks to the parent controller is deferred (§ Deferred).
- **What does `policy: 'never'` actually change at runtime?** The service resolves every ask for that session to `rejected` before dispatching any answerer (in-service, so no registration order can bypass it); the next atomic runtime-context snapshot states the policy; each successful auto-rejection records the audit pair.
- **What happens across a hot reload, or when an answerer unloads mid-session?** Answerers dispose with their owning fiber, so the next ask degrades to `unavailable` instead of hanging on a dead channel; remounting re-registers the answerer with no catch-up state.
- **Where does a client get approval context?** The request carries the exact `callId` and the asker's human-readable `reason`; channel adapters may correlate richer tool-call state without duplicating arguments in the approval seam.
diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md b/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md
index 9dedfddadc..ef4ccf5fd2 100644
--- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md
@@ -123,7 +123,7 @@ ACP 桥只应答其会话映射所拥有的精确 agent 对象。它携带既有
- **谁决定一次调用是否需要 ask?** 策略生产者:返回 `permissionDecision: ask` 的钩子、任何 `tools/pre-execute` 监听器、或沙箱升级门禁。seam 和桥只负责路由和应答;二者都不注入自己对「什么值得弹出提示」的判断。
- **用户关闭提示或轮次在 ask 进行中中止时会发生什么?** 关闭映射为 `cancelled` 并携带自己的拒绝文本。已中止的 signal 直接结算为 `cancelled` 而不派发;ask 进行中的中止丢弃迟到的应答。当两个审计追加都提交时,任一路径都记录恰好一对事件,绝不会两对。
- **如果客户端以 harness 从未提供的选项应答呢?** 除已提供的 `allow_once` 之外的任何选项都映射为 `rejected`——来自不合规客户端的未知 optionId 永远不能授权。
-- **subagent 的审批如何路由?** 没有应答者拥有的 agent 穿过整个 waterfall 委派并失败关闭——进程内 subagent 被刻意设计为不可应答。`'never'` 父级会把该覆盖项预置到每个进程内子 agent 的日志中([决策](2026-07-25-subagent-policy-inheritance.md)),因此子 agent 一开始就会得知,而不是向空的 waterfall 发出 ask。`subagent-acp` 的子侧自动应答是独立的;将子 agent 的 ask 路由到父控制器已延后(§ 延后)。
+- **subagent 的审批如何路由?** 不路由:委派会把每个进程内子 agent 钉定为 `'never'`([审批钉定决策](2026-08-10-subagent-approval-pinned-never.md)),因此子 agent 的每次 ask 都在任何应答者之前解析为 `rejected`,子 agent 则通过其运行时上下文一开始就会得知。`subagent-acp` 的子侧自动应答是独立的;将子 agent 的 ask 路由到父控制器已延后(§ 延后)。
- **`policy: 'never'` 在运行时实际改变了什么?** 服务在派发任何应答者之前,将该会话的每次 ask 解析为 `rejected`(在服务内部,因此没有注册顺序能绕过它);下一份原子化的运行时上下文快照会声明该策略;每次成功的自动拒绝都会记录审计对。
- **热重载或应答者在会话中途卸载时会发生什么?** 应答者随其拥有的 fiber 一起 dispose,因此下一次 ask 降级为 `unavailable` 而非挂在死通道上;重新挂载会重新注册应答者,无需追赶状态。
- **客户端从哪里获得审批上下文?** 请求携带精确的 `callId` 和发起方的人类可读 `reason`;通道适配器可自行关联更丰富的工具调用状态,而无需在审批 seam 中重复携带参数。
diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml b/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml
index 289e9a582a..85fdd2b82c 100644
--- a/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-06-sandbox.md
-2026-07-06-sandbox.md: 0214a202a41983c76fa25e3a82e1cfaec70a0d55
-2026-07-06-sandbox.zh.md: bd8dcdb74f723a955ea2a1cc5b224ef2ded4a8d5
+2026-07-06-sandbox.md: e1ec35cdcf3be2af03232f823d9a9cee57b4e8e8
+2026-07-06-sandbox.zh.md: d3b74d441f72fc4c218979c327546031fd1b24f1
diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.md b/.agents/notes/implemented/feature/2026-07-06-sandbox.md
index 0214a202a4..e1ec35cdcf 100644
--- a/.agents/notes/implemented/feature/2026-07-06-sandbox.md
+++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.md
@@ -171,7 +171,7 @@ Costs and accepted limits:
- **The one-wrapper illusion is given up knowingly.** A `tools/pre-execute` wrapper plus prompt conventions does not solve sandbox approval — the correct design costs structured denials, native runner probes, per-call policy carriage, and consistent cross-family enforcement, and this design pays it.
- **`read-only` became a cross-family boundary through a follow-up.** This RFC shipped bash-only enforcement; the [cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md) extends the same mode vocabulary to the filesystem tools through a sandboxed `ctx.fs` provider and relocates the mode/root config and the `sandbox/mode` override to `ctx.sandboxPolicy` (§ In-process tools).
-- **Windows has no backend.** Its chain slot is reserved empty — fail-closed, never a fallthrough; filling it is a deferred phase.
+- **Windows is a partial backend.** This RFC originally reserved an empty, fail-closed win32 chain; the later [Windows ACL sandbox decision](2026-08-08-windows-acl-restricted-token-sandbox.md) filled it with the restricted-token runner. Its Everyone and hard-link gaps are reported as `enforcement: 'partial'`, never promoted to the full promise.
- **The Seatbelt rung leans on Apple's deprecated-but-shipped `sandbox-exec` CLI.** As darwin's sole candidate it is selected without probing, so a future removal under a usable workdir surfaces as a runner-attributable spawn failure and an executable refusal through its fatal signature — both become `SANDBOX_UNAVAILABLE`, and the command never runs; fail closed, never open.
- **Landlock confinement is only as complete as the running kernel's ABI.** Reported as `enforcement: 'partial'` rather than refused — the deliberate trade that keeps the fallback available on older-kernel hosts.
- **Runner attribution uses an in-band protocol.** Exit status plus stderr cannot cryptographically identify the writer, so a confined child can mimic a fatal runner line and status to cause an availability/diagnostic false attribution. The conjunction and exact notice exclusion reduce accidental matches; this is not a sandbox bypass because the child is already confined.
diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md b/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md
index bd8dcdb74f..d3b74d441f 100644
--- a/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md
@@ -171,7 +171,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自能力边
- **单一包装的幻觉被有意放弃。**`tools/pre-execute` 包装加提示词约定无法解决沙箱批准——正确的设计需要结构化拒绝、原生 runner 探测、按调用策略承载和一致的跨工具族强制,本设计为此付出了代价。
- **`read-only` 通过后续设计成为跨工具族边界。** 本 Agent Note 最初只交付 bash 强制;[跨工具族 fs 沙箱 Agent Note](2026-07-14-cross-family-fs-sandbox.md) 通过沙箱化的 `ctx.fs` 提供方把同一模式词汇扩展到文件系统工具,并将 mode/root 配置和 `sandbox/mode` 覆盖迁移到 `ctx.sandboxPolicy`(§ 进程内工具)。
-- **Windows 没有后端。** 其链槽保留为空——失败关闭,绝不穿透;填充它是延迟阶段。
+- **Windows 后端只提供部分强制执行。** 本 RFC 最初预留了一条空的、失败关闭的 win32 链;后续的 [Windows ACL 沙箱决策](2026-08-08-windows-acl-restricted-token-sandbox.md)以受限令牌 runner 填充了它。其 Everyone 与硬链接缺口报告为 `enforcement: 'partial'`,绝不提升为完整承诺。
- **Seatbelt 层级依赖 Apple 已弃用但仍交付的 `sandbox-exec` CLI。** 作为 darwin 的唯一候选,它无需探测即被选中,因此在 workdir 可用时,未来移除会表现为可归因于 runner 的 spawn 失败,可执行文件拒绝则通过其致命签名体现——两者都会变为 `SANDBOX_UNAVAILABLE`,且命令绝不会运行;失败关闭,绝不开放。
- **Landlock 约束的完整度取决于运行内核的 ABI。** 报告为 `enforcement: 'partial'` 而非拒绝——这是有意的权衡,使备选在旧内核主机上仍可用。
- **Runner 归因使用带内协议。** 退出状态与 stderr 无法以密码学方式识别写入者,因此受限子进程可以模仿 runner 的致命诊断行和状态,造成可用性或诊断误归因。多项证据的合取与精确通知排除减少了意外匹配;这不是沙箱绕过,因为子进程已经受到限制。
diff --git a/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.i18n.yaml b/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.i18n.yaml
index ca61a3783c..678ae169cf 100644
--- a/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-07-mcp-client-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 .agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md
-2026-07-07-mcp-client-plugin.md: 756a5c4dc9f1152ecb1955d93b8dc47fcf07c661
-2026-07-07-mcp-client-plugin.zh.md: 9f45203c479379087c5a19eccce3ab7a339d4ed0
+2026-07-07-mcp-client-plugin.md: 077d978d8815d759574f89ed524ab3bc8c24a267
+2026-07-07-mcp-client-plugin.zh.md: 8f58c9359ca8447717cc353f97f1333370fa6fda
diff --git a/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md b/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md
index 756a5c4dc9..077d978d88 100644
--- a/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md
+++ b/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md
@@ -153,13 +153,7 @@ Build the child environment from the subprocess seam's shared `scrubbedParentEnv
### Disconnection / crash
-No auto-reconnect. If the MCP server process exits or the transport closes:
-
-1. The effect disposes → all registered tools are unregistered (fiber-scoped disposers).
-2. Subsequent model calls to those tools → `ToolNotFoundError` → `isError: true`.
-3. Recovery: user edits `cordis.yml` (triggers HMR reload) or restarts the harness.
-
-This matches the ACP subagent pattern: "crash = terminal, report error, clean up, don't retry."
+A per-instance connection supervisor reconnects automatically after a lost connection with bounded exponential backoff and a per-outage attempt budget, re-running discovery on success; exhaustion unregisters the server's tools and stops until reload. The [auto-reconnect Agent Note](2026-08-06-mcp-client-auto-reconnect.md) owns that decision, including the `reconnect` config block and the `reconnect.enabled: false` opt-out that restores manual HMR/restart recovery.
## Alternatives considered
@@ -173,7 +167,7 @@ Rejected. There is no foreseeable alternative MCP client implementation — MCP
### Auto-reconnect with exponential backoff
-Rejected for v1. Adds complexity (partial-availability state where tools are registered but temporarily non-functional), and stdio process crashes usually indicate a configuration problem that retrying won't fix. HMR already provides the manual recovery path. Can be added as a future `reconnect: boolean` config if needed.
+Rejected for v1: it added a partial-availability state (tools registered but temporarily non-functional), and stdio crashes often indicate configuration problems retrying cannot fix; HMR was the recovery path. Operational feedback reversed the deferral — the [auto-reconnect Agent Note](2026-08-06-mcp-client-auto-reconnect.md) implements it with a bounded per-outage budget and an opt-out.
### Bridge Resources and Prompts
@@ -211,4 +205,4 @@ Coverage is named per tier; each behavior lives at the cheapest tier that can ex
- **MCP SDK stability**: the `@modelcontextprotocol/sdk` is still evolving; breaking changes require updating the bridge. The version is pinned, and the SDK is widely adopted (Claude Desktop, Cursor, VS Code) so breaking changes are unlikely to be silent.
- **Tool schema quality**: MCP servers may expose poorly-described tools (vague descriptions, incomplete JSON schemas). The harness passes them through as-is — garbage-in-garbage-out; that is the server author's responsibility, not the bridge's.
- **Stdio process management**: a misbehaving MCP server that ignores signals could wedge dispose. The Cordis fiber disposal has bounded quiescence; a stuck transport eventually times out at the framework level.
-- Crash recovery is manual (HMR edit or restart) — accepted for v1; a `reconnect` config remains open as future work.
+- Crash recovery is automatic within the [reconnect budget](2026-08-06-mcp-client-auto-reconnect.md); manual reload remains the path after exhaustion or with `reconnect.enabled: false`.
diff --git a/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.zh.md b/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.zh.md
index 9f45203c47..8f58c9359c 100644
--- a/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.zh.md
@@ -153,13 +153,7 @@ MCP 仅保证工具名在[单个服务器内](https://modelcontextprotocol.io/sp
### 断连 / 崩溃
-不自动重连。如果 MCP 服务器进程退出或传输层关闭:
-
-1. effect dispose → 所有已注册工具被注销(fiber 作用域的 disposer)。
-2. 后续模型对这些工具的调用 → `ToolNotFoundError` → `isError: true`。
-3. 恢复:用户编辑 `cordis.yml`(触发 HMR 重载)或重启 harness。
-
-这与 ACP subagent 模式一致:「崩溃即终态,报告错误,清理资源,不重试。」
+每个实例的连接监督器在连接丢失后以有界指数退避和单次故障尝试预算自动重连,成功后重新执行发现流程;尝试耗尽则注销该服务器的工具并停止,直到重新加载。[自动重连 Agent Note](2026-08-06-mcp-client-auto-reconnect.md) 拥有该决策,包括 `reconnect` 配置块和恢复手动 HMR/重启恢复的 `reconnect.enabled: false` opt-out。
## 曾考虑的替代方案
@@ -173,7 +167,7 @@ MCP 仅保证工具名在[单个服务器内](https://modelcontextprotocol.io/sp
### 指数退避自动重连
-v1 否决。引入复杂性(工具已注册但暂时不可用的部分可用状态),且 stdio 进程崩溃通常表明配置问题,重试无法修复。HMR 已提供手动恢复路径。如有需要,可在未来作为 `reconnect: boolean` 配置项添加。
+v1 否决:引入了部分可用状态(工具已注册但暂时不可用),且 stdio 崩溃往往表明配置问题,重试无法修复;HMR 曾是恢复路径。运营反馈扭转了该延期决定——[自动重连 Agent Note](2026-08-06-mcp-client-auto-reconnect.md) 以有界的单次故障预算和 opt-out 实现了自动重连。
### 桥接 Resources 和 Prompts
@@ -211,4 +205,4 @@ v1 否决。它能防止跨服务器冲突,但无法将 MCP 注册与原生 ha
- **MCP SDK 稳定性**:`@modelcontextprotocol/sdk` 仍在演进中;破坏性变更需要更新桥接。版本已固定,且该 SDK 被广泛采用(Claude Desktop、Cursor、VS Code),因此破坏性变更不太可能悄然发生。
- **工具 schema 质量**:MCP 服务器可能暴露描述不佳的工具(模糊的描述、不完整的 JSON Schema)。harness 原样透传——垃圾进垃圾出;这是服务器作者的责任,不是桥接的。
- **Stdio 进程管理**:行为异常的 MCP 服务器如果忽略信号,可能卡住 dispose。Cordis fiber 的 dispose 具有有界的完全停稳过程;卡住的传输层最终会在框架层面超时。
-- 崩溃恢复是手动的(HMR 编辑或重启)——v1 已接受;`reconnect` 配置作为未来工作保持开放。
+- 崩溃恢复在[重连预算](2026-08-06-mcp-client-auto-reconnect.md)内自动进行;耗尽后或配置 `reconnect.enabled: false` 时回退为手动重新加载。
diff --git a/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml
index 3023f86bba..da61b3fa95 100644
--- a/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
-2026-07-08-self-referential-cordis-toolset.md: 5fc2fb07fcd0b00bf72c818d3806b298312cdc31
-2026-07-08-self-referential-cordis-toolset.zh.md: 8f34c97d94cad9b79a0e823406c07cdcfb38793f
+2026-07-08-self-referential-cordis-toolset.md: 0d78e0adff487edae00c2422acc1ef8941e7636a
+2026-07-08-self-referential-cordis-toolset.zh.md: 665387a9fb24bf0e6fc04fdfb1ced88b932742ad
diff --git a/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
index 5fc2fb07fc..0d78e0adff 100644
--- a/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
+++ b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
@@ -40,7 +40,7 @@ The boundary normalizes unambiguous JSON-Schema forms into `ParameterSchemaSpec`
Every temporary Plugin is a child of one internal `cordis-dynamic` group beneath the tool plugin, so ordinary fiber disposal handles toolset reload and unload. `cordis_mount` awaits settlement; startup failure disposes the fiber before returning an error. A settled pending Plugin remains visible with its missing injections. `cordis_unmount` awaits the Plugin fiber's disposal.
-Temporary Plugins exist only in process memory. They create no Plugin file, install no package, change no `cordis.yml` or personal/project configuration, do not survive restart, and have no automatic save, promote, or install path. Keeping an experiment means asking the Agent to implement a normal local, project, or repository Plugin through the regular development workflow.
+Temporary Plugins exist only in process memory. They create no Plugin file, install no package, change no `cordis.yml` or personal/project configuration, do not survive restart, and have no automatic save, promote, or install path. Keeping an experiment means asking the Agent to implement a normal project Plugin or installable profile bundle through the regular development workflow.
### Cross-mount composition via provide/inject
diff --git a/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md
index 8f34c97d94..665387a9fb 100644
--- a/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md
@@ -40,7 +40,7 @@ vm 隔离了意外的全局污染,上下文门面隐藏了框架内部细节
每个临时 Plugin 都是工具插件下方内部 `cordis-dynamic` 分组的子节点,因此普通的 fiber 释放即可处理工具集重载和卸载。`cordis_mount` 会等待 settlement;启动失败时在返回错误前释放 fiber。已 settle 但处于 pending 状态的 Plugin 仍然可见,并列出其缺失的注入。`cordis_unmount` 等待 Plugin fiber 的释放完成。
-临时 Plugin 只存在于进程内存中。它不会创建 Plugin 文件、安装 package、修改 `cordis.yml` 或个人/项目配置、跨重启存续,也不存在自动保存、转正式或安装路径。若要保留实验结果,应让 Agent 通过常规开发流程实现普通的本地、项目或仓库 Plugin。
+临时 Plugin 只存在于进程内存中。它不会创建 Plugin 文件、安装 package、修改 `cordis.yml` 或个人/项目配置、跨重启存续,也不存在自动保存、转正式或安装路径。若要保留实验结果,应让 Agent 通过常规开发流程实现普通的项目 Plugin 或可安装的 profile 组合包。
### 通过 provide/inject 实现跨挂载组合
diff --git a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.i18n.yaml b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.i18n.yaml
index 527dd9bead..830b6467af 100644
--- a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md
-2026-07-10-parallel-tool-call-execution.md: 830691b7596accd1c5746fc51e38a9022f09e501
-2026-07-10-parallel-tool-call-execution.zh.md: bd336919adbf9b96f64477c1e24964172cde6d92
+2026-07-10-parallel-tool-call-execution.md: 47a95e88c3d0d8bb9e3ddd820b1e63849effd914
+2026-07-10-parallel-tool-call-execution.zh.md: 024e5d4e4cdbf9d1d980bc67db5969fb34941752
diff --git a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md
index 830691b759..47a95e88c3 100644
--- a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md
+++ b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md
@@ -60,7 +60,7 @@ Any shared state touched during execution must be concurrency-safe. This include
`maxParallelToolCalls` is a positive AgentLoop deployment cap shared by every agent the factory creates. It defaults to `10`; `1` preserves serial execution. Exact fields and defaults live in the generated [configuration catalog](../../../../docs/config-catalog.md).
-The shipped declarations are conservative. Web search, web fetch, and filesystem read opt in. Filesystem writes and edits, bash tools, subagent delegation, workflow, user interaction, todo mutation, Code Mode, and Cordis mutation tools remain exclusive. A subagent may share its parent's workspace or external resources, and the unary classifier cannot prove that sibling delegations have disjoint effects. Bash has no proven input-sensitive classifier and remains exclusive.
+The shipped declarations are conservative. Web search, web fetch, filesystem read, the session-query trace/read tools, and subagent delegation opt in — delegation because a child works in its own session and its run never mutates the parent session, with sibling workspace coordination owned by the model ([parallel subagent Agent Note](2026-08-09-parallel-subagent-delegations.md)). Filesystem writes and edits, bash tools, the session-query search tools, workflow, user interaction, todo mutation, Code Mode, and Cordis mutation tools remain exclusive. Bash has no proven input-sensitive classifier and remains exclusive.
Filesystem read relies on a narrow recorder exception: its synchronous observation updates may settle out of order, but write and edit re-check the observed version before mutation, so stale state only produces `FS_STALE_VERSION`.
diff --git a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md
index bd336919ad..024e5d4e4c 100644
--- a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md
@@ -60,7 +60,7 @@ Code Mode 仍不使用此调度器,因为模型只会发出一个原生 `run_c
`maxParallelToolCalls` 是 AgentLoop 的正整数部署上限,由工厂创建的所有 agent(智能体)共享。默认值为 `10`;`1` 保持串行执行。字段和默认值的精确定义见生成的[配置目录](../../../../docs/config-catalog.md)。
-当前实现中的声明保持保守。Web 搜索、Web 获取和文件系统读取选择并行。文件系统写入与编辑、bash 工具、subagent 委派、工作流、用户交互、todo 变更、Code Mode 以及 Cordis 变更工具仍按独占方式执行。subagent 可能共享父级的工作区或外部资源,而一元分类器无法证明并列委派的作用互不重叠。Bash 没有已证明的输入敏感分类器,因此仍按独占方式执行。
+当前实现中的声明保持保守。Web 搜索、Web 获取、文件系统读取、会话查询的 trace/read 工具和 subagent 委派选择并行;委派之所以并行,是因为子 agent 在自己的会话中工作,其运行绝不变更父会话,并列委派间的工作区协调由模型负责([并行 subagent Agent Note](2026-08-09-parallel-subagent-delegations.md))。文件系统写入与编辑、bash 工具、会话查询的 search 工具、工作流、用户交互、todo 变更、Code Mode 以及 Cordis 变更工具仍按独占方式执行。Bash 没有已证明的输入敏感分类器,因此仍按独占方式执行。
文件系统读取依赖一个范围很窄的记录器例外:其同步观察更新可以不按顺序结算,但写入和编辑在变更前会重新检查已观察的版本,因此陈旧状态只会导致 `FS_STALE_VERSION`。
diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml
index 445f6548b3..9d13851a0d 100644
--- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md
-2026-07-20-dsh-cli-personal-config.md: f5207c5ffbd963b9b7c4a7166fa9f17a460707a9
-2026-07-20-dsh-cli-personal-config.zh.md: 24478a4b4fd5878032bf80f5b30ab9e2008da785
+2026-07-20-dsh-cli-personal-config.md: ed04725e92848bbab550a27ef2f4c021536f765e
+2026-07-20-dsh-cli-personal-config.zh.md: cc97987f803f7fb513e94ce0ce079558f5e3dc75
diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md
index f5207c5ffb..ed04725e92 100644
--- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md
+++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.md
@@ -14,12 +14,12 @@ The entry modes and the personal file's name and location below are superseded b
Two coupled pieces, aligned with the `apps/` assembly tier proposed by the `dsh web` PR (#443):
-**The `dsh` CLI (`apps/cli`, npm name `@deepseek-ai/dsh`).** `apps/*` is the product-assembly tier over `packages/*` libraries. One bin dispatches the default interactive TUI, `-p`/`--prompt` headless turns, and the `web` surface. The TUI boots `examples/tui-agent/cordis.yml` (or `--config`) with the invoking directory as the workspace. The committed `bin/dsh` launcher resolves the checkout through its own real path and runs the app with tsx's ESM hook; the [source-launch decision](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md) owns that contract. `pnpm run demo:tui` runs the same entry.
+**The `dsh` CLI (`apps/cli`, npm name `@deepseek-ai/dsh`).** `apps/*` is the product-assembly tier over `packages/*` libraries. One bin dispatches the default interactive TUI, `-p`/`--prompt` headless turns, and the `web` surface. The TUI boots `examples/tui-agent/cordis.yml` (or `--config`) with the invoking directory as the workspace. From a source checkout, the root `pnpm dsh` script builds the repository and runs the same entry with tsx's ESM hook; the [source-launch decision](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md) owns that contract.
**Personal config (`dsh-app-boot`).** The personal overlay lives in the Harness home — `$DSH_HOME`, else `~/.dsh` — resolved by the shared [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md) (`@deepseek-ai/dsh-paths`), the same single root skills and AGENTS.md resolve against. The dsh TUI, Web, and headless surfaces consume its two optional files; the demo bins boot their committed trees verbatim:
- `.env` — loaded after the invoking directory's `.env`; `process.loadEnvFile` never overrides, so precedence is ambient > project `.env` > personal `.env`.
-- `config.yaml` — a top-level YAML array of `@cordisjs/plugin-include` `PatchOptions`, parsed with the include's own `!!js` dialect (`loadPersonalPatches`) and passed to `boot()`, which forwards it as the root include's `patches`. Patch semantics match the shipped surface overlays: an id-targeted patch replaces the named entry's whole `config`, `insert` appends entries, and an unmatched id is a silent no-op. The [repository Plugin integration](2026-07-30-config-only-repository-plugins.md) uses one shipped row to make an exact GitHub source list a config-only choice.
+- `config.yaml` — a top-level YAML array of `@cordisjs/plugin-include` `PatchOptions`, parsed with the include's own `!!js` dialect (`loadPersonalPatches`) and passed to `boot()`, which forwards it as the root include's `patches`. Patch semantics match the shipped surface overlays: an id-targeted patch replaces the named entry's whole `config`, `insert` appends entries, and an unmatched id is a silent no-op. External packages are installed as [profile bundles](../simplification/2026-08-09-remove-repository-plugin.md); this personal layer configures the Loader rows those bundles contribute.
- A missing file means no overlay; a present-but-unreadable, unparsable, or non-array file throws at boot (misconfiguration fails loud, never a silent skip).
The PTY smoke's launcher isolates `$DSH_HOME` to a per-test directory, exactly as it already isolates `DSH_AGENTS_HOME`, so a developer's real personal overlay cannot leak into fixtures; only the dsh CLI reads personal config, so no other test launcher needed changes.
@@ -40,7 +40,7 @@ The TUI and Web register the exact personal path through Cordis HMR after boot.
## Consequences
-- `dsh` from any directory (and `pnpm run demo:tui`) can apply personal providers, models, repository Plugins, and other Loader entries with no checkout edit; verified end-to-end against a personal Anthropic proxy with Opus 4.8, including a bash tool round trip.
+- An installed `dsh` command can run from any directory, while source users invoke `pnpm dsh` from the checkout; both can apply personal providers, models, installed bundle entries, and other Loader entries with no checkout edit. The behavior was verified end to end against a personal Anthropic proxy with Opus 4.8, including a bash tool round trip.
- Because an id-targeted patch replaces the whole `config`, a personal override restates the base fields it keeps and can drift when the base entry changes shape; the loader's entry-not-found/name-mismatch warnings and [`dsh --dump-config`](../../../../apps/cli/README.md#profiles) (which prints the composed tree those patches produce) are the diagnostics.
- Personal patches resolve ids against the booted file's own tree, so nested-include overlays (Code Mode) are not personalized; live-run parity for those leaves is deferred.
- `dsh-app-boot` depends on `js-yaml` and imports the include's `!!js` YAML dialect (`entryListSchema`) directly, and, like `apps/cli`, depends on `@deepseek-ai/dsh-paths` for `resolveDshHome`.
diff --git a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md
index 24478a4b4f..cc97987f80 100644
--- a/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md
@@ -14,12 +14,12 @@ Status: implemented
两个耦合的部分,与 `dsh web` PR(#443)提出的 `apps/` 装配层对齐:
-**`dsh` CLI(`apps/cli`,npm 名 `@deepseek-ai/dsh`)。** `apps/*` 是位于 `packages/*` 库之上的产品组装层。一个 bin 负责分发默认交互式 TUI、`-p`/`--prompt` 无头轮次和 `web` 界面。TUI 以调用目录为 workspace,启动 `examples/tui-agent/cordis.yml`(或 `--config` 指定的配置)。已提交的 `bin/dsh` 启动器通过自身真实路径解析 checkout,并使用 tsx 的 ESM hook 运行应用;该约定由[源码启动决策](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md)维护。`pnpm run demo:tui` 运行同一入口。
+**`dsh` CLI(命令行界面;`apps/cli`,npm 名 `@deepseek-ai/dsh`)。** `apps/*` 是位于 `packages/*` 库之上的产品组装层。一个 bin 负责分发默认交互式 TUI、`-p`/`--prompt` 无头轮次和 `web` 界面。TUI 以调用目录为 workspace,启动 `examples/tui-agent/cordis.yml`(或 `--config` 指定的配置)。在源码检出中,根目录的 `pnpm dsh` 脚本先构建仓库,再使用 tsx 的 ESM hook 运行同一入口;该约定由[源码启动决策](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md)维护。
**个人配置(`dsh-app-boot`)。** 个人 overlay 存放在 Harness home——`$DSH_HOME`,否则 `~/.dsh`——由共享的 [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md)(`@deepseek-ai/dsh-paths`)解析,与 skills、AGENTS.md 解析所依据的单一根目录相同。dsh 的 TUI、Web 和无头界面使用其中两个可选文件;各示例 bin 仍然逐字节按已提交的配置树启动:
- `.env`——在调用目录的 `.env` 之后加载;`process.loadEnvFile` 从不覆盖已有值,因此优先级为环境变量 > 项目 `.env` > 个人 `.env`。
-- `config.yaml`——顶层 YAML 数组,元素为 `@cordisjs/plugin-include` 的 `PatchOptions`,用 include 自己的 `!!js` 方言解析(`loadPersonalPatches`)并传给 `boot()`,由它作为根 include 的 `patches` 转发。补丁语义与交付的 surface overlay 一致:按 id 定位的补丁替换该配置项的整个 `config`,`insert` 追加配置项,未匹配的 id 静默不执行任何操作。[仓库插件集成](2026-07-30-config-only-repository-plugins.md)通过一个已交付配置项,使精确 GitHub 源列表成为纯配置选择。
+- `config.yaml`——顶层 YAML 数组,元素为 `@cordisjs/plugin-include` 的 `PatchOptions`,用 include 自己的 `!!js` 方言解析(`loadPersonalPatches`)并传给 `boot()`,由它作为根 include 的 `patches` 转发。补丁语义与交付的 surface overlay 一致:按 id 定位的补丁替换该配置项的整个 `config`,`insert` 追加配置项,未匹配的 id 静默不执行任何操作。外部包作为 [profile 组合包](../simplification/2026-08-09-remove-repository-plugin.md)安装;这个个人层负责配置这些组合包提供的 Loader 配置项。
- 文件缺失即无 overlay;文件存在但不可读、不可解析或非数组则在启动时抛出(配置错误响亮失败,绝不静默跳过)。
PTY 冒烟测试的启动器把 `$DSH_HOME` 隔离到每个测试自己的目录,与它已有的 `DSH_AGENTS_HOME` 隔离方式完全一致,开发者真实的个人 overlay 不可能泄漏进 fixture;只有 dsh CLI 读取个人配置,因此其他测试启动器无需改动。
@@ -40,7 +40,7 @@ TUI 和 Web 启动后通过 Cordis HMR(热模块替换)注册确切的个人
## Consequences
-- 在任意目录运行 `dsh`(以及 `pnpm run demo:tui`),无需修改 checkout,即可应用个人提供方、模型、仓库插件和其他 Loader 配置项;已针对个人 Anthropic 代理与 Opus 4.8 端到端验证,包括一次 bash 工具往返。
+- 已安装的 `dsh` 命令可从任意目录运行,源码用户则从 checkout 调用 `pnpm dsh`;两者都无需修改 checkout 即可应用个人提供方、模型、已安装组合包的配置项和其他 Loader 配置项。该行为已针对个人 Anthropic 代理与 Opus 4.8 端到端验证,包括一次 bash 工具往返。
- 由于按 id 定位的补丁替换整个 `config`,个人覆盖必须复述它保留的基础字段,并可能随基础配置项形态变化而漂移;诊断手段是 loader 的「配置项未找到/名称不匹配」警告和 [`dsh --dump-config`](../../../../apps/cli/README.md#profiles)(打印这些补丁合成出的配置树)。
- 个人补丁只在被启动文件自身的树里解析 id,因此嵌套 include 的 overlay(Code Mode)不会被个性化;这些叶子的实际运行等价性暂缓。
- `dsh-app-boot` 依赖 `js-yaml`,并直接导入 include 的 `!!js` YAML 方言(`entryListSchema`);与 `apps/cli` 一样依赖 `@deepseek-ai/dsh-paths` 以获取 `resolveDshHome`。
diff --git a/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.i18n.yaml b/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.i18n.yaml
index 0308005515..dbfaaad95a 100644
--- a/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md
-2026-07-25-subagent-policy-inheritance.md: aeff83795eedead9c75de6bbb74c1da1945092ca
-2026-07-25-subagent-policy-inheritance.zh.md: c26e6bf8b79c86855022c384673957fe04ff761d
+2026-07-25-subagent-policy-inheritance.md: 34751a4e29e48c84d37425857b8b1b56c8d866eb
+2026-07-25-subagent-policy-inheritance.zh.md: 5fa8edf04ed63da9b2e1b9a062ca2f649c8c96fb
diff --git a/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md b/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md
index aeff83795e..34751a4e29 100644
--- a/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md
+++ b/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md
@@ -1,4 +1,4 @@
-# Agent Note: In-process subagent policy inheritance — the child starts under the parent's sandbox and approval overrides
+# Agent Note: In-process subagent policy inheritance — the child starts under the parent's sandbox override
Status: implemented
@@ -6,11 +6,11 @@ English | [中文](2026-07-25-subagent-policy-inheritance.zh.md)
## Problem
-Sandbox and approval overrides are per-session log folds. An in-process subagent gets a new session, so a spawn child once fell back to deployment defaults and a fork child saw only switches inside its completed-turn prefix. Delegation could therefore widen a parent that had switched to `read-only`, or turn a parent's unattended `'never'` approval stance back into prompting behavior.
+Sandbox and approval overrides are per-session log folds. An in-process subagent gets a new session, so a spawn child once fell back to deployment defaults and a fork child saw only switches inside its completed-turn prefix. Delegation could therefore widen a parent that had switched to `read-only`.
## Decision
-The shared in-process driver snapshots `sandboxPolicy.overrideOf(parent.session)` and `approval.overrideOf(parent.session)` before its first await. A later parent switch belongs to the parent's future; cancel-and-redelegate takes a new snapshot. Both services are optional, and only explicit session overrides are copied, never deployment defaults or one-shot grants.
+The delegation boundary snapshots `sandboxPolicy.overrideOf(parent.session)` before its first await, through the shared child-agent helpers (`captureDelegatedPolicyOverrides`/`appendDelegatedPolicyOverrides` in `dsh-subagent`), which the one-shot driver and the [continuable start](2026-08-10-continuable-subagent-policy-inheritance.md) both call. A later parent switch belongs to the parent's future; cancel-and-redelegate takes a new snapshot. The sandbox-policy service is optional, and only the explicit session override is copied, never deployment defaults or one-shot grants. The approval policy is not inherited: the same capture pins every child to `'never'` — the [approvals-pinned decision](2026-08-10-subagent-approval-pinned-never.md) supersedes this note's original approval-override inheritance.
Each captured value becomes a source-tagged `sandbox/mode` or `approval/policy` event appended during the child factory's unpublished setup. The session constructor has already fixed `Session.firstLiveSeq` at the fork-prefix length, so the inherited facts follow fork history, reach telemetry when the child is announced, and leave `SessionHeader.seedLength` at the prefix length. Existing last-event-wins folds therefore make the delegation snapshot beat stale fork history and let a later child switch beat the snapshot. A grandchild folds its parent's logged state, so the rule composes without another inheritance mechanism.
@@ -18,7 +18,7 @@ Ordinary session appends validate the inherited events before publication, and p
### What a blocked child experiences
-A confined child gets the ordinary denial marker. No answerer currently owns an in-process child, so an escalation request fails closed and the child reports upward; a controller-owned parent may widen its own session and delegate again. An inherited `'never'` policy tells the child not to request escalation in its first system prompt.
+A confined child gets the ordinary denial marker, and an escalation request is rejected deterministically by the child's pinned `'never'` policy; the `subagent:delegation` runtime-context statement tells the child to report the limitation instead of retrying, and a controller-owned parent may widen its own session and delegate again ([approvals-pinned decision](2026-08-10-subagent-approval-pinned-never.md)).
## Alternatives considered
@@ -27,10 +27,10 @@ A confined child gets the ordinary denial marker. No answerer currently owns an
- **A first-prompt listener** — rejected: it introduces listener ordering and a later timing boundary even though the creation transaction already permits log appends before publication.
- **Copying deployment defaults** — rejected: defaults remain operator-owned and may change; an unswitched parent stamps nothing, so its child follows the current deployment.
- **Live resolution walking `parentSession` at each call** — rejected: it breaks the "two sessions never see each other's state" isolation invariant, requires the parent session to stay loaded for the child's lifetime, and makes a mid-run parent switch retroactively change a running child. Snapshot-at-delegation is the semantic: the child keeps the policy it was handed; cancel-and-respawn picks up a tightening.
-- **Forcing `'never'` or routing asks to the root controller** — rejected as inheritance behavior. A forced value forecloses a future child answerer; parent routing needs parent-chain ownership and the spawning `callId`, and remains deferred in [the approval-seam Agent Note](2026-07-06-approval-seam.md).
+- **Forcing `'never'`** — originally rejected here as inheritance behavior because a forced value forecloses a future child answerer; that verdict is reversed by the [approvals-pinned decision](2026-08-10-subagent-approval-pinned-never.md), which owns the current rationale. Routing asks to the root controller needs parent-chain ownership and the spawning `callId`, and remains deferred in [the approval-seam Agent Note](2026-07-06-approval-seam.md).
## Consequences
-- Spawn, fork, and nested in-process children retain a parent's explicit sandbox and approval overrides. The focused suite proves real filesystem denial, stale-fork precedence, delegation-time capture, the live-event boundary, default omission, and context disposal.
+- Spawn, fork, and nested in-process children retain a parent's explicit sandbox override and are pinned to `'never'` approvals. The focused suite proves real filesystem denial, stale-fork precedence, delegation-time capture, the live-event boundary, default omission, and context disposal.
- The keyless headless snapshot is the assembled regression: only the parent is `read-only`, the deployment default is `workspace-write`, and the child's persisted event plus denied disk write both fail if capture is removed.
-- Each delegation adds at most two log-only events. `dsh-subagent-inprocess` has optional peer types for the two policy services; compositions without either service behave unchanged. Out-of-process children retain their own deployment policy, and a running child does not follow later parent switches.
+- Each delegation adds at most two log-only events. `dsh-subagent` owns the optional peer types for the two policy services — its shared helpers hold the `ctx.get` consumption; compositions without either service behave unchanged. Out-of-process children retain their own deployment policy, and a running child does not follow later parent switches.
diff --git a/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md b/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md
index c26e6bf8b7..5fa8edf04e 100644
--- a/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md
@@ -1,4 +1,4 @@
-# Agent Note: 进程内 subagent 策略继承——子 agent 在父级的沙箱与审批覆盖项下启动
+# Agent Note: 进程内 subagent 策略继承——子 agent 在父级的沙箱覆盖项下启动
Status: implemented
@@ -6,11 +6,11 @@ Status: implemented
## 问题
-沙箱与审批覆盖项都是按会话的日志折叠。进程内 subagent 会获得一个新会话,因此 spawn 子 agent(智能体)过去会回退到部署默认值,fork 子 agent 则只能看到其已完成轮次前缀中的切换。因此,委派可能放宽已经切换到 `read-only` 的父级,或让父级无人值守的 `'never'` 审批立场重新变成会发起提示的行为。
+沙箱与审批覆盖项都是按会话的日志折叠。进程内 subagent 会获得一个新会话,因此 spawn 子 agent(智能体)过去会回退到部署默认值,fork 子 agent 则只能看到其已完成轮次前缀中的切换。因此,委派可能放宽已经切换到 `read-only` 的父级。
## 决策
-共享的进程内驱动器在第一次 await 之前对 `sandboxPolicy.overrideOf(parent.session)` 和 `approval.overrideOf(parent.session)` 获取快照。父级后续的切换属于父级的未来;取消后重新委派会取得新快照。这两个服务均为可选,仅复制显式会话覆盖项,绝不复制部署默认值或一次性授权。
+委派边界在第一次 await 之前,经由共享的子 agent 辅助函数(`dsh-subagent` 中的 `captureDelegatedPolicyOverrides`/`appendDelegatedPolicyOverrides`)对 `sandboxPolicy.overrideOf(parent.session)` 获取快照;一次性驱动器与[可继续启动](2026-08-10-continuable-subagent-policy-inheritance.md)都会调用这些辅助函数。父级后续的切换属于父级的未来;取消后重新委派会取得新快照。沙箱策略服务为可选,仅复制显式会话覆盖项,绝不复制部署默认值或一次性授权。审批策略不继承:同一次捕获会把每个子 agent 钉定为 `'never'`——[审批钉定决策](2026-08-10-subagent-approval-pinned-never.md)取代了本 note 原先的审批覆盖项继承。
每个捕获值都会成为子 agent 工厂在未发布设置阶段追加的一条带来源标记的 `sandbox/mode` 或 `approval/policy` 事件。会话构造函数已将 `Session.firstLiveSeq` 固定为 fork 前缀的长度,因此继承事实会排在 fork 历史之后,在子 agent 公布时进入遥测,同时让 `SessionHeader.seedLength` 保持为此前缀的长度。因此,既有的末事件胜出折叠会让委派快照压过陈旧的 fork 历史,并让子 agent 后续的切换压过该快照。孙代 agent 会折叠其父级已记录的状态,因此无需另一套继承机制即可组合此规则。
@@ -18,7 +18,7 @@ Status: implemented
### 被拦住的子 agent 会经历什么
-受限子 agent 会得到普通拒绝标记。目前没有应答器认领进程内子 agent,因此升级请求会以拒绝方式失败,由子 agent 向上汇报;由控制器持有的父 agent 可以放宽自己的会话后重新委派。继承的 `'never'` 策略会在第一份系统提示词中告知子 agent 不要请求升级。
+受限子 agent 会得到普通拒绝标记,升级请求则被子 agent 钉定的 `'never'` 策略确定性拒绝;`subagent:delegation` 运行时上下文声明告知子 agent 上报限制而不是重试,由控制器持有的父 agent 可以放宽自己的会话后重新委派([审批钉定决策](2026-08-10-subagent-approval-pinned-never.md))。
## 考虑过的替代方案
@@ -27,10 +27,10 @@ Status: implemented
- **首个提示词监听器**:不予采纳。尽管创建事务已经允许在发布前追加日志,它仍会引入监听器顺序与更晚的时序边界。
- **复制部署默认值**:不予采纳。默认值仍由运维人员拥有且可能变化;未切换的父级不会记录任何值,因此其子 agent 跟随当前部署。
- **每次调用时沿 `parentSession` 实时解析**:不予采纳。这会打破「两个会话永远看不到彼此状态」的隔离不变量,要求父会话在子 agent 的整个生命周期内保持加载,还会让父级在子 agent 运行途中做的切换追溯性地改变一个正在运行的子 agent。委派时快照才是本设计的语义:子 agent 保持它被交付时的策略;取消后重新 spawn 即可拿到收紧后的策略。
-- **强制使用 `'never'` 或把 ask 路由到根控制器**:不作为继承行为采纳。强制值会排除未来的子 agent 应答器;父级路由需要父链所有权与发起 spawn 的 `callId`,仍按[审批 seam Agent Note](2026-07-06-approval-seam.md) 所述延期。
+- **强制使用 `'never'`**:本 note 当初不作为继承行为采纳,理由是强制值会排除未来的子 agent 应答器;该结论已被[审批钉定决策](2026-08-10-subagent-approval-pinned-never.md)推翻,现行理由归其所有。把 ask 路由到根控制器需要父链所有权与发起 spawn 的 `callId`,仍按[审批 seam Agent Note](2026-07-06-approval-seam.md) 所述延期。
## 后果
-- spawn、fork 和嵌套的进程内子 agent 会保留父级显式的沙箱与审批覆盖项。聚焦测试套件证明真实文件系统拒绝、陈旧 fork 优先级、委派时捕获、实时事件边界、默认值省略与上下文释放。
+- spawn、fork 和嵌套的进程内子 agent 会保留父级显式的沙箱覆盖项,并被钉定为 `'never'` 审批。聚焦测试套件证明真实文件系统拒绝、陈旧 fork 优先级、委派时捕获、实时事件边界、默认值省略与上下文释放。
- 无密钥 headless 快照是组装后应用层面的回归测试:只有父级是 `read-only`,部署默认值是 `workspace-write`;若移除捕获,子 agent 的持久化事件与被拒的磁盘写入这两项检查都会失败。
-- 每次委派最多增加两条仅日志事件。`dsh-subagent-inprocess` 为两个策略服务提供可选 peer 类型;未组合任一服务的组合保持原有行为。进程外子 agent 仍采用自身的部署策略,正在运行的子 agent 不跟随父级后续切换。
+- 每次委派最多增加两条仅日志事件。两个策略服务的可选 peer 类型由 `dsh-subagent` 拥有——其共享辅助函数持有 `ctx.get` 消费;未组合任一服务的组合保持原有行为。进程外子 agent 仍采用自身的部署策略,正在运行的子 agent 不跟随父级后续切换。
diff --git a/.agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.i18n.yaml b/.agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.i18n.yaml
index b73a552168..af4c04bc0e 100644
--- a/.agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.md
-2026-07-27-trajectory-inspection-ledger.md: a905e65942365c17b7513028b275288c82428221
-2026-07-27-trajectory-inspection-ledger.zh.md: a8dcfa97a89f3adc6ab540f3d6cc5020cbefb53f
+2026-07-27-trajectory-inspection-ledger.md: 74ed1f8ec6f6efcbf77e9caec7e254cb114efbd9
+2026-07-27-trajectory-inspection-ledger.zh.md: c6bb315b72b8a7274ca0e1b245cb2c5583328c8a
diff --git a/.agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.md b/.agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.md
index a905e65942..74ed1f8ec6 100644
--- a/.agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.md
+++ b/.agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.md
@@ -12,20 +12,20 @@ Trajectory has to make prose, machine payloads, token usage, timing, and nested
**Render a compact, turn-aware event ledger with a local record inspector, using the existing DeepSeek design system.**
-- The ledger keeps session events in sequence within rewind-delimited branches. Turn boundaries use a slightly heavier rule, the raw Turn id, and a continuous left rail; Request boundaries appear as small points integrated into that structure and use one chronological numbering space across ordinary and compaction requests.
+- The ledger keeps materialized business records in Session Event order within the loaded window. Turn boundaries use a slightly heavier rule, the raw Turn id, and a continuous left rail; Request boundaries appear as small points integrated into that structure and use one chronological numbering space across ordinary and compaction requests.
- Event kind and content form the two stable columns. Role tags align toward the content, nested subtools receive a small indentation, and CSS truncation preserves the available preview width. Token usage and duration stay in the inspector.
- Product prose uses the existing sans stack. Turn ids, token counts, durations, tool calls, raw payloads, and other machine data use the existing code stack.
- Existing theme tokens own both light and dark rendering. Neutral borders and surfaces form the structure; distinct low-emphasis role hues support scanning without carrying success or failure meaning, while business blue identifies selection, links, and focus.
-- The client runtime exposes a read-only history source independent from Session and SessionManager. Each activated source owns its raw entries, paging, live gap repair, and reconnect rebuild; the ordinary conversation snapshot remains the folded Chat projection. Trajectory opens the source's tail while mounted and requests one older page when the user reaches the loaded range's top, then lazily derives event order, context lineage, schema index, and Requests instead of imposing those structures on every conversation consumer.
+- Session owns one contiguous Event window, paging state, live gap repair, and reconnect rebuild. Chat and Trajectory register separate business Definitions against the shared `ConversationNodeAssembler`; Trajectory reads its target snapshot from `Session.views` and requests one older Session page when the user reaches the loaded range's top. The [Trajectory Context assembly decision](../architecture/2026-08-11-trajectory-conversation-context-assembly.md) owns its exact-ID Definitions, stage Builder, and complexity bounds.
- Ordinary generation and compaction calls form one chronological Request projection, distinguished by purpose rather than separate collections. Effective prompt state and its change ride the Request that introduced them; compaction and prompt changes are not independent inspection entities. Request numbering and cumulative usage cover the loaded history window and expand as older pages arrive.
- Call schemas come from the active recorded Request header. Keyless snapshot fixtures deliberately replace that catalog with the non-array `{{tools}}` token, which the durable inspection boundary treats as unavailable instead of attempting to project or fabricate schemas.
- Selecting a record or Request opens an inspector inside Trajectory. Tabs and Summary sections follow the selected entity: Markdown messages expose rendered content, source fields, provider/model fields, and hierarchy views; tools add JSON payload/result and schema views; Requests add options, usage, timing, and result navigation. Scrollable Summary regions keep their scrollbar thumbs transparent until hover or `focus-within`, while retaining the scrollbar reservation and scroll behavior. Images render as media rather than serialized data.
- Turn folding removes all rows after its first record and replaces them with a compact step/tool-call count; Assistant folding applies the same interaction to its tool-call descendants. Global controls fold or expand both levels.
-- A long ledger initially positions the loaded tail at the bottom and mounts only the viewport's row window plus bounded overscan. Request-only separators join the next measurable virtual item, with a terminal separator retaining its own fixed clearance, so the virtualizer never owns a zero-height item. Semantic DOM-safe row keys and ARIA indexes expose identity independently from mount position. A tail with known older history virtualizes immediately even when its loaded projection is below the ordinary row threshold. Stable-key virtualizer anchoring preserves the visible item across prepends and appends; the manual scroll-height fallback applies only when completing pagination disables virtualization. Selection, timeline focus, folding, search, and bottom following address records by stable event or tool-call identity rather than requiring their DOM rows to exist. An explicit loading row covers records until initial positioning finishes and while an older page is pending. The raw window base sequence detects a prepend even when a page adds no surface-visible node.
-- The separate Waterfall tab is removed. A fixed Overview above the ledger projects every loaded record with known `startedAt` onto three semantic timing lanes using its own duration. While an older prefix remains unloaded and the viewport includes the loaded domain's start, a neutral ellipsis control covers the truncated edge and loads one earlier page without assigning unknown history a fabricated duration; hovering that control suppresses the ordinary timeline cursor. Finalized Assistant spans divide the recorded interval at the first non-empty token delta, so distinct TTFT and decoding colors retain their actual ratio; incomplete timing falls back to one Assistant color. Hovering for 500 ms exposes exact start/end, total duration, TTFT, and decoding time without relying on the browser's native tooltip delay. Dragging left or right commits an inclusive interval filter: any record whose active interval overlaps either boundary remains visible, records without known timing leave the focused ledger, and clearing the selection restores the full branch. Wheel gestures zoom the time domain. A right-button click clears the interval selection; dragging instead pans an already zoomed viewport without mutating it. The Overview keeps the full time domain while focused so the selection can be resized or cleared without losing orientation.
+- A long ledger initially positions the loaded tail at the bottom and mounts only the viewport's row window plus bounded overscan. Request-only separators join the next measurable virtual item, with a terminal separator retaining its own fixed clearance, so the virtualizer never owns a zero-height item. Semantic DOM-safe row keys and ARIA indexes expose identity independently from mount position. A tail with known older history virtualizes immediately even when its loaded projection is below the ordinary row threshold. Stable-key virtualizer anchoring preserves the visible item across prepends and appends; the manual scroll-height fallback applies only when completing pagination disables virtualization. Selection, timeline focus, folding, search, and bottom following address records by stable event or tool-call identity rather than requiring their DOM rows to exist. An explicit loading row covers records until initial positioning finishes and while an older Session page is pending.
+- The separate Waterfall tab is removed. A fixed Overview above the ledger projects every loaded record with known `startedAt` onto three semantic timing lanes using its own duration. While an older prefix remains unloaded and the viewport includes the loaded domain's start, a neutral ellipsis control covers the truncated edge and loads one earlier page without assigning unknown history a fabricated duration; hovering that control suppresses the ordinary timeline cursor. Finalized Assistant spans divide the recorded interval at the first non-empty token delta, so distinct TTFT and decoding colors retain their actual ratio; incomplete timing falls back to one Assistant color. Hovering for 500 ms exposes exact start/end, total duration, TTFT, and decoding time without relying on the browser's native tooltip delay. Dragging left or right commits an inclusive interval filter: any record whose active interval overlaps either boundary remains visible, records without known timing leave the focused ledger, and clearing the selection restores the full loaded ledger. Wheel gestures zoom the time domain. A right-button click clears the interval selection; dragging instead pans an already zoomed viewport without mutating it. The Overview keeps the full time domain while focused so the selection can be resized or cleared without losing orientation.
- Live history updates retain the ledger's bottom position only while the user is already following its tail. Scrolling upward clears that follow state, so streamed chunks and newly appended records do not interrupt inspection of earlier rows. Tail following and virtualizer measurement react to row keys and heights rather than content identity, so text-only stream frames neither discard the measurement cache nor repeat a DOM scroll write.
-- Token streaming reuses the finalized history inspection, layout, Request numbering, Overview projection, and search results. A frame appends only the current partial Assistant cells and searches that partial when a query is active; text and reasoning deltas do not re-fold or rescan the loaded prefix, while message completion, tool lifecycle, compaction, rewrites, and other structural events rebuild the affected projections. Before those rebuilds, the inspection ledger drops completed-step token payloads that no projection reads while retaining the first visible token for timing, every usage chunk for accounting, and every chunk from unfinished or interrupted steps; the independent history source retains the raw entries.
-- History folding rebases only the loaded surface events into a compact contiguous input for the canonical surface manager, then maps its nodes back to absolute session sequences. Structural events therefore retain canonical replacement validation without replaying token chunks or materializing synthetic events for unloaded sequences.
+- Token streaming updates only the matching Trajectory Assistant Context, while publication is coalesced to at most once per animation frame. The target snapshot preserves the existing stage, layout, Request numbering, Overview, and search inputs; completed Assistant State retains assembled blocks, timing, and usage rather than every raw chunk payload, while Session keeps the raw Event window.
+- Each Trajectory Definition extracts a stable ID from the current Event, and the shared Assembler replays only Contexts affected by matching, Location, or Reader changes. Older Session pages prepend into the same engine window; the Trajectory target builder converts its materialized Nodes into the existing stage-oriented snapshot consumed by the ledger.
- Trajectory opts into a conversation-owned composer overlay through `data-conversation-composer-overlay`. `ConversationRoot` positions the composer seat and publishes its live height; Trajectory keeps the ledger at full height and reserves that height plus 16 px inside its vertical table and inspector scrollers. Those panes adapt to the available width instead of exposing horizontal scrollbars beneath the overlay.
- This local inspector remains independent from the conversation-wide Chat details column. At narrow widths it overlays the ledger and remains dismissible by keyboard or pointer.
@@ -53,4 +53,4 @@ Trajectory has to make prose, machine payloads, token usage, timing, and nested
## Consequences
-Trajectory shows more useful records per viewport while retaining Turn and Request orientation. Context rewrites and compactions remain inline with their surrounding history, while a rewind begins a successor branch that inherits only the retained prefix. The floating composer leaves the ledger visible to the viewport edge without covering its final rows or hiding horizontal controls. The main ledger omits token usage and duration so content receives the available width; the local inspector exposes those facts together with full payloads, provider/model and source fields, schemas, and request timing. The Overview uses recorded start/duration and token-boundary facts without fabricating live elapsed time, and its inclusive focus behavior matches the interaction users already know from Chrome DevTools Network. Tail-first paging bounds initial transport and projection work, virtualization bounds mounted row elements, incremental partial projection removes loaded-history length from ordinary token-frame work, and completed-step chunk compaction makes structural rebuilds proportional to inspection-relevant entries rather than the raw token count. Focused component tests pin tail-first paging, prepend anchoring and identity retention, the virtual window, tail following, content-only streaming without repeated scroll writes, streaming structural sharing, high-sequence window folding, timing projection, delayed detail disclosure, folding, record and interval selection, entity-specific tabs, and running/error semantics. A real-browser long-ledger contract pins stable prepend geometry, bounded mounting, top/middle/bottom reachability, and bounded scroll writes across a paced stream; the assembled Web snapshot pins the ledger, Overview timing details, composer overlay geometry, and inspector through the real client composition.
+Trajectory shows more useful records per viewport while retaining Turn and Request orientation. Context rewrites and compactions appear as the current materialized business records in sequence with surrounding history. The floating composer leaves the ledger visible to the viewport edge without covering its final rows or hiding horizontal controls. The main ledger omits token usage and duration so content receives the available width; the local inspector exposes those facts together with full payloads, provider/model and source fields, schemas, and request timing. The Overview uses recorded start/duration and token-boundary facts without fabricating live elapsed time, and its inclusive focus behavior matches the interaction users already know from Chrome DevTools Network. Tail-first paging bounds initial transport work, virtualization bounds mounted row elements, exact-ID dispatch avoids re-folding unrelated business Contexts, and animation-frame publication caps streaming snapshot frequency. The retained stage-oriented target builder may still perform work proportional to the loaded materialized Nodes for a publication; this migration does not add a stronger Trajectory-specific complexity guarantee. Focused component tests pin tail-first paging, prepend anchoring and identity retention, the virtual window, tail following, content-only streaming without repeated scroll writes, timing projection, delayed detail disclosure, folding, record and interval selection, entity-specific tabs, and running/error semantics. A real-browser long-ledger contract pins stable prepend geometry, bounded mounting, top/middle/bottom reachability, and bounded scroll writes across a paced stream; the assembled Web snapshot pins the ledger, Overview timing details, composer overlay geometry, and inspector through the real client composition.
diff --git a/.agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.zh.md b/.agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.zh.md
index a8dcfa97a8..c6bb315b72 100644
--- a/.agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.zh.md
@@ -12,20 +12,20 @@ Status: implemented
**使用现有 DeepSeek 设计系统,渲染保留轮次结构的紧凑事件记录表,并提供局部记录检查器。**
-- 记录表在以 `rewind` 划分的分支内按会话事件顺序展示。轮次边界由稍粗的分割线、原始轮次 id 和连续的左侧竖线表示;请求边界以融入该结构的小圆点表示,普通请求与压缩(compaction)请求在整个时间序列中共用一套编号。
+- 记录表在已加载窗口内按 Session Event 顺序展示物化后的业务记录。轮次边界由稍粗的分割线、原始轮次 id 和连续的左侧竖线表示;请求边界以融入该结构的小圆点表示,普通请求与压缩(compaction)请求在整个时间序列中共用一套编号。
- 事件类型与内容构成两个稳定列。角色标签朝内容侧对齐,嵌套子工具略微缩进,内容预览使用 CSS 截断以适应可用宽度。token 用量和耗时留在检查器中。
- 产品正文使用现有无衬线字体栈。轮次 id、token 数、耗时、工具调用、原始载荷和其他机器数据使用现有代码字体栈。
- 现有主题 token 同时负责亮色和暗色渲染。中性边框与表面构成整体结构;区分度较低的角色色帮助扫读而不表达成功或失败语义,业务蓝色则标识选择状态、链接和焦点。
-- 客户端运行时提供独立于 Session 和 SessionManager 的只读历史数据源。每个已激活的数据源自行拥有原始条目、分页、实时缺口修复和重连重建;普通会话快照仍然只是 Chat 所需的折叠投影。Trajectory 在挂载期间打开该数据源的尾部,当用户到达已加载范围顶部时请求一页更早的历史,再按需派生事件顺序、上下文谱系、schema 索引和请求,避免让所有会话消费方承担这些结构。
+- Session 统一拥有一份连续 Event 窗口、分页状态、实时缺口修复与重连重建。Chat 与 Trajectory 针对共享的 `ConversationNodeAssembler` 分别注册业务 Definition;Trajectory 从 `Session.views` 读取自己的 target snapshot,并在用户到达已加载范围顶部时请求一页更早的 Session 历史。[Trajectory Context 组装决策](../architecture/2026-08-11-trajectory-conversation-context-assembly.md)负责其精确 ID Definition、stage Builder 与复杂度上界。
- 普通生成调用与压缩调用形成一条按时间排序的请求投影,以用途区分而不是放入不同集合。生效的提示词状态及其变化附着在引入它们的请求上;压缩和提示词变化都不是独立检查实体。请求编号和累计用量覆盖已加载的历史窗口,并随更早页面到达而扩展。
- 调用 schema 来自当前生效且已记录的请求头。无密钥快照 fixture(测试前置数据)有意将该目录替换为非数组 token `{{tools}}`,持久化检查边界会将其视为不可用,而不是尝试投影或虚构 schema。
- 选择记录或请求后,Trajectory 内部会打开检查器,其标签页和概述区域随实体类型变化:Markdown 消息提供渲染内容、来源字段、提供方/模型字段和层级视图;工具提供 JSON 载荷/结果和 schema 视图;请求提供选项、用量、计时和结果跳转。可滚动的概述区域默认保持滚动条滑块透明,直到悬停或 `focus-within` 时才显示,同时保留滚动条预留空间和滚动行为。图片以媒体形式渲染,而不是显示为序列化数据。
- 折叠轮次时保留其第一条记录,并用紧凑的步骤数和工具调用数替换后续所有行;折叠助手时对其工具调用后代应用相同操作。全局控件会折叠或展开这两个层级。
-- 长记录表初始时将已加载尾部置于底部,只挂载视口对应的行窗口及有界的额外缓冲行。仅含请求的分隔行并入下一个具备可测高度的虚拟项,末尾分隔行则保留固定留白,因此虚拟化器不会管理零高度项。可安全用于 DOM 的语义行键与 ARIA 索引使标识不依赖挂载位置。只要已知尾部之前仍有更早历史,即使当前已加载投影低于常规行数阈值,也会立即启用虚拟化。基于稳定键的虚拟化器锚定会在向前补页和尾部追加时保留当前可见项;只有分页完成导致虚拟化停用时,才使用手动滚动高度兜底。选择、时间线聚焦、折叠、搜索和末尾跟随均按稳定的事件或工具调用标识定位,不要求对应 DOM 行已存在。初始定位完成前以及更早页面仍在等待时,明确的加载行会遮住真实记录。原始窗口的基准序号即使在一页未增加任何 surface 可见节点时,也能检测到这次向前补页。
-- 移除独立的 waterfall(瀑布式事件)标签页。固定在记录表上方的 Overview 区域将所有 `startedAt` 已知的已加载记录按各自耗时投影到三条语义计时轨道。仍有更早前缀尚未加载且 viewport 包含已加载时间域起点时,中性的省略号控件会遮住截断边缘并加载一页更早历史,而不会为未知历史虚构耗时;悬停在该控件上会隐藏普通的时间线光标。已完成的助手时间条以首个非空 token 增量为分界,用不同颜色按真实比例表示 TTFT 与解码时间;计时不完整时退化为单一助手色。悬停 500 ms 后会显示精确起止时刻、总耗时、TTFT 和解码时间,而不依赖浏览器原生 tooltip 的延迟。向左或向右拖动会提交包含边界的区间筛选:任何活动区间与所选区间任一边界重叠的记录都会保留,计时未知的记录会从聚焦后的记录表中移除,清除选择则恢复完整分支。滚轮手势用于缩放时间域。右键单击会清除区间选择;右键拖动则只会平移已放大的 viewport,不会改变该选区。聚焦后,Overview 区域仍保留完整时间范围,以便在不失去方位的情况下调整或清除选择。
+- 长记录表初始时将已加载尾部置于底部,只挂载视口对应的行窗口及有界的额外缓冲行。仅含请求的分隔行并入下一个具备可测高度的虚拟项,末尾分隔行则保留固定留白,因此虚拟化器不会管理零高度项。可安全用于 DOM 的语义行键与 ARIA 索引使标识不依赖挂载位置。只要已知尾部之前仍有更早历史,即使当前已加载投影低于常规行数阈值,也会立即启用虚拟化。基于稳定键的虚拟化器锚定会在向前补页和尾部追加时保留当前可见项;只有分页完成导致虚拟化停用时,才使用手动滚动高度兜底。选择、时间线聚焦、折叠、搜索和末尾跟随均按稳定的事件或工具调用标识定位,不要求对应 DOM 行已存在。初始定位完成前以及更早 Session 页面仍在等待时,明确的加载行会遮住真实记录。
+- 移除独立的 waterfall(瀑布式事件)标签页。固定在记录表上方的 Overview 区域将所有 `startedAt` 已知的已加载记录按各自耗时投影到三条语义计时轨道。仍有更早前缀尚未加载且 viewport 包含已加载时间域起点时,中性的省略号控件会遮住截断边缘并加载一页更早历史,而不会为未知历史虚构耗时;悬停在该控件上会隐藏普通的时间线光标。已完成的助手时间条以首个非空 token 增量为分界,用不同颜色按真实比例表示 TTFT 与解码时间;计时不完整时退化为单一助手色。悬停 500 ms 后会显示精确起止时刻、总耗时、TTFT 和解码时间,而不依赖浏览器原生 tooltip 的延迟。向左或向右拖动会提交包含边界的区间筛选:任何活动区间与所选区间任一边界重叠的记录都会保留,计时未知的记录会从聚焦后的记录表中移除,清除选择则恢复完整的已加载记录表。滚轮手势用于缩放时间域。右键单击会清除区间选择;右键拖动则只会平移已放大的 viewport,不会改变该选区。聚焦后,Overview 区域仍保留完整时间范围,以便在不失去方位的情况下调整或清除选择。
- 实时历史更新仅在用户已经跟随记录表末尾时保留底部位置。向上滚动会清除跟随状态,因此流式分块和新追加的记录不会打断对旧记录的检查。末尾跟随与虚拟化器测量仅响应行键和高度,而非内容标识,因此仅含文本的流式帧既不会丢弃测量缓存,也不会重复执行 DOM 滚动写入。
-- token 流式输出会复用已完成历史的检查结果、布局、请求编号、Overview 投影和搜索结果。每个帧只追加当前未完成助手的单元格,并在查询处于激活状态时搜索这部分内容;文本与推理(reasoning)增量不会重新折叠或扫描已加载前缀,而消息完成、工具生命周期、压缩、`rewrite` 及其他结构事件会重建受影响的投影。在这些投影重建前,检查记录表会丢弃已完成步骤中没有任何投影读取的 token 载荷,但会保留首个可见 token 用于计时、保留所有用量分片用于核算,并保留未完成或中断步骤的所有分片;独立历史数据源仍保留原始条目。
-- 历史折叠只把已加载的 surface 事件重新编号为紧凑连续的输入并交给规范 surface manager,再将其节点映射回会话绝对序号。因此,结构事件会保留规范的替换校验,而无需重放 token 分片,也不会为未加载的序号实体化合成事件。
+- token 流式输出只更新命中的 Trajectory Assistant Context,发布则合并为每个 animation frame 最多一次。target snapshot 继续提供既有 stage、layout、请求编号、Overview 与搜索输入;已完成的 Assistant State 只保留组装后的 blocks、计时与 usage,不保留每条原始 chunk payload,而 Session 继续保存原始 Event 窗口。
+- 每个 Trajectory Definition 都从当前 Event 提取稳定 ID,共享 Assembler 只 replay 因 Match、Location 或 Reader 变化而受影响的 Context。更早 Session 页面 prepend 到同一个引擎窗口;Trajectory target builder 再把已物化 Node 转换为记录表继续消费的 stage-oriented snapshot。
- Trajectory 通过 `data-conversation-composer-overlay` 启用由会话持有的 composer 浮层模式。`ConversationRoot` 负责定位 composer seat 并发布其实时高度;Trajectory 让记录表保持全高,并在记录表与检查器的纵向滚动容器内预留该高度加 16 px。这两个窗格会根据可用宽度自适应,而不会在浮层下方暴露横向滚动条。
- 此局部检查器与会话级 Chat 详情栏相互独立。在窄屏下,检查器会覆盖记录表,并且仍可通过键盘或指针关闭。
@@ -53,4 +53,4 @@ Status: implemented
## 后果
-轨迹视图在保留轮次与请求定位的同时,每个视口可以显示更多有效记录。上下文 `rewrite` 与压缩保持在周边历史中的原始位置,`rewind` 则建立仅继承保留前缀的后继分支。浮动 composer 让记录表一直显示到视口边缘,同时不会遮住最后几行,也不会隐藏横向控件。主记录表省略 token 用量和耗时,让内容获得可用宽度;局部检查器展示这些数据以及完整载荷、提供方/模型字段、来源字段、schema 和请求计时。Overview 区域使用记录的开始时间、耗时与 token 边界数据,而不虚构实时流逝时间,其包含边界的聚焦行为与用户熟悉的 Chrome DevTools Network 交互一致。尾部优先分页限制初始传输和投影工作量,虚拟化限制已挂载的行元素数量,未完成部分的增量投影让普通 token 帧的工作量不再随已加载历史长度增长,而已完成步骤的分片压缩则让结构重建的工作量与检查所需条目数量成正比,而非与原始 token 数量成正比。针对性组件测试锁定尾部优先分页、向前补页锚定与标识保持、虚拟窗口、末尾跟随、仅含内容的流式输出不会重复写入滚动位置、流式输出的结构共享、高序号窗口折叠、计时投影、延迟展示详情、折叠、记录与区间选择、实体特定标签页和运行/错误语义。真实浏览器中的长记录表约定锁定向前补页时稳定的几何位置、有界挂载、顶部/中部/底部可达性,以及按节奏进行的流式输出中有界的滚动写入;组装后的 Web 快照则通过真实客户端组合锁定记录表、Overview 计时详情、composer 浮层几何形状与检查器。
+轨迹视图在保留轮次与请求定位的同时,每个视口可以显示更多有效记录。上下文 `rewrite` 与压缩会作为当前物化的业务记录,按顺序出现在周边历史中。浮动 composer 让记录表一直显示到视口边缘,同时不会遮住最后几行,也不会隐藏横向控件。主记录表省略 token 用量和耗时,让内容获得可用宽度;局部检查器展示这些数据以及完整载荷、提供方/模型字段、来源字段、schema 和请求计时。Overview 区域使用记录的开始时间、耗时与 token 边界数据,而不虚构实时流逝时间,其包含边界的聚焦行为与用户熟悉的 Chrome DevTools Network 交互一致。尾部优先分页限制初始传输工作,虚拟化限制已挂载的行元素数量,精确 ID 分发避免重新 fold 无关业务 Context,animation-frame 发布则限制流式 snapshot 频率。保留的 stage-oriented target builder 在一次发布中仍可能执行与已加载物化 Node 数量成比例的工作;本次迁移不额外承诺更强的 Trajectory 专属复杂度。针对性组件测试锁定尾部优先分页、向前补页锚定与标识保持、虚拟窗口、末尾跟随、仅含内容的流式输出不会重复写入滚动位置、计时投影、延迟展示详情、折叠、记录与区间选择、实体特定标签页和运行/错误语义。真实浏览器中的长记录表约定锁定向前补页时稳定的几何位置、有界挂载、顶部/中部/底部可达性,以及按节奏进行的流式输出中有界的滚动写入;组装后的 Web 快照则通过真实客户端组合锁定记录表、Overview 计时详情、composer 浮层几何形状与检查器。
diff --git a/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml b/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml
index 809e37044f..e0ba016659 100644
--- a/.agents/notes/implemented/feature/2026-07-28-feedback-command.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-28-feedback-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 .agents/notes/implemented/feature/2026-07-28-feedback-command.md
-2026-07-28-feedback-command.md: 3edb29283c289d6d006891a4c19087b01fa8166f
-2026-07-28-feedback-command.zh.md: c2513d2570474cbbaf8d94f87603d8ce10d40c14
+2026-07-28-feedback-command.md: d3b2774e41a82f6edb4303280f813ddbed75ebd1
+2026-07-28-feedback-command.zh.md: 3eeef92f2ed39c9546f013f217dd7f851d30c78c
diff --git a/.agents/notes/implemented/feature/2026-07-28-feedback-command.md b/.agents/notes/implemented/feature/2026-07-28-feedback-command.md
index 3edb29283c..d3b2774e41 100644
--- a/.agents/notes/implemented/feature/2026-07-28-feedback-command.md
+++ b/.agents/notes/implemented/feature/2026-07-28-feedback-command.md
@@ -18,7 +18,7 @@ The package declares the log-only `feedback/record { text }` session event and e
`dsh-commands` still writes its `command/run` / `command/done` lifecycle pair around `/feedback`, but this command sets `recordInput: false`. Its `command/run` therefore carries the command identity and source without `args`; the feedback text exists only in `feedback/record`, while `command/done` carries the acknowledgement outcome. All three records are log-only and non-surface. Their appends enter persistence's ordinary bounded write path; nothing forces a flush, so acknowledgement reports that the feedback is in the log rather than already on disk.
-Capture remains inert for the running agent and model. The optional OTel telemetry package later adds one infrastructure consumer: it observes `feedback/record` as a release trigger in `FEEDBACK_ONLY` mode and as the local-only warning trigger in `DISABLED` mode, without changing the feedback event or command path. See [Feedback-gated session telemetry](2026-08-05-feedback-gated-session-telemetry.md).
+Capture remains inert for the running agent and model. The optional OTel telemetry package later adds one infrastructure consumer: it observes `feedback/record` as a release trigger in `FEEDBACK_ONLY` mode and as the local-only warning trigger in `DISABLED` mode, without changing the feedback event or command path. See [Feedback-gated session telemetry](2026-08-05-feedback-gated-session-telemetry.md) and the [acknowledgement sharing disclosure](2026-08-07-feedback-acknowledgement-sharing-disclosure.md).
### Why feedback owns an event
diff --git a/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md b/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md
index c2513d2570..3eeef92f2e 100644
--- a/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-28-feedback-command.zh.md
@@ -18,7 +18,7 @@ Status: implemented
`dsh-commands` 仍会围绕 `/feedback` 写入 `command/run` / `command/done` 生命周期配对,但该命令设置了 `recordInput: false`。因此,它的 `command/run` 携带命令标识与来源,但不携带 `args`;反馈文本只存在于 `feedback/record` 中,而 `command/done` 携带确认结果。三个记录都仅写入日志且非 surface。它们的追加会进入持久化的常规有界写入路径;没有任何环节强制 flush,因此确认文本报告的是反馈已进入日志,而非已经落盘。
-采集对正在运行的 agent(智能体)与模型仍不产生后续动作。可选的 OTel 遥测包后续增加了一个基础设施消费方:它在 `FEEDBACK_ONLY` 模式下将 `feedback/record` 作为释放触发器,在 `DISABLED` 模式下将其作为仅限本地的警告触发器,且不改变反馈事件或命令路径。见[反馈门控的会话遥测](2026-08-05-feedback-gated-session-telemetry.md)。
+采集对正在运行的 agent(智能体)与模型仍不产生后续动作。可选的 OTel 遥测包后续增加了一个基础设施消费方:它在 `FEEDBACK_ONLY` 模式下将 `feedback/record` 作为释放触发器,在 `DISABLED` 模式下将其作为仅限本地的警告触发器,且不改变反馈事件或命令路径。见[反馈门控的会话遥测](2026-08-05-feedback-gated-session-telemetry.md)与[确认文本中的共享披露](2026-08-07-feedback-acknowledgement-sharing-disclosure.md)。
### 为何反馈拥有自己的事件
diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml
index 42a8e3e6bd..8753c066cf 100644
--- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md
-2026-07-29-persistent-bash-str-replace-editor.md: c4750e30370bfd253064c39cb1adc0f5b2baa60d
-2026-07-29-persistent-bash-str-replace-editor.zh.md: 83159d9792fd9fadaaa342cc289300b35da34e4a
+2026-07-29-persistent-bash-str-replace-editor.md: 2c077a08e6027245779a0db364c83d17a9c74fce
+2026-07-29-persistent-bash-str-replace-editor.zh.md: f642f2100cbc40ddf688400c5e6124ca9a6ff72d
diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md
index c4750e3037..2c077a08e6 100644
--- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md
+++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md
@@ -18,7 +18,7 @@ Some deployments need a one-call Bash schema whose shell state survives across m
Both plugins are included in the Python runtime closure. The persistent Bash closure also includes the PTY service/local backend and the sandbox services required by that backend. Because `node-pty` executes a native `spawn-helper` on macOS, each packaged macOS runtime executable ships with a `-spawn-helper` sibling; Linux uses `forkpty` directly. A pinned `node-pty` patch checks `DSH_NODE_PTY_SPAWN_HELPER` first, so it remains a true override for a current external consumer that supplies a non-sibling helper. When the override is unset, the patch resolves the packaged executable sibling if present and otherwise preserves upstream lookup in ordinary Node runs. The macOS builders fail before publication when the helper is absent or not executable.
-The shipped [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) overlay composes both plugins over the ordinary Web surface for the Claude SWE-compatible RL contract. It pins native tool mode and makes the complete system prompt `DSH_SYSTEM_PROMPT` when set or `You are a helpful software engineer assistant.` otherwise, with no harness identity, source-checkout section, Web orientation, Workspace instructions, or tool-mode guidance. It disables every other model-facing consumer, so the model receives exactly the persistent `bash` and `str_replace_editor` schemas, while the Web host, browser, Workspace, persistence, sandbox, and permission stack remains in place. The local PTY backend resolves the effective session sandbox mode when it creates the shell. While that owner has an open shell or a spawn in progress, a different permission mode is rejected before its session event commits; the editor continues through the Web filesystem sandbox.
+The shipped [`minimal` agent preset](../../../../apps/cli/config/agent-presets/minimal/agent.cordis.yml) composes both plugins for the Claude SWE-compatible RL contract. Its entry-local PTY realm carries the registry, local backend, and persistent Bash tool; the editor registers beside that realm against the host filesystem. The preset fixes the complete system prompt, follows the deployment tool-presentation mode, omits every other model-facing consumer, and leaves browser, Workspace, persistence, sandbox, and permission services on the shared Web host. The local PTY backend resolves the effective session sandbox mode when it creates the shell. While that owner has an open shell or a spawn in progress, a different permission mode is rejected before its session event commits; the editor continues through the Web filesystem sandbox. The [minimal-preset decision](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md) owns this composition boundary.
## Alternatives considered
@@ -32,4 +32,4 @@ The shipped [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.
## Consequences
-Profiles can reproduce an external agent by configuring persona and descriptions while the underlying packages remain general. Persistent Bash requires an owning Agent and real PTY backend. Shell exit, timeout, or cancellation loses state. The editor delegates security and mutation policy to the mounted filesystem stack. The Core Web profile retains Web permissions but must close its persistent shell before changing modes. Runtime-wheel consumers still need no Node installation; Linux wheels contain one executable, while macOS wheels also contain its private native helper.
+Profiles can reproduce an external agent by configuring persona and descriptions while the underlying packages remain general. Persistent Bash requires an owning Agent and real PTY backend. Shell exit, timeout, or cancellation loses state. The editor delegates security and mutation policy to the mounted filesystem stack. A minimal Web agent retains Web permissions but must close its persistent shell before changing modes. Runtime-wheel consumers still need no Node installation; Linux wheels contain one executable, while macOS wheels also contain its private native helper.
diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md
index 83159d9792..f642f2100c 100644
--- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md
@@ -18,7 +18,7 @@ Status: implemented
两个插件都进入 Python runtime 闭包。持久 Bash 的闭包还包含 PTY 服务/本地后端,以及该后端要求的沙箱服务。由于 `node-pty` 在 macOS 上会执行原生 `spawn-helper`,每个打包后的 macOS 运行时可执行文件都会携带一个 `-spawn-helper` 伴随文件;Linux 直接使用 `forkpty`。固定版本的 `node-pty` 补丁会先检查 `DSH_NODE_PTY_SPAWN_HELPER`,因此对当前提供非伴随 helper 的外部消费方而言,该变量仍是真正的覆盖项。未设置该覆盖时,补丁会在打包可执行文件的伴随文件存在时解析它,否则在普通 Node 运行中保留上游查找方式。若 helper 缺失或不可执行,macOS 构建器会在发布前失败。
-已交付的 [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) overlay 会在常规 Web 界面之上组合这两个插件,以满足与 Claude SWE 兼容的 RL 约定。它固定使用原生工具模式;完整的系统提示词在设置 `DSH_SYSTEM_PROMPT` 时采用其值,否则采用 `You are a helpful software engineer assistant.`,且不包含 harness 身份、源码 checkout 提示词段、Web 界面定位、Workspace 指令或工具模式指引。它会禁用其他所有面向模型的消费方,使模型恰好只收到持久 `bash` 和 `str_replace_editor` 两个 schema,同时保留 Web 宿主、浏览器、Workspace、持久化、沙箱与权限栈。本地 PTY 后端会在创建 shell 时解析会话的有效沙箱模式。只要该所有者仍有打开的 shell 或仍在进行中的 spawn,另一种权限模式就会在对应的会话事件提交前遭到拒绝;编辑器则继续经由 Web 文件系统沙箱运行。
+随附的 [`minimal` agent preset](../../../../apps/cli/config/agent-presets/minimal/agent.cordis.yml) 会组合这两个插件,以满足与 Claude SWE 兼容的 RL 约定。其 entry 本地 PTY realm 持有注册表、本地后端和持久 Bash 工具;编辑器在该 realm 旁注册,并使用宿主文件系统。preset 会固定完整系统提示词、跟随部署的工具呈现模式,省略其他所有面向模型的消费方,并将浏览器、Workspace、持久化、沙箱与权限服务留在共享 Web 宿主上。本地 PTY 后端会在创建 shell 时解析会话的有效沙箱模式。只要该所有者仍有打开的 shell 或仍在进行中的 spawn,另一种权限模式就会在对应的会话事件提交前遭到拒绝;编辑器则继续经由 Web 文件系统沙箱运行。这一组合边界由 [minimal-preset 决策](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md)负责说明。
## 考虑过的替代方案
@@ -32,4 +32,4 @@ Status: implemented
## 后果
-Profile 可以通过配置 persona 和描述复现外部 Agent,而底层包保持通用。持久 Bash 需要拥有它的 Agent 与真实 PTY 后端;shell 退出、超时或取消会丢失状态。编辑器把安全与变更策略委托给挂载的文件系统栈。Core Web profile 保留 Web 权限,但必须先关闭持久 shell 才能更改权限模式。运行时 wheel 包的消费方仍无需安装 Node;Linux wheel 包包含一个可执行文件,macOS wheel 包还包含其私有原生 helper。
+Profile 可以通过配置 persona 和描述复现外部 Agent,而底层包保持通用。持久 Bash 需要拥有它的 Agent 与真实 PTY 后端;shell 退出、超时或取消会丢失状态。编辑器把安全与变更策略委托给挂载的文件系统栈。minimal Web agent 保留 Web 权限,但必须先关闭持久 shell 才能更改权限模式。运行时 wheel 包的消费方仍无需安装 Node;Linux wheel 包包含一个可执行文件,macOS wheel 包还包含其私有原生 helper。
diff --git a/.agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.md b/.agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.md
deleted file mode 100644
index 35327a30e0..0000000000
--- a/.agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# Agent Note: Config-only repository Plugins for standalone dsh
-
-Status: implemented
-
-English | [中文](2026-07-30-config-only-repository-plugins.zh.md)
-
-## Problem
-
-A standalone `dsh` user has no developer-owned SDK project whose `package.json`, lockfile, and `cordis.yml` can carry an external Plugin dependency. Requiring an install command or another state file would make “use this repository” a multi-step workflow, while trusted repository code still needs an exact-source, transactional lifecycle owned by the [repository package format](../architecture/2026-08-08-trusted-repository-package-code.md). Long-running TUI and Web processes also need a failed edit to preserve their usable Plugin generation and tell observers why the candidate was rejected.
-
-## Decision
-
-The shipped TUI and Web/headless `cordis.yml` trees contain an empty `repository-plugins` entry. A user changes only `$DSH_HOME/config.yaml`, replacing that entry's config with a `repositories` list. Each item uses `github:owner/repository#` plus an optional `&path:/.../.dsh-plugin`; omission selects `/.dsh-plugin`. An explicit ref is mandatory, paths are absolute within the repository and end in `.dsh-plugin`, and duplicate normalized specifiers reject before installation. There is no marketplace, discovery index, HTTPS URL vocabulary, or implicit latest generation.
-
-`@deepseek-ai/dsh-repository-plugin` validates and normalizes each source, then resolves it through the generic vendored [`RepositoryCache`](../architecture/2026-07-30-package-manager-native-repository-cache.md). The default cache is `$DSH_HOME/cache/repository-plugins`; `cacheDir` is the explicit deployment override. Bundled pnpm selects the configured repository subpackage, installs its dependencies, runs its package-authored `prepack`, and atomically publishes the exact specifier. The selected package's direct development dependency on `@deepseek-ai/dsh-repository-plugin` supplies `dsh-plugin-prepare` through package-local `node_modules/.bin`; the lifecycle invokes it after any package-owned build. The DSH host imports the generated `dsh-plugin.mjs` wrapper and mounts it as a child fiber; that wrapper composes static skill and MCP owners plus an explicit trusted Cordis entry when declared.
-
-## Live update and failure
-
-`dsh-app-boot` mounts the root Include through one helper that retains its exact Loader `Entry`. The TUI and Web register `$DSH_HOME/config.yaml` through Cordis HMR; headless reads the same file at startup without retaining a watcher. A watcher update rebuilds the Include patch list as immutable app-owned patches followed by the newly parsed personal patches, so Web-generated port, session-root, trust, and frontend values survive every personal edit unless a later personal patch deliberately replaces that row.
-
-Cordis serializes and coalesces exact-path changes. Include and Loader reconcile a candidate transactionally: success commits the new source list, while fetch, preparation, wrapper import, format, or child-Plugin failure rejects the candidate and retains or restores the last good tree. HMR normalizes the caught value to `Error`, logs it, and broadcasts the parallel `hmr/config-update-failed(filename, error)` event; observer failures cannot break refresh processing. Repository MCP servers use strict startup, so an initial connection, discovery, or tool-registration failure rejects the candidate and becomes a config-update failure; non-strict standalone MCP clients retain their contained successful-Plugin/no-tools behavior.
-
-An identical specifier permanently reuses its cache generation. HMR watches configuration, not cached repository code; the user changes the ref, path, or source list to select another generation.
-
-## Trust boundary
-
-Configuring a repository authorizes package-manager lifecycle code, dependencies, the explicit `dsh.entry`, and spawned MCP servers from that repository to run with the user's filesystem authority. The pnpm child removes ambient environment variables whose names contain `KEY`, `PASSWORD`, `SECRET`, or `TOKEN`, but this is credential-exposure reduction rather than a sandbox. The prepared wrapper validates composition boundaries and lifecycle state; it does not make repository code safe to run when the source is untrusted.
-
-## Alternatives considered
-
-**Require an SDK project dependency.** Rejected for the standalone app path because there is no project manifest to edit. Developer-owned SDK projects keep their native package-manager workflow as a separate capability.
-
-**Add a `dsh plugin install` command and installation database.** Rejected because the personal Loader overlay already owns machine-local composition. A second mutation interface and durable registry would duplicate config identity and rollback.
-
-**Resolve repositories directly in the DSH package.** Rejected because Git transport, GitHub subpackage selection, lifecycle execution, and content storage belong to pnpm and the generic Loader cache, not a DSH-specific adapter.
-
-**Watch cache contents or refresh the same ref automatically.** Rejected because one config value must identify one immutable prepared generation. Background remote resolution would change executable code without a config diff and make rollback depend on mutable remote state.
-
-**Broadcast an `unknown` failure payload.** Rejected at the HMR boundary. JavaScript may throw any value internally, but the public event always receives a normalized `Error`, giving observers one stable contract while retaining the original value as its cause when needed.
-
-## Consequences
-
-- A repository that adds `.dsh-plugin/package.json` can reach standalone users through one personal-config edit without changing its existing skills or `.mcp.json` layout.
-- Long-running apps can add, replace, or remove configured generations without restart; rejected candidates retain the last good runtime and produce one generic Cordis event.
-- First use may require Git/network access and preparation time. Later starts reuse the exact prepared cache; old generations consume disk until a separate cache-management policy exists.
-- Skills and common MCP definitions retain portable static adapters, while an explicit `dsh.entry` can contribute DSH-native Cordis behavior. Format-specific compatibility shims, OAuth-bearing MCP definitions, and marketplaces remain intentionally absent.
-
-## Testing
-
-Repository-package tests pin source normalization, default and nested `.dsh-plugin` paths, cache-root resolution, duplicate rejection, prepared-wrapper loading, and disposal. App-boot tests drive exact-path add, two failure classes, recovery, removal, failure events, and generated-patch preservation through the real HMR/Include/Loader path. A keyless PTY smoke boots the shipped `dsh` composition from personal config alone and invokes a skill from a seeded immutable cache generation.
diff --git a/.agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.zh.md b/.agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.zh.md
deleted file mode 100644
index 5755045560..0000000000
--- a/.agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.zh.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# Agent Note: 仅凭配置为独立 dsh 接入仓库插件
-
-Status: implemented
-
-[English](2026-07-30-config-only-repository-plugins.md) | 中文
-
-## 问题
-
-独立 `dsh` 用户没有开发者自有的 SDK 项目,无法由其 `package.json`、lockfile 和 `cordis.yml` 承载外部插件依赖。若要求运行安装命令或维护另一份状态文件,「使用这个仓库」就会变成多步骤流程;受信任的 repository 代码仍需要由[repository 包格式](../architecture/2026-08-08-trusted-repository-package-code.md)负责一套锁定精确来源且具事务性的生命周期。长时间运行的 TUI 和 Web 进程还必须在编辑失败时保留仍可使用的插件版本,并向观察者说明候选配置被拒绝的原因。
-
-## 决策
-
-已交付的 TUI 和 Web/无头 `cordis.yml` 配置树包含一个空的 `repository-plugins` 配置项。用户只需修改 `$DSH_HOME/config.yaml`,用 `repositories` 列表替换该配置项的配置。每一项采用 `github:owner/repository#`,并可追加 `&path:/.../.dsh-plugin`;省略时选择 `/.dsh-plugin`。必须显式指定 ref;路径是仓库内的绝对路径,并以 `.dsh-plugin` 结尾;重复的规范化说明符在安装前即被拒绝。不提供插件市场、发现索引、HTTPS URL 词汇或隐式的最新版本。
-
-`@deepseek-ai/dsh-repository-plugin` 校验并规范化每个源,再通过 vendor 中的通用 [`RepositoryCache`](../architecture/2026-07-30-package-manager-native-repository-cache.md) 解析。默认缓存位于 `$DSH_HOME/cache/repository-plugins`;`cacheDir` 是显式的部署覆盖项。随应用提供的 pnpm 选择已配置的 repository 子包,安装其依赖,运行包所定义的 `prepack`,并原子发布该精确说明符。所选包对 `@deepseek-ai/dsh-repository-plugin` 的直接开发依赖通过包内 `node_modules/.bin` 提供 `dsh-plugin-prepare`;该生命周期会在任何包自有构建完成后调用它。DSH 宿主会导入生成的 `dsh-plugin.mjs` 包装层并将其挂载为子 fiber;该包装层组合静态 skill(技能)与 MCP 所有者,并在声明时组合显式的受信任 Cordis 入口。
-
-## 实时更新与失败
-
-`dsh-app-boot` 通过一个辅助函数挂载根 Include,并保留其确切的 Loader `Entry`。TUI 和 Web 通过 Cordis HMR(热模块替换)注册 `$DSH_HOME/config.yaml`;无头模式在启动时读取同一文件,但不保留监视器。监视器更新会重新构建 Include 补丁列表,先放置不可变的应用自有补丁,再放置新解析的个人补丁。因此,Web 生成的端口、会话根目录、信任和前端值会在每次个人编辑后保留,除非后续个人补丁有意替换相应配置项。
-
-Cordis 会串行处理并合并该确切路径上的变更。Include 与 Loader 以事务方式协调候选配置:成功时提交新源列表;拉取、准备、包装模块导入、格式或子插件失败时拒绝候选配置,并保留或恢复最后一个可用树。HMR 会把捕获的值规范化为 `Error`,记录错误,并广播并行的 `hmr/config-update-failed(filename, error)` 事件;观察者失败不会中断刷新处理。Repository MCP 服务器采用严格启动,因此初始连接、发现或工具注册失败会拒绝候选配置,并构成配置更新失败;非严格的独立 MCP 客户端仍保留其所收束的「插件成功加载但无工具」行为。
-
-相同说明符会永久复用同一个缓存版本。HMR 监视配置,而非已缓存的仓库代码;用户必须改变 ref、路径或源列表,才能选择另一个版本。
-
-## 信任边界
-
-配置仓库即授权该仓库中的包管理器生命周期代码、依赖、显式 `dsh.entry` 和 spawn 的 MCP server 以用户的文件系统权限运行。pnpm 子进程会移除名称中含有 `KEY`、`PASSWORD`、`SECRET` 或 `TOKEN` 的环境变量,但这只会减少凭据暴露,并非沙箱。已准备的包装层会校验组合边界和生命周期状态;当来源不受信任时,它无法让 repository 代码变得可安全运行。
-
-## 考虑过的替代方案
-
-**要求声明 SDK 项目依赖。** 独立应用路径没有可编辑的项目 manifest(元数据清单),因此否决。开发者自有的 SDK 项目仍可使用原生包管理器工作流,这是一项独立能力。
-
-**新增 `dsh plugin install` 命令和安装数据库。** 否决,因为个人 Loader 覆盖层已经负责机器本地组合。第二个变更接口和持久注册表会重复配置身份与回滚机制。
-
-**由 DSH 包直接解析仓库。** 否决,因为 Git 传输、GitHub 子包选择、生命周期执行和内容存储属于 pnpm 与通用 Loader 缓存,而非 DSH 专用适配器。
-
-**监视缓存内容,或自动刷新相同 ref。** 否决,因为一个配置值必须标识一个不可变的已准备版本。后台远端解析会在没有配置差异的情况下改变可执行代码,并使回滚依赖可变的远端状态。
-
-**广播 `unknown` 失败载荷。** 在 HMR 边界否决。JavaScript 内部可以抛出任意值,但公开事件始终接收规范化的 `Error`,从而为观察者提供稳定约定,并在需要时把原始值保留为错误原因。
-
-## 后果
-
-- 添加 `.dsh-plugin/package.json` 的仓库只需一次个人配置编辑即可供独立用户使用,无需改变现有 skill 或 `.mcp.json` 布局。
-- 长时间运行的应用无需重启即可新增、替换或移除已配置版本;被拒绝的候选配置会保留最后一个可用运行时,并产生一个通用 Cordis 事件。
-- 首次使用可能需要 Git/网络访问和准备时间。后续启动会复用这份精确的已准备缓存;在另行制定缓存管理政策之前,旧版本会持续占用磁盘空间。
-- skill 和通用 MCP 定义保留可移植静态适配器,而显式 `dsh.entry` 可以贡献 DSH 原生 Cordis 行为。格式专用的兼容 shim、带 OAuth 的 MCP 定义和插件市场仍有意不提供。
-
-## 测试
-
-仓库包测试固定源规范化、默认和嵌套 `.dsh-plugin` 路径、缓存根解析、重复项拒绝、已准备包装层加载及资源释放。App-boot 测试通过真实 HMR/Include/Loader 路径驱动确切路径的新增、两类失败、恢复、移除、失败事件及生成补丁保留。一个无密钥 PTY 冒烟测试仅通过个人配置启动已交付的 `dsh` 组合,并从预置的不可变缓存版本中调用一个 skill。
diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml
index 151d74e3dc..41756caeb4 100644
--- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md
-2026-07-31-even-out-shipped-tool-rosters.md: 0195620055da5e570d2f54792d950a88bab8d652
-2026-07-31-even-out-shipped-tool-rosters.zh.md: ab6982e33c4a0a25cbc2fde386456840ce99d9c5
+2026-07-31-even-out-shipped-tool-rosters.md: 7647506e5d9c39d64b686ab18923f9681a48cd87
+2026-07-31-even-out-shipped-tool-rosters.zh.md: f04a59c9f00455b00f70975ad9b6bd4defd3d847
diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md
index 0195620055..7647506e5d 100644
--- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md
+++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md
@@ -12,11 +12,11 @@ The result was a user-visible difference nobody had decided: the same model, ask
## Decision
-The rows that are not surface-specific move into [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-guard`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster: twenty-two tools on every host — the twenty shared rows plus `glob` and `grep`, which are fixed members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../architecture/2026-08-01-packaged-ripgrep-search.md). `tool-session-query` joined and then left again — the [session-search-not-shipped-default decision](2026-08-02-session-search-not-shipped-default.md) keeps the model-facing consumer opt-in — while the rest of this roster stands.
+The rows that are not surface-specific move into [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-guard`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster, including fixed `glob` and `grep` members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../architecture/2026-08-01-packaged-ripgrep-search.md). Two later decisions narrow that roster: the [session-search decision](2026-08-02-session-search-not-shipped-default.md) keeps `tool-session-query` opt-in, and the [single-editor decision](../simplification/2026-08-10-default-presets-single-editor.md) keeps `tool-str-replace-editor` out of the general-purpose presets while retaining it in `minimal`.
Two rows stay surface-specific. `tmux-context` is TUI-only because a browser surface has no terminal multiplexer to describe. `session-reference` is TUI-only because it drives the shared session-query index from the launcher's process-local path, and the browser sidebar reconciles that index on its own first search.
-**This roster decision added only at the time.** No tool row was removed from either surface when it landed, and a catalog comparison found additions and nothing else. One of those additions, `tool-session-query`, was subsequently removed by the [session-search-not-shipped-default decision](2026-08-02-session-search-not-shipped-default.md). The shared executors, sandbox composition, and access default are owned independently by the [workspace-write default decision](2026-07-31-workspace-write-surface-default.md).
+**This roster decision added only at the time.** No tool row was removed from either surface when it landed, and a catalog comparison found additions and nothing else. The later session-search and single-editor decisions own their respective default-roster exceptions. The shared executors, sandbox composition, and access default are owned independently by the [workspace-write default decision](2026-07-31-workspace-write-surface-default.md).
### What stays unmounted, and why
diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md
index ab6982e33c..f04a59c9f0 100644
--- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md
@@ -12,11 +12,11 @@ Status: implemented
## 决策
-那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-guard`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单:每台宿主上都有二十二个工具——二十个共享行加上 `glob` 和 `grep`,它们成为固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.md)。`tool-session-query` 加入后又退出了——[session-search-not-shipped-default 决策](2026-08-02-session-search-not-shipped-default.md)让面向模型的消费方保持需显式启用——而这份清单的其余部分保持不变。
+那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-guard`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单,其中 `glob` 和 `grep` 是固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.md)。之后有两项决策收窄这份清单:[session-search 决策](2026-08-02-session-search-not-shipped-default.md)让 `tool-session-query` 保持需显式启用,[单一编辑器决策](../simplification/2026-08-10-default-presets-single-editor.md)让通用 preset 不提供 `tool-str-replace-editor`,但在 `minimal` 中保留它。
有两行仍是 surface 专属。`tmux-context` 只在 TUI,因为浏览器 surface 没有终端复用器可描述。`session-reference` 只在 TUI,因为它以 launcher 的进程本地路径驱动共享的 session-query 索引,而浏览器侧边栏会在自己的首次搜索里重建该索引。
-**本次工具清单决策当时只做加法。** 落地时两个 surface 均未移除任何工具行,目录对比只发现了新增,别无其他。这些新增中的一项 `tool-session-query` 随后被[session-search-not-shipped-default 决策](2026-08-02-session-search-not-shipped-default.md)移除。共享执行器、沙箱组合与访问默认值独立归属[workspace-write 默认值决策](2026-07-31-workspace-write-surface-default.md)。
+**本次工具清单决策当时只做加法。** 落地时两个 surface 均未移除任何工具行,目录对比只发现了新增,别无其他。后续的 session-search 与单一编辑器决策分别负责对应的默认清单例外。共享执行器、沙箱组合与访问默认值独立归属[workspace-write 默认值决策](2026-07-31-workspace-write-surface-default.md)。
### 什么保持不挂,以及为什么
diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml
index 49b713f534..24219d9ff0 100644
--- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md
-2026-08-01-windows-pwsh-default.md: f0da86e52bcdd53a10b60164d7cc12261cfc5c49
-2026-08-01-windows-pwsh-default.zh.md: 41a6429eab8f86a8960ac4aa372aeacfda4661c4
+2026-08-01-windows-pwsh-default.md: 4e681b32088954d870df86898e26fe2cae669f14
+2026-08-01-windows-pwsh-default.zh.md: a9d600f8a8e47db49c3733f33091e667e341c6a7
diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md
index f0da86e52b..4e681b3208 100644
--- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md
+++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md
@@ -12,9 +12,9 @@ The harness's shipped execution profile is bash-first on every platform. Windows
Windows hosts booting a shipped profile (`dsh web`, `dsh --profile headless`, one-shot tasks) get the PowerShell stack by default; POSIX hosts are unchanged.
-- **The platform layer is a data file, not a roster rewrite.** `@deepseek-ai/dsh-base` ships [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml) alongside its universal `cordis.patch.yml`: it disables `bash-sandbox`/`tool-bash` (the POSIX-only executor and its dialect tool) and inserts `pwsh-local`/`tool-pwsh`. Windows has no OS sandbox runner (landlock/bwrap/seatbelt are POSIX-only), so the layer drops the sandbox stack entirely — `sandbox`, `sandbox-policy`, and `fs-sandbox` are disabled and the unconfined `dsh-fs-local` provides `ctx.fs` — and degrades to danger-full-access: `permission`/`ui-permission` leave the roster (dsh-permission requires a confining executor — presets bundle a sandbox mode the unconfined executor cannot honor; see its constructor guard — and the client knob would advertise a boundary that does not exist), and the `approval` service is disabled — nothing in the Windows roster asks for approval, so the model is never told approval exists or that asks are auto-rejected. Keeping fs-only path rules would be theater: the unconfined shell can bypass them with one command, so the honest Windows posture is full access rather than a boundary only the fs tools pretend to enforce.
-- **The launcher injects the layer by platform.** `apps/cli/src/windows-shell.ts` resolves it from the base bundle layer's `packageDir` between the bundle layers and the user layers on `win32` hosts, in every composition path (boot, config-only HMR recomposition, config dumps). Overriding the shipped default is a composition decision: a Windows host that prefers the bash stack — or confinement — re-enables the bash rows through its profile or home `cordis.patch.yml`. Custom profiles without the base bundle are skipped (they own their shell stack); a base bundle that ships no Windows shell patch fails loud.
-- **Module resolution is restored for cold starts.** The profiles-rework CLI dropped the pwsh packages from `apps/cli`'s dependency closure, so `healProfilesModuleFallback` never linked them into `$DSH_HOME/profiles/node_modules` and a fresh Windows host could not resolve the inserted rows. `apps/cli` and `dsh-base` re-declare `dsh-pwsh-local`/`dsh-tool-pwsh`, and `dsh-base` also declares `dsh-fs-local`; the base bundle lists every row plugin as a dependency by house style.
+- **The platform layer is a data file, not a roster rewrite.** `@deepseek-ai/dsh-base` ships [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml) alongside its universal `cordis.patch.yml`. It disables the POSIX-only `bash-sandbox`/`tool-bash` rows and inserts `pwsh-sandbox`/`tool-pwsh`. The later [Windows ACL sandbox decision](2026-08-08-windows-acl-restricted-token-sandbox.md) filled the win32 runner chain and superseded this note's original unconfined roster: `sandbox`, `sandbox-policy`, `fs-sandbox`, `permission`/`ui-permission`, and `approval` now stay enabled exactly as on POSIX, while the ACL backend truthfully reports its Everyone and hard-link gaps as partial enforcement.
+- **The launcher injects the layer by platform.** `apps/cli/src/windows-shell.ts` resolves it from the base bundle layer's `packageDir` between the bundle layers and the user layers on `win32` hosts, in every composition path (boot, config-only HMR recomposition, config dumps). Overriding the shipped default is a composition decision: a Windows host that prefers the bash stack re-enables the bash rows and disables both pwsh rows through its profile or home `cordis.patch.yml`. Custom profiles without the base bundle are skipped (they own their shell stack); a base bundle that ships no Windows shell patch fails loud.
+- **Module resolution is restored for cold starts.** The profiles-rework CLI dropped the pwsh packages from `apps/cli`'s dependency closure, so `healProfilesModuleFallback` never linked them into `$DSH_HOME/profiles/node_modules` and a fresh Windows host could not resolve the inserted rows. `apps/cli` and `dsh-base` declare `dsh-pwsh-sandbox`/`dsh-tool-pwsh`; the executor's dependency chain supplies `dsh-pwsh-local`, and the base bundle lists every row plugin as a dependency by house style.
The pwsh GUI rendering shipped earlier with the [pwsh UI presentation matches bash decision](2026-08-05-pwsh-ui-bash-parity.md); the [pwsh tool bash parity decision](2026-08-02-pwsh-tool-bash-parity.md) ships the tool's surface. Nothing in this decision changes POSIX behavior.
@@ -24,21 +24,21 @@ The pwsh GUI rendering shipped earlier with the [pwsh UI presentation matches ba
**Ship the platform layer from `apps/cli` code instead of a bundle data file.** Rejected: the patch belongs next to the rows it replaces, in the bundle that owns them, so the shipped roster stays visible as composition data and dumps carry its provenance; the launcher contributes only the win32 gate.
-**Keep `permission`/`ui-permission` on Windows.** Rejected: `dsh-permission` hard-requires `ctx.bash.sandboxMode` and fails loud at load over an unconfined executor; making it tolerate an unconfined shell would advertise presets the shell cannot honor.
+**Keep `permission`/`ui-permission` on Windows without a confining runner.** Rejected by the original delivery: `dsh-permission` hard-requires `ctx.bash.sandboxMode` and fails loud at load over an unconfined executor. The later ACL runner removed that premise, so the current roster retains both rows.
-**Keep fs path-rule confinement on Windows (`sandbox-policy` + `fs-sandbox` without OS runners).** Rejected: the shell is the model's primary tool and unconfined on Windows, so fs-only path rules are trivially bypassable and would overstate the boundary; the honest posture is full degradation to danger-full-access.
+**Keep fs path-rule confinement on Windows without an OS runner.** Rejected by the original delivery: an unconfined shell could bypass fs-only path rules. The current ACL runner confines the shell and the fs provider under one policy, so this rejected half-boundary is no longer the shipped shape.
**Ship a `DSH_WINDOWS_SHELL` environment escape hatch.** Rejected: decisive behavior changes belong in composition config, which already overrides the platform layer row by id; a second override channel would split the single source of truth for roster decisions.
## Consequences
- A Windows host running a shipped `dsh` surface gets `pwsh` as its shell tool and PowerShell as the `ctx.bash` executor without configuration; `bash` is absent from the model-visible roster there (its tool row is disabled).
-- Windows has no sandbox at all: the fs tools run unconfined (`dsh-fs-local`), the approval service is absent (nothing asks for approval, and the model is never told approval exists), and the permission switcher is gone. The model-visible posture is honest full access rather than a boundary the shell can bypass.
+- Windows commands and fs operations share the sandbox policy, permission switcher, and approval service. The ACL runner confines writes but reports `enforcement: 'partial'`; explicit `danger-full-access` remains the approved bypass rather than the platform default.
- POSIX hosts are unchanged: the platform layer never applies, and the bash stack remains the universal `cordis.patch.yml` rows.
-- Windows hosts that prefer the bash stack (e.g. with WSL/Git-Bash on PATH) override the shipped default through their profile or home `cordis.patch.yml` — disabling `pwsh-local`/`tool-pwsh` and re-enabling `bash-sandbox`/`tool-bash` (both executors register the same `bash` service, so an incomplete recipe fails loud at load) — composition config is the one override channel.
+- Windows hosts that prefer the bash stack (e.g. with WSL/Git-Bash on PATH) override the shipped default through their profile or home `cordis.patch.yml` — disabling `pwsh-sandbox`/`tool-pwsh` and re-enabling `bash-sandbox`/`tool-bash` (both executors register the same `bash` service, so an incomplete recipe fails loud at load) — composition config is the one override channel.
## Verification
-- Unit: `apps/cli/tests/windows-shell.spec.ts` pins the win32 default, the custom-profile skip, and the missing-patch failure with the platform injected, and composes the REAL shipped bundle layers (dsh-base + dsh-web-app resolved from the app installation) through the boot's patch algorithm to assert the win32 danger-full-access roster and the base-only-profile warning; `packages/bundle/base/tests/base.spec.ts` pins the shipped Windows patch file shape (disables, inserts, and the absent approval service).
+- Unit: `apps/cli/tests/windows-shell.spec.ts` pins the win32 default, custom-profile skip, missing-patch failure, cold-start dependency closure, and real composed roster; `packages/bundle/base/tests/base.spec.ts` pins that the Windows layer disables only the bash rows, inserts the confined pwsh rows, and leaves sandbox, permission, fs, and approval ownership untouched.
- Keyless: a win32 `dsh --profile --dump-config` shows the pwsh rows with `windows.cordis.patch.yml` provenance and the bash rows disabled; the POSIX dump (CI Linux) is unchanged.
- The real-composition smoke boots the web profile on win32 with the pwsh stack mounted (the exact roster this note describes).
diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md
index 41a6429eab..a9d600f8a8 100644
--- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md
+++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md
@@ -12,9 +12,9 @@ harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机
启动交付 profile(`dsh web`、`dsh --profile headless`、一次性任务)的 Windows 主机默认获得 PowerShell 栈;POSIX 主机不变。
-- **平台层是数据文件,不是清单重写。** `@deepseek-ai/dsh-base` 随通用 `cordis.patch.yml` 一起交付 [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml):它禁用 `bash-sandbox`/`tool-bash`(仅 POSIX 的执行器及其方言工具)并插入 `pwsh-local`/`tool-pwsh`。Windows 上没有 OS 级 sandbox runner(landlock/bwrap/seatbelt 均为 POSIX 专属),因此该层整体移除 sandbox 栈——`sandbox`、`sandbox-policy`、`fs-sandbox` 被禁用,由不限权的 `dsh-fs-local` 提供 `ctx.fs`——并完全退化为 danger-full-access:`permission`/`ui-permission` 离开清单(dsh-permission 要求有限权能力的执行器——preset 捆绑的是无限制执行器无法兑现的 sandbox 模式;见其构造函数守卫——客户端旋钮会宣传一个并不存在的边界),`approval` 服务也被禁用——Windows 清单里没有任何动作需要审批,模型也不会被告知"审批存在"或"请求会被自动拒绝"。保留仅限 fs 的路径规则是摆设:不限权的 shell 一条命令即可绕过,因此诚实的 Windows 姿态是全权访问,而不是一个只有 fs 工具假装执行的边界。
-- **启动器按平台注入该层。** `apps/cli/src/windows-shell.ts` 在 `win32` 主机上从 base bundle 层的 `packageDir` 解析它,置于 bundle 层与用户层之间,覆盖所有组合路径(启动、config-only HMR 重组合、配置转储)。覆盖交付默认是组合决策:偏好 bash 栈(或偏好有限权)的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 重新启用 bash 行。未挂 base bundle 的自定义 profile 被跳过(它们自己拥有 shell 栈);base bundle 缺 `windows.cordis.patch.yml` 时 fail loud。
-- **冷启动的模块解析已恢复。** profiles 重构把 pwsh 包从 `apps/cli` 的依赖闭包中删掉了,`healProfilesModuleFallback` 因此从未把它们链接进 `$DSH_HOME/profiles/node_modules`,新 Windows 主机解析不到插入的行。`apps/cli` 与 `dsh-base` 重新声明 `dsh-pwsh-local`/`dsh-tool-pwsh`,`dsh-base` 还声明 `dsh-fs-local`;按仓库惯例,base bundle 把每个行插件都列为依赖。
+- **平台层是数据文件,不是清单重写。** `@deepseek-ai/dsh-base` 随通用 `cordis.patch.yml` 一起交付 [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml)。它禁用仅限 POSIX 的 `bash-sandbox`/`tool-bash` 行,并插入 `pwsh-sandbox`/`tool-pwsh`。后续的 [Windows ACL 沙箱决策](2026-08-08-windows-acl-restricted-token-sandbox.md)填充了 win32 runner 链,并取代了本笔记最初的不限权清单:`sandbox`、`sandbox-policy`、`fs-sandbox`、`permission`/`ui-permission` 与 `approval` 均与 POSIX 上一样保持启用,而 ACL 后端则如实把 Everyone 与硬链接缺口报告为部分强制执行。
+- **启动器按平台注入该层。** `apps/cli/src/windows-shell.ts` 在 `win32` 主机上从 base bundle 层的 `packageDir` 解析它,置于 bundle 层与用户层之间,覆盖所有组合路径(启动、config-only HMR 重组合、配置转储)。覆盖交付默认是组合决策:偏好 bash 栈的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 重新启用 bash 行,并禁用两个 pwsh 行。未挂 base bundle 的自定义 profile 被跳过(它们自己拥有 shell 栈);base bundle 缺 `windows.cordis.patch.yml` 时 fail loud。
+- **冷启动的模块解析已恢复。** profiles 重构把 pwsh 包从 `apps/cli` 的依赖闭包中删掉了,`healProfilesModuleFallback` 因此从未把它们链接进 `$DSH_HOME/profiles/node_modules`,新 Windows 主机解析不到插入的行。`apps/cli` 与 `dsh-base` 声明 `dsh-pwsh-sandbox`/`dsh-tool-pwsh`;执行器的依赖链提供 `dsh-pwsh-local`,按仓库惯例,base bundle 把每个行插件都列为依赖。
pwsh GUI 渲染已随 [pwsh UI 呈现与 bash 对齐决策](2026-08-05-pwsh-ui-bash-parity.md) 先行交付;[pwsh 工具与 bash 对齐决策](2026-08-02-pwsh-tool-bash-parity.md) 交付了工具表面。本决策不改变任何 POSIX 行为。
@@ -24,21 +24,21 @@ pwsh GUI 渲染已随 [pwsh UI 呈现与 bash 对齐决策](2026-08-05-pwsh-ui-b
**从 `apps/cli` 代码而非 bundle 数据文件交付平台层。** 否决:patch 应放在它替换的行旁边、属于拥有这些行的 bundle,让交付清单作为组合数据保持可见、转储带有出处;启动器只贡献 win32 门控。
-**在 Windows 上保留 `permission`/`ui-permission`。** 否决:`dsh-permission` 硬性要求 `ctx.bash.sandboxMode`,在无限制执行器上加载即 fail loud;让它容忍无限制 shell 会宣传 shell 无法兑现的 preset。
+**在 Windows 没有隔离 runner 时保留 `permission`/`ui-permission`。** 最初交付时否决:`dsh-permission` 硬性要求 `ctx.bash.sandboxMode`,并在不限权执行器上加载时 fail loud。后续的 ACL runner 消除了该前提,因此当前清单保留这两行。
-**在 Windows 上保留 fs 路径规则限制(无 OS runner 的 `sandbox-policy` + `fs-sandbox`)。** 否决:shell 是模型的主工具且在 Windows 上不限权,仅限 fs 的路径规则一行命令即可绕过,会夸大边界;诚实的姿态是完全退化到 danger-full-access。
+**在 Windows 没有 OS runner 时保留 fs 路径规则限制。** 最初交付时否决:不限权 shell 可以绕过仅限 fs 的路径规则。当前 ACL runner 用同一策略约束 shell 与 fs 提供方,因此这项被否决的半边界已不是当前交付形态。
**交付 `DSH_WINDOWS_SHELL` 环境变量逃生门。** 否决:决定性的行为变更应集中在组合配置中,而组合配置已能按行 id 覆盖平台层;第二条覆盖通道会分裂清单决策的单一事实来源。
## 后果
- 运行交付版 `dsh` 表面的 Windows 主机无需配置即获得 `pwsh` 作为 shell 工具、PowerShell 作为 `ctx.bash` 执行器;那里的模型可见清单中没有 `bash`(其工具行被禁用)。
-- Windows 上没有任何沙箱:fs 工具不限权运行(`dsh-fs-local`)、`approval` 服务不存在(没有任何动作需要审批,模型也不会被告知审批存在)、权限切换器消失。模型可见的姿态是诚实的全权访问,而不是一个 shell 可以绕过的边界。
+- Windows 命令与 fs 操作共用沙箱策略、权限切换器和 approval 服务。ACL runner 限制写入,但报告 `enforcement: 'partial'`;显式的 `danger-full-access` 仍是获准的绕过方式,而非平台默认。
- POSIX 主机不变:平台层永不生效,bash 栈仍是通用 `cordis.patch.yml` 的行。
-- 偏好 bash 栈的 Windows 主机(例如 PATH 上有 WSL/Git-Bash 时)通过其 profile 或 home 的 `cordis.patch.yml` 覆盖交付默认——禁用 `pwsh-local`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`(两个执行器注册同一个 `bash` 服务,配方不完整会在加载时 fail loud)——组合配置是唯一的覆盖通道。
+- 偏好 bash 栈的 Windows 主机(例如 PATH 上有 WSL/Git-Bash 时)通过其 profile 或 home 的 `cordis.patch.yml` 覆盖交付默认——禁用 `pwsh-sandbox`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`(两个执行器注册同一个 `bash` 服务,配方不完整会在加载时 fail loud)——组合配置是唯一的覆盖通道。
## 验证
-- 单元:`apps/cli/tests/windows-shell.spec.ts` 以平台注入固定 win32 默认、自定义 profile 跳过与缺文件失败,并通过启动所用的 patch 算法组合真实交付的 bundle 层(从应用安装解析的 dsh-base + dsh-web-app)断言 win32 danger-full-access 清单与 base-only profile 警告;`packages/bundle/base/tests/base.spec.ts` 固定交付的 Windows patch 文件形状(禁用、插入与缺席的 approval 服务)。
+- 单元:`apps/cli/tests/windows-shell.spec.ts` 固定 win32 默认、自定义 profile 跳过、缺少 patch 时失败、冷启动依赖闭包和真实组合清单;`packages/bundle/base/tests/base.spec.ts` 固定 Windows 层仅禁用 bash 行、插入受限的 pwsh 行,并且不改变沙箱、权限、fs 与审批的归属。
- Keyless:win32 上的 `dsh --profile --dump-config` 显示带 `windows.cordis.patch.yml` 出处的 pwsh 行、被禁用的 bash 行;POSIX 转储(CI Linux)不变。
- 真实组合冒烟在 win32 上启动 web profile,pwsh 栈挂载成功(即本笔记描述的确切清单)。
diff --git a/.agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.i18n.yaml b/.agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.i18n.yaml
index fd479c217e..1a4d923540 100644
--- a/.agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.md
-2026-08-02-session-search-not-shipped-default.md: 65bd72fff76210b726e7562fb8e88e5f8802434a
-2026-08-02-session-search-not-shipped-default.zh.md: 4eb0851c1e584b84847b6bb5118c8bb2f3156845
+2026-08-02-session-search-not-shipped-default.md: c1bfd7f8e354a4480c5635619514fe782ea71d2c
+2026-08-02-session-search-not-shipped-default.zh.md: 9b80c549425c26055700480dd57f1a0a7d01e4a8
diff --git a/.agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.md b/.agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.md
index 65bd72fff7..c1bfd7f8e3 100644
--- a/.agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.md
+++ b/.agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.md
@@ -10,7 +10,7 @@ The [shipped-roster decision](2026-07-31-even-out-shipped-tool-rosters.md) made
## Decision
-The shipped TUI, Web, and headless surfaces no longer mount `@deepseek-ai/dsh-tool-session-query`: the row is removed from the shared `cordis.patch.yml`, the now-dangling `disabled` patch in the opt-in [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) profile goes with it, and the workspace dependency drops from `apps/cli/package.json`. The consumer stays opt-in exactly as the model-facing-session-query-tools note describes: the ACP example's [`session-query.cordis.yml`](../../../../examples/acp-agent/session-query.cordis.yml) and its snapshot counterpart remain the mounted reference, and a custom composition can mount the package with the timeout and spill policies.
+The shipped TUI, Web, and headless surfaces do not mount `@deepseek-ai/dsh-tool-session-query`, and no shipped agent preset carries it. The consumer stays opt-in exactly as the model-facing-session-query-tools note describes: the ACP example's [`session-query.cordis.yml`](../../../../examples/acp-agent/session-query.cordis.yml) and its snapshot counterpart remain the mounted reference, and a custom composition can mount the package with the timeout and spill policies.
The `ctx.sessionQuery` service itself stays mounted. `session-query-sqlite` remains a base row — the TUI's `session-reference` consumes it for `/resume` — and the Web overlay keeps patching it to an in-memory index for the browser content search. Only the model-facing consumer is removed.
diff --git a/.agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.zh.md b/.agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.zh.md
index 4eb0851c1e..9b80c54942 100644
--- a/.agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.zh.md
+++ b/.agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.zh.md
@@ -10,7 +10,7 @@ Status: implemented
## 决策
-交付的 TUI、Web 与无头 surface 不再挂载 `@deepseek-ai/dsh-tool-session-query`:该行从共享的 `cordis.patch.yml` 移除,opt-in 的 [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) profile 中那条已悬空的 `disabled` patch 也随之删除,workspace 依赖也从 `apps/cli/package.json` 中移除。该消费方仍保持 opt-in,与面向模型的会话查询工具决策所述完全一致:ACP(Agent Client Protocol)示例的 [`session-query.cordis.yml`](../../../../examples/acp-agent/session-query.cordis.yml) 及其快照对侧文件仍是挂载参考,自定义组合也可以连同超时与 spill 策略一起挂载该包。
+交付的 TUI、Web 与无头 surface 均不挂载 `@deepseek-ai/dsh-tool-session-query`,交付的 agent preset 也都不包含它。该消费方仍保持 opt-in,与面向模型的会话查询工具决策所述完全一致:ACP(Agent Client Protocol)示例的 [`session-query.cordis.yml`](../../../../examples/acp-agent/session-query.cordis.yml) 及其快照对侧文件仍是挂载参考,自定义组合也可以连同超时与 spill 策略一起挂载该包。
`ctx.sessionQuery` 服务本身保持挂载。`session-query-sqlite` 仍是 base 的一行,TUI 的 `session-reference` 消费它来实现 `/resume`,Web overlay 也继续把它 patch 成内存索引,供浏览器内容搜索使用。被移除的只有面向模型的消费方。
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
index 89e150f59c..84cb091651 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
-2026-08-04-claude-code-and-codex-subagent-backends.md: 2063b99a7b0ca34f434b3e56628f3ce765d90d81
-2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 71d4a8e9fc060f8c96483b22641fd67ee1f71c08
+2026-08-04-claude-code-and-codex-subagent-backends.md: ccc96d6c998c4ab958a7eea1e502d036d16ec90d
+2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 740eeb633e336d5b01cb0b84fb656612e690959d
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
index 2063b99a7b..ccc96d6c99 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
+++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
@@ -12,7 +12,7 @@ The product integrations must not become second owners for task text, cwd, cance
## Decision
-The harness publishes two sibling one-shot providers as independently installable, opt-in packages. A user loads a provider and the existing common subagent tool in their own `cordis.yml`: `subagent_codex` binds `codex`, while `subagent_claude_code` binds `claude-code`. The shipped CLI dependency closure and base, Web, and headless configurations load neither provider. Each tool accepts only a standalone text task; product selection and background execution are not model arguments.
+The harness publishes two sibling one-shot provider packages: `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [shared-profile-host placement decision](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md) supersedes the original opt-in composition placement. Loading either provider starts no product process, and each tool accepts only a standalone text task; product selection and background execution are not model arguments.
Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools disable background execution and use `maxDepth: 'provider-managed'`, leaving recursion policy with the out-of-process product instead of sending a limit the provider cannot enforce. Every call creates a fresh product process and a non-resumable product conversation. The shared subagent service continues to own request resolution, lifecycle events, result settlement, and foreground collection; the shared subprocess service owns credential scrubbing, process-tree termination, and whole-tree exit observation.
@@ -47,7 +47,7 @@ Codex 0.147.0 speaks the Responses protocol, while DeepSeek's public OpenAI-comp
## Claude Code provider
-`@deepseek-ai/dsh-subagent-claude-code` registers the fixed `claude-code` provider and invokes `@anthropic-ai/claude-agent-sdk@0.3.220`. The SDK's platform `optionalDependency` supplies the real Claude Code 2.1.220 CLI. The provider uses the official `query()` entrypoint and passes the SDK's `spawnClaudeCodeProcess` command, arguments, cwd, environment, and forwarded signal unchanged to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
+`@deepseek-ai/dsh-subagent-claude-code` registers the fixed `claude-code` provider and invokes `@anthropic-ai/claude-agent-sdk@0.3.220`. Before each run, the provider resolves the fixed `claude` name through the host subprocess execution world and passes that exact path as `pathToClaudeCodeExecutable`; the SDK therefore uses the native product that launched DSH rather than selecting its platform `optionalDependency`. A Windows `.cmd` or `.bat` path crosses `cmd.exe /v:off` as a quoted per-spawn environment expansion, so percent, ampersand, and exclamation path components remain data without changing the shared subprocess contract. The provider uses the official `query()` entrypoint and passes the SDK's `spawnClaudeCodeProcess` arguments, cwd, environment, and forwarded signal to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
The public configuration contains the same two deployment-owned values as the Codex sibling: an explicit `env` overlay and a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`. Each run creates its own `AbortController`, sets `persistSession: false`, and disables `AskUserQuestion`. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. It supplies no `canUseTool`, elicitation, or dialog callback, so unattended interactions fail through the SDK rather than waiting for a user interface the provider does not own.
@@ -65,7 +65,7 @@ The Codex evidence pins `@openai/codex@0.147.0` and `codex-cli 0.147.0`. Its rea
The Codex credentialed e2e registers the production provider, starts the same real app-server, and requests one random nonce through the test-private bridge described above. It fixes the external endpoint and model, stores no credential or request payload, requires exactly one completed upstream response, compares the trimmed product answer byte-for-byte with the nonce, and waits for every managed handle to exit.
-The Claude Code evidence pins Agent SDK 0.3.220 and its platform-distributed Claude Code 2.1.220 CLI. Its real-product spec observes the exact `x-api-key`, original task, byte-exact final answer, inherited temporary host-setting marker, process failure, local cancellation, and whole-tree exit. The Loader e2e resolves both product packages by name while neither product command is available and records zero child starts.
+The Claude Code evidence pins Agent SDK 0.3.220 and uses its platform-distributed Claude Code 2.1.220 CLI as the deterministic compatibility fixture, routed through the same native executable-resolution path production uses. Its real-product spec observes the exact `x-api-key`, original task, byte-exact final answer, inherited temporary host-setting marker, process failure, local cancellation, whole-tree exit, and a real Windows batch shim under a path containing percent, ampersand, and exclamation metacharacters. This evidence proves the official SDK/CLI integration path, not compatibility with every independently installed product version. The Loader and shipped-profile evidence resolve both product packages by name while starting neither product, and the provider suite proves that the SDK receives the executable resolved from the host `PATH`.
The Claude Code credentialed e2e maps the key and fixed official endpoint only in the provider's in-memory environment, uses the documented `deepseek-v4-pro[1m]` and `deepseek-v4-flash` model variables, and traverses the production provider, official SDK, and real CLI. It compares the trimmed result with a random nonce and proves whole-tree exit without calling the Messages API directly from the test.
@@ -87,7 +87,7 @@ The project owner's distribution authorization is scoped to the official `@anthr
## Consequences
-Users can install either or both product providers, bind stable foreground tools in their own Cordis configuration, and delegate one self-contained task through the existing subagent contract. Official product integrations preserve native settings and behavior while shared services retain the sole ownership of task settlement and process-tree quiescence.
+Users delegate through two stable foreground tools backed by the official product integrations. Their Profile placement and per-Preset exposure are owned by the [shared-host placement decision](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md); this note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of task settlement and process-tree quiescence.
Every delegation pays for a fresh product process and independent model context, and only final text reaches the parent. Product-native configuration makes behavior depend on the deployment's installed product, account state, and workspace settings. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
index 71d4a8e9fc..740eeb633e 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
+++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
@@ -12,7 +12,7 @@ Status: implemented
## 决策
-harness 将两个一次性兄弟提供方作为可独立安装、选择启用的包交付。用户在自己的 `cordis.yml` 中加载提供方与现有的通用 subagent 工具:`subagent_codex` 绑定 `codex`,`subagent_claude_code` 绑定 `claude-code`。随产品交付的 CLI(命令行界面)依赖闭包,以及基础、Web 与 headless 配置都不会加载任一提供方。每个工具只接受独立文本任务;产品选择与后台执行都不作为模型参数。
+harness 交付两个同级的一次性提供方包:`codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[共享 profile 宿主归属决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)取代原先由用户选择启用的组装位置。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择与后台执行都不作为模型参数。
这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动能力,并传递父会话 cwd,但不会复制父级对话。文档所示的工具会禁用后台执行,并使用 `maxDepth: 'provider-managed'`,将递归策略留给进程外产品,而不是发送提供方无法强制执行的限制。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。共享 subagent 服务继续负责请求解析、生命周期事件、结果结算和前台收集;共享子进程服务负责凭证清洗、进程树终止以及整棵进程树的退出观测。
@@ -47,7 +47,7 @@ Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端
## Claude Code 提供方
-`@deepseek-ai/dsh-subagent-claude-code` 注册固定的 `claude-code` 提供方,并调用 `@anthropic-ai/claude-agent-sdk@0.3.220`。SDK 的平台 `optionalDependency` 提供真实的 Claude Code 2.1.220 CLI。提供方使用官方 `query()` 入口点,并将 SDK 的 `spawnClaudeCodeProcess` 命令、参数、cwd、环境和转发的信号原样传入 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
+`@deepseek-ai/dsh-subagent-claude-code` 注册固定的 `claude-code` 提供方,并调用 `@anthropic-ai/claude-agent-sdk@0.3.220`。每次运行前,提供方经宿主 subprocess 执行世界解析固定名称 `claude`,并把准确路径作为 `pathToClaudeCodeExecutable` 交给 SDK;SDK 因此使用启动 DSH 的原生产品,而不是选择自身的 platform `optionalDependency`。Windows `.cmd` 或 `.bat` 路径会作为带引号、仅供本次 spawn 使用的环境展开值穿过 `cmd.exe /v:off`,因此路径中的百分号、与号和感叹号仍只是数据,且无需改变共享子进程约定。提供方使用官方 `query()` 入口点,并将 SDK 的 `spawnClaudeCodeProcess` 参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
公开配置包含与 Codex 兄弟提供方相同、由部署方负责的两个值:显式的 `env` 覆盖项,以及须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false` 并禁用 `AskUserQuestion`。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。提供方不设置 `canUseTool`、elicitation 或对话回调,因此无人值守交互会经 SDK 失败,而不会等待本提供方不负责的用户界面。
@@ -65,7 +65,7 @@ Codex 证据锁定 `@openai/codex@0.147.0` 与 `codex-cli 0.147.0`。其真实
带密钥 Codex e2e 会注册生产提供方,启动同样的真实 app-server,并通过上述测试专用桥接层请求一个随机数。该测试固定外部端点与模型,不存储任何凭据或请求载荷,要求上游恰好完成一次响应,将去除首尾空白后的产品答案与该随机数逐字节比较,并等待所有受管句柄退出。
-Claude Code 证据锁定 Agent SDK 0.3.220 及其平台分发的 Claude Code 2.1.220 CLI。其真实产品测试会观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、继承的临时宿主设置标记、进程失败、本地取消以及整棵进程树退出。Loader e2e 会在两个产品命令均不可用时按名称解析两个产品包,并记录零次子级启动。
+Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Claude Code 2.1.220 CLI 作为确定性兼容性 fixture(测试前置数据),且该 fixture 经生产环境所用的同一原生可执行文件解析路径运行。其真实产品测试会观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、继承的临时宿主设置标记、进程失败、本地取消、整棵进程树退出,以及位于同时含百分号、与号和感叹号路径中的真实 Windows batch shim。这项证据证明官方 SDK/CLI 集成路径,而不证明它与每个独立安装的产品版本兼容。Loader 与随附 profile 证据会按名称解析两个产品包且不启动产品,provider 测试则证明 SDK 收到由宿主 `PATH` 解析出的可执行文件。
带密钥 Claude Code e2e 仅在提供方的内存环境中映射密钥与固定的官方端点,把模型变量设为文档所示的 `deepseek-v4-pro[1m]` 与 `deepseek-v4-flash`,并实际经过生产提供方、官方 SDK 与真实 CLI。它将去除首尾空白后的结果与一个随机数比较,并证明整棵进程树退出,且测试不会直接调用 Messages API。
@@ -79,7 +79,7 @@ Claude Code 证据锁定 Agent SDK 0.3.220 及其平台分发的 Claude Code 2.1
**面向模型的产品选择器。** 产品可用性和身份验证属于部署事实。两个固定工具使各自的 schema 与提供方绑定保持明确,也避免在通用服务中添加动态选择状态。
-**以产品替身作为强制证据。** 替身可以穷尽覆盖私有协议分支,但无法证明包导出、官方发行版、身份验证或真实进程行为。强制证据会驱动每个官方产品连接回环模型 fixture(测试前置数据)。
+**以产品替身作为强制证据。** 替身可以穷尽覆盖私有协议分支,但无法证明包导出、官方发行版、身份验证或真实进程行为。强制证据会驱动每个官方产品连接回环模型 fixture。
**由插件管理登录、产品主目录、模型、设置或权限。** 这些选择会在每个产品的原生配置之外建立另一套权威来源,并将一次性提供方扩张为账户管理功能。提供方只公开显式环境覆盖项和清理宽限期;无人值守交互会以默认拒绝方式失败。
@@ -87,7 +87,7 @@ Claude Code 证据锁定 Agent SDK 0.3.220 及其平台分发的 Claude Code 2.1
## 后果
-用户可以安装任一或两个产品提供方,在自己的 Cordis 配置中绑定稳定的前台工具,并通过现有 subagent 约定委派一项自包含任务。官方产品集成会保留原生设置与行为,而共享服务继续独占任务结算与进程树完全停稳的责任。
+用户通过官方产品集成支持的两个稳定前台工具进行委派。它们在 Profile 中的归属和按 Preset 暴露方式由[共享宿主归属决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)负责;本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占任务结算与进程树完全停稳的责任。
每次委派都要承担新建产品进程和独立模型上下文的开销,且只有最终文本会到达父级。产品原生配置使行为取决于部署环境中安装的产品、账户状态和工作区设置。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
diff --git a/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.i18n.yaml b/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.i18n.yaml
index 86b48310e7..362aef352f 100644
--- a/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md
-2026-08-04-web-context-source-and-steer-marks.md: 01bdca873a847f70b4b8632b961e01e099ae4f04
-2026-08-04-web-context-source-and-steer-marks.zh.md: b6a9cc5692826b402b5a08ec65a5c8fc3c547b6b
+2026-08-04-web-context-source-and-steer-marks.md: d4fee3ee25aceaf05106d6bd1bdb73e7c51c3f78
+2026-08-04-web-context-source-and-steer-marks.zh.md: 8e0ffa6c15ea7506e1aaed9f0b142925727856aa
diff --git a/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md b/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md
index 01bdca873a..d4fee3ee25 100644
--- a/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md
+++ b/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md
@@ -14,13 +14,13 @@ The distinctions are already durable. Every producer must supply a merge-extensi
The transcript names all three roles a non-prompt message can play — injected context, recalled session, and steering.
-`TranscriptAdapter` and the history fold attach a `provenance` view containing the producer role and label to every `ContextMessageNode`; `contextProvenance()` computes it from the durable source alone. It returns a `role` (`inject`, or `recall` for a cross-session snapshot) and a `label` naming the producer. `ContextInjectionRow` titles itself from the role and shows the label beside that title in `ToolRow`'s summary geometry, so the collapsed row already answers what was added and by whom; the 141px scrollport and truncation bound are unchanged from the [archived disclosure decision](../../archived/feature/2026-07-30-web-context-injection-disclosure.md). What renders inside that scrollport is chosen by the independent form axis added in the [context form decision](2026-08-05-context-form-vocabulary.md).
+The Chat Message Definition attaches a `provenance` view containing the producer role and label to every `ContextMessageNode`; `contextProvenance()` computes it from the durable source alone. It returns a `role` (`inject`, or `recall` for a cross-session snapshot) and a `label` naming the producer. `ContextInjectionRow` titles itself from the role and shows the label beside that title in `ToolRow`'s summary geometry, so the collapsed row already answers what was added and by whom; the 141px scrollport and truncation bound are unchanged from the [archived disclosure decision](../../archived/feature/2026-07-30-web-context-injection-disclosure.md). What renders inside that scrollport is chosen by the independent form axis added in the [context form decision](2026-08-05-context-form-vocabulary.md).
**The label is read out of the log, never from a client-side table of producer names.** `workspace-instructions` is named by the distinct instruction paths it reconciled, `session-reference` by the titles of the sessions it read, a plugin source by its logged plugin id, and any other source by its own `kind` — the documented default arm for a merge-extensible union. A source carrying no readable kind degrades to an unnamed injection. A new or renamed producer is therefore identifiable without a client release, no label can go stale against the code, and a resumed, forked, or foreign log projects exactly like a live session.
`recall` covers `session-reference` because that is the one shipped source that lifts another session's material into this one. No Web leaf mounts `dsh-session-reference` today — it had only a terminal host — so the arm exists for log portability rather than for a bundled producer, and it is exercised by unit coverage rather than an assembled Web scenario.
-`MessageItem` captions durable and pending steering bubbles with `插话`. The runtime replays durable `agent/inbox/spliced` events and projects a user-origin `user/message` as `SteeringMessageNode` when that same message identity was claimed from `next-step`; a queued-turn claim stays a `UserMessageNode`, and a non-user next-step message stays context. This reverses one clause of the [archived no-steer decision](../../archived/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md), which removed the badge because the composer could not steer and the label named a gesture users could not perform. The composer gained a Steer gesture afterwards without amending that note; this decision supplies the product decision its reintroduction clause required, and corrects the stale facts left in it. The caption is the only steering chrome here: composer modes, the Queue dock's strict-steer action, and pending-steering lifecycle stay with their own owners.
+`MessageItem` captions durable and pending steering bubbles with `插话`. The Chat Inbox and Message Definitions replay durable `agent/inbox/spliced` events and project a user-origin `user/message` as `SteeringMessageNode` when that same message identity was claimed from `next-step`; a queued-turn claim stays a `UserMessageNode`, and a non-user next-step message stays context. This reverses one clause of the [archived no-steer decision](../../archived/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md), which removed the badge because the composer could not steer and the label named a gesture users could not perform. The composer gained a Steer gesture afterwards without amending that note; this decision supplies the product decision its reintroduction clause required, and corrects the stale facts left in it. The caption is the only steering chrome here: composer modes, the Queue dock's strict-steer action, and pending-steering lifecycle stay with their own owners.
## Alternatives considered
diff --git a/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.zh.md b/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.zh.md
index b6a9cc5692..8e0ffa6c15 100644
--- a/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.zh.md
+++ b/.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.zh.md
@@ -14,13 +14,13 @@ Status: implemented
transcript 为非提示消息可能承担的三种角色分别命名:注入上下文、召回会话、steering。
-`TranscriptAdapter` 与历史折叠为每个 `ContextMessageNode` 附加一份包含生产者角色和名称的 `provenance` 视图;`contextProvenance()` 仅依据持久来源计算该视图。它返回 `role`(`inject`,跨会话快照则为 `recall`)与命名生产者的 `label`。`ContextInjectionRow` 以角色作为标题,并按 `ToolRow` 摘要的几何在标题旁展示该名称,因此折叠态就已经回答了「注入了什么、由谁注入」;141px 滚动视口与截断上限沿用[已归档的展开项决策](../../archived/feature/2026-07-30-web-context-injection-disclosure.md),未作改动。视口里渲染什么,则由[上下文形态决策](2026-08-05-context-form-vocabulary.md)引入的、相互独立的形态轴决定。
+Chat Message Definition 为每个 `ContextMessageNode` 附加一份包含生产者角色和名称的 `provenance` 视图;`contextProvenance()` 仅依据持久来源计算该视图。它返回 `role`(`inject`,跨会话快照则为 `recall`)与命名生产者的 `label`。`ContextInjectionRow` 以角色作为标题,并按 `ToolRow` 摘要的几何在标题旁展示该名称,因此折叠态就已经回答了「注入了什么、由谁注入」;141px 滚动视口与截断上限沿用[已归档的展开项决策](../../archived/feature/2026-07-30-web-context-injection-disclosure.md),未作改动。视口里渲染什么,则由[上下文形态决策](2026-08-05-context-form-vocabulary.md)引入的、相互独立的形态轴决定。
**名称从日志中读出,绝不来自客户端维护的生产者名称表。** `workspace-instructions` 以它对账过的去重指令文件路径命名,`session-reference` 以它读取的会话标题命名,插件来源以其记录的插件 id 命名,其余来源则以自身的 `kind` 命名——这正是可合并扩展联合类型有文档记载的默认分支。没有可读 kind 的来源降级为无名注入。于是新增或重命名的生产者无需客户端发版即可辨识,任何名称都不会相对代码变味,恢复、fork 或来自外部的日志与实时会话的投影结果完全一致。
`recall` 覆盖 `session-reference`,因为它是当前唯一会把另一个会话的材料搬进本会话的已发布来源。今天没有任何 Web 叶子挂载 `dsh-session-reference`——它此前只有终端宿主——因此该分支的存在是为了日志可移植性,而不是为了某个已打包的生产方,其覆盖来自单元测试而非组装后的 Web 场景。
-`MessageItem` 为持久与待处理的 steering 气泡加上 `插话` 标注。runtime 会重放持久 `agent/inbox/spliced` 事件;如果一条用户来源的消息以相同身份从 `next-step` 被领取,后续 `user/message` 就投影为 `SteeringMessageNode`。从排队轮次领取的消息仍是 `UserMessageNode`,非用户来源的 next-step 消息仍是上下文。这推翻了[已归档的取消 steer 入口与插话装饰决策](../../archived/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md)中的一条结论。当时移除徽章,是因为 composer 无法 steer,标签指向了用户做不到的动作。此后 composer 获得了 Steer 手势,却没有同步修订那份 note;本决策提供了它在「重新引入」条款中要求的产品决策,并订正了其中留下的过时事实。标注是这里唯一的 steering 装饰:composer 模式、Queue dock 的严格 steer 操作、待处理 steering 的生命周期仍归各自的所有者。
+Chat Inbox 与 Message Definition 会重放持久 `agent/inbox/spliced` 事件;如果一条用户来源的消息以相同身份从 `next-step` 被领取,后续 `user/message` 就投影为 `SteeringMessageNode`。`MessageItem` 为这种持久消息与待处理 steering 气泡加上 `插话` 标注。从排队轮次领取的消息仍是 `UserMessageNode`,非用户来源的 next-step 消息仍是上下文。这推翻了[已归档的取消 steer 入口与插话装饰决策](../../archived/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md)中的一条结论。当时移除徽章,是因为 composer 无法 steer,标签指向了用户做不到的动作。此后 composer 获得了 Steer 手势,却没有同步修订那份 note;本决策提供了它在「重新引入」条款中要求的产品决策,并订正了其中留下的过时事实。标注是这里唯一的 steering 装饰:composer 模式、Queue dock 的严格 steer 操作、待处理 steering 的生命周期仍归各自的所有者。
## 考虑过的替代方案
diff --git a/.agents/notes/implemented/feature/2026-08-05-per-agent-tool-presentation.i18n.yaml b/.agents/notes/implemented/feature/2026-08-05-per-agent-tool-presentation.i18n.yaml
index 24b04ea72f..92610dde43 100644
--- a/.agents/notes/implemented/feature/2026-08-05-per-agent-tool-presentation.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-08-05-per-agent-tool-presentation.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-per-agent-tool-presentation.md
-2026-08-05-per-agent-tool-presentation.md: 348f7ab0a26e9b39057dbac885304e0d52e0b1fb
-2026-08-05-per-agent-tool-presentation.zh.md: 4920ee6eb061d44934bfc9f5176e244f5aac8553
+2026-08-05-per-agent-tool-presentation.md: adb93b51c73d341c153b8fcafe2a08f0a5598478
+2026-08-05-per-agent-tool-presentation.zh.md: fa83bd4daec9d8e6e9e43295bb81af97782b1fdf
diff --git a/.agents/notes/implemented/feature/2026-08-05-per-agent-tool-presentation.md b/.agents/notes/implemented/feature/2026-08-05-per-agent-tool-presentation.md
index 348f7ab0a2..adb93b51c7 100644
--- a/.agents/notes/implemented/feature/2026-08-05-per-agent-tool-presentation.md
+++ b/.agents/notes/implemented/feature/2026-08-05-per-agent-tool-presentation.md
@@ -12,16 +12,16 @@ The naive reading of "move tools down to the agent plane" does not work. `ctx.to
## Decision
-Split the registry from its projection. The registry stays host-plane; the **presentation** becomes per-agent state inside it, alongside the per-agent restrictions and guards that already live there.
+Split the registry from its projection. The registry stays host-plane; the **presentation** becomes scope state inside it, alongside the scoped restrictions and guards that already live there.
-`ToolRegistry.presentAs(mode)` is scoped-only and mirrors `restrict()`: it writes one cell on the calling scope's `ToolLayer` through `ScopedLayers.effect`, so it unwinds with the agent that declared it. `modeFor(scope)` resolves that cell against the config `mode`, which becomes the default for agents declaring nothing rather than a process-wide fact. The three reads that decided presentation — the wire schemas, the `run_code` entry in the visibility view, and the generated SDK section — take the scope's mode instead of the service's.
+`ToolRegistry.presentAs(mode)` is scoped-only and mirrors `restrict()`: it writes one cell on the calling scope's `ToolLayer` through `ScopedLayers.effect`, so it unwinds with the scope that declared it. In the shipped Web surface that scope is an agent preset's standing mount — the `code` preset carries the `tool-mode` row — so one declaration covers every agent joined to that preset, and `modeFor(scope)` takes the nearest declaration on the chain. It resolves against the config `mode`, which becomes the default for scopes declaring nothing rather than a process-wide fact. The three reads that decided presentation — the wire schemas, the `run_code` entry in the visibility view, and the generated SDK section — take the scope's mode instead of the service's.
Two consequences fell out and are load-bearing:
- **`run_code` is appended per scope.** Previously the transport entered every view whenever the transport existed. Per-agent, a native agent must not find `run_code` in its dispatch table because some other agent in the process presents it — so the append is conditional on that scope's own mode, and the transport is built lazily on first need.
- **The reserved name is now unconditional.** `run_code` was rejected as a registration only while a code mode was configured. Any agent may now select a code mode, so a name that was free to take under a native deployment would become a collision the moment a preset mounted.
-The SDK prompt section is registered globally by a code-mode deployment (unchanged) and additionally per agent by `presentAs`, where it shadows by name. Its body renders empty for a native scope, which the prompt renderer drops — that is what keeps an agent opting OUT of a code-mode deployment free of an SDK section.
+The SDK prompt section is registered globally by a code-mode deployment (unchanged) and additionally per scope by `presentAs`, where it shadows by name. Its body renders empty for a native scope, which the prompt renderer drops — that is what keeps an agent opting OUT of a code-mode deployment free of an SDK section.
The preset expresses the choice through one row, `@deepseek-ai/dsh-agent-tool-mode`, whose whole body is a `presentAs` call. A code mode waits for `ctx.codeRuntime` through `ctx.inject` rather than assuming it: the runtime is host-plane, and a pending row is what `dsh-agent-presets` already reports as an unusable mount, naming the row — so a preset selecting Code Mode against a runtime-less deployment fails where an operator can act.
diff --git a/.agents/notes/implemented/feature/2026-08-05-per-agent-tool-presentation.zh.md b/.agents/notes/implemented/feature/2026-08-05-per-agent-tool-presentation.zh.md
index 4920ee6eb0..fa83bd4dae 100644
--- a/.agents/notes/implemented/feature/2026-08-05-per-agent-tool-presentation.zh.md
+++ b/.agents/notes/implemented/feature/2026-08-05-per-agent-tool-presentation.zh.md
@@ -12,16 +12,16 @@ agent preset 已经能按会话组装一个 agent 的工具,却管不了这些
## Decision
-把注册表和它的投影拆开。注册表留在宿主平面;**呈现方式**变成它内部按 agent 的状态,与已经住在那里的按 agent 限制和守卫并列。
+把注册表和它的投影拆开。注册表留在宿主平面;**呈现方式**变成它内部按 scope 的状态,与已经住在那里的作用域限制和守卫并列。
-`ToolRegistry.presentAs(mode)` 只接受 scoped 上下文,形状照抄 `restrict()`:它通过 `ScopedLayers.effect` 在调用方 scope 的 `ToolLayer` 上写一个单元,因此会随声明它的那个 agent 一起卸载。`modeFor(scope)` 将该单元与 config 的 `mode` 一并解析,后者于是成为「未作声明的 agent」的默认值,而不再是进程级事实。原先决定呈现方式的三处读取——wire schema、可见性视图里的 `run_code` 条目、以及生成的 SDK 段——改为读取该 scope 的模式,而非服务的。
+`ToolRegistry.presentAs(mode)` 只接受 scoped 上下文,形状照抄 `restrict()`:它通过 `ScopedLayers.effect` 在调用方 scope 的 `ToolLayer` 上写一个单元,因此会随声明它的那个 scope 一起卸载。在随附的 Web 界面里那个 scope 是某个 agent preset 的常驻挂载——`code` preset 携带 `tool-mode` 行——因此一份声明覆盖加入该 preset 的每个 agent,而 `modeFor(scope)` 取作用域链上最近的那份声明。它与 config 的 `mode` 一并解析,后者于是成为「未作声明的 scope」的默认值,而不再是进程级事实。原先决定呈现方式的三处读取——wire schema、可见性视图里的 `run_code` 条目、以及生成的 SDK 段——改为读取该 scope 的模式,而非服务的。
有两个随之而来的结果,且都是承重的:
- **`run_code` 按 scope 追加。** 此前只要传输存在,它就进入每一个视图。按 agent 之后,一个 native agent 不能因为进程里别的 agent 呈现了它、就在自己的分发表里看到 `run_code`——因此这次追加以该 scope 自身的模式为条件,传输也改为首次需要时才构建。
- **保留名现在无条件生效。** `run_code` 此前只在配置了 code 模式时才被拒绝注册。如今任何 agent 都可能选择 code 模式,因此一个在 native 部署下可以随便占用的名字,会在某个 preset 挂载的那一刻变成冲突。
-SDK 提示词段由 code 模式的部署全局注册(不变),并由 `presentAs` 额外按 agent 注册一份,后者按名字遮蔽前者。它的正文对 native scope 渲染为空,而提示词渲染器会丢弃空段——正是这一点让「在 code 模式部署下选择退出」的 agent 不带 SDK 段。
+SDK 提示词段由 code 模式的部署全局注册(不变),并由 `presentAs` 额外按 scope 注册一份,后者按名字遮蔽前者。它的正文对 native scope 渲染为空,而提示词渲染器会丢弃空段——正是这一点让「在 code 模式部署下选择退出」的 agent 不带 SDK 段。
preset 用一行来表达这个选择:`@deepseek-ai/dsh-agent-tool-mode`,其全部内容就是一次 `presentAs` 调用。code 类模式通过 `ctx.inject` 等待 `ctx.codeRuntime` 而非假定它存在:运行时在宿主平面,而一个 pending 的行正是 `dsh-agent-presets` 已经会报告的「不可用挂载」并会指名该行——于是在无运行时的部署上选择 Code Mode 的 preset,会在操作者能够动手的地方失败。
diff --git a/.agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.i18n.yaml b/.agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.i18n.yaml
new file mode 100644
index 0000000000..2f6713b4ab
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.md
+2026-08-06-mcp-client-auto-reconnect.md: 99a8aec1abe3713822f8f17c17d8efaca5d61a4d
+2026-08-06-mcp-client-auto-reconnect.zh.md: 8d4dc935e6edee9a05f556774e743e48234ca709
diff --git a/.agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.md b/.agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.md
new file mode 100644
index 0000000000..99a8aec1ab
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.md
@@ -0,0 +1,48 @@
+# Agent Note: MCP client auto-reconnect with bounded backoff
+
+Status: implemented
+
+English | [中文](2026-08-06-mcp-client-auto-reconnect.zh.md)
+
+## Problem
+
+The [MCP client](2026-07-07-mcp-client-plugin.md) connected once at plugin load. When a stdio server crashed or was killed, its registered tools stayed visible but every call failed with `Not connected` until a human edited the config (HMR) or restarted the Host — v1 explicitly deferred reconnection. Long-running hosts (ACP automation, web) cannot be bounced because a child process died, and for stdio the harness composition is the only party that can respawn it. External feedback escalated this as a real operational gap (issue #1746).
+
+## Decision
+
+`packages/mcp/mcp-client/src/connection.ts` owns a per-instance connection supervisor; `apply()` shrinks to config resolution plus two effects (the `serverName` reservation and the supervisor's lifecycle). The supervisor owns the client/transport generations, the live tool registrations, and the reconnect loop.
+
+**Trigger.** The supervisor arms `client.onclose` per generation. The SDK fires it when the stdio child exits, so a crash is observed without polling. `StreamableHTTPClientTransport` fires `onclose` only for deliberate closes — it owns its internal SSE-stream recovery and surfaces request failures per call — so HTTP servers are effectively outside supervisor restarts; the package README records that limitation.
+
+**Generations without interleaving.** Each attempt builds a fresh transport and `Client` (the SDK binds a Protocol to one transport for life). One per-supervisor queue serializes every `syncTools` call — initial syncs and `list_changed` re-syncs across all generations — and an `isCurrent` fence makes stale generations inert, so no two syncs can interleave the dispose-previous/register-next swap (which would double-dispose one generation and leak another). The queue also closes a pre-existing race where two rapid `list_changed` notifications re-synced concurrently. The activation attempt, rather than the first queue entrant, explicitly owns strict startup registration: an early `list_changed` notification uses contained re-sync semantics and cannot consume `failOnStartupError`. Failure signals are idempotent per generation: a connect rejection racing its own transport close schedules exactly one retry. A failed attempt cannot enter backoff until both `Client.close()` settles and the transport reports `onclose`, which for stdio proves the child exited; a missing close signal stops reconnection after the SDK's bounded termination window instead of allowing two server processes to overlap. Disposal uses the same bounded close-signal barrier and reports an incomplete shutdown without ever restarting.
+
+**Bounded backoff with an outage budget.** Delays double from `initialDelayMs` up to `maxDelayMs`. One outage shares `maxAttempts` consecutive failed attempts; exhaustion unregisters the server's tools, logs at error level, and stops until disposal or reload. A connection that survives past the stability window — `maxDelayMs`, derived rather than a fifth tunable, as the longest configured backoff spacing — resets the budget, so an occasionally-crashing server recovers indefinitely while a crash loop whose connects briefly succeed cannot launder its budget into a restart storm.
+
+**Config and resolution.** Both transports accept `reconnect { enabled, initialDelayMs, maxDelayMs, maxAttempts }` with schemastery defaults (on, 500ms, 30s, 10). `resolveReconnectPolicy()` is the explicit resolve step: it re-judges every bound and cross-field constraint because programmatic construction may bypass Schemastery, and misconfiguration fails the plugin instance at load.
+
+**Observable states.** An initial or retry-attempt failure says `connection failed`; an established generation ending says `connection lost`. Retrying logs at warn with attempt count and delay, recovery at info, final failure and disabled recovery at error. During an outage the last good generation stays registered and calls against it fail — deterministic public names mean a recovered unchanged tool list reproduces identical definitions, keeping the model-visible schema prefix stable instead of flapping. With `reconnect.enabled: false` a lost connection keeps the v1 manual-recovery behavior.
+
+**Disposal.** Dispose flips the fence, cancels any pending timer, closes the current client, then awaits the in-flight attempt and the sync queue before unregistering — quiescence, not just a request to stop. The reconnect timer is unref'd so a waiting backoff never holds a finishing process open.
+
+## Alternatives considered
+
+**Consecutive-failure counter that resets on every successful connect.** Rejected: a crash-looping server whose connects briefly succeed would reset the budget each cycle and restart forever — exactly the restart storm the failure cap exists to prevent. The uptime-gated reset distinguishes a recovered server from a looping one without new configuration.
+
+**Reuse one SDK `Client` across reconnects.** The Protocol clears its transport on close and can technically connect again, but the SDK's own guidance is one connection per Protocol instance, and reuse carries notification handlers and negotiated capability state across server incarnations. A fresh `Client` per generation plus the `isCurrent` fence is unambiguous.
+
+**Unregister tools immediately on disconnect, re-register on recovery.** Rejected: a transient outage would flap the model-visible tool list (two schema-prefix invalidations per crash) for no information gain; failing calls already signal the outage, and the swap on recovery is atomic per generation. Tools are unregistered at final failure so a permanently dead server does not leak permanently broken tools.
+
+**Route Streamable HTTP request failures into the supervisor.** Rejected for now: the HTTP transport already reconnects its SSE stream with its own backoff, per-request errors do not imply a dead server, and there is no child process the harness could respawn. Transport close stays the single trigger.
+
+**Restart through Loader/HMR machinery instead of an in-plugin supervisor.** Rejected: the Loader owns config-driven recomposition, not runtime health. A plugin restarting itself through the Loader would conflate config generations with connection generations and lose the per-outage budget.
+
+## Testing
+
+Unit (`tests/reconnect.spec.ts`, mocked SDK): recovery swaps generations without duplication or leaks and serves post-recovery calls, diagnostics distinguish initial or retry failure from established connection loss, strict startup registration survives a pre-connect `list_changed` notification, failed initialization waits for the old generation's close signal and fails closed when that signal never arrives, disposal waits for the same signal with a bounded incomplete-shutdown path, the failure cap unregisters tools and stops, dispose cancels a pending backoff and quiesces an in-flight sync, a close after dispose schedules nothing, disabled mode keeps the v1 behavior, the stability window resets the budget while a crash loop exhausts it, double failure signals schedule one retry, stale generations and handlers are inert, and `resolveReconnectPolicy` rejects each invalid bound. E2E (`tests/mcp-client.e2e.ts`, keyless): the fixture server gained a `crash` tool that replies then exits; real-process tests prove a stdio crash recovers end to end and that unloading the plugin mid-outage stops reconnection promptly. Snapshot: deliberately none, per the original note's rationale — reconnection adds no new presentation shape, and a snapshot composition spawning a crashing server would make replays timing-dependent.
+
+## Consequences
+
+- A crashed stdio MCP server recovers without human intervention: bounded backoff, re-discovery, atomic generation swap. Default policy retries an outage for roughly 2.5 minutes before giving up.
+- Connection state is genuinely more intricate than connect-once — the partial-availability window v1 avoided now exists (registered tools failing during an outage), concentrated in one module with the invariants named.
+- `reconnect` is new config surface on both transports, and the stability window is deliberately derived from `maxDelayMs`; making it independently tunable is a compatible future change.
+- After final failure or with reconnect disabled, the plugin stays loaded with no (or failing) tools until reload — deliberate and logged, so a chronically broken server cannot restart forever.
diff --git a/.agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.zh.md b/.agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.zh.md
new file mode 100644
index 0000000000..8d4dc935e6
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-06-mcp-client-auto-reconnect.zh.md
@@ -0,0 +1,48 @@
+# Agent Note: MCP client auto-reconnect with bounded backoff
+
+Status: implemented
+
+[English](2026-08-06-mcp-client-auto-reconnect.md) | 中文
+
+## 问题
+
+[MCP 客户端](2026-07-07-mcp-client-plugin.md)在插件加载时仅连接一次。stdio 服务器崩溃或被终止后,其已注册的工具仍然可见,但每次调用均以 `Not connected` 失败,直到人工编辑配置触发 HMR(热模块替换)重载,或重启 Host——v1 明确推迟了重连机制。长时间运行的 Host(ACP 自动化、Web)不能因为子进程死亡就被重启;而对于 stdio 传输,harness 组合层是唯一能重新拉起子进程的一方。外部反馈将此升级为真实的运维缺口(issue #1746)。
+
+## 决策
+
+`packages/mcp/mcp-client/src/connection.ts` 拥有一个逐实例的连接监督器;`apply()` 收缩为配置解析加两个副作用(`serverName` 预留和监督器的生命周期)。监督器负责管理 client/transport 代、活跃的工具注册以及重连循环。
+
+**触发条件。** 监督器在每一代上挂载 `client.onclose`。SDK 在 stdio 子进程退出时触发该回调,因此崩溃无需轮询即可感知。`StreamableHTTPClientTransport` 仅在主动关闭时触发 `onclose`——它内部拥有自己的 SSE(Server-Sent Events)流恢复机制,并将请求失败以逐调用方式暴露——因此 HTTP 服务器实际上不在监督器的重启范围内;包 README 记录了该限制。
+
+**代隔离,无交错。** 每次尝试构建一个全新的 transport 和 `Client`(SDK 将一个 Protocol 绑定到一个 transport 上终身使用)。每个监督器内部有一个队列将所有 `syncTools` 调用串行化——跨所有代的初始同步和 `list_changed` 再同步——`isCurrent` 栅栏使过时的代变为惰性,从而确保不会有两次同步交错执行 dispose 上一代/注册下一代的切换(否则会对同一代执行两次 dispose 并泄漏另一代)。该队列还消除了一个先前存在的竞态:两次快速的 `list_changed` 通知同时触发重新同步。严格启动注册由激活尝试本身显式拥有,而非由首个入队者拥有;提前到达的 `list_changed` 采用故障隔离的再同步语义,不能消费 `failOnStartupError`。失败信号按代幂等:一次连接拒绝与其自身 transport 关闭竞态时,仅调度恰好一次重试。失败尝试只有在 `Client.close()` 结算且 transport 报告 `onclose` 后才能进入退避;对 stdio 而言,`onclose` 证明子进程已退出;若关闭信号始终未到,则在 SDK 的有界终止窗口结束后停止重连,而不是允许两个服务器进程重叠运行。dispose 使用同一个有界关闭信号屏障;若关停未完成则予以报告,且绝不重启。
+
+**有界退避与故障预算。** 延迟从 `initialDelayMs` 起逐次翻倍,上限为 `maxDelayMs`。一次故障期间共享 `maxAttempts` 次连续失败尝试的预算;耗尽后注销该服务器的工具、以 error 级别记录日志并停止,直到 dispose 或重新加载。连接在存活超过稳定窗口——即 `maxDelayMs`,作为最长退避间隔从配置推导得出而非作为第五个独立调参项——之后重置预算;因此偶尔崩溃的服务器可无限恢复,而连接短暂成功后立即再次崩溃的循环无法将其预算洗白为重启风暴。
+
+**配置与解析。** 两种传输均接受 `reconnect { enabled, initialDelayMs, maxDelayMs, maxAttempts }` 配置,Schemastery 默认值为(启用、500ms、30s、10)。`resolveReconnectPolicy()` 是显式的解析步骤:它重新校验每个边界值和跨字段约束,因为程序化构造可能绕过 Schemastery,配置错误在加载时即令插件实例失败。
+
+**可观测状态。** 初始尝试或重试尝试失败时记录 `connection failed`,已建立的代结束时记录 `connection lost`;重试的 warn 日志包含尝试次数和延迟,恢复以 info 级别记录,最终失败和禁用重连时的断连以 error 级别记录。故障期间,上一个正常代保持注册,对其工具的调用返回失败——确定性公开名称意味着恢复后未变化的工具列表会复现相同的定义,保持模型可见 schema 前缀稳定而非反复抖动。设置 `reconnect.enabled: false` 后,断连保持 v1 的手动恢复行为。
+
+**资源释放。** dispose 翻转栅栏、取消待执行的定时器、关闭当前 client,然后等待正在进行的尝试和同步队列完成后再注销工具——完全停稳,而非仅发出停止请求。重连定时器使用 unref,因此等待中的退避不会阻止进程正常退出。
+
+## 曾考虑的替代方案
+
+**连续失败计数器,每次成功连接即重置。** 否决:连接短暂成功后立即崩溃的循环服务器会在每个周期重置预算并永远重启——恰恰是失败上限旨在防止的重启风暴。基于运行时间的重置能区分已恢复的服务器与循环崩溃的服务器,无需新增配置。
+
+**跨重连复用同一个 SDK `Client`。** Protocol 在关闭时清除其 transport,技术上可以再次连接,但 SDK 自身的指导方针是每个 Protocol 实例对应一次连接,且复用会将通知处理器和已协商的能力状态带入新的服务器实例。每代创建全新 `Client` 加 `isCurrent` 栅栏的方式无歧义。
+
+**断连时立即注销工具,恢复时重新注册。** 否决:短暂故障会使模型可见工具列表抖动(每次崩溃触发两次 schema 前缀失效),而无任何信息增益;失败的调用已足以标示故障,恢复时的切换按代原子执行。工具仅在最终失败时注销,确保永久死亡的服务器不会泄漏永久失效的工具。
+
+**将 Streamable HTTP 请求失败路由到监督器。** 暂不采纳:HTTP 传输已使用自己的退避机制重连其 SSE 流,逐请求错误并不意味着服务器已死,且 harness 没有可重新拉起的子进程。transport 关闭仍是唯一触发条件。
+
+**通过 Loader/HMR 机制重启,而非使用插件内监督器。** 否决:Loader 负责配置驱动的重组合,而非运行时健康管理。插件通过 Loader 重启自身会混淆配置代与连接代,并丢失逐故障预算。
+
+## 测试
+
+单元测试(`tests/reconnect.spec.ts`,mock SDK):恢复在不产生重复或泄漏的前提下切换代并服务恢复后的调用、诊断区分初始或重试尝试失败与已建立连接丢失、严格启动注册在连接前收到 `list_changed` 通知后仍然生效、初始化失败会等待旧代的关闭信号,若该信号始终未到则停止重连、dispose 同样等待同一关闭信号,并在有界等待到期时报告关停未完成、失败上限注销工具并停止、dispose 取消待执行的退避并使进行中的同步完全停稳、dispose 后的关闭不调度任何操作、禁用模式保持 v1 行为、稳定窗口重置预算而崩溃循环耗尽预算、双重失败信号仅调度一次重试、过时的代和处理器为惰性、`resolveReconnectPolicy` 拒绝每个无效边界值。E2E(`tests/mcp-client.e2e.ts`,无需密钥):fixture 服务器新增了一个 `crash` 工具(先回复再退出);真实进程测试证明 stdio 崩溃端到端恢复,以及在故障期间卸载插件能立即停止重连。快照:刻意不做,原因与原 Agent Note 相同——重连不引入新的展示形态,而在快照组合中 spawn 崩溃服务器会使回放依赖时序。
+
+## 后果
+
+- 崩溃的 stdio MCP 服务器无需人工干预即可恢复:有界退避、重新发现、原子代切换。默认策略对一次故障大约重试 2.5 分钟后放弃。
+- 连接状态确实比一次性连接更复杂——v1 刻意回避的部分可用窗口现已存在(故障期间已注册工具返回失败),集中在一个模块中并命名了所有不变式。
+- `reconnect` 是两种传输上的新配置表面,稳定窗口刻意从 `maxDelayMs` 推导;将其设为独立可调参数是兼容的未来变更。
+- 最终失败后或禁用重连时,插件保持加载状态但无(或失败的)工具,直到重新加载——行为是刻意的且有日志记录,确保长期故障的服务器不能永远重启。
diff --git a/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.i18n.yaml b/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.i18n.yaml
new file mode 100644
index 0000000000..b5c7f142f1
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.md
+2026-08-07-feedback-acknowledgement-sharing-disclosure.md: 1e9cd0fb95d78aff9f6434e0583154e2c3f847da
+2026-08-07-feedback-acknowledgement-sharing-disclosure.zh.md: ac26b18ad523feeabc297b212210dd73eff93a0a
diff --git a/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.md b/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.md
new file mode 100644
index 0000000000..1e9cd0fb95
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.md
@@ -0,0 +1,27 @@
+# Agent Note: Feedback acknowledgement sharing disclosure
+
+Status: implemented
+
+English | [中文](2026-08-07-feedback-acknowledgement-sharing-disclosure.zh.md)
+
+## Problem
+
+The `/feedback` command records a log-only `feedback/record` event and acknowledges the user, but the acknowledgement carried no durable context about what happened to the session: deployments that mount session telemetry (`FULL`, `FEEDBACK_ONLY`, or `DISABLED`) had no way to tell the user whether their feedback and session left the process, and the receiving session id was not echoed. The command plugin could not read the sharing policy because the telemetry seam exposed capture only, and the OTel mode enum lived in the optional backend package.
+
+## Decision
+
+The telemetry seam (`@deepseek-ai/dsh-session-telemetry`) now owns a backend-independent sharing vocabulary: `TelemetrySharingStatus` (`full` | `feedback-only` | `disabled`) plus a required abstract `sharing` member on the `Telemetry` service class — every backend must disclose its policy, so a consumer renders "not configured" only when no telemetry service is mounted. `@deepseek-ai/dsh-session-telemetry-otel` maps its serialized `TelemetryMode` (the [feedback-gated delivery decision](2026-08-05-feedback-gated-session-telemetry.md) owns the mode semantics) onto that status in the constructor and discloses it, including in `DISABLED`. The `/feedback` handler reads the mounted service through the plugin context (`ctx.get('telemetry')`, never a declared injection, so the command loads and runs without telemetry) and appends one sharing sentence to the acknowledgement: `Feedback recorded for session {id}. `. No service → `Session sharing is not configured.`; `disabled` → `Session sharing is disabled.`; `feedback-only` → `Session sharing is feedback-gated; recording feedback releases the session prefix for sharing.`; `full` → `Session sharing is enabled.`
+
+The disclosure states the current sharing policy only; it never promises delivery or retention. Handoff is the backend's non-blocking enqueue and batching, retry, and loss policy stay the backend SDK's, and a later reconfiguration can change what was shared, so the sentences claim nothing about what reached a collector or about future retention. The disclosure adds no session event and never reaches the model surface; the web client renders it through the existing command row (`CommandNode` outcome text) with no client change.
+
+## Alternatives considered
+
+**A client-side status RPC and badge.** Rejected because the acknowledgement is host-produced and the web client already renders the command result text verbatim in the command row; a separate RPC would duplicate the status in a second surface and add a wire contract for a sentence.
+
+**Declared `telemetry` injection in `command-feedback`.** Rejected because telemetry is optional: a declared injection fails plugin load when the service is absent, while the command must work without it. The plugin reads the service with `ctx.get('telemetry')` at handler time instead.
+
+**OTel package owns the vocabulary.** Rejected because `command-feedback` must not depend on the optional OTel backend package. The seam owns `TelemetrySharingStatus` so any backend can disclose a policy.
+
+## Consequences
+
+The acknowledgement is user-visible: it names the receiving session and reports the current sharing policy, honest about the fire-and-forget handoff. Package tests pin the sentence for each status and for the absent-service case; the assembled-browser e2e mounts the shipped telemetry row in FULL mode against a local dead endpoint and pins the shipped default sentence (`Session sharing is enabled.`) as a golden. The seam member is required, so a mounted backend always discloses a policy and the "not configured" sentence truthfully means no telemetry service; the `/feedback` command keeps working with no telemetry mounted. A still-blank web session renders no command row, so feedback recorded before the first message gets no visible acknowledgement (documented under the package README's limitations).
diff --git a/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.zh.md b/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.zh.md
new file mode 100644
index 0000000000..ac26b18ad5
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-07-feedback-acknowledgement-sharing-disclosure.zh.md
@@ -0,0 +1,27 @@
+# Agent Note: 反馈确认中的会话共享披露
+
+Status: implemented
+
+[English](2026-08-07-feedback-acknowledgement-sharing-disclosure.md) | 中文
+
+## 问题
+
+`/feedback` 命令会记录一个仅写入日志的 `feedback/record` 事件并确认用户,但确认文本没有携带关于会话去向的持久信息:挂载了会话遥测(`FULL`、`FEEDBACK_ONLY` 或 `DISABLED`)的部署无法告知用户其反馈和会话是否离开了进程,确认文本也没有回显接收会话的 id。命令插件无法读取共享策略,因为遥测 seam 只暴露采集能力,而 OTel 模式枚举位于可选的后端包中。
+
+## 决策
+
+遥测 seam(`@deepseek-ai/dsh-session-telemetry`)现在拥有与后端无关的共享词汇:`TelemetrySharingStatus`(`full` | `feedback-only` | `disabled`),并在 `Telemetry` 服务类上增加一个必需的抽象 `sharing` 成员——每个后端都必须披露其策略,因此消费方只有在未挂载任何遥测服务时才渲染「未配置」。`@deepseek-ai/dsh-session-telemetry-otel` 在构造函数中把序列化的 `TelemetryMode`(模式语义由[反馈门控投递决策](2026-08-05-feedback-gated-session-telemetry.md)负责)映射到该状态并披露,包括 `DISABLED` 模式。`/feedback` 处理器通过插件上下文读取已挂载的服务(`ctx.get('telemetry')`,绝不是声明的注入,因此命令在无遥测时也能加载和运行),并在确认文本后追加一句共享披露:`Feedback recorded for session {id}. <句子>`。无服务 → `Session sharing is not configured.`;`disabled` → `Session sharing is disabled.`;`feedback-only` → `Session sharing is feedback-gated; recording feedback releases the session prefix for sharing.`;`full` → `Session sharing is enabled.`
+
+披露只陈述当前的共享策略,绝不承诺投递或留存:交接是后端的非阻塞入队,批处理、重试与丢失策略仍归后端 SDK,且后续重新配置可能改变已共享的内容,因此句子不声称任何内容已到达采集端,也不声称未来的留存。披露不新增任何会话事件,也绝不会进入模型 surface;Web 客户端通过现有的命令行(`CommandNode` 的结果文本)原样渲染,无需客户端改动。
+
+## 备选方案
+
+**客户端新增状态 RPC 与徽标。** 拒绝,因为确认文本由宿主生成,Web 客户端已经在命令行中原样渲染命令结果文本;单独的 RPC 会在第二个 surface 重复该状态,并为一句文案新增线上契约。
+
+**在 `command-feedback` 中声明 `telemetry` 注入。** 拒绝,因为遥测是可选的:服务缺失时声明注入会导致插件加载失败,而命令必须在无遥测时可用。插件改为在处理器执行时用 `ctx.get('telemetry')` 读取服务。
+
+**由 OTel 包拥有词汇。** 拒绝,因为 `command-feedback` 不能依赖可选的 OTel 后端包。seam 拥有 `TelemetrySharingStatus`,任何后端都能披露策略。
+
+## 后果
+
+确认文本对用户可见:它点名接收会话并报告当前的共享策略,如实说明 fire-and-forget 交接。包级测试为每种状态以及无服务场景固定句子;组装浏览器 e2e 以 FULL 模式挂载随附的遥测行(指向本地 dead 端点),并以 golden 固定随附默认句子(`Session sharing is enabled.`)。seam 成员是必需的,因此已挂载的后端总会披露策略,「未配置」句子如实地表示没有遥测服务;`/feedback` 命令在未挂载遥测时仍能正常工作。仍为空白的新 Web 会话不渲染命令行,因此首条消息之前记录的反馈没有可见确认(已在包 README 的限制中记录)。
diff --git a/.agents/notes/implemented/feature/2026-08-07-workspace-picker-composer-entry.i18n.yaml b/.agents/notes/implemented/feature/2026-08-07-workspace-picker-composer-entry.i18n.yaml
new file mode 100644
index 0000000000..0d52923b6e
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-07-workspace-picker-composer-entry.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-07-workspace-picker-composer-entry.md
+2026-08-07-workspace-picker-composer-entry.md: dc9c26c291de6e7614ace3c787030c0032a9740d
+2026-08-07-workspace-picker-composer-entry.zh.md: 585c59b12392823a7388ab7a635a864bd4108929
diff --git a/.agents/notes/implemented/feature/2026-08-07-workspace-picker-composer-entry.md b/.agents/notes/implemented/feature/2026-08-07-workspace-picker-composer-entry.md
new file mode 100644
index 0000000000..dc9c26c291
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-07-workspace-picker-composer-entry.md
@@ -0,0 +1,29 @@
+# Agent Note: The no-Workspace composer opens the existing picker
+
+Status: implemented
+
+English | [中文](2026-08-07-workspace-picker-composer-entry.zh.md)
+
+## Problem
+
+The [session-scope decision](../architecture/2026-07-25-web-client-session-scope-and-provide-channel.md) keeps one resident composer before a Workspace exists, but its textarea was disabled and only the smaller Workspace chip could open the picker. The largest and most familiar starting affordance therefore rejected the user's first click even though a recovery action was available on the same surface.
+
+## Decision
+
+While no Workspace owns the new Session, the whole composer card activates the existing `conversation.hero.workspace` picker by pointer click — the card owns the click handler and its disabled controls let pointer events fall through, so the full capsule is one target — and the read-only resident textarea does the same by Enter or Space. `aria-haspopup="menu"` and `aria-expanded` describe the shared picker menu while it is mounted. On a fresh installation with no Workspace rows, the picker immediately hands off to the directory dialog and clears its expanded state; that dialog exposes its own accessibility semantics. A dashed l4 stroke (an SVG dash ring, since native `dashed` has a fixed pattern) with a business-blue hover marks the card as the pick affordance. The card contains `pointerdown`, so the open picker's outside-close cannot race the click's reopen — that close-then-open flickered the chip's expansion echo. Message submission, command, permission, model, and other Session-scoped controls remain locked until Workspace selection creates or reconnects a real Session.
+
+Workspace selection retains the existing owner and flow. `ConversationRoot` opens the picker, `WorkspacePicker` lists or creates the Workspace, and the same textarea DOM node becomes the editable composer after the Session arrives.
+
+## Alternatives considered
+
+**Keep the textarea disabled and emphasize the Workspace chip.** This preserves the old control boundary but leaves the dominant composer surface inert during the first action.
+
+**Place a transparent button over the textarea.** A button has direct trigger semantics, but it creates a second focusable element over the resident textarea and complicates the DOM-identity transition that preserves focus, IME, and draft behavior.
+
+**Accept a draft before Workspace selection.** This would require a client-owned draft Session or another pre-Session state axis. The feature only needs a discoverable path into the existing picker.
+
+## Consequences
+
+The first composer click now continues the required setup flow, and keyboard users can activate the same path. The textarea accurately reports read-only state until a Session exists, while adjacent controls remain disabled. The UI introduces no new Workspace state, transport, or directory-selection flow.
+
+Component coverage pins pointer and keyboard activation, the card-wide click target, the contained `pointerdown`, locked adjacent controls, picker expansion, and the same-node transition to an editable textarea. The assembled Web helper begins fresh Workspace setup through the textarea, so replayed browser scenarios exercise the shipped path.
diff --git a/.agents/notes/implemented/feature/2026-08-07-workspace-picker-composer-entry.zh.md b/.agents/notes/implemented/feature/2026-08-07-workspace-picker-composer-entry.zh.md
new file mode 100644
index 0000000000..585c59b123
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-07-workspace-picker-composer-entry.zh.md
@@ -0,0 +1,29 @@
+# Agent Note: 未选择 Workspace 时从编辑器打开现有选择器
+
+Status: implemented
+
+[English](2026-08-07-workspace-picker-composer-entry.md) | 中文
+
+## 问题
+
+[Session scope 决策](../architecture/2026-07-25-web-client-session-scope-and-provide-channel.md)会在 Workspace 存在前保留同一个常驻编辑器,但 textarea 处于禁用状态,只有较小的 Workspace chip 能打开选择器。用户首次点击最显眼、也最熟悉的输入区域时,界面不会响应,尽管同一界面已有继续操作的入口。
+
+## 决策
+
+新会话尚未归属任何 Workspace 时,整张输入卡片都可通过鼠标点击激活现有的 `conversation.hero.workspace` 选择器——点击处理器归卡片所有,其禁用控件放行指针事件,因此整个胶囊是同一个目标;只读的常驻 textarea 也可经 Enter 或 Space 激活。`aria-haspopup="menu"` 和 `aria-expanded` 在共享选择器菜单挂载时描述其展开状态。全新安装没有 Workspace 行时,选择器会立即转交目录对话框并清除自身的展开状态;该对话框使用自己的可访问性语义。虚线 l4 描边(SVG dash ring,因为原生 `dashed` 的间距不可调)配合 hover 时的 business 蓝,把卡片标记为选择入口。卡片会拦下 `pointerdown`,使已打开选择器的外点关闭无法与点击的重新打开竞态——先关后开会让 chip 的展开回显闪动。消息提交、命令、权限、模型及其他 Session 作用域控件会保持锁定,直到用户选择 Workspace 并创建或重新连接真实 Session。
+
+Workspace 选择继续使用现有 owner 和流程。`ConversationRoot` 打开选择器,`WorkspacePicker` 列出或创建 Workspace;Session 到达后,同一个 textarea DOM 节点变为可编辑状态。
+
+## 考虑过的替代方案
+
+**保持 textarea 禁用并突出 Workspace chip。** 这样能保留原有控件边界,但首次操作时最主要的编辑器区域仍然没有响应。
+
+**在 textarea 上方放置透明按钮。** 按钮具备直接的触发器语义,但它会在常驻 textarea 上方增加第二个可聚焦元素,并使保留焦点、输入法和草稿行为的 DOM identity 过渡更复杂。
+
+**在选择 Workspace 前接收草稿。** 这需要由 client 拥有的草稿 Session 或另一条 Session 前状态轴。此功能只需要提供一个更容易发现的现有选择器入口。
+
+## 后果
+
+用户首次点击编辑器即可继续必要的设置流程,键盘用户也能激活同一路径。textarea 会如实报告只读状态,直到 Session 存在;相邻控件仍处于禁用状态。界面没有引入新的 Workspace 状态、传输或目录选择流程。
+
+组件测试会固定鼠标和键盘激活、覆盖整卡的点击目标、被拦下的 `pointerdown`、相邻控件锁定、选择器展开,以及同一节点变为可编辑 textarea 的过渡。组装后的 Web helper 会通过 textarea 开始全新 Workspace 设置,因此重放浏览器场景会覆盖实际交付路径。
diff --git a/.agents/notes/implemented/feature/2026-08-08-web-background-task-display.i18n.yaml b/.agents/notes/implemented/feature/2026-08-08-web-background-task-display.i18n.yaml
new file mode 100644
index 0000000000..9984adfa0f
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-08-web-background-task-display.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-08-web-background-task-display.md
+2026-08-08-web-background-task-display.md: 558d0e26b3b8da83602296c5fdd0944886d4eab7
+2026-08-08-web-background-task-display.zh.md: 9c9f4643bb08c748381ede6bf585858ca1204ff7
diff --git a/.agents/notes/implemented/feature/2026-08-08-web-background-task-display.md b/.agents/notes/implemented/feature/2026-08-08-web-background-task-display.md
new file mode 100644
index 0000000000..558d0e26b3
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-08-web-background-task-display.md
@@ -0,0 +1,136 @@
+# Agent Note: Web background-task display
+
+Status: implemented
+
+English | [中文](2026-08-08-web-background-task-display.zh.md)
+
+## Problem
+
+`ctx.tasks` already runs every long-lived piece of work the harness starts in the background — `bash`, `pwsh`, `pty-send`, and one-shot background subagents — but its only reader was the model. [`dsh-tool-tasks`](../../../../packages/tasks/tool-tasks/README.md) exposes `task_list`, `task_output`, and `task_kill`, and nothing else observed the registry.
+
+A human at the Web client therefore could not see that a build was running, could not distinguish a finished task from a stuck one, and could not stop one. The only trace was the `run_in_background` tool card that printed a task id somewhere earlier in the transcript, and that card never updates again.
+
+The session header was already the place where per-session background activity lives: [`dsh-client-ui-subagent`](../../../../packages/client/ui-subagent/README.md) contributes the subagent catalog to `conversation.session.header.actions`. Placement was settled. What was missing was any channel at all that carried task state to a browser.
+
+## Decision
+
+Task state reaches the browser as **one whole-snapshot mux frame per session**, pushed at every registry commit point that changes what that session can see. The client keeps a last-wins mirror; a header action renders it. There is no RPC, no polling, and no client-side staleness bookkeeping.
+
+This ships the list alone. Per-task streamed output and a human-initiated cancellation are separate phases, and the channel is shaped so neither has to undo it.
+
+### Wire shape
+
+One frame in the mux stream:
+
+```ts ignore-check
+| { type: 'session/tasks'; sessionId: SessionId; tasks: TaskView[] }
+```
+
+`TaskView` is browser-safe and owned by the carrier at [`packages/host/apiproxy/src/api/tasks.ts`](../../../../packages/host/apiproxy/src/api/tasks.ts), alongside the other domain contracts, with its wire schema beside it in `tasks.schema.ts`:
+
+```ts
+import type { TaskId } from '@deepseek-ai/dsh-tasks/brand'
+
+export interface TaskView {
+ id: TaskId
+ kind: string
+ label: string
+ status: 'running' | 'stopping' | 'completed' | 'killed' | 'failed'
+ detail?: string
+ startedAt: number
+ finishedAt?: number
+}
+```
+
+`TaskId` comes from the cordis-free [`@deepseek-ai/dsh-tasks/brand`](../../../../packages/tasks/tasks/src/brand.ts) leaf — the same arrangement as the `@deepseek-ai/dsh-llm/brand` import `api/subagents.ts` already uses, because the `dsh-tasks` root reaches `dsh-agent` and is unreachable from a client program even as a type. Like every other non-root subpath in this workspace, it carries an explicit `tsconfig.base.json` `paths` entry; without one the TypeRT analyzer resolves the specifier to `lib/types/` and rejects the reference as unexported.
+
+`kind` is `string` on the wire rather than `TaskKind`. The kind map is merge-extensible by producer plugins, so a client build cannot enumerate the closed set; presentation falls through a documented default for an unrecognized kind.
+
+Three `TaskSnapshot` fields are deliberately absent: `ownerSession` (the frame's `sessionId` already carries it), `reported` (an internal notice-delivery bit with no user meaning), and `outputLimitBytes` (producer-owned model-presentation policy).
+
+The frame carries a whole snapshot rather than a delta for the reason [`session/queue`](../../../../packages/host/apiproxy/src/api/events.ts) states for itself: start, kill, settlement, reconnect, and a second browser tab all converge through one authoritative value. A session's task set is single-digit; the frame is small.
+
+### The task-registry change feed
+
+`TaskService` owns one observation method:
+
+```ts ignore-check
+abstract onTasksChanged(listener: TasksChangedListener): () => void
+```
+
+It fires **after** every commit that changes what `list(owner)` returns: registration at the end of `start()`, the `stopping` transition in `kill()`, settlement, and the removal `disposeOwner()` performs. An `undefined` owner means an unowned task changed, and therefore every caller's view changed.
+
+The listener is owner-granular rather than task-granular. The only consumer pushes whole snapshots, so a per-task record would be discarded on arrival — and a per-task feed cannot express the owner-disposal removal at all without inventing a tombstone status nothing else needs.
+
+`onTaskDone` is not a subset of this. It delivers the terminal record with the exact owner `Agent` under first-wins semantics that `dsh-tool-tasks` couples to `reported`; `onTasksChanged` is pure observation with no delivery meaning and marks nothing reported. Listener throws are contained and never awaited, matching `onTaskDone`, and each registration is an effect on the calling fiber.
+
+Service disposal deliberately announces nothing. Every `onTasksChanged` registration is an effect on the registry's own fiber, so the listeners are already gone by the time teardown clears the store; an observer learns the registry left through its own disposal, not through a final empty set.
+
+### The api-proxy carrier
+
+`mux()` subscribes `ctx.tasks.onTasksChanged` and pushes `session/tasks`; the subscription baseline rides next to the existing `session/subscribed` control frames, so a reconnecting client is current before it renders.
+
+Four rules the carrier keeps:
+
+- **Never resume.** A change push reads `tasks.list(owner)` with the exact `Agent` the listener supplied, which stays correct even while that owner's scope is tearing down and a lookup by id would already miss. The baseline instead reads `ctx.tasks.list(ctx.agents.get(session.id))` — the non-resuming registry read, where a session with no live Agent correctly yields only the unowned tasks. Neither path touches the [`api-remotes` Agent resolver](../../../../packages/api/remotes/src/agent-lookup.ts), which resumes a cold session as a side effect of lookup; listing must never revive a session the user merely scrolled past.
+- **Fan out unowned changes.** An `undefined` owner pushes a fresh snapshot to every subscribed session, because unowned tasks are visible to every caller.
+- **Stay optional.** The carrier reads `ctx.get('tasks')`. A composition without the registry emits no frames, and the client renders no entry point — the posture `sessionProjections` already has in this file.
+- **Say nothing about nothing.** The baseline is pushed only for sessions whose list is non-empty, and an absent key on the client means an empty list. A change that empties a list still pushes `[]`, because that one transition is the only thing the client cannot infer from absence.
+
+### The client mirror
+
+`SessionListState` carries `tasksBySession: Readonly>`, owned by `SessionManager` and folded from the frame under last-wins, with an emptied set stored as an absent key so absence and `[]` are one representation.
+
+It lives on the list mirror rather than on `Session` for three reasons: the header action already reads list state through `useSessions`, nothing needs the pre-instantiation buffering `session/queue` requires (no composer behavior depends on tasks), and a later sidebar indicator gets the data without opening a second channel.
+
+Two clears keep it honest. On re-subscribe the manager drops the session's mirror — the rule `session/queue` already follows, because a fresh baseline is arriving and this generation sends none for an empty set, so a retained list would survive as a phantom. On `host/session-removed` it drops the mirror again: owner disposal already removed the records registry-side, but that lands on the mux stream while the removal frame rides the host stream, so the two have no relative order.
+
+### The header action
+
+[`@deepseek-ai/dsh-client-ui-task`](../../../../packages/client/ui-task/README.md) registers one entry in `conversation.session.header.actions`, ordered after the subagent catalog. Its own README owns the presentation contract; the decisions worth recording here are that the control does not render at all until the session has a task, that the live badge is omitted at zero so a history-only session keeps a quiet entry point, and that settled rows stay visible because a failed task's `detail` is the only place its failure is legible.
+
+A running one-shot background subagent therefore appears both there and in the subagent catalog. The two answer different questions — the catalog navigates into the child's transcript, this list is the only handle a cancellation can ever attach to — and suppressing `kind: 'subagent'` here would leave the cancellation phase with no entry point for exactly those tasks.
+
+### What this deliberately does not do
+
+**No web path calls `ctx.tasks.read()`.** It consumes the single output cursor, so a browser read would silently take bytes the model's `task_output` will never see. This is an invariant worth a test rather than a convention, because the failure is invisible at the call site.
+
+**No cancellation.** That phase owes a decision the seam does not currently answer: `kill()` marks terminal delivery reported, so a human interrupt written against today's contract would leave the model believing its task is still running.
+
+**No output watermark on the frame.** The output phase's delta channel is where an anchor field earns its place; one added now would have no reader.
+
+## Alternatives considered
+
+**Signal frame plus RPC pull, the subagent-catalog shape.** Push a payload-free `tasks-changed` signal, debounce, then re-read authoritative state over a unary RPC. This is what the subagent catalog does, and the cost is visible in [`SessionManager`](../../../../packages/client/runtime/src/client/sessions/manager.ts): `catalogInflight` for single-flight, `catalogStale` for a trailing re-pull when a membership frame lands mid-request, `updateCatalogActivity` patching loaded rows in place *and* writing into the in-flight request so a response older than the frame gets overwritten, `parentAvailableOverride` replaying a stale `false`, and a reconnect path re-pulling every open catalog. That apparatus exists because the catalog's authority is split — durable lineage from a projection, liveness sampled at response time — and tasks have no durable half to justify inheriting it. It also fails specifically at the moment the output phase cares about: a task settles, its output stream closes immediately, but status only arrives after debounce plus round-trip, so the UI shows a running task with a dead stream for that window.
+
+**Popover-scoped polling with no seam change.** Cheapest to build and the only option that avoids touching `TaskService`. It cannot support a resident count on the trigger without a resident poll, and both later phases need a real change feed anyway, so it buys a week and spends it back.
+
+**A session-projection unit over durable task events.** Projection units fold over committed session events, so this would first require task lifecycle to become durable — `task/started` … `task/settled` as a standalone open/close bracket, with the last [`session/end-seed`](../../../../packages/core/session/src/types.ts) marking any unmatched opener as dead history, exactly as the compaction bracket already does. It is genuinely cheaper on the client: `dsh-tool-todo` shows the whole pattern in a fifteen-line unit, and the existing `session/projection` frames, history-tail block, and persisted checkpoint cache would have carried the data with no new wire surface, no carrier subscription, and no manager state. It was rejected because it buys that with a durable format change in service of a browser list, and because it does not extend to the phase it would most need to: [`spill/`](../../../../packages/spill/README.md) exists precisely so oversized tool output stays out of the log, so streamed task output cannot ride durable events either way. Nothing here forecloses revisiting it if durable task history becomes valuable on its own merits.
+
+**Reusing `PublicTaskSnapshot` from `dsh-tool-tasks`.** Nearly the right fields, but it belongs to the model-facing control surface. A wire type a browser program imports from a tool package couples client presentation to prompt-facing decisions and drags a host-only package into a client build.
+
+**Folding tasks into the subagent catalog as one "activity" panel.** One entry point instead of two. Rejected because `SubagentCatalogAction` is already 605 lines whose subject is a durable session-lineage tree including finished children; process-scoped tasks are a second data model with different identity, lifetime, and affordances, and the catalog's lazily-expanded branch, duration, and token contracts would all need rewriting to host them.
+
+**A host-global task list across every session.** The literal reading of "show all running tasks". Rejected because the registry's authorization fence is per-owner-session, so a global read needs a new access rule, and a global list has no business in a session's header — it would need its own home in the sidebar. Nothing in this design blocks adding it later; the per-session frames are the same data.
+
+## Testing
+
+The [web e2e scenario](../../../../apps/web/tests/background-task-list.e2e.ts) is the end-to-end proof and runs keyless: a real `run_in_background` bash call registers with `ctx.tasks`, the header count and row appear with no user interaction, and killing the task through the registry flips the open list to its producer detail. It asserts the whole delivery path rather than any single layer.
+
+Below it, [`tasks-local`](../../../../packages/tasks/tasks-local/tests/tasks.spec.ts) pins the change feed at all four commit points, its containment of a throwing observer, and its removal on both explicit disposal and fiber teardown; [`api-proxy-tasks`](../../../../packages/host/apiproxy/tests/api-proxy-tasks.spec.ts) pins the baseline-only-when-non-empty rule, the three change pushes, the dropped internal fields, the unowned fan-out, the no-resume guarantee, and the registry-absent composition; and the client suites pin the last-wins fold, the absent-key representation, both clears, and the component's ordering, duration, and dismissal behavior.
+
+## Consequences
+
+**A missed commit point leaks rows.** If `disposeOwner()` removal ever stops firing the feed, the client keeps tasks that no longer exist until the session disappears. The whole-snapshot shape makes this recoverable rather than corrupting — the next legitimate change repairs the list — but the disposal path is the one most easily forgotten, so it carries its own test.
+
+**Unowned-task fan-out is easy to under-implement.** Pushing only to the changed owner's session is correct for owned tasks and silently wrong for unowned ones, which are visible everywhere. The bug would surface only in compositions that create unowned tasks, which is why the carrier suite covers it directly.
+
+**The UI set is not the registry's set.** The header shows what one session can see, so a task owned by another session never appears in it even though the registry holds it — and because the registry is process-local, a restart empties every list while the transcript still shows the `run_in_background` cards that started them. Unowned tasks are the opposite case: they reach every session's list, exactly as `list(caller)` reports them to every caller.
+
+**Settled rows accumulate.** The registry retains settled tasks until owner disposal, so a long session with many background commands grows a long list. Capping the settled tail is a presentation change, not a protocol one, if it becomes a real complaint.
+
+**`stopping` is nearly unreachable today.** Only the model's `task_kill` produces it, so the state is rendered but rarely seen until human cancellation lands. It is in the union now because leaving a status out would have made that phase a wire change.
+
+**Two entry points for one running subagent.** Accepted deliberately, and bounded to one-shot background delegations. If it reads as noise in practice, the fix is presentational — the catalog row can cite the task rather than the task list hiding the kind.
+
+**A new non-root subpath needs its `paths` entry.** `@deepseek-ai/dsh-tasks/brand` had to be registered in `tsconfig.base.json` before the TypeRT analyzer would accept the reference. The failure mode is a confusing "not exported by" error from a generator far from the edit, so the entry is part of adding a subpath, not an optimization.
diff --git a/.agents/notes/implemented/feature/2026-08-08-web-background-task-display.zh.md b/.agents/notes/implemented/feature/2026-08-08-web-background-task-display.zh.md
new file mode 100644
index 0000000000..9c9f4643bb
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-08-web-background-task-display.zh.md
@@ -0,0 +1,136 @@
+# Agent Note: Web 后台任务展示
+
+Status: implemented
+
+[English](2026-08-08-web-background-task-display.md) | 中文
+
+## 问题
+
+`ctx.tasks` 已经承载了 harness 在后台启动的全部长时工作——`bash`、`pwsh`、`pty-send`,以及一次性后台 subagent——但它唯一的读者是模型。[`dsh-tool-tasks`](../../../../packages/tasks/tool-tasks/README.md) 暴露了 `task_list`、`task_output` 和 `task_kill`,除此之外没有任何东西观察这个注册表。
+
+于是 Web 端的人类看不到构建正在跑,分不清一个任务是已经完成还是卡死,也无法把它停掉。唯一的痕迹是 transcript 里更早某处那张打印了 task id 的 `run_in_background` 工具卡片,而那张卡片此后再也不会更新。
+
+会话 header 本来就是每会话后台活动的落点:[`dsh-client-ui-subagent`](../../../../packages/client/ui-subagent/README.md) 把 subagent 目录贡献到 `conversation.session.header.actions`。位置没有争议。缺的是任何一条把任务状态送到浏览器的通道。
+
+## 决策
+
+任务状态以**每会话一帧的整份快照**到达浏览器,在注册表每一个会改变该会话可见内容的提交点推出。客户端保持一份 last-wins 镜像,由一个 header 入口渲染。没有 RPC,没有轮询,客户端不需要任何过期状态管理。
+
+本次只交付列表。每个任务的流式输出与人类发起的中断是各自独立的阶段,而通道的形状让两者都不必推翻它。
+
+### 线路形状
+
+mux 流中的一帧:
+
+```ts ignore-check
+| { type: 'session/tasks'; sessionId: SessionId; tasks: TaskView[] }
+```
+
+`TaskView` 是浏览器安全类型,由载体在 [`packages/host/apiproxy/src/api/tasks.ts`](../../../../packages/host/apiproxy/src/api/tasks.ts) 里拥有,与其他领域契约并列,线路 schema 就在旁边的 `tasks.schema.ts`:
+
+```ts
+import type { TaskId } from '@deepseek-ai/dsh-tasks/brand'
+
+export interface TaskView {
+ id: TaskId
+ kind: string
+ label: string
+ status: 'running' | 'stopping' | 'completed' | 'killed' | 'failed'
+ detail?: string
+ startedAt: number
+ finishedAt?: number
+}
+```
+
+`TaskId` 取自不依赖 cordis 的 [`@deepseek-ai/dsh-tasks/brand`](../../../../packages/tasks/tasks/src/brand.ts) 叶子——与 `api/subagents.ts` 已经在用的 `@deepseek-ai/dsh-llm/brand` 导入是同一种安排,因为 `dsh-tasks` 根出口会牵到 `dsh-agent`,即便只作类型也无法被客户端程序触及。和本仓库其他每一个非根子路径一样,它带有显式的 `tsconfig.base.json` `paths` 条目;没有这一条,TypeRT 分析器会把该 specifier 解析到 `lib/types/` 并判定该引用未被导出。
+
+线路上的 `kind` 是 `string` 而非 `TaskKind`。kind 映射由生产者插件按声明合并扩展,客户端构建无法枚举这个闭集;遇到无法识别的 kind,呈现层走一条有文档的默认分支。
+
+`TaskSnapshot` 的三个字段被刻意省去:`ownerSession`(帧的 `sessionId` 已经带了)、`reported`(内部的通知投递位,对用户无意义),以及 `outputLimitBytes`(生产者拥有的模型呈现策略)。
+
+这一帧带整份快照而非增量,理由就是 [`session/queue`](../../../../packages/host/apiproxy/src/api/events.ts) 为自己写下的那条:启动、中断、结算、重连,以及第二个浏览器标签页,全都通过同一个权威值收敛。一个会话的任务集是个位数,帧很小。
+
+### 任务注册表变更订阅
+
+`TaskService` 拥有一个观察方法:
+
+```ts ignore-check
+abstract onTasksChanged(listener: TasksChangedListener): () => void
+```
+
+它在每一个会改变 `list(owner)` 返回内容的提交点**之后**触发:`start()` 末尾的注册、`kill()` 里转入 `stopping`、结算,以及 `disposeOwner()` 执行的移除。`owner` 为 `undefined` 表示一个无主任务发生了变化,因而每一个调用方的视图都变了。
+
+监听器按 owner 而非按任务分粒度。唯一的消费方推的是整份快照,逐任务记录到手即弃——而且逐任务的订阅根本无法表达 owner 销毁时的移除,除非发明一个别处都不需要的墓碑状态。
+
+`onTaskDone` 不是它的子集。后者按 first-wins 语义投递终态记录和确切的 owner `Agent`,`dsh-tool-tasks` 把这套语义与 `reported` 绑在一起;`onTasksChanged` 是纯观察,不含任何投递含义,也不把任何东西标为已上报。监听器抛错被包住且从不 await,与 `onTaskDone` 一致,每次注册都是调用方 fiber 上的 effect。
+
+服务销毁刻意什么都不通告。每个 `onTasksChanged` 注册都是注册表自身 fiber 上的 effect,等到 teardown 清空 store 时监听器早已消失;观察者通过自己的销毁而不是一份最终空集来得知注册表离开了。
+
+### api-proxy 载体
+
+`mux()` 订阅 `ctx.tasks.onTasksChanged` 并推送 `session/tasks`;订阅 baseline 紧挨着既有的 `session/subscribed` 控制帧发出,让重连的客户端在渲染前就是最新的。
+
+载体守着四条规则:
+
+- **绝不 resume。** 变更推送用监听器给出的确切 `Agent` 调 `tasks.list(owner)`,即使该 owner 的 scope 正在拆除、按 id 查找已经查不到,它依然正确。baseline 则读 `ctx.tasks.list(ctx.agents.get(session.id))`——不触发 resume 的注册表读法,没有活体 Agent 的会话正确地只得到无主任务。两条路径都不碰 [`api-remotes` 的 Agent 解析器](../../../../packages/api/remotes/src/agent-lookup.ts),那个解析器会把查询变成复活冷会话的副作用;列个任务不该让用户随手划过的会话活过来。
+- **无主变更要扇出。** `owner` 为 `undefined` 时向每一个已订阅会话推一份新快照,因为无主任务对所有调用方可见。
+- **保持可选。** 载体读 `ctx.get('tasks')`。没有挂注册表的组合不发任何帧,客户端也就不渲染入口——`sessionProjections` 在这个文件里已经是这个姿态。
+- **没有就不说。** baseline 只为列表非空的会话推送,客户端上键缺失即表示空列表。把列表清空的那次变更仍然推 `[]`,因为这一个转换是客户端唯一无法从「缺失」推断出来的东西。
+
+### 客户端镜像
+
+`SessionListState` 带有 `tasksBySession: Readonly>`,由 `SessionManager` 拥有,按 last-wins 从帧折叠而来;被清空的集合存为缺失的键,使「缺失」与 `[]` 成为同一种表示。
+
+它放在列表镜像而不是 `Session` 上,有三个理由:header 入口本来就通过 `useSessions` 读列表状态;没有任何东西需要 `session/queue` 那种实例化前的缓冲(没有 composer 行为依赖任务);将来侧栏加指示器时不必再开第二条通道。
+
+两处清理让它保持诚实。重新订阅时 manager 丢弃该会话的镜像——`session/queue` 已经遵循的规则,因为新的 baseline 正在路上,而这一世代对空集不发 baseline,被留下的列表会变成幽灵。`host/session-removed` 时再丢一次:owner 销毁在注册表侧已经移除了记录,但那件事落在 mux 流上而这一帧走 host 流,两者没有相对顺序。
+
+### header 入口
+
+[`@deepseek-ai/dsh-client-ui-task`](../../../../packages/client/ui-task/README.md) 在 `conversation.session.header.actions` 注册一个条目,排在 subagent 目录之后。呈现契约归它自己的 README;值得记在这里的决策是:会话没有任务时控件根本不渲染;活跃角标为零时省略,让只剩历史的会话保留一个安静的入口;终态行保持可见,因为失败任务的 `detail` 是其失败唯一可读之处。
+
+因此一个运行中的一次性后台 subagent 会同时出现在那里和 subagent 目录里。两者回答不同的问题——目录负责进入子会话的 transcript,而这个列表是中断能力唯一可能附着的句柄——在这里屏蔽 `kind: 'subagent'` 会让中断那一期恰好对这批任务没有入口。
+
+### 刻意不做的事
+
+**没有任何 Web 路径调用 `ctx.tasks.read()`。** 它消费唯一的输出游标,浏览器读一次就悄悄拿走了模型 `task_output` 永远看不到的字节。这该是一条有测试兜底的不变量而不是一条约定,因为它的故障在调用点完全不可见。
+
+**不做中断。** 那一期欠一个 seam 目前没有回答的决策:`kill()` 会把终态投递标为已上报,所以照今天的契约写出来的人类中断,会让模型一直以为它的任务还在跑。
+
+**帧上不带输出水位。** 输出那一期的增量通道才是锚点字段该出现的地方;现在加就是一个没有读者的字段。
+
+## 备选方案
+
+**信号帧加 RPC 拉取,即 subagent 目录的形状。** 推一个无 payload 的 `tasks-changed` 信号,防抖后用一元 RPC 重读权威状态。subagent 目录就是这么做的,代价在 [`SessionManager`](../../../../packages/client/runtime/src/client/sessions/manager.ts) 里一览无余:`catalogInflight` 做单飞行、`catalogStale` 在成员帧落于请求中途时补一次尾拉、`updateCatalogActivity` 既就地打补丁又往在途请求里写一份好让比帧更旧的响应被覆盖、`parentAvailableOverride` 重放一个过期的 `false`,还有重连时逐一重拉每个打开的目录。这套装置之所以存在,是因为目录的权威被劈成两半——持久血缘来自投影,活跃度是响应时刻的采样——而任务没有持久的那一半,不该继承这份复杂度。它还恰好在输出那一期最在意的时刻失效:任务结算,输出流立即关闭,状态却要等防抖加一次往返才到,那段窗口里 UI 显示一个流已死的运行中任务。
+
+**只在弹层打开时轮询,不改 seam。** 最省事,也是唯一不碰 `TaskService` 的选项。它无法在不常驻轮询的前提下支持触发器上的常驻计数,而后面两期反正都需要一条真正的变更订阅,所以它省下一周又还回去。
+
+**基于持久任务事件的 session-projection 单元。** 投影单元在已提交的会话事件上折叠,所以这条路要先让任务生命周期变持久——`task/started` … `task/settled` 作为一对独立的开合括号,由最后一个 [`session/end-seed`](../../../../packages/core/session/src/types.ts) 把未配对的开括号标为死历史,与 compaction 括号已有的做法完全一致。它在客户端确实更省:`dsh-tool-todo` 用十五行的单元展示了整套模式,而现成的 `session/projection` 帧、history-tail 块和持久化 checkpoint 缓存本可以承载这批数据,无需新线路面、无需载体订阅、无需 manager 状态。否决它,是因为这要拿一次持久格式变更去换一个浏览器列表,而且它并不能延伸到最需要它的那一期:[`spill/`](../../../../packages/spill/README.md) 的存在正是为了让超大工具输出留在日志之外,所以流式任务输出无论如何都不能骑在持久事件上。如果持久任务历史将来凭自身价值站得住,本设计不阻挡重新考虑它。
+
+**复用 `dsh-tool-tasks` 的 `PublicTaskSnapshot`。** 字段几乎就是对的,但它属于面向模型的控制面。浏览器程序从一个 tool 包导入线路类型,会把客户端呈现耦合到面向 prompt 的决策上,并把一个 host-only 包拖进客户端构建。
+
+**并进 subagent 目录做成统一的「活动」面板。** 一个入口而不是两个。否决的理由是 `SubagentCatalogAction` 已经 605 行,其主题是含已结束子会话的持久会话血缘树;进程域的任务是第二套数据模型,身份、生命期和可用动作都不同,而目录的懒展开分支、时长与 token 契约全都要重写才能容纳它们。
+
+**跨全部会话的 host 全局任务列表。**「显示所有运行中任务」的字面读法。否决是因为注册表的鉴权围栏是按 owner 会话的,全局读需要一条新的访问规则,而且全局列表不该出现在某个会话的 header 里——它需要侧栏里自己的位置。本设计没有阻挡后续再加;按会话的帧就是同一批数据。
+
+## 测试
+
+[web e2e 场景](../../../../apps/web/tests/background-task-list.e2e.ts)是端到端的证据,且无需密钥:一次真实的 `run_in_background` bash 调用注册进 `ctx.tasks`,header 的计数与行在没有任何用户操作的情况下出现,通过注册表杀掉该任务后打开着的列表翻到生产者给出的 detail。它断言的是整条投递链路,而不是其中某一层。
+
+在它之下,[`tasks-local`](../../../../packages/tasks/tasks-local/tests/tasks.spec.ts) 钉住变更订阅的全部四个提交点、对抛错观察者的包容,以及显式销毁与 fiber 拆除两条路径上的注销;[`api-proxy-tasks`](../../../../packages/host/apiproxy/tests/api-proxy-tasks.spec.ts) 钉住「非空才发 baseline」、三次变更推送、被丢弃的内部字段、无主扇出、不 resume 的保证,以及没有注册表的组合;客户端各套件钉住 last-wins 折叠、缺失键表示、两处清理,以及组件的排序、时长与关闭行为。
+
+## 影响
+
+**漏掉一个提交点会漏行。** 如果 `disposeOwner()` 的移除有朝一日不再触发订阅,客户端会一直留着已经不存在的任务,直到会话消失。整份快照的形状让这件事可恢复而非损坏——下一次正当变更就修好了——但销毁路径是最容易被忘掉的一条,所以它自带测试。
+
+**无主任务的扇出很容易做漏。** 只推给变更 owner 所在的会话,对有主任务是对的,对处处可见的无主任务则是悄悄错的。这个 bug 只会在会创建无主任务的组合里显形,所以载体套件直接覆盖了它。
+
+**UI 的集合不等于注册表的集合。** header 显示的是「一个会话能看到什么」,所以别的会话拥有的任务在这里永远不出现,尽管注册表里有它;而由于注册表是进程本地的,一次重启会清空所有列表,transcript 里那些启动它们的 `run_in_background` 卡片却还在。无主任务是反过来的情形:它们会进入每一个会话的列表,正如 `list(caller)` 对每个调用方都报告它们。
+
+**终态行会堆积。** 注册表把已结算任务留到 owner 销毁,所以一个跑了很多后台命令的长会话会积出长列表。如果真的成为抱怨,给终态尾巴加上限是呈现层改动而非协议改动。
+
+**`stopping` 今天几乎不可达。** 只有模型的 `task_kill` 会产生它,所以这个状态会被渲染但在人类中断落地之前很少见到。现在就纳入联合类型,是因为把它留在外面会让那一期变成一次线路变更。
+
+**一个运行中的 subagent 有两个入口。** 这是刻意接受的,且被限制在一次性后台委派这一种情况。如果实际用起来读着像噪声,修法是呈现层的——可以让目录行引用那个任务,而不是让任务列表隐藏这个 kind。
+
+**新增非根子路径必须补 `paths` 条目。** `@deepseek-ai/dsh-tasks/brand` 得先登记进 `tsconfig.base.json`,TypeRT 分析器才会接受该引用。它的故障表现是一条来自远离改动处的生成器的、令人困惑的「not exported by」错误,所以这个条目是新增子路径的组成部分,而不是优化。
diff --git a/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.i18n.yaml b/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.i18n.yaml
index c9d678da0e..970799a54b 100644
--- a/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md
-2026-08-08-windows-acl-restricted-token-sandbox.md: 7e8f229269233d9ac9baa65241ca02a4cf4c3f7c
-2026-08-08-windows-acl-restricted-token-sandbox.zh.md: eeb346b228b3559f487448e5d4ec525b7bb89525
+2026-08-08-windows-acl-restricted-token-sandbox.md: 972713e02860218853f421aa700a8b60b33ada5b
+2026-08-08-windows-acl-restricted-token-sandbox.zh.md: da41cb3f9aa46bab96a5fbb6c035205b22c442aa
diff --git a/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md b/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md
index 7e8f229269..972713e028 100644
--- a/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md
+++ b/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md
@@ -6,15 +6,15 @@ English | [中文](2026-08-08-windows-acl-restricted-token-sandbox.zh.md)
## Problem
-The [sandbox decision](2026-07-06-sandbox.md) leaves `PLATFORM_CHAINS.win32` empty, so shipped Windows profiles degrade to danger-full-access because no confining executor exists. The win32 rung must confine the two file-effect modes the sandbox vocabulary promises — `read-only` (zero writes) and `workspace-write` (writes under the workspace root plus a backend-defined temp area) — while leaving reads, network, and process visibility alone, because every mode permits reading.
+The original [sandbox decision](2026-07-06-sandbox.md) left `PLATFORM_CHAINS.win32` empty, so shipped Windows profiles degraded to danger-full-access because no confining executor existed. The win32 rung must govern the two file-effect modes in the sandbox vocabulary — `read-only` (no explicit writable root) and `workspace-write` (writes under the workspace root plus a backend-defined temp area) — while reporting any effects its mechanism cannot govern; reads, network, and process visibility remain outside this vocabulary.
## Decision
-Implement the rung directly on the raw ACL mechanism: duplicate the caller's token into a `WRITE_RESTRICTED` token (`CreateRestrictedToken` with `WRITE_RESTRICTED` + `DISABLE_MAX_PRIVILEGE` + `LUA_TOKEN`) whose restricting SIDs include a write SID (`S-1-4-x-y`); the write SID's Write ACEs on the workspace and temp roots are the entire write allowlist, because `WRITE_RESTRICTED` intersects write accesses only and reads keep the caller's full ambient access. The mechanism is the one huoyaoyuan/windows-acl-restrict-poc (`10e4dfb`) demonstrates; this port checks every API call and fails closed (the POC fail-opened on every ignored return value). The write SID is the PER-WORKSPACE identity, derived deterministically from the canonical workspace path (`workspaceWriteSid` — sha256 → `S-1-4-x-y`) and stored NOWHERE: the workspace-root ACE therefore materializes once per workspace per machine — the standing ACE is the cross-session reuse cache, and every later provision hits the exact-ACE skip (idempotent re-grant skips the eager full-tree re-propagation — no garbage collection) — instead of once per session, which is what the earlier per-session random SID paid a full tree propagation per session for. The seam derives the session's PRIVATE temp subdirectory from the session id + workspace (sha256, 16 hex — stored nowhere, so no tamper surface exists) and creates it exclusively; it is removed on provider dispose, and a crash leaves it as `%TEMP%` litter whose next resume fails loudly at the exclusive creation until temp hygiene reclaims it. The seam materializes the workspace ACE STANDING (never revoked — the cache) and the temp ACE REVOCABLY (revoked on provider dispose, so an inheritable ACE never outlives its session's temp directory on the ambient temp root). The token's restricting list is the keep-alive group plus the write SID only under workspace-write: read-only = [logon SID, Everyone] and workspace-write = [logon SID, Everyone, write SID]. The keep-alive invariants are logon SID + Everyone (early DLL init dies with 0xC0000142 and CNG crashes pwsh with 0xE0434352 without them). Read-only carries no write SID: a standing grant ACE from an earlier workspace-write period stays INERT (the pass-2 check grants only what the list carries, so read-only remains strictly zero-grant across a `/permission` downgrade or a crash-resumed session, while the standing ACE keeps the re-upgrade free). Authenticated Users is absent from BOTH lists — the WMI namespace security check fails (0x80041003), so CIM is unavailable in every confined mode, and the C:\-root tree-creation escape (standing `AU:(AD)` + `AU:(OI)(CI)(IO)(M)` ACEs) is closed in both; INTERACTIVE/LOCAL are likewise absent from both (the Public tree writes are denied — pinned by the runner's Public-probe regression). Workspace-write children see a PRIVATE per-session temp subdirectory (`\dsh-<16 hex>` derived from the session id + workspace — created exclusively, reparse points rejected, removed on provider dispose — TMP/TEMP rewritten by the runner — bwrap `--tmpfs /tmp` semantics). The restricted token's DEFAULT DACL is extended with a full-access write-SID ACE (`SetTokenInformation(TokenDefaultDacl)`): new objects created without an explicit security descriptor (anonymous pipes — CreatePipe, sync objects) then carry a restricting-SID ACE and pass the write pass-2 check at creation; NAMED pipes are exempt — their default security descriptor is the Win32 layer's user-mode default SD template (built by KernelBase — owner/SYSTEM/Admins full, Everyone/ANONYMOUS read-only), which the token cannot influence, so piped stdio capture stays denied for confined grandchildren (the POC-documented boundary, pinned by the runner suite). It ships as [`@deepseek-ai/dsh-sandbox-windows-acl`](../../../../packages/sandbox/sandbox-windows-acl/README.md) (backend plus the `./runner` argv-prefix entry), the `win32` chain rung of [`dsh-sandbox-local`](../../../../packages/sandbox/sandbox-local/README.md), and [`@deepseek-ai/dsh-pwsh-sandbox`](../../../../packages/bash/pwsh-sandbox/README.md) as the confining executor; the Windows platform layer re-enables the full permission surface (sandbox/sandbox-policy/permission/approval/fs-sandbox) over the confined pwsh stack.
+Implement the rung directly on the raw ACL mechanism: duplicate the caller's token into a `WRITE_RESTRICTED` token (`CreateRestrictedToken` with `WRITE_RESTRICTED` + `DISABLE_MAX_PRIVILEGE` + `LUA_TOKEN`) whose restricting SIDs carry distinct workspace and private-temp capabilities. `WRITE_RESTRICTED` intersects write accesses only, so reads keep the caller's ambient access while a write must also match one of these capability ACEs. The mechanism is the one huoyaoyuan/windows-acl-restrict-poc (`10e4dfb`) demonstrates; this port checks every API call and fails closed (the POC fail-opened on ignored failures). The per-workspace SID is derived deterministically from the canonical workspace path (`workspaceWriteSid` — sha256 → `S-1-4-x-y`); its standing workspace ACE is the cross-session reuse cache, and an exact-ACE skip prevents repeated eager tree propagation. Each live session/workspace pair instead gets a random private temp directory and a domain-separated SID derived from that path (`tempWriteSid`); its ACE is revocable, TMP/TEMP point at that directory, and the token default DACL names the temp SID so newly created temp objects do not acquire the shared workspace capability. A fork therefore cannot write its sibling's temp tree. A fresh provider chooses a new path and SID even for the same resumed session, so crash residue is inert litter rather than a collision or inherited capability; agentless calls create and remove the same shape per invocation. The ambient temp root is never an implicit grant. A workspace equal to or containing the temp root fails before any ACL mutation because its inheritable standing ACE would otherwise reach every private child; the direct API rejects overlap in either direction between a writable root and the actual private temp directory. PowerShell can complete its startup AppLocker probe through this private-temp capability, so `workspace-write` remains FullLanguage absent a host-wide policy; `read-only` cannot create the probe files and conservatively enters ConstrainedLanguage. That split is PowerShell startup behavior, not part of the ACL boundary. The token lists are read-only = [logon SID, Everyone] and workspace-write = [logon SID, Everyone, workspace SID, optional temp SID]. Logon SID + Everyone are keep-alive invariants (early DLL init dies with 0xC0000142 and CNG crashes pwsh with 0xE0434352 without them). Because Everyone remains, an external object granting Everyone write access clears both checks; because NTFS ACLs belong to file objects, a granted workspace hard link also grants an external alias. Rejecting all hard links would reject ordinary pnpm workspaces, so the provider reports `enforcement: 'partial'` and the native suite pins both gaps. Read-only carries no capability SID, so standing workspace ACEs remain inert across a mode downgrade. Authenticated Users is absent from both lists — CIM is unavailable, closing the C:\-root tree-creation escape — and INTERACTIVE/LOCAL are absent, denying Public-tree writes. New anonymous pipes and sync objects inherit the temp SID (or workspace SID when temp is disabled, Everyone under read-only) through `SetTokenInformation(TokenDefaultDacl)`; named pipes keep the Win32 layer's owner/SYSTEM/Admins-full, Everyone/ANONYMOUS-read-only template, so piped grandchild stdio remains denied. It ships as [`@deepseek-ai/dsh-sandbox-windows-acl`](../../../../packages/sandbox/sandbox-windows-acl/README.md), the `win32` rung of [`dsh-sandbox-local`](../../../../packages/sandbox/sandbox-local/README.md), and [`@deepseek-ai/dsh-pwsh-sandbox`](../../../../packages/bash/pwsh-sandbox/README.md) as the confining executor.
## How the restriction works (why no new identity)
-The identity routes restrict by *who* runs the child; this rung restricts by *token derivation*. An identity route (landstrip's restricted-user, AppContainer) runs the child under a fresh account or container SID that starts with zero ACEs on the host's files — everything, reads included, defaults to denied, and every path the child may touch must then be opened back up by writing ACEs for that identity: the wholesale DACL mutation that disqualified both alternatives. The restricted token keeps the caller's own SID and logon session: [`CreateRestrictedToken`](https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-createrestrictedtoken) derives a token that adds the restricting SIDs and the `WRITE_RESTRICTED` flag, so Windows performs the access check twice — once against the normal SIDs, once against the restricting SIDs — and grants write-class access only where both checks pass. Reads pass on the normal check alone (the caller's SIDs already carry read access everywhere the caller can read), which is why this rung needs no read grants and no new account; writes must additionally clear the orphan-SID check, which only the workspace and temp ACEs satisfy. `DISABLE_MAX_PRIVILEGE | LUA_TOKEN` synthesize the limited-user effect of a fresh account token-side, so even an elevated caller derives a filtered token. The same primitive could restrict reads (`SidsToDisable` turning SIDs deny-only), but a read-restricted token would need per-path read grants — reintroducing exactly the cost the identity routes pay — and the sandbox vocabulary never requires read confinement.
+The identity routes restrict by *who* runs the child; this rung restricts by *token derivation*. An identity route (landstrip's restricted-user, AppContainer) runs the child under a fresh account or container SID that starts with zero ACEs on the host's files — everything, reads included, defaults to denied, and every path the child may touch must then be opened back up by writing ACEs for that identity: the wholesale DACL mutation that disqualified both alternatives. The restricted token keeps the caller's own SID and logon session: [`CreateRestrictedToken`](https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-createrestrictedtoken) derives a token that adds the restricting SIDs and the `WRITE_RESTRICTED` flag, so Windows performs the access check twice — once against the normal SIDs, once against the restricting SIDs — and grants write-class access only where both checks pass. Reads pass on the normal check alone (the caller's SIDs already carry read access everywhere the caller can read), which is why this rung needs no read grants and no new account; writes must additionally clear the capability-SID check, which only the workspace and temp ACEs satisfy. `DISABLE_MAX_PRIVILEGE | LUA_TOKEN` synthesize the limited-user effect of a fresh account token-side, so even an elevated caller derives a filtered token. The same primitive could restrict reads (`SidsToDisable` turning SIDs deny-only), but a read-restricted token would need per-path read grants — reintroducing exactly the cost the identity routes pay — and the sandbox vocabulary never requires read confinement.
## Alternatives considered
@@ -32,11 +32,11 @@ The [landstrip evaluation](../../rejected/feature/2026-07-26-evaluate-landstrip-
## Consequences
-Bought: write-only confinement with no new OS floor (`CreateRestrictedToken` predates the mxc releases by two decades), reads/network/process visibility untouched exactly as the mode vocabulary requires, and fail-closed errors carrying the API name and the exact Win32 code. Cost: no read-side or network isolation; console isolation unavailable (hidden-console children die with `STATUS_DLL_INIT_FAILED`; children share the host console); standing ACE mutations on the granted roots (caller-owned directories; workspace ACEs stand forever by design — the reuse cache, invisible residue when a workspace is renamed — temp ACEs revoked by provider dispose together with the derived private temp directory — a crash leaves both behind and the next resume fails loudly at the exclusive creation until temp hygiene reclaims the directory); grant materialization is an EAGER full-tree propagation (`SetNamedSecurityInfoW` walks every descendant immediately — tens of seconds on large workspaces), paid once per workspace per machine by the per-workspace identity; CIM is unavailable in BOTH confined modes (AuthUsers dropped from both lists — the WMI namespace security check fails, and `Get-ComputerInfo` silently returns incomplete results) as the price of closing the C:\-root tree-creation escape in both; FAT-class (non-ACL) targets outside the granted roots remain writable under both modes (no security descriptors to intersect — a legacy residue treated as unsupported, warn-only, documented in the README); NULL-DACL directories are not identity-preserving under a grant+revoke round-trip (documented edge, the POC shares it); `whoami` and token-inspection cmdlets fail under the restricted token (diagnostic noise, documented); and BOTH confined modes run `pwsh` in ConstrainedLanguage mode — the restricted token trips PowerShell's lockdown detection, so `Add-Type`, non-core .NET statics (`[System.IO.*]::`, `[math]::`), COM objects, and reflection fail with "only core types" errors while `-f` formatting, property access, and core cmdlets/types keep working, and the language mode cannot be lifted back to FullLanguage from inside — taught to the model in the pwsh tool description and documented in the package README's Known Limitations; BOTH confined modes also deny named-pipe opens — libuv's piped-stdio spawns fail with EPERM (the POC-documented "no output redirection" boundary; inherited/ignored stdio and anonymous pipes work) — documented in the package README's Known Limitations and taught to the model in the pwsh tool description.
+Bought: write-only confinement with no new OS floor (`CreateRestrictedToken` predates the mxc releases by two decades), reads/network/process visibility untouched exactly as the mode vocabulary requires, and fail-closed errors carrying the API name and exact Win32 code. Sessions share the intentionally standing workspace capability but not their revocable temp capabilities; restart residue cannot block or authorize a resumed session. Cost: enforcement is structurally partial because Everyone-granted writes and NTFS hard-link aliases cannot be path-confined by this token shape; no read-side or network isolation; console isolation unavailable (hidden-console children die with `STATUS_DLL_INIT_FAILED`; children share the host console); standing workspace ACE mutations (the reuse cache, plus inert residue when a workspace is renamed) and random temp litter after an unclean shutdown until OS hygiene reclaims it; EAGER full-tree workspace propagation (`SetNamedSecurityInfoW` walks every descendant immediately — tens of seconds on large workspaces), paid once per workspace per machine; CIM unavailable in both confined modes (Authenticated Users is absent, closing the C:\-root tree-creation escape); FAT-class non-ACL targets still writable; NULL-DACL directories not identity-preserving under a grant/revoke round trip; `whoami` and token-inspection cmdlets failing under the restricted token; read-only pwsh entering ConstrainedLanguage while workspace-write remains FullLanguage absent host policy; and named-pipe opens remaining denied, so libuv piped-stdio grandchildren fail with EPERM while inherited/ignored stdio and anonymous pipes work. The package README owns these operational limits.
## Testing
-The product-visible Windows roster flip is win32-only, so the keyless snapshot fixtures — which must replay on macOS/Linux — cannot cover it; the bundle composition specs ([`base.spec.ts`](../../../../packages/bundle/base/tests/base.spec.ts), [`windows-shell.spec.ts`](../../../../apps/cli/tests/windows-shell.spec.ts)) plus the win32 real-runner suites (`packages/sandbox/sandbox-windows-acl/tests/`, `packages/bash/pwsh-sandbox/tests/`) are the substitute evidence, and the CI Windows lane owns the assembled signal. The grant machinery is pinned cross-platform by `packages/sandbox/sandbox-local/tests/acl-grants.spec.ts` (the derived private-temp identity — deterministic per session + workspace, distinct across sessions — one-shot materialization, exclusive temp creation with reparse-point rejection and self-cleanup on failure, clean-restart re-grant of the same derived directory, the standing-vs-revocable lifecycle across dispose and the mode-switch cycle, and the derived-SID argv contract — with the Win32 surface mocked) and on win32 by `workspace-sid.spec.ts` (derivation determinism/shape/distinctness), `grant.spec.ts` (real-DACL materialization: revocable paths revoke on dispose, standing paths survive it), the `acl.spec.ts` idempotent-grant fast-path and standing-ACE-after-dispose contract, the `failure-paths.spec.ts` suspension-orphan regression (AssignProcessToJobObject failure terminates the child), and the `runner.spec.ts` `--write-sid` contract (caller-owned grants, private temp subdir through TMP/TEMP, both-mode CIM-denial probes, the mode-downgrade regression — a standing grant ACE is inert under read-only and effective again on re-upgrade — the ambient-writable Public-probe regression (a C:\Users\Public subdirectory write is denied under both modes), and the ConstrainedLanguage pins in both modes, plus the grandchild-stdio matrix pins — inherited/ignored stdio spawns succeed while piped capture is DENIED in both modes). The runner-failure classification is exit-gated on 127 (a confined command that merely prints the `windows-acl-run:` signature on a non-127 exit is never misclassified as "the command did not run" — pinned in the pwsh-sandbox helper suite).
+The product-visible Windows roster flip is win32-only, so keyless snapshots that must replay on macOS/Linux cannot cover it; bundle composition specs plus the win32 real-runner suites are the substitute evidence, and the CI Windows lane owns the assembled signal. `sandbox-local/tests/acl-grants.spec.ts` pins random temp allocation, per-session/workspace reuse, fork/workspace separation, crash-resume non-collision, paired argv SIDs, failure cleanup, and standing-versus-revocable lifecycle with Win32 mocked. On Windows, `workspace-sid.spec.ts` pins workspace/temp derivation and domain separation; `acl.spec.ts` pins real DACL lifecycle; and `runner.spec.ts` pins paired-SID validation, sibling temp denial under a shared workspace SID, per-call agentless temp creation/removal, TMP/TEMP rewriting, mode downgrade, Public denial, Everyone/hard-link partial boundaries, mode-specific PowerShell language behavior, and grandchild stdio. ARM64 and emulated x64 native runs own the architecture-specific acceptance evidence.
## Related
diff --git a/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md b/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md
index eeb346b228..da41cb3f9a 100644
--- a/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md
+++ b/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md
@@ -6,15 +6,15 @@ Status: implemented
## Problem
-[沙盒决策](2026-07-06-sandbox.md)把 `PLATFORM_CHAINS.win32` 留空,交付的 Windows profile 因为没有可用的隔离执行器而退化为 danger-full-access。win32 档必须实现沙盒词汇表承诺的两个文件效果模式——`read-only`(零写入)与 `workspace-write`(仅工作区根目录加后端定义的临时区域可写)——同时保持读、网络与进程可见性不受影响,因为所有模式都允许读取。
+最初的[沙箱决策](2026-07-06-sandbox.md)将 `PLATFORM_CHAINS.win32` 留空,因此交付的 Windows profile 因不存在隔离执行器而退化为 danger-full-access。win32 档必须约束沙箱词汇表中的两种文件效果模式——`read-only`(不显式授予任何可写根目录)与 `workspace-write`(允许写入工作区根目录及后端定义的临时区域)——并报告其机制无法约束的任何效果;读取、网络与进程可见性仍在这套词汇之外。
## Decision
-直接基于原始 ACL 机制实现该档:把调用者令牌复制为 `WRITE_RESTRICTED` 受限令牌(`CreateRestrictedToken`,`WRITE_RESTRICTED` + `DISABLE_MAX_PRIVILEGE` + `LUA_TOKEN`),其 restricting SIDs 中包含写入 SID(`S-1-4-x-y`);工作区与临时目录上写入 SID 的 Write ACE 就是全部写入白名单,因为 `WRITE_RESTRICTED` 只对写访问做交集检查,读保持调用者的完整环境访问。该机制来自 huoyaoyuan/windows-acl-restrict-poc(`10e4dfb`)的演示;本移植检查每一个 API 调用并 fail-closed(POC 因忽略返回值而 fail-open)。写入 SID 是**按工作区**的身份,由规范工作区路径确定性派生(`workspaceWriteSid`——sha256 → `S-1-4-x-y`),且**任何地方都不存储**:工作区根目录 ACE 因此每台机器每个工作区只物化一次——常驻 ACE 就是跨会话复用缓存,此后每次供给都命中精确 ACE 跳过(幂等重授权跳过急切的全树重传播——不做垃圾回收)——而不是每会话一次,这正是先前每会话随机 SID 每个会话都要付一次全树传播的代价。seam 从会话 id + 工作区派生会话的**私有**临时子目录(sha256、16 位 hex——任何地方都不存储,因此不存在篡改面)并独占创建;它在提供方 dispose 时移除,崩溃则把它留作 `%TEMP%` 垃圾,其下一次恢复会在独占创建处大声失败,直到临时目录卫生机制将其回收。seam 把工作区 ACE **常驻**物化(绝不撤销——就是缓存),把临时 ACE **可回收**物化(提供方 dispose(资源释放)时撤销,因此可继承 ACE 不会在环境临时根目录上比其会话的临时目录活得更久)。令牌的 restricting list 是保活组加上仅 workspace-write 下的写入 SID:read-only = [登录 SID、Everyone],workspace-write = [登录 SID、Everyone、写入 SID]。保活不变式是登录 SID + Everyone(没有它们,早期 DLL init 会以 0xC0000142 死亡,CNG 会让 pwsh 以 0xE0434352 崩溃)。Read-only 不含写入 SID:先前 workspace-write 时期留下的常驻授权 ACE 保持**失效**(pass-2 检查只授予列表所携带的内容,因此 read-only 在 `/permission` 降级或崩溃后恢复的会话中始终保持严格零授权,而常驻 ACE 让重新升级保持零成本)。Authenticated Users 在**两种**列表中都缺席——WMI namespace 安全校验失败(0x80041003),因此 CIM 在每一种受限模式下都不可用,且 C:\-root 建树逃逸(驻留的 `AU:(AD)` + `AU:(OI)(CI)(IO)(M)` ACE)在两种模式下都被关闭;INTERACTIVE/LOCAL 同样在两种列表中都缺席(Public 树的写入被拒绝——由 runner 的 Public-probe 回归钉住)。Workspace-write 子进程看到的是私有的每会话临时子目录(`\dsh-<16 hex>`——由会话 id + 工作区派生、独占创建、拒绝 reparse point、提供方 dispose 时移除——TMP/TEMP 由 runner 重写——bwrap `--tmpfs /tmp` 语义)。受限令牌的**默认 DACL** 被扩展一条写入 SID 全权 ACE(`SetTokenInformation(TokenDefaultDacl)`):此后不带显式安全描述符创建的新对象(匿名管道——CreatePipe、同步对象)自带 restricting SID ACE,创建时的写 pass-2 检查通过;**named pipe 例外**——其默认安全描述符是 Win32 层在用户态安装的默认 SD 模板(由 KernelBase 构建——owner/SYSTEM/Admins 全权、Everyone/ANONYMOUS 只读),令牌无法影响,因此受限孙进程的管道 stdio 捕获保持拒绝(POC 记载的边界,由 runner 套件钉住)。它以 [`@deepseek-ai/dsh-sandbox-windows-acl`](../../../../packages/sandbox/sandbox-windows-acl/README.md)(后端加 `./runner` argv 前缀入口)、[`dsh-sandbox-local`](../../../../packages/sandbox/sandbox-local/README.md) 的 `win32` 链档、以及作为隔离执行器的 [`@deepseek-ai/dsh-pwsh-sandbox`](../../../../packages/bash/pwsh-sandbox/README.md) 交付;Windows 平台层在受限 pwsh 栈之上重新启用完整权限面(sandbox/sandbox-policy/permission/approval/fs-sandbox)。
+直接基于原始 ACL 机制实现该档:把调用者令牌复制为 `WRITE_RESTRICTED` 受限令牌(`CreateRestrictedToken`,`WRITE_RESTRICTED` + `DISABLE_MAX_PRIVILEGE` + `LUA_TOKEN`),其 restricting SIDs 携带彼此独立的工作区能力与私有临时目录能力。`WRITE_RESTRICTED` 只对写访问做交集检查,因此读取保留调用者的环境访问,而写入还必须匹配这些能力 ACE 之一。该机制来自 huoyaoyuan/windows-acl-restrict-poc(`10e4dfb`)的演示;本移植检查每个 API 调用并 fail-closed(POC 因忽略失败而 fail-open)。工作区 SID 由规范工作区路径确定性派生(`workspaceWriteSid`——sha256 → `S-1-4-x-y`);其常驻工作区 ACE 是跨会话复用缓存,精确 ACE 跳过可避免重复的急切全树传播。每个活跃的会话/工作区对则获得一个随机私有临时目录,以及一个从该路径派生的、经过域分离的 SID(`tempWriteSid`);其 ACE 可回收,TMP/TEMP 指向该目录,令牌默认 DACL 列入该临时 SID,因此新建的临时对象不会获得共享的工作区能力。fork 因此无法写入同级会话的临时目录树。即使恢复的是同一会话,新的提供方也会选择新的路径和 SID,因此崩溃残留只是失效垃圾,而非冲突或继承的能力;无 agent(智能体)的调用会逐调用创建并移除同样的形态。环境临时根目录绝不会被隐式授权。如果工作区等于或包含临时根目录,调用会在任何 ACL 改动发生前失败,因为否则其可继承的常驻 ACE 会向每个私有子目录授权;直接 API 会拒绝可写根目录与实际私有临时目录在任一方向上的重叠。PowerShell 可借助这项私有临时目录能力完成启动时的 AppLocker 探针,因此在没有主机范围策略时,`workspace-write` 会保持 FullLanguage;`read-only` 无法创建探针文件,会保守地进入 ConstrainedLanguage。这一区别属于 PowerShell 启动行为,不是 ACL 边界的一部分。令牌列表为 read-only = [登录 SID、Everyone],workspace-write = [登录 SID、Everyone、工作区 SID、可选临时 SID]。登录 SID + Everyone 是保活不变式(没有它们,早期 DLL 初始化会以 0xC0000142 死亡,CNG 会让 pwsh 以 0xE0434352 崩溃)。由于 Everyone 仍在列表中,向 Everyone 授予写访问的外部对象会通过两次检查;由于 NTFS ACL 属于文件对象,工作区内获授权的硬链接也会使同一对象的外部别名获得授权。拒绝所有硬链接会让普通 pnpm 工作区不可用,因此提供方报告 `enforcement: 'partial'`,原生套件则钉住这两个缺口。Read-only 不含任何能力 SID,因此常驻工作区 ACE 在模式降级后保持失效。Authenticated Users 在两种列表中都不存在——CIM 不可用,从而关闭 C:\-root 建树逃逸——INTERACTIVE/LOCAL 也不存在,因此 Public 树写入被拒绝。新建匿名管道和同步对象通过 `SetTokenInformation(TokenDefaultDacl)` 继承临时 SID(禁用临时目录时继承工作区 SID,read-only 下继承 Everyone);named pipe 保持 Win32 层 owner/SYSTEM/Admins 全权、Everyone/ANONYMOUS 只读的模板,因此受限孙进程的管道 stdio 仍被拒绝。它以 [`@deepseek-ai/dsh-sandbox-windows-acl`](../../../../packages/sandbox/sandbox-windows-acl/README.md)、[`dsh-sandbox-local`](../../../../packages/sandbox/sandbox-local/README.md) 的 `win32` 档,以及作为隔离执行器的 [`@deepseek-ai/dsh-pwsh-sandbox`](../../../../packages/bash/pwsh-sandbox/README.md) 交付。
## How the restriction works (why no new identity)
-身份路线靠"**谁**在跑子进程"来限制,本档靠"令牌派生"来限制。身份路线(landstrip 的 restricted-user、AppContainer)用全新账户或容器 SID 运行子进程,该身份在宿主的文件上从零条 ACE 开始——一切访问(包括读)默认拒绝,子进程要碰的每条路径都必须事后为那个身份补写 ACE 才能放行:这正是让两个备选方案出局的全盘 DACL 改造。受限令牌保留调用者自己的 SID 与 logon session:[`CreateRestrictedToken`](https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-createrestrictedtoken) 派生一个加入 restricting SIDs 与 `WRITE_RESTRICTED` 标志的令牌,于是 Windows 做两次访问检查——一次按正常 SID,一次按 restricting SIDs——只有两次都放行,写类访问才被授予。读只凭正常检查即可通过(调用者的 SID 在其可读范围内本来就携带读权限),所以本档不需要任何读授权、也不需要新账户;写还必须额外通过孤儿 SID 检查,而只有工作区与临时目录的 ACE 能满足它。`DISABLE_MAX_PRIVILEGE | LUA_TOKEN` 在令牌侧合成了新账户的受限用户效果,即使提升过的调用者派生的也是过滤令牌。同一原语其实也能限制读(`SidsToDisable` 把 SID 变为 deny-only),但受限读的令牌需要逐路径的读授权——恰好重新引入身份路线付出的代价——而沙盒词汇表从不要求读隔离。
+身份路线靠"**谁**在跑子进程"来限制,本档靠"令牌派生"来限制。身份路线(landstrip 的 restricted-user、AppContainer)用全新账户或容器 SID 运行子进程,该身份在宿主的文件上从零条 ACE 开始——一切访问(包括读)默认拒绝,子进程要碰的每条路径都必须事后为那个身份补写 ACE 才能放行:这正是让两个备选方案出局的全盘 DACL 改造。受限令牌保留调用者自己的 SID 与 logon session:[`CreateRestrictedToken`](https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-createrestrictedtoken) 派生一个加入 restricting SIDs 与 `WRITE_RESTRICTED` 标志的令牌,于是 Windows 做两次访问检查——一次按正常 SID,一次按 restricting SIDs——只有两次都放行,写类访问才被授予。读只凭正常检查即可通过(调用者的 SID 在其可读范围内本来就携带读权限),所以本档不需要任何读授权、也不需要新账户;写还必须额外通过能力 SID 检查,而只有工作区与临时目录的 ACE 能满足它。`DISABLE_MAX_PRIVILEGE | LUA_TOKEN` 在令牌侧合成了新账户的受限用户效果,即使提升过的调用者派生的也是过滤令牌。同一原语其实也能限制读(`SidsToDisable` 把 SID 变为 deny-only),但受限读的令牌需要逐路径的读授权——恰好重新引入身份路线付出的代价——而沙盒词汇表从不要求读隔离。
## Alternatives considered
@@ -32,11 +32,11 @@ AppContainer 令牌没有环境读访问:每个可读路径都必须预先通
## Consequences
-所得:仅写隔离、不引入新的 OS 版本下限(`CreateRestrictedToken` 比 mxc 的版本早二十年)、读/网络/进程可见性完全不受影响(与模式词汇表一致)、fail-closed 错误携带 API 名与精确 Win32 错误码。所失:无读侧或网络隔离;控制台隔离不可用(隐藏控制台子进程以 `STATUS_DLL_INIT_FAILED` 死亡;子进程共享宿主控制台);被授权根目录上有驻留 ACE 改动(目录须为调用者所有;工作区 ACE 按设计永久常驻——复用缓存,工作区改名时成为不可见残留——临时 ACE 由提供方 dispose 连同派生的私有临时目录一起回收——崩溃会把两者都留下,下一次恢复会在独占创建处大声失败,直到临时目录卫生回收该目录);授权物化是急切的全树传播(`SetNamedSecurityInfoW` 立即遍历每个后代——在大型工作区上耗时数十秒),因按工作区身份,每台机器每个工作区只付一次;CIM 在**两种**受限模式下都不可用(AuthUsers 从两种列表中被移除——WMI namespace 安全校验失败,`Get-ComputerInfo` 静默返回不完整结果),作为关闭两种模式下 C:\-root 建树逃逸的代价;位于被授权根目录之外的 FAT 类(无 ACL)目标在两种模式下仍可写(没有可做交集的安全描述符——作为历史残留处理:不支持、仅警告、已在 README 中记录);NULL DACL 目录在 grant+revoke 往返下不保持身份(记录在案的边角,POC 亦有此行为);`whoami` 与令牌检查 cmdlet 在受限令牌下失败(诊断噪音,已记录);且**两种**受限模式都以 ConstrainedLanguage 模式运行 `pwsh`——受限令牌触发 PowerShell 的锁定检测,因此 `Add-Type`、非核心 .NET 静态调用(`[System.IO.*]::`、`[math]::`)、COM 对象与反射都会以“only core types”错误失败,而 `-f` 格式化、属性访问与核心 cmdlet/类型继续工作,语言模式也无法从内部提升回 FullLanguage——已在 pwsh 工具描述中教给模型,并记录在包 README 的 Known Limitations 中;**两种**受限模式同样拒绝 named-pipe 打开——libuv 的管道 stdio spawn 以 EPERM 失败(POC 记载的“无法重定向输出”边界;继承/忽略的 stdio 与匿名管道可用)——记录在包 README 的 Known Limitations 中,并在 pwsh 工具描述中教给模型。
+所得:仅写隔离、不引入新的 OS 版本下限(`CreateRestrictedToken` 比 mxc 的版本早二十年)、读/网络/进程可见性完全不受影响(与模式词汇表一致),且 fail-closed 错误携带 API 名与精确 Win32 错误码。会话共享有意常驻的工作区能力,但不共享各自可回收的临时能力;重启残留既不能阻塞恢复的会话,也不能向其授权。所失:强制执行在结构上只能是部分的,因为此令牌形态无法把 Everyone 授予的写入与 NTFS 硬链接别名限制在路径边界内;无读侧或网络隔离;控制台隔离不可用(隐藏控制台子进程以 `STATUS_DLL_INIT_FAILED` 死亡;子进程共享宿主控制台);工作区常驻 ACE 改动(复用缓存,以及工作区改名后的失效残留)与异常关闭后遗留的随机临时目录垃圾,直到 OS 卫生机制将其回收;工作区授权采用急切的全树传播(`SetNamedSecurityInfoW` 立即遍历每个后代——大型工作区上耗时数十秒),每台机器每个工作区只付一次;CIM 在两种受限模式下均不可用(Authenticated Users 不存在,从而关闭 C:\-root 建树逃逸);FAT 类无 ACL 目标仍可写;NULL-DACL 目录在 grant/revoke 往返下不保持身份;`whoami` 与令牌检查 cmdlet 在受限令牌下失败;read-only pwsh 会进入 ConstrainedLanguage,而在没有主机策略时 workspace-write 保持 FullLanguage;named pipe 打开仍被拒绝,因此 libuv 管道 stdio 的孙进程以 EPERM 失败,而继承/忽略的 stdio 与匿名管道可用。包 README 负责记录这些运行限制。
## Testing
-产品可见的 Windows 阵容切换仅存在于 win32,而 keyless 快照夹具必须在 macOS/Linux 上可重放,因此无法覆盖它;替代证据是 bundle 组合 spec([`base.spec.ts`](../../../../packages/bundle/base/tests/base.spec.ts)、[`windows-shell.spec.ts`](../../../../apps/cli/tests/windows-shell.spec.ts))加上 win32 真实 runner 套件(`packages/sandbox/sandbox-windows-acl/tests/`、`packages/bash/pwsh-sandbox/tests/`),组装态信号由 CI 的 Windows lane 负责。授权机制在跨平台侧由 `packages/sandbox/sandbox-local/tests/acl-grants.spec.ts` 钉住(派生的私有临时身份——按会话 + 工作区确定性、跨会话相异——一次性物化、独占临时目录创建并拒绝 reparse point、失败时自我清理、干净重启时对同一派生目录的重新授权、dispose 与模式切换循环中的常驻/可回收生命周期,以及派生 SID 的 argv 契约——mock 掉 Win32 表面),win32 侧由 `workspace-sid.spec.ts`(派生的确定性/形态/相异性)、`grant.spec.ts`(真实 DACL 物化:可回收路径在 dispose 时撤销、常驻路径存活)、`acl.spec.ts` 的幂等授权快速路径与 dispose 后常驻 ACE 契约、`failure-paths.spec.ts` 的 suspension-orphan 回归(AssignProcessToJobObject 失败会终止子进程)与 `runner.spec.ts` 的 `--write-sid` 契约(调用者所有目录的授权、经 TMP/TEMP 的私有临时子目录、两种模式下的 CIM 拒绝探针、模式降级回归——驻留授权 ACE 在 read-only 下失效并在重新升级后再度生效——环境可写 Public-probe 回归(对 C:\Users\Public 子目录的写入在两种模式下都会被拒绝),以及两种模式下对 ConstrainedLanguage 的钉定,加上孙进程 stdio 矩阵钉定——继承/忽略的 stdio spawn 成功,而管道捕获在两种模式下都被**拒绝**)钉住。runner 失败分类以 127 退出码为门槛(受限命令仅仅在非 127 退出时打印 `windows-acl-run:` 签名,也绝不会被误分类为"命令未运行"——由 pwsh-sandbox helper 套件钉住)。
+产品可见的 Windows 阵容切换仅存在于 win32,而必须在 macOS/Linux 上可重放的 keyless 快照无法覆盖它;替代证据是 bundle 组合 spec 加上 win32 真实 runner 套件,组装态信号由 CI 的 Windows lane 负责。`sandbox-local/tests/acl-grants.spec.ts` 在 mock Win32 的情况下钉住随机临时目录分配、按会话/工作区复用、fork/工作区分离、崩溃后恢复不冲突、成对 argv SID、失败清理,以及常驻/可回收生命周期。在 Windows 上,`workspace-sid.spec.ts` 钉住工作区/临时目录派生与域分离;`acl.spec.ts` 钉住真实 DACL 生命周期;`runner.spec.ts` 钉住成对 SID 验证、共享工作区 SID 时对同级会话临时目录的拒绝、无 agent 调用的逐调用临时目录创建/移除、TMP/TEMP 重写、模式降级、Public 拒绝、Everyone/硬链接部分边界、按模式区分的 PowerShell 语言行为与孙进程 stdio。ARM64 与模拟 x64 原生运行负责提供架构特定的验收证据。
## Related
diff --git a/.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.i18n.yaml b/.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.i18n.yaml
new file mode 100644
index 0000000000..65e73bdff8
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.md
+2026-08-09-parallel-subagent-delegations.md: 01dc2043c9a79666857c4f5aa988fc45dc7cdf56
+2026-08-09-parallel-subagent-delegations.zh.md: 2c2bd700e362000b41adbd1f3cec9ba26ff60b93
diff --git a/.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.md b/.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.md
new file mode 100644
index 0000000000..01dc2043c9
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.md
@@ -0,0 +1,47 @@
+# Agent Note: Parallel subagent delegations
+
+Status: implemented
+
+English | [中文](2026-08-09-parallel-subagent-delegations.zh.md)
+
+## Problem
+
+A model that wants fan-out batches several `subagent` calls into one assistant message — that batch is the parallel intent. The delegation tool declared no `isConcurrencySafe` classifier, so the fail-closed scheduler ([parallel tool-call Agent Note](2026-07-10-parallel-tool-call-execution.md)) treated every foreground delegation as an exclusive barrier: nine cards in the GUI, one child running, eight queued behind it for its full runtime.
+
+The original conservative stance — a unary classifier cannot prove that sibling delegations have disjoint workspace effects — had stopped protecting anything. `run_in_background: true` and continuable delegations already overlap with every later call, including writes; `dsh-workflow-workerthread` already runs up to its concurrency ceiling of children through the same `ctx.subagents.start()` providers against the shared workspace. Only the foreground variant was serialized.
+
+## Decision
+
+`dsh-tool-subagent` declares `isConcurrencySafe: () => true` for every call form (foreground, one-shot background, continuable), so sibling delegations in one assistant step overlap under the loop's rolling pool up to `maxParallelToolCalls`, with results still committed in model order.
+
+The declaration satisfies the scheduler's safety contract structurally: a child works in its own session, a run never mutates the parent session (the start-time appends — `sandbox/mode`, `approval/policy`, `subagent/descriptor` — land only in the child's own log), and the tool returns its outputs to the loop for ordered commit. The one-shot background form's one parent-owned write is registering a Task through `tasks.start` — a synchronous, commutative insertion that satisfies the scheduler note's shared-state clause rather than the stronger no-mutation property. The provider seam requires concurrent starts and continuable preparations for distinct children to isolate operation-local state, cancellation, settlement, and cleanup. The bundled providers satisfy that contract: spawn and fork keep no mutable state between starts, fork reads only the parent's completed-turn prefix, out-of-process providers allocate state per run, and the continuation manager reserves a unique child identity and lock for each preparation.
+
+Coordinating sibling workspace effects is the model's responsibility, the stance the product already takes for background, continuable, and workflow children. Peer harnesses agree: Claude Code's Task tool is unconditionally concurrency-safe (cap 10), oh-my-pi's task tool defaults to its overlapping `shared` class, opencode's task tool runs unbounded under its SDK, and Codex sidesteps the question by making delegation an asynchronous spawn/wait mailbox.
+
+Capacity stays where the scheduler note put it: `maxParallelToolCalls` caps one step's unsettled tool calls — and therefore concurrently running foreground children — while background and continuable calls settle at start and free their pool slot, so children they leave running are not capped by it. LLM providers own their own capacity controls.
+
+## Testing
+
+Package tests pin the classifier for both call forms. A gate test drives the registry directly with two children that each block until both have started, proving the half the declaration depends on: the tool body and provider start path tolerate concurrent dispatch — hidden serialization in that stack would deadlock instead of passing silently. A continuable gate holds two provider preparations at the same await, cancels one caller before publication, and proves that the cancelled child leaves no Agent or durable Session while its sibling reaches inbox acceptance and persists independently. The scheduling half, classification actually producing overlap, is owned by the classifier pin and the snapshot below.
+
+The authored `subagent-parallel` snapshot pins the assembled-app transcript: one assistant message carries two subagent calls, the parent log records `tool/call, tool/call, tool/result, tool/result` (serial execution would interleave call/result pairs), and both children complete as separate sessions. Its twin delegations are deliberately identical: `dsh-llm-replay` binds child scripts by first-call order and the harvester orders children by `createdAt`, and neither is deterministic across concurrent children (the `XXX(concurrent-subagents)` marker), so only interchangeable twins replay race-free today.
+
+## Alternatives considered
+
+**Keep delegations exclusive.** The status quo protected nothing: background and workflow children already overlap freely with writes, so serializing the foreground variant only added latency and contradicted the model's explicit batching intent.
+
+**An input-sensitive classifier.** The call's arguments are a free-text description and prompt; nothing in them distinguishes a safe delegation from an unsafe one, so a conditional classifier would be theater.
+
+**A Codex-style asynchronous spawn/wait redesign.** Continuable children plus `send_message` already provide the asynchronous channel; rebuilding the foreground contract around a mailbox would discard a working synchronous result path to solve a scheduling problem one declaration fixes.
+
+**A per-instance `concurrencySafe` config knob.** No consumer needs a serial deployment: `maxParallelToolCalls: 1` already restores global serial execution, and peer-harness prior art defaults delegation to concurrency-safe.
+
+## Consequences
+
+Sibling children can race on shared workspace or external resources; the model owns that coordination, as it already does for every other overlapping child. Concurrent children also compete for LLM provider quota; `maxParallelToolCalls` caps only unsettled calls, not children a background or continuable call left running.
+
+Two one-shot background delegations in one message acquire their model-visible task ids (`subagent-`) in dispatch-race order. The ids are logged, so replay stays valid, but a snapshot scenario that distinguishes its background children would inherit the same determinism constraint as twin child sessions.
+
+Ordered commits may hold a fast child's result behind a slow earlier sibling — the trade the [scheduler note](2026-07-10-parallel-tool-call-execution.md) already accepted; live surfaces still show each child's own progress.
+
+A concurrent-children snapshot scenario with distinct prompts still needs replay-harness support (deterministic child-script binding and harvest ordering); until then such scenarios must use interchangeable twin delegations.
diff --git a/.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.zh.md b/.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.zh.md
new file mode 100644
index 0000000000..2c2bd700e3
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.zh.md
@@ -0,0 +1,47 @@
+# Agent Note: 并行 subagent 委派
+
+Status: implemented
+
+[English](2026-08-09-parallel-subagent-delegations.md) | 中文
+
+## 问题
+
+想要扇出的模型会把多个 `subagent` 调用合并进同一条 assistant 消息:这个批次本身就是并行意图。委派工具此前没有声明 `isConcurrencySafe` 分类器,按安全侧原则设计的调度器([并行工具调用 Agent Note](2026-07-10-parallel-tool-call-execution.md))便把每个前台委派都当作独占屏障:GUI 里显示九张卡片,却只有一个子 agent(智能体)在运行,其余八个要在它的整个运行期间排在其后等待。
+
+最初的保守立场(一元分类器无法证明同级委派的工作区效果互不相交)已经不再保护任何东西:`run_in_background: true` 和可继续委派本来就会与其后的每个调用重叠执行,包括写入;`dsh-workflow-workerthread` 也早已通过同样的 `ctx.subagents.start()` 提供方在共享工作区上并发运行子 agent,数量可达其并发上限。只有前台形态被串行化。
+
+## 决策
+
+`dsh-tool-subagent` 为每种调用形态(前台、一次性后台、可继续)都声明 `isConcurrencySafe: () => true`,因此同一 assistant 步骤中的同级委派会在循环的滚动池下重叠执行,上限为 `maxParallelToolCalls`,结果仍按模型顺序提交。
+
+该声明在结构上满足调度器的安全约定:子 agent 在自己的会话中工作,运行绝不变更父会话(启动时追加的 `sandbox/mode`、`approval/policy`、`subagent/descriptor` 只落在子 agent 自己的日志里),工具把输出返回给循环,由循环按顺序提交。一次性后台形态对父级拥有状态的唯一写入是通过 `tasks.start` 注册一个 Task——这是一次同步、可交换的插入,满足的是调度器 Agent Note 中的共享状态条款,而非更强的「无变更」性质。提供方 seam 要求针对不同子 agent 的并发启动和可继续准备分别隔离操作局部状态、取消、结算和清理。内置提供方满足这项约定:spawn 和 fork 在各次启动之间不保留可变状态,fork 只读取父级已完成轮次的前缀,进程外提供方按每次运行分配状态,继续执行管理器则为每次准备预留唯一的子 agent 身份和锁。
+
+协调同级工作区效果是模型的职责,产品对后台、可继续和工作流子 agent 已经采取同样的立场。同类 harness 的做法一致:Claude Code 的 Task 工具无条件并发安全(上限 10);oh-my-pi 的 task 工具默认归入其可重叠的 `shared` 类别;opencode 的 task 工具在其 SDK 下不设上限地运行;Codex 则把委派做成异步 spawn/wait 信箱,绕开了这个问题。
+
+容量控制仍保持在调度器 Agent Note 所定的位置:`maxParallelToolCalls` 限制单个步骤中未结算的工具调用数量——因而也限制并发运行的前台子 agent 数量——而后台和可继续调用在启动时即结算并释放池位,它们留下运行的子 agent 不受该上限约束。LLM(大语言模型)提供方负责自身的容量控制。
+
+## 测试
+
+包测试固定了两种调用形态的分类器。一个门控测试直接驱动注册表,其两个子 agent 各自阻塞,直到两者都已启动,以此证明该声明所依赖的那一半:工具体和提供方启动路径能容忍并发分发——这条栈中任何隐藏的串行化都会造成死锁,而不是静默通过。一个可继续门控测试让两项提供方准备停在同一个 await 上,在发布前取消其中一个调用方,并证明已取消的子 agent 不会留下 agent 或持久会话,而其同级则到达 inbox 接受状态并独立持久化。另一半(分类真正产生重叠执行)由分类器 pin 测试和下述快照负责。
+
+人工编写的 `subagent-parallel` 快照固定了组装后应用的 transcript(文本记录):一条 assistant 消息携带两个 subagent 调用,父级日志记录为 `tool/call, tool/call, tool/result, tool/result`(串行执行会让调用/结果成对交错出现),两个子 agent 各自作为独立会话完成。其中的孪生委派刻意做成完全相同:`dsh-llm-replay` 按首次调用顺序绑定子脚本,harvester 按 `createdAt` 对子 agent 排序,二者在并发子 agent 之间都不具确定性(即 `XXX(concurrent-subagents)` 标记),因此目前只有可互换的孪生委派才能无竞态地回放。
+
+## 备选方案
+
+**保持委派独占。** 现状没有保护任何东西:后台和工作流子 agent 本来就可以带着写入自由重叠,串行化前台形态只会增加延迟,还违背模型显式表达的批量意图。
+
+**使用输入敏感的分类器。** 该调用的参数只有自由文本的描述和提示词;其中没有任何内容能区分安全委派与不安全委派,因此条件式分类器只会流于形式。
+
+**按 Codex 风格重新设计为异步 spawn/wait。** 可继续子 agent 加上 `send_message` 已经提供了异步通道;围绕信箱重建前台约定,等于为了解决一条声明就能修复的调度问题,丢弃一条可用的同步结果路径。
+
+**按实例提供 `concurrencySafe` 配置开关。** 没有消费方需要串行部署:`maxParallelToolCalls: 1` 已能恢复全局串行执行,同类 harness 的先例也默认委派并发安全。
+
+## 影响
+
+同级子 agent 可能在共享工作区或外部资源上发生竞态;这项协调由模型负责,正如模型对其他所有重叠子 agent 已经承担的那样。并发子 agent 还会争用 LLM 提供方配额;`maxParallelToolCalls` 只限制未结算的调用,不限制后台或可继续调用留下运行的子 agent。
+
+同一条消息中的两个一次性后台委派按分发竞态顺序获得各自模型可见的 task id(`subagent-`)。这些 id 已被记录,因此回放仍然有效;但需要区分后台子 agent 的快照场景会继承与孪生子会话相同的确定性约束。
+
+有序提交可能让快速子 agent 的结果排在更早的缓慢同级之后等待,这是[调度器 Agent Note](2026-07-10-parallel-tool-call-execution.md)已经接受的取舍;实时界面仍会展示每个子 agent 各自的进度。
+
+使用不同提示词的并发子 agent 快照场景仍需要回放 harness 的支持(确定性的子脚本绑定与收集排序);在此之前,此类场景必须使用可互换的孪生委派。
diff --git a/.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.i18n.yaml
new file mode 100644
index 0000000000..ac90a1c70d
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.md
+2026-08-10-continuable-subagent-policy-inheritance.md: c9b75f2840eb2f124f040d138b761ee145fc6f83
+2026-08-10-continuable-subagent-policy-inheritance.zh.md: 8bd7f68c578ed827c756a415f017eb8cb61e5721
diff --git a/.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.md b/.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.md
new file mode 100644
index 0000000000..c9b75f2840
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.md
@@ -0,0 +1,29 @@
+# Agent Note: Continuable subagent policy inheritance — the durable child log owns the delegation-time snapshot
+
+Status: implemented
+
+English | [中文](2026-08-10-continuable-subagent-policy-inheritance.zh.md)
+
+## Problem
+
+The one-shot in-process driver has seeded parent sandbox/approval overrides into its children since the [in-process policy-inheritance decision](2026-07-25-subagent-policy-inheritance.md), but the continuable path never did: `SubagentContinuationManager` materialization applied only child composition and the activation setup registry. The default bundle wires both delegation tools as `backgroundMode: continuable`, so in a default deployment every background child silently fell back to deployment defaults — a parent switched to `danger-full-access` produced children stuck at `workspace-write` whose every out-of-workspace operation raised an approval prompt, and a parent's unattended `'never'` approval stance reverted to prompting ([dsh-external/issues#334](https://github.com/dsh-external/issues/issues/334)).
+
+## Decision
+
+The capture/append pair moved from the one-shot driver into the seam's shared child-agent module (`dsh-subagent/src/child-agent.ts`), the declared one home for shared child composition: `captureDelegatedPolicyOverrides(parent)` snapshots `sandboxPolicy.overrideOf(parent.session)` through optional `ctx.get` and pins the child approval policy to `'never'` ([approvals-pinned decision](2026-08-10-subagent-approval-pinned-never.md)), and `appendDelegatedPolicyOverrides(childSession, overrides)` appends the `source: 'delegation'` events. The one-shot driver and the continuation manager both call them, so the two paths cannot drift.
+
+`startContinuable` captures before its first await (`prepareContinuable`), the same "a later parent switch belongs to the parent's future" boundary as one-shot. The snapshot travels in `MaterializeInputs.create`, so only fresh materialization appends the events during unpublished setup, after any fork seed. A cold resume passes no `create` inputs and appends nothing: the persisted child log already carries the delegation events, and replaying the log IS the state. The durable child log — not the current Activation, not the resuming parent — owns the child's effective policy, so a parent switch between residency epochs never retroactively changes a durable child.
+
+## Alternatives considered
+
+- **An activation-setup-registry contribution** (`registerContinuableSetup`) — rejected: a contribution receives only the child context, so it cannot capture the parent's overrides at the delegation boundary; the registry applies on cold resume as well as fresh creation, which would re-append or re-capture; and nothing ties a contribution's capture to the start call's synchronous prefix, so the pre-await capture guarantee would be lost.
+- **Re-capturing the parent's overrides at cold resume** — rejected: a resumed child would silently change policy with the parent's later switches, breaking the snapshot-at-delegation semantic and making effective policy depend on resume timing instead of the child's own log. A parent that wants a resumed child under new policy re-delegates.
+- **Importing the one-shot driver's inline logic from the continuation manager** — rejected: the Service Definition package cannot depend on its own provider package, and duplicating the capture/append pair in `continuation.ts` invites drift; `child-agent.ts` already holds every other shared composition step.
+- **Seeding the events into the descriptor seed turn** — rejected: the capture value is not known when the seed is assembled for every caller, and the one-shot precedent already establishes unpublished-setup appends as the ordering that places inherited facts after fork history with `firstLiveSeq` intact.
+
+## Consequences
+
+- Default-bundle background delegation (`backgroundMode: continuable`) now inherits a parent's explicit sandbox override and pins the child to `'never'` approvals; compositions without either policy service behave unchanged.
+- `dsh-subagent` gains optional peer types on `dsh-sandbox-policy` and `dsh-user-approval` (the `ctx.get` pattern the one-shot driver used); `dsh-subagent-inprocess` drops its policy-service peers and type imports entirely and delegates to the shared helpers.
+- The continuable suite (`packages/subagent/subagent/tests/continuation-inheritance.spec.ts`) pins fresh-start seeding, pre-await capture, default omission, cold-resume snapshot stability, and fork-seed precedence; the ACP snapshot scenario `subagent-continuable-inheritance` pins the child's delegation event and read-only runtime context through the assembled app and fails when the capture is removed.
+- Out-of-process providers (`acp`, `dsh-sdk`, `claude-code`, `codex`) support no continuable children (`prepareContinuable` absent), and their one-shot children keep their own deployment policy (`inheritsParentContext = false`); cross-process policy propagation remains out of scope.
diff --git a/.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.zh.md b/.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.zh.md
new file mode 100644
index 0000000000..8bd7f68c57
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.zh.md
@@ -0,0 +1,29 @@
+# Agent Note: 可继续 subagent 策略继承——持久化子日志拥有委派时快照
+
+Status: implemented
+
+[English](2026-08-10-continuable-subagent-policy-inheritance.md) | 中文
+
+## 问题
+
+自[进程内策略继承决策](2026-07-25-subagent-policy-inheritance.md)以来,一次性进程内驱动器一直会把父级的沙箱/审批覆盖项注入其子级,但可继续路径从未这样做:`SubagentContinuationManager` 的物化只应用子级组合与 Activation(激活)设置注册表。默认组合包把两个委派工具都配置为 `backgroundMode: continuable`,因此在默认部署中,每个后台子 agent(智能体)都静默回退到部署默认值:切换到 `danger-full-access` 的父级产出的子 agent 卡在 `workspace-write`,每次工作区外操作都会触发审批提示;父级无人值守的 `'never'` 审批立场也退回为发起提示的行为([dsh-external/issues#334](https://github.com/dsh-external/issues/issues/334))。
+
+## 决策
+
+捕获/追加这对函数从一次性驱动器移入该 seam 的共享子 agent 模块(`dsh-subagent/src/child-agent.ts`),即声明的共享子级组合唯一归属之处:`captureDelegatedPolicyOverrides(parent)` 通过可选的 `ctx.get` 对 `sandboxPolicy.overrideOf(parent.session)` 建立快照,并把子级审批策略钉定为 `'never'`([审批钉定决策](2026-08-10-subagent-approval-pinned-never.md)),`appendDelegatedPolicyOverrides(childSession, overrides)` 则追加 `source: 'delegation'` 事件。一次性驱动器与继续执行管理器都调用它们,因此两条路径不会出现偏差。
+
+`startContinuable` 在其第一次 await(`prepareContinuable`)之前完成捕获,沿用与一次性路径相同的「父级后续切换属于父级的未来」边界。快照放在 `MaterializeInputs.create` 中传递,因此只有全新物化会在未发布的设置阶段、排在任何 fork 种子之后追加这些事件。冷恢复(cold resume)不传入 `create` 输入,也不追加任何内容:持久化的子日志已经携带委派事件,而回放该日志本身就是状态。子 agent 的生效策略由持久化子日志拥有,而不是当前 Activation,也不是发起恢复的父级,因此父级在驻留纪元(residency epoch)之间的切换绝不会追溯性地改变一个持久化子 agent。
+
+## 考虑过的替代方案
+
+- **一项 Activation 设置注册表贡献**(`registerContinuableSetup`):不予采纳。贡献只接收子级上下文,因此无法在委派边界捕获父级的覆盖项;该注册表在冷恢复与全新创建时都会应用,会导致重复追加或重复捕获;而且没有任何机制把贡献的捕获绑定到 start 调用的同步前缀,await 前捕获的保证会因此丢失。
+- **在冷恢复时重新捕获父级覆盖项**:不予采纳。恢复的子 agent 会随父级后续切换静默改变策略,这会破坏委派时快照的语义,并让生效策略取决于恢复时机而非子级自身的日志。希望恢复的子 agent 采用新策略的父级应重新委派。
+- **让继续执行管理器导入一次性驱动器的内联逻辑**:不予采纳。Service Definition 包不能依赖自己的提供方包,而在 `continuation.ts` 中复制捕获/追加这对函数会招致偏差;`child-agent.ts` 已经承载其余每个共享组合步骤。
+- **把这些事件写入描述符种子轮次**:不予采纳。种子为每个调用方组装时,捕获值尚不可知;而且一次性路径的先例已经确立:在未发布的设置阶段追加,才是把继承事实排在 fork 历史之后、同时保持 `firstLiveSeq` 不变的顺序。
+
+## 后果
+
+- 默认组合包的后台委派(`backgroundMode: continuable`)现在会继承父级显式的沙箱覆盖项,并把子级钉定为 `'never'` 审批;未组合任一策略服务的组合保持原有行为。
+- `dsh-subagent` 新增针对 `dsh-sandbox-policy` 与 `dsh-user-approval` 的可选 peer 类型(即一次性驱动器所用的 `ctx.get` 模式);`dsh-subagent-inprocess` 完全移除自己的策略服务 peer 与类型导入,委托给共享辅助函数。
+- 可继续测试套件(`packages/subagent/subagent/tests/continuation-inheritance.spec.ts`)锁定全新启动的种子写入、await 前捕获、默认值省略、冷恢复快照稳定性与 fork 种子优先级;ACP 快照场景 `subagent-continuable-inheritance` 经组装后的应用锁定子级的委派事件与只读运行时上下文,移除捕获时即失败。
+- 进程外提供方(`acp`、`dsh-sdk`、`claude-code`、`codex`)不支持可继续子 agent(没有 `prepareContinuable`),其一次性子 agent 保留自身的部署策略(`inheritsParentContext = false`);跨进程策略传播仍不在范围内。
diff --git a/.agents/notes/implemented/feature/2026-08-10-creator-guidance-introduce-cue.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-creator-guidance-introduce-cue.i18n.yaml
new file mode 100644
index 0000000000..08233cc4f0
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-creator-guidance-introduce-cue.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-10-creator-guidance-introduce-cue.md
+2026-08-10-creator-guidance-introduce-cue.md: 888fee7b3def585ed3098fedcb7bc6169ee26a22
+2026-08-10-creator-guidance-introduce-cue.zh.md: d80260abd1995df1f95e3f24fefcb265bda64c11
diff --git a/.agents/notes/implemented/feature/2026-08-10-creator-guidance-introduce-cue.md b/.agents/notes/implemented/feature/2026-08-10-creator-guidance-introduce-cue.md
new file mode 100644
index 0000000000..888fee7b3d
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-creator-guidance-introduce-cue.md
@@ -0,0 +1,33 @@
+# Agent Note: Creator guidance lands as an introduce cue on the preset chip
+
+Status: implemented
+
+English | [中文](2026-08-10-creator-guidance-introduce-cue.zh.md)
+
+## Problem
+
+Authoring a preset happens inside a Creator-mode session, but the settings section gave no path into that fact. The creator entry sat outside the roster groups, the custom group vanished entirely while it had no member, and clicking the entry dropped the user onto the new-session screen with nothing marking what had changed: the staged preset chip rendered exactly as if the user had picked it by hand. Users reported not understanding that the flow had moved, or that the session they were about to start was the place where the preset gets built (#2184).
+
+## Decision
+
+The custom group stays on screen while empty — heading plus the creator entry, which lives inside the group as the standing "your preset will appear here" affordance rather than floating below the roster.
+
+A pick staged from another screen carries a one-shot `introduce` flag through the seat store (`stage(id, introduce)`), and the chip announces it: the preset icon eases in over 150ms, then the name's characters fade up on a stagger the moment the icon lands. The stagger is capped twice — 40ms per tick for short CJK names, and one shared 200ms reveal window (`min(40, 200/(n-1))`) so a long Latin name finishes in the same time as its CJK counterpart instead of dragging the run out per character. CSS owns the motion; the component arms it and acknowledges the cue once the run is over, so the flag never replays on a later mount. `prefers-reduced-motion` and an empty display name acknowledge immediately with no run.
+
+The cue is pure presentation: it is client-side seat-store state, never a session event, because the model-visible composition is already carried by the staged preset itself.
+
+## Alternatives considered
+
+**A toast or callout on the new-session screen.** It explains more, but it points at nothing — the chip is the artifact the user must find again later, and a dismissable box teaches the box, not the control. The cue puts the motion on the control itself.
+
+**A fixed per-character tick.** The first implementation used 60ms per character unconditionally; an English preset name took over three times as long as its four-character Chinese counterpart, reading as lag rather than emphasis. The shared reveal window makes duration a property of the cue, not of the locale.
+
+**Animating the pick inside the settings dialog before leaving.** The dialog closes as part of the gesture — leaving settings is how the flow says the work happens in the session — so anything played there would be cut off or would delay the navigation it exists to explain.
+
+## Consequences
+
+The intro timeline lives in two places that must agree: the component's `INTRO_TEXT_DELAY_MS` and the `.introIcon` CSS animation duration. The component's constants are the source of the character delays and the acknowledgement timeout; the CSS comment names the coupling. The seat store gains one bit of UI state (`introduce`) that every stage decides explicitly, and the section keeps rendering a group with no members — a shape the section golden and unit tests now pin.
+
+## Testing
+
+Component tests pin the capped stagger (11-character Latin name at 20ms steps, 4-character CJK name at the 40ms tick, single character with no stagger), the acknowledgement timing, and the reduced-motion and empty-name skips. `apply.spec.ts` drives the cross-screen stage end to end: the creator draft stages with the cue set, one acknowledgement clears it, and a repeat acknowledgement leaves the snapshot untouched. The `agent-preset-authoring` web e2e holds the empty custom group (heading plus creator entry) in its goldens.
diff --git a/.agents/notes/implemented/feature/2026-08-10-creator-guidance-introduce-cue.zh.md b/.agents/notes/implemented/feature/2026-08-10-creator-guidance-introduce-cue.zh.md
new file mode 100644
index 0000000000..d80260abd1
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-creator-guidance-introduce-cue.zh.md
@@ -0,0 +1,33 @@
+# Agent Note: 创造模式引导以介绍动效落在预设 chip 上
+
+Status: implemented
+
+[English](2026-08-10-creator-guidance-introduce-cue.md) | 中文
+
+## 问题
+
+预设的创作发生在创造模式 session 内部,但设置分区没有把这条路径讲清楚。创建入口游离在名册分组之外;自定义分组在没有成员时整个消失;点击入口后用户被抛到新会话屏幕,没有任何标记说明发生了什么变化:暂存的预设 chip 渲染得和用户亲手挑选时一模一样。用户反馈看不懂流程已经移动,也不明白即将开始的 session 正是构建预设的地方(#2184)。
+
+## 决定
+
+自定义分组在空的时候也常驻屏幕——分组标题加创建入口,入口移入分组内部,作为"你的预设会出现在这里"的常设指引,而不是漂在名册下方。
+
+从另一屏幕暂存的选择会经由 seat store 携带一次性的 `introduce` 标志(`stage(id, introduce)`),chip 据此自我介绍:预设图标在 150ms 内缓入,落定的瞬间名称逐字符错峰浮现。错峰有两重上限——短的中文名按每字符 40ms 的节拍,同时共享一个 200ms 的整体揭示窗口(`min(40, 200/(n-1))`),让长的拉丁名与中文名在相同时间内完成,而不是按字符数拖长整轮动画。动效由 CSS 负责;组件只负责触发,并在一轮结束后确认该提示,因此标志不会在后续挂载时重放。`prefers-reduced-motion` 与空显示名会立即确认、不播放动画。
+
+该提示纯属呈现层:它是客户端 seat-store 状态,永远不是 session 事件,因为模型可见的组合已由暂存的预设本身承载。
+
+## 曾考虑的替代方案
+
+**在新会话屏幕上弹 toast 或提示框。** 它能解释更多,但什么也没指向——chip 才是用户之后必须再次找到的对象,可关闭的提示框教会的是提示框本身,不是控件。介绍动效把动作放在控件本体上。
+
+**固定的每字符节拍。** 第一版实现无条件使用每字符 60ms;英文预设名的时长超过四字中文名的三倍,读起来像卡顿而非强调。共享揭示窗口让时长成为提示的属性,而不是语言的属性。
+
+**离开前在设置对话框内播放选中动画。** 关闭对话框本身就是这个手势的一部分——离开设置正是流程在表达"工作发生在 session 里"——在那里播放的任何内容要么被截断,要么会拖延它本要解释的跳转。
+
+## 后果
+
+介绍时间线存在于两处且必须一致:组件的 `INTRO_TEXT_DELAY_MS` 与 `.introIcon` 的 CSS 动画时长。组件常量是字符延迟与确认超时的来源;CSS 注释点明了这层耦合。seat store 多出一位 UI 状态(`introduce`),每次暂存都显式决定它;分区则会渲染没有成员的分组——这一形态现由分区 golden 与单元测试钉住。
+
+## 测试
+
+组件测试钉住带上限的错峰(11 字符拉丁名走 20ms 步进、4 字中文名走 40ms 节拍、单字符无错峰)、确认时机,以及 reduced-motion 与空名的跳过路径。`apply.spec.ts` 端到端驱动跨屏暂存:创造模式草稿携带提示暂存,一次确认将其清除,重复确认让快照原样不动。`agent-preset-authoring` web e2e 在 golden 中保持空自定义分组(标题加创建入口)。
diff --git a/.agents/notes/implemented/feature/2026-08-10-minimal-read-image-tool.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-minimal-read-image-tool.i18n.yaml
new file mode 100644
index 0000000000..dcd01fc6d3
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-minimal-read-image-tool.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-10-minimal-read-image-tool.md
+2026-08-10-minimal-read-image-tool.md: a43e53d70e98bac7a50aa6bbabbb1e177237df01
+2026-08-10-minimal-read-image-tool.zh.md: a94e4b296425ad50876b0b45a689442c896a85a1
diff --git a/.agents/notes/implemented/feature/2026-08-10-minimal-read-image-tool.md b/.agents/notes/implemented/feature/2026-08-10-minimal-read-image-tool.md
new file mode 100644
index 0000000000..a43e53d70e
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-minimal-read-image-tool.md
@@ -0,0 +1,33 @@
+# Agent Note: A minimal read_image tool over existing seams
+
+Status: implemented
+
+English | [中文](2026-08-10-minimal-read-image-tool.zh.md)
+
+## Problem
+
+The multimodal attachment work gave user uploads a complete durable path — bytes committed to the content-addressed attachment store before the owning `user/message`, an `ImageBlock` carrying only the `sha256:` reference, and the pi-ai route re-reading verified bytes per request — but the model itself had no way to look at an image on disk. `read` rejects binary content by contract, so an agent asked about a screenshot or a rendered chart either failed or shelled out to lossy workarounds. A first standalone attempt (PR #598) solved this together with loop-level route scoping: an `agent/request-ready` extension point publishing exact-model modalities before assembly, per-route schema/guidance visibility, and a reversible `image-placeholder-v1` history projection so text routes could continue over placeholder text. That design worked but coupled a tool to new agent-loop machinery, three new session-log concepts, and per-step registration churn — far more surface than the capability needs.
+
+## Decision
+
+Ship the smallest tool that loads an image into the next request's context, entirely over existing seams; the withdrawn PR #598 design is the explicit counter-example this note records.
+
+- **`read_image` lives in `dsh-tool-fs`** beside `read`/`write`/`edit`. Extension selects the declared PNG/JPEG/WebP/GIF media type; the attachment store's magic-byte and pixel validation stays authoritative. Bytes travel `ctx.fs.stat` → bounded `ctx.fs.readBytes` → `ctx.attachments.saveImage` → `fs/observed`, and the tool result is the metadata envelope plus a real `ImageBlock` — `ToolResultBlock.content` already admits image blocks, the pi-ai adapter already renders them, and the Web host's model-switch guard already scans tool results, so nothing downstream changes.
+- **`FileSystem.readBytes(target, signal, maxBytes)`** is a new required provider primitive: the byte bound lives at the seam so no backend can buffer an unbounded file, with the stat-size short-circuit and a one-byte-past-cap stream guard against post-stat growth (`FS_TOO_LARGE`).
+- **Registration is composition-conditional, execution is route-gated.** The tool registers only under `ctx.inject(['attachments'], …)` — no store, no tool. At execution, before any I/O, the strict gate resolves the calling route (latest `request/header` config, falling back to agent options) through `ctx.llm.resolveModelInfo` and requires `image` in `inputModalities`; unknown capability refuses. A refusal is a plain `isError` result, so a text route's durable history never acquires an image block and the session cannot brick its own route.
+- **Code Mode forwards the image out-of-band**: a nested dispatch returns the canonical value (execution-local, no image block) and defers a `user`-role context message carrying the envelope and image, so the picture still reaches the next request.
+- **llm-replay models may declare `inputModalities`**, which is what lets the two keyless ACP snapshots pin both sides of the gate — the sha256-referenced success on an image-capable replay route and the verbatim refusal on a text-only one.
+
+## Alternatives considered
+
+- **PR #598's route-scoped design** (request-ready seam, per-route schema/guidance visibility, reversible history projection) — withdrawn in favor of this note's shape. What it bought: text routes could keep running after images entered history, and the tool disappeared from prompts where it cannot succeed. What it cost: agent-loop changes, three new durable concepts (`agent/request-ready`, `messageProjection`, availability notices), and registration that churned per step. The capability itself — see an image on the next request — never needed any of it. If per-route projection becomes a real requirement, that PR's history is the reference implementation.
+- **`agent.inject()` instead of the image-bearing tool result** — routes the image around the tool result as a separate injected user message. Rejected: the image *is* the tool's result; splitting them adds a second logged message with no gain, and the tool-result path already works end to end.
+- **Magic-byte sniffing instead of extension declaration** — sniffing duplicates detection the attachment store already owns (sharp-backed, authoritative). The extension is only a *declaration*; a mismatch fails closed with a rename remedy rather than being silently accepted, which also keeps the model's mental map (file name ↔ content) honest.
+- **Registering unconditionally and failing on a missing store** — rejected; a deployment without an attachment store cannot ever satisfy the tool, so its schema would be a standing lie. The route gate, by contrast, is per-call state and correctly lives at the execution boundary.
+
+## Consequences
+
+- A text-only route refuses instead of degrading: no placeholder projection means no delegated-viewing story here — that is deliberately the next PR (subagent image readback rebuilt on the current subagent seams).
+- The route gate races a concurrent model switch; the Web host's image-aware switch guard covers its surface, and other front doors own their equivalent. Recorded as a tool-fs Known Limitation.
+- Repeated image results accumulate request-token cost until compaction; content addressing deduplicates bytes only.
+- The tool-result card renders the durable reference, not pixels; inline preview is deferred to the UI packages.
diff --git a/.agents/notes/implemented/feature/2026-08-10-minimal-read-image-tool.zh.md b/.agents/notes/implemented/feature/2026-08-10-minimal-read-image-tool.zh.md
new file mode 100644
index 0000000000..a94e4b2964
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-minimal-read-image-tool.zh.md
@@ -0,0 +1,33 @@
+# Agent Note: 基于既有 seam 的最小 read_image 工具
+
+Status: implemented
+
+[English](2026-08-10-minimal-read-image-tool.md) | 中文
+
+## 问题
+
+多模态附件工作为用户上传建立了完整的持久路径:字节在所属 `user/message` 之前提交到内容寻址的附件存储,`ImageBlock` 只携带 `sha256:` 引用,pi-ai 路由在每次请求时重新读取并校验字节。但模型自己没有查看磁盘图像的手段。`read` 按约定拒绝二进制内容,因此被问到截图或渲染图表的 agent 要么失败,要么退到有损的变通做法。第一次独立尝试(PR #598)把这个问题与循环级路由作用域一起解决:新增在组装前发布确切模型模态的 `agent/request-ready` 扩展点、按路由控制 schema/指导可见性,以及可逆的 `image-placeholder-v1` 历史投影让文本路由能在占位符上继续。该设计可行,但让一个工具耦合了新的 agent-loop 机制、三个新的会话日志概念和每步的注册变动,远超这项能力本身的需要。
+
+## 决定
+
+只交付能把图像载入下一次请求上下文的最小工具,完全建立在既有 seam 之上;撤回的 PR #598 设计是本记录明确保留的反例。
+
+- **`read_image` 放在 `dsh-tool-fs`**,与 `read`/`write`/`edit` 并列。扩展名选择声明的 PNG/JPEG/WebP/GIF 媒体类型;附件存储的魔数与像素校验保持权威。字节沿 `ctx.fs.stat` → 有界 `ctx.fs.readBytes` → `ctx.attachments.saveImage` → `fs/observed` 流动,工具结果是元数据信封加真正的 `ImageBlock`——`ToolResultBlock.content` 本就允许图像块,pi-ai 适配器本就会渲染它们,Web 宿主的模型切换防护本就会扫描工具结果,下游无需任何改动。
+- **`FileSystem.readBytes(target, signal, maxBytes)`** 是新的必备提供方原语:字节上限放在 seam 上,任何后端都无法无界缓冲文件;stat 大小先短路,随后的流最多多读一个字节以防 stat 之后的增长(`FS_TOO_LARGE`)。
+- **注册随组合条件挂载,执行按路由门禁。** 工具只在 `ctx.inject(['attachments'], …)` 作用域内注册——没有存储就没有工具。执行时在任何 I/O 之前,严格门禁通过 `ctx.llm.resolveModelInfo` 解析调用路由(最新 `request/header` 配置,缺失时回退到 agent 选项),要求 `inputModalities` 包含 `image`;能力未知即拒绝。拒绝是普通的 `isError` 结果,因此文本路由的持久历史绝不会出现图像块,会话不会毁掉自己的路由。
+- **Code Mode 以带外方式转发图像**:嵌套分派返回规范值(仅限本次执行,不含图像块),并延迟提交一条携带信封和图像的 `user` 角色上下文消息,图片仍会到达下一次请求。
+- **llm-replay 模型可以声明 `inputModalities`**,这正是两个 keyless ACP 快照能钉住门禁两侧的原因:图像路由上以 sha256 引用的成功结果,和纯文本路由上逐字的拒绝。
+
+## 考虑过的替代方案
+
+- **PR #598 的路由作用域设计**(request-ready 扩展点、按路由的 schema/指导可见性、可逆历史投影)——被本记录的形态取代后撤回。它换来的是:图像进入历史后文本路由仍能运行,工具在注定失败的提示词里消失。它付出的是:改动 agent-loop、三个新的持久概念(`agent/request-ready`、`messageProjection`、可用性通知)和每步变动的注册。而这项能力本身——下一次请求看到图像——从不需要这些。如果按路由投影将来成为真实需求,该 PR 的历史就是参考实现。
+- **用 `agent.inject()` 代替带图像的工具结果**——把图像绕过工具结果,作为单独注入的用户消息。拒绝:图像就是工具的结果;拆开只会多一条无收益的日志消息,而工具结果路径本就端到端可用。
+- **用魔数嗅探代替扩展名声明**——嗅探重复了附件存储已拥有的检测(基于 sharp,权威)。扩展名只是声明;不匹配时按改名修复提示失败关闭,而不是被静默接受,这也让模型对文件名与内容的对应保持诚实。
+- **无条件注册、缺存储时执行报错**——拒绝;没有附件存储的部署永远无法满足该工具,其 schema 会是常态谎言。相反,路由门禁是逐调用状态,正确的位置就是执行边界。
+
+## 后果
+
+- 纯文本路由得到拒绝而不是降级:没有占位符投影意味着这里没有委托查看的方案——那有意留给下一个 PR(基于当前 subagent seam 重建的 subagent image readback)。
+- 路由门禁与并发模型切换存在竞态;Web 宿主的图像感知切换防护覆盖其表面,其他前端拥有各自的等价防护。已记入 tool-fs 的已知限制。
+- 重复的图像结果在压缩之前持续累积请求 token 成本;内容寻址只去重字节。
+- 工具结果卡片渲染持久引用而非像素;内嵌预览延后到 UI 包处理。
diff --git a/.agents/notes/implemented/feature/2026-08-10-subagent-approval-pinned-never.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-subagent-approval-pinned-never.i18n.yaml
new file mode 100644
index 0000000000..322d645a70
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-subagent-approval-pinned-never.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-10-subagent-approval-pinned-never.md
+2026-08-10-subagent-approval-pinned-never.md: a21c6b966b1ad00ed63e0fe87b0ce982f0daf490
+2026-08-10-subagent-approval-pinned-never.zh.md: db44ae134d34904a53691cfe78eaa5a899cf64e0
diff --git a/.agents/notes/implemented/feature/2026-08-10-subagent-approval-pinned-never.md b/.agents/notes/implemented/feature/2026-08-10-subagent-approval-pinned-never.md
new file mode 100644
index 0000000000..a21c6b966b
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-subagent-approval-pinned-never.md
@@ -0,0 +1,34 @@
+# Agent Note: Delegated subagents run with approvals pinned to `'never'`
+
+Status: implemented
+
+English | [中文](2026-08-10-subagent-approval-pinned-never.zh.md)
+
+## Problem
+
+A delegated child that asked for approval had no one to ask. Under an interactive parent (`'ask'`), a background child's escalation became a pending question no product surface showed — subagent sessions are omitted from the Web sidebar, the parent's `list_agents` reports plain `running`/`idle`, and the catalog rows show only activity — so a permission-blocked child was indistinguishable from a working one; headless and unanswered compositions failed the same ask closed as `'unavailable'`. The rejection audit landed only in the child's own log, and no tool parameter or Web control can adjust a running child session's sandbox mode or approval policy (Issue #1723). The mechanism-heavy fix — a durable blocked-state projection, parent notices, catalog badges, and a permission write path through the subagent ownership fence — was disproportionate directly before release.
+
+## Decision
+
+A delegated child acts only within the permission scope fixed at delegation, and approval prompts are removed from its world entirely: `captureDelegatedPolicyOverrides(parent)` (`dsh-subagent/src/child-agent.ts`) still snapshots the parent session's explicit sandbox override, but pins `approvalPolicy: 'never'` whenever the approval capability is composed — it no longer reads the parent's own approval policy. `appendDelegatedPolicyOverrides()` writes the pin as the durable `approval/policy { policy: 'never', source: 'delegation' }` event on the child's log, through the same one-shot and continuable delegation paths as the sandbox snapshot, so cold resume replays it and a fork seed's stale parent policy loses to it.
+
+Enforcement is the existing `ApprovalService` `'never'` semantics at the one operation that decides asks: every child ask — a `sandbox_permissions` escalation from bash or fs, a hook-driven permission question, any future asker — resolves `'rejected'` deterministically before any answerer is consulted, still leaving the `approval/asked`/`approval/decided` audit pair on the child log. The child's whole permission story is therefore its sandbox scope: a `danger-full-access` parent delegates children that need no approvals, a `read-only` parent delegates children with no escape hatch, and a widening decision always belongs to the parent side (widen the parent session, then delegate or follow up again).
+
+Every in-process child is told, not trapped: `applyChildComposition` registers the scoped `subagent:delegation` runtime-context statement (order 120, after the `sandbox:policy` and `approval:policy` sentences) stating that the scope was fixed at start, approval-requiring operations are rejected automatically, and a task needing wider access ends with a reported limitation instead of retries. The statement is a runtime-context contribution rather than a system-prompt section, so the deployment's system prompt stays uniform across parents and children (the snapshot suite pins that uniformity) and the fact rides the same durable snapshot as the policy sentences.
+
+This supersedes the approval half of the [in-process delegation-policy decision](2026-07-25-subagent-policy-inheritance.md) and reverses its "forcing `'never'` forecloses a future child answerer" verdict: approval inheritance shipped, produced the invisible blocked states above, and a future child answerer now requires reversing this note first.
+
+## Alternatives considered
+
+- **Inheriting the parent's approval override** (the prior behavior) — rejected: only a parent already at `'never'` produced deterministic children; an interactive parent seeded children whose asks waited on a prompt no one was watching or failed closed `'unavailable'`, and the outcome depended on which surfaces happened to be attached.
+- **Blocked-state visibility and per-child permission adjustment** (the original #1723 acceptance) — deferred, not rejected: a `list_agents` blocked annotation, parent notices over the settlement-delivery seam, catalog badges, and a subagent-routed permission channel remain the richer design, but each needs its own seam work and none is required once children cannot enter a blocked-waiting state.
+- **Routing child asks to the parent controller** — still deferred in the [approval-seam Agent Note](2026-07-06-approval-seam.md): it needs parent-chain ownership and the spawning `callId`.
+- **Pinning inside `ApprovalService` by session origin** — rejected: it couples the approval package to delegation vocabulary and duplicates a decision the delegation boundary already owns; the delegation-seeded event is enforceable because no current write path can switch a child session's policy (the `/permission` command requires generic Host routing, which the subagent ownership fence denies to child sessions).
+
+## Consequences
+
+- The child's sandbox inheritance is the complete delegation permission model; the `DelegatedPolicyOverrides.approvalPolicy` field narrows to `'never' | undefined` (`undefined` only without a composed approval capability).
+- Model-visible: each child's runtime-context snapshot carries the `subagent:delegation` statement plus the standing disabled-approvals sentence; parent requests are unchanged. The executor-boundary test proves a child escalation is rejected without consulting a root answerer that would have granted it, with the audit pair logged.
+- Boundaries: in-process one-shot, continuable, and workflow-spawned children are enforced through the shared helpers; `subagent-acp` children keep that provider's explicit machine `permission` policy; `claude-code`, `codex`, and `dsh-sdk` children run in external processes under their own composition.
+- Children persisted before the pin fold to the deployment approval default on cold resume; pre-release, no migration is added.
+- Snapshot fixtures record the pin: every in-process child log gains the delegation `approval/policy` event, and `subagent-published-run-failure` now persists a one-event child log where the child previously left no durable events.
diff --git a/.agents/notes/implemented/feature/2026-08-10-subagent-approval-pinned-never.zh.md b/.agents/notes/implemented/feature/2026-08-10-subagent-approval-pinned-never.zh.md
new file mode 100644
index 0000000000..db44ae134d
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-subagent-approval-pinned-never.zh.md
@@ -0,0 +1,34 @@
+# Agent Note: 被委派的 subagent 以钉定为 `'never'` 的审批策略运行
+
+Status: implemented
+
+[English](2026-08-10-subagent-approval-pinned-never.md) | 中文
+
+## 问题
+
+被委派的子 agent 发起审批请求时无人可问。在交互式父级(`'ask'`)之下,后台子 agent 的升级请求会变成一个任何产品界面都不展示的挂起问题——subagent 会话不进入 Web 侧边栏,父级的 `list_agents` 只报告普通的 `running`/`idle`,目录树的行也只显示活动状态——因此被权限拦住的子 agent 与正常干活的子 agent 无法区分;headless 与无应答者的组合则让同一次 ask 以 `'unavailable'` 失败关闭。拒绝的审计记录只落在子 agent 自己的日志里,而且没有任何工具参数或 Web 控件能调整一个正在运行的子会话的沙箱模式或审批策略(Issue #1723)。机制繁重的修复方案——持久化的受阻状态投影、父级通知、目录树徽标,以及穿过 subagent 所有权围栏的权限写入路径——在临近发布时代价不成比例。
+
+## 决策
+
+被委派的子 agent 只在委派时固定的权限范围内行动,审批提示则从它的世界中彻底移除:`captureDelegatedPolicyOverrides(parent)`(`dsh-subagent/src/child-agent.ts`)仍对父会话的显式沙箱覆盖项建立快照,但只要审批能力已组合,就把 `approvalPolicy: 'never'` 钉定下来——不再读取父级自身的审批策略。`appendDelegatedPolicyOverrides()` 把这个钉定作为持久化的 `approval/policy { policy: 'never', source: 'delegation' }` 事件写入子 agent 的日志,与沙箱快照走完全相同的一次性与可继续委派路径,因此冷恢复会重放它,fork 种子中陈旧的父级策略也会输给它。
+
+强制执行沿用既有的 `ApprovalService` `'never'` 语义,落在裁决 ask 的唯一操作上:子 agent 的每次 ask——bash 或 fs 的 `sandbox_permissions` 升级、hook 驱动的权限询问、任何未来的请求方——都在咨询任何应答者之前确定性地解析为 `'rejected'`,同时仍在子日志上留下 `approval/asked`/`approval/decided` 审计对。子 agent 的全部权限故事因此就是它的沙箱范围:`danger-full-access` 父级委派出的子 agent 无需任何审批,`read-only` 父级委派出的子 agent 没有任何逃生通道,而放宽的决定始终属于父级一侧(先放宽父会话,再重新委派或继续 follow-up)。
+
+每个进程内子 agent 都被告知而非被困住:`applyChildComposition` 注册作用域内的 `subagent:delegation` 运行时上下文声明(order 120,位于 `sandbox:policy` 与 `approval:policy` 语句之后),声明权限范围已在启动时固定、需要审批的操作会被自动拒绝、需要更宽访问的任务应以上报限制收尾而不是重试。该声明是运行时上下文贡献而非系统提示词 section,因此部署的系统提示词在父子之间保持统一(快照测试套件钉住了这一统一性),该事实也随策略语句乘坐同一份持久化快照。
+
+本决策取代[进程内委派策略决策](2026-07-25-subagent-policy-inheritance.md)中的审批一半,并推翻其「强制 `'never'` 会排除未来的子 agent 应答器」的结论:审批继承已经落地,产生的正是上述不可见的受阻状态;未来若要引入子 agent 应答器,必须先推翻本 note。
+
+## 考虑过的替代方案
+
+- **继承父级的审批覆盖项**(先前的行为):不予采纳。只有已处于 `'never'` 的父级才产生确定性的子 agent;交互式父级种出的子 agent,其 ask 要么等待一个无人在看的提示,要么以 `'unavailable'` 失败关闭,结果取决于当时恰好接入了哪些界面。
+- **受阻状态可见性与逐子级权限调整**(#1723 原有的验收):延后而非否决。`list_agents` 的受阻标注、经由结算投递 seam 的父级通知、目录树徽标,以及 subagent 专用的权限通道仍是更完整的设计,但每一项都需要独立的 seam 工作;一旦子 agent 不可能进入等待审批的受阻状态,这些都不再是必需。
+- **把子 agent 的 ask 路由到父控制器**:仍按[审批 seam Agent Note](2026-07-06-approval-seam.md) 延后。它需要父链所有权与发起 spawn 的 `callId`。
+- **在 `ApprovalService` 内按会话来源钉定**:不予采纳。这会让审批包耦合委派词汇,并重复一个委派边界已经拥有的决定;委派种入的事件之所以可强制执行,是因为当前不存在任何能切换子会话策略的写入路径(`/permission` 命令要求通用 Host 路由,而 subagent 所有权围栏对子会话拒绝该路由)。
+
+## 后果
+
+- 子 agent 的沙箱继承就是委派权限模型的全部;`DelegatedPolicyOverrides.approvalPolicy` 字段收窄为 `'never' | undefined`(仅在未组合审批能力时为 `undefined`)。
+- 模型可见:每个子 agent 的运行时上下文快照携带 `subagent:delegation` 声明以及固定的审批已禁用语句;父级请求不变。executor 边界测试证明:即使根部有一个本会批准的应答者,子 agent 的升级仍被拒绝且不咨询该应答者,审计对照常落日志。
+- 边界:进程内一次性、可继续以及 workflow 派生的子 agent 都经由共享辅助函数强制执行;`subagent-acp` 子 agent 保留该提供方显式的机器 `permission` 策略;`claude-code`、`codex` 与 `dsh-sdk` 子 agent 运行在外部进程中,由各自的组合决定。
+- 在钉定之前持久化的子 agent 冷恢复时折叠到部署审批默认值;处于预发布阶段,不添加迁移。
+- 快照夹具记录了该钉定:每个进程内子日志都新增委派 `approval/policy` 事件,`subagent-published-run-failure` 现在会持久化一份单事件子日志,而此前该子 agent 不留任何持久化事件。
diff --git a/.agents/notes/implemented/feature/2026-08-10-web-session-log-export.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-web-session-log-export.i18n.yaml
new file mode 100644
index 0000000000..0c8a3f2781
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-web-session-log-export.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-10-web-session-log-export.md
+2026-08-10-web-session-log-export.md: 427b6478ac44fb28030aa932630f276de7bb2edc
+2026-08-10-web-session-log-export.zh.md: 63b9804a54cda7eea4ff793d78a925fe296d06cb
diff --git a/.agents/notes/implemented/feature/2026-08-10-web-session-log-export.md b/.agents/notes/implemented/feature/2026-08-10-web-session-log-export.md
new file mode 100644
index 0000000000..427b6478ac
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-web-session-log-export.md
@@ -0,0 +1,30 @@
+# Agent Note: Web session-log export as a host-streamed ZIP download
+
+Status: implemented
+
+English | [中文](2026-08-10-web-session-log-export.zh.md)
+
+## Problem
+
+The Trajectory view had no way to hand a debugging artifact to a human: the raw session log lived on disk and in the host, the client history face served folded projections (not raw entries), and a session with subagents spans many independent session logs. A bug report needs the complete raw log of the whole tree, in a shape that survives being emailed around.
+
+## Decision
+
+- **The export is a host-only download, not an RPC**: `GET /api/session.export?sessionId=…&includeDescendants=true` streams one ZIP attachment. Every file is a session's **stored artifact text verbatim**: `readRaw` on the persistence service reads the backend's own durable bytes (the JSONL backend decodes its physical zstd frames, or returns plaintext) — never a reconstruction from parsed events, so packed-chunk rows, key order, and line breaks survive byte-for-byte — under its original base name (`session.jsonl` at the root, `subagents//session.jsonl` for descendants). Compression runs on the host with fflate's streaming `Zip`/`ZipDeflate` API, each entry deflated in bounded chunks as it is produced, so the response is chunked as it is generated and the host never holds the whole archive in one buffer (at most one descendant's artifact text beyond the preloaded root), and production yields whenever the response queue fills, so a slow consumer bounds the accumulation (fflate's callback is synchronous — the drain point is the only backpressure). No manifest is written — every file is byte-identical to the durable artifact and self-describing through its own header line.
+- **Error vocabulary is HTTP-native**: missing services → 500, missing root session → 404 (both decided before any byte streams), a descendant without a stored artifact → the stream errors (fail-loud, never silent under-export). The carrier (`toFetchHandler`) already applies the `/api` trust fence; the GET branch sits beside the existing SSE GET routes, and `ApiProxy.downloads.sessionLog` (host-only, no wire envelope, absent from `IApiClient`) implements it.
+- **The UI just downloads**: the 导出 button fetches the endpoint and saves the response; the `session.log` RPC that an earlier iteration shipped was removed — the download endpoint is its only consumer, and the repo rule is no public interface without a current owner. The client bundle no longer carries fflate (the earlier browser-entry-alias pitfall is moot).
+- The 导出 button lives in the Trajectory toolbar; the plugin exposes `exportLog` through the view's inject face (components never touch ctx) and resolves the view tab label through the locale service (`轨迹` in Chinese, `Trajectory` in English). In-flight state disables the button; a failure surfaces in a visible alert bar under the toolbar.
+
+## Alternatives considered
+
+- **`session.log` data RPC + client-side zip** — shipped first, rejected with the user: the browser pulls the full raw JSON (≈10× the final zip size) and compresses on the main thread; for the 23 MB sessions in real use the host-side stream is strictly better. The RPC was deleted with the migration rather than left as a dead public surface.
+- **Single JSONL with envelope lines for multiple sessions** — rejected with the user: mixing sessions in one JSONL loses clean per-file boundaries; a ZIP keeps one canonical file per session.
+- **jszip** — heavier (~100 kB) and its dependency graph pulls readable-stream browser mappings; fflate is purpose-built and small.
+- **Vendoring fflate's browser entry** — the repo vendoring procedure targets cordis-scale pinned sources; a resolveId alias keeps the maintained dependency without shipping a copy (and host-side fflate needs no alias at all).
+
+## Consequences
+
+- Export fidelity: every exported file is byte-identical to the backend's durable artifact as of the read moment (a live session may append after the read; the export reflects the durable state at read time). The archive name is `dsh-session-.zip` and archive paths sanitize ids before they can shape entries.
+- `readRaw` joins the persistence service as a concrete default (`undefined` for backends without a per-session artifact, e.g. SQLite) with a JSONL-backend override that owns the compression decode. `ApiProxy.downloads.sessionLog` adds one host-only member to the contract plus a host-side query schema and a GET branch in the fetch handler — no RPC map row, envelope schema, or client `IApiClient` surface.
+- Fixture mode (no host) answers 404 for the export, so the button's error bar explains the gap instead of hanging; the navigation-panes golden snapshot includes the 导出 button.
+- Deferred: transcript.md and a report/feedback bundle remain future work; the byte-faithful, manifest-free shape keeps the v2 bundle extension cheap.
diff --git a/.agents/notes/implemented/feature/2026-08-10-web-session-log-export.zh.md b/.agents/notes/implemented/feature/2026-08-10-web-session-log-export.zh.md
new file mode 100644
index 0000000000..63b9804a54
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-10-web-session-log-export.zh.md
@@ -0,0 +1,30 @@
+# Agent Note:Web 会话日志导出——宿主流式 ZIP 下载
+
+状态:implemented
+
+[English](2026-08-10-web-session-log-export.md) | 中文
+
+## 问题
+
+Trajectory 视图没有任何方式把调试工件交到人手里:原始会话日志存放在磁盘与宿主侧,客户端历史面只提供折叠后的投影(而非原始事件),而带子代理的会话横跨多个相互独立的会话日志。bug 报告需要整棵会话树的完整原始日志,并且形态要能在被转发后仍然可用。
+
+## 决策
+
+- **导出是宿主侧的下载面,不是 RPC**:`GET /api/session.export?sessionId=…&includeDescendants=true` 流式返回一个 ZIP 附件。每个文件都是会话**存储工件的逐字原文**:持久化服务新增的 `readRaw` 读取后端自己的持久化字节(jsonl 后端解码其物理 zstd 帧,或直接返回明文)——绝非从解析后事件重建,因此 chunk 打包、键序、换行全部逐字节保留——放在其原始基础文件名下(根为 `session.jsonl`,子代理为 `subagents//session.jsonl`)。压缩在宿主侧用 fflate 的流式 `Zip`/`ZipDeflate` API 完成,每个条目按有界分块边产出边压缩,响应随生成分块写出,宿主从不把整个归档放进单个缓冲区(除预载的根外,最多同时持有一条后代的工件文本),且每当响应队列填满时生产会让出,慢消费者因此只产生有界的积压(fflate 的回调是同步的——让出点是唯一的背压手段)。不写清单——每个文件都与持久化工件逐字节一致,并通过自身 header 行自描述。
+- **错误词汇是 HTTP 原生的**:服务缺失 → 500,根会话缺失 → 404(两者都在任何字节流出前判定),后代缺少存储工件 → 流失败(fail-loud,绝不静默少导出)。载体(`toFetchHandler`)已对 `/api` 应用信任围栏;GET 分支与既有 SSE GET 路由并列,由 `ApiProxy.downloads.sessionLog`(host-only、无 wire 信封、不在 `IApiClient` 上)实现。
+- **UI 只负责下载**:「导出」按钮 fetch 该端点并保存响应;早先迭代发布的 `session.log` RPC 已删除——下载端点是它唯一的消费者,仓库规则是不留无当前所有者的公共接口。客户端 bundle 不再携带 fflate(早先的浏览器入口别名坑随之消失)。
+- 「导出」按钮位于 Trajectory 工具栏;插件通过视图的 inject face 暴露 `exportLog`(组件从不接触 ctx),并通过 locale 服务解析视图标签页标题(中文「轨迹」、英文 "Trajectory")。进行中状态会禁用按钮;失败会在工具栏下方的可见警示条中显示。
+
+## 考虑过的替代方案
+
+- **`session.log` 数据 RPC + 客户端打包**——先发布,后与用户共同否决:浏览器要拉取完整原始 JSON(约为最终 zip 的 10 倍)并在主线程压缩;对实际使用中 23 MB 级别的会话,宿主流式严格更优。迁移时把该 RPC 一并删除,而不是留作无消费者的公共接口。
+- **用信封行把多会话编码进单一 JSONL**——与用户共同否决:把多个会话混进一个 JSONL 会失去干净的按文件边界;ZIP 让每个会话保持一个规范文件。
+- **jszip**——更重(约 100 kB),依赖图还会拉入 readable-stream 的浏览器映射;fflate 专为此而生且体积小。
+- **将 fflate 浏览器入口 vendoring 进仓库**——仓库的 vendoring 流程面向 cordis 级别的固定源码;resolveId 别名在保持维护中的依赖的同时无需复制代码(宿主侧 fflate 根本不需要别名)。
+
+## 后果
+
+- 导出保真度:每个导出文件都与读取时刻的后端持久化工件逐字节一致(活跃会话可能在读取后继续追加;导出反映的是读取时的持久化状态)。压缩包名为 `dsh-session-.zip`,归档路径在塑造条目前会先净化会话 id。
+- `readRaw` 以具体默认(无每会话工件的后端如 SQLite 返回 `undefined`)加入持久化服务,jsonl 后端覆写并自持压缩解码。`ApiProxy.downloads.sessionLog` 为契约新增一个 host-only 成员,外加宿主侧 query schema,并在 fetch handler 加一个 GET 分支——没有 RPC map 行、信封 schema 或客户端 `IApiClient` 面。
+- fixture 模式(无宿主)对导出应答 404,按钮的错误条会解释这个缺口而非挂起;navigation-panes golden 快照包含「导出」按钮。
+- 暂缓:transcript.md 以及 report/feedback 打包留待后续;逐字节忠实、无清单的形态让 v2 的打包扩展保持廉价。
diff --git a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.i18n.yaml b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.i18n.yaml
index 36f6181c46..56f184e831 100644
--- a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-13-documentation-site-projection.md
-2026-07-13-documentation-site-projection.md: d9af915754fa6a1df51a27d18d412597472aaa73
-2026-07-13-documentation-site-projection.zh.md: 7d7b4752b8f27d55aae8426a7dc001ce4340e661
+2026-07-13-documentation-site-projection.md: 309dbd96c5f1ca87d137cdc3839acfc5c5aa22f2
+2026-07-13-documentation-site-projection.zh.md: b01965fb1b46ab618b5b8bcb748bb5a8940d6e69
diff --git a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md
index d9af915754..309dbd96c5 100644
--- a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md
+++ b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md
@@ -18,9 +18,9 @@ Canonical Markdown remains in the repository tier that owns it. Product-facing g
Locale home projections retain only the canonical YAML frontmatter. The repository-facing body can keep its H1 and bilingual source links, while the VitePress home theme owns the rendered hero and features and the site navigation owns locale switching.
-The projector parses Markdown links without reserializing the document. A link to another published source becomes a site-relative route; a link to an unpublished repository file becomes a source link under the public `deepseek-ai/deepseek-harness-sdk` home; a repository image is copied into the generated tree and referenced from there ([why](2026-08-06-doc-site-carries-its-images.md)). Missing relative targets fail projection. Unit tests pin these transformations, and `docs:check` runs the projector tests plus a production VitePress build as part of `doc-sync` and the parallel documentation gates.
+The projector parses Markdown links without reserializing the document. A link to another published source becomes a site-relative route; a link to an unpublished repository file becomes a source link under the `deepseek-ai/deepseek-harness` repository home; a repository image is copied into the generated tree and referenced from there ([why](2026-08-06-doc-site-carries-its-images.md)). Missing relative targets fail projection. Unit tests pin these transformations, and `docs:check` runs the projector tests plus a production VitePress build as part of `doc-sync` and the parallel documentation gates.
-`verify-public-repository-links` rejects internal repository remotes from tracked files. Public source links use the public home, while work tracking stays in repository metadata and source carries a TODO only when the local boundary matters to maintainers.
+`verify-public-repository-links` rejects references to the unavailable legacy repository from tracked files. Source and edit links use the current repository home.
`website/AGENTS.md` is the only maintained Markdown file in the website subtree. The projector test enumerates tracked and unignored files and rejects any other website Markdown, so site-specific locale, route, API, or generated source copies cannot bypass the publication manifest.
diff --git a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.zh.md b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.zh.md
index 7d7b4752b8..b01965fb1b 100644
--- a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.zh.md
+++ b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.zh.md
@@ -18,9 +18,9 @@ Status: implemented
各 locale 的首页投影只保留权威 YAML frontmatter。面向仓库的正文可以保留其 H1 和双语源文件链接,而 VitePress 首页主题负责渲染 hero 与功能区,网站导航负责切换 locale。
-投影器解析 Markdown 链接,但不会重新序列化文档。指向另一个已发布源文件的链接会变成站内相对路由;指向未发布仓库文件的链接会变成公开 `deepseek-ai/deepseek-harness-sdk` 主页下的源文件链接;仓库图片会被拷贝进生成树并从那里引用([原因](2026-08-06-doc-site-carries-its-images.md))。相对目标不存在时,投影会失败。单元测试会锁定这些转换行为,`docs:check` 则运行投影器测试和 VitePress 生产构建,并将二者纳入 `doc-sync` 和并行文档门禁。
+投影器解析 Markdown 链接,但不会重新序列化文档。指向另一个已发布源文件的链接会变成站内相对路由;指向未发布仓库文件的链接会变成 `deepseek-ai/deepseek-harness` 仓库主页下的源文件链接;仓库图片会被拷贝进生成树并从那里引用([原因](2026-08-06-doc-site-carries-its-images.md))。相对目标不存在时,投影会失败。单元测试会锁定这些转换行为,`docs:check` 则运行投影器测试和 VitePress 生产构建,并将二者纳入 `doc-sync` 和并行文档门禁。
-`verify-public-repository-links` 会拒绝已跟踪文件中的内部仓库远程链接。公开源文件链接使用公开主页,而工作跟踪留在仓库元数据中;只有本地边界对维护者有意义时,源文件才保留 TODO。
+`verify-public-repository-links` 会拒绝已跟踪文件中指向不可用旧仓库的引用。源文件链接和编辑链接使用当前仓库主页。
`website/AGENTS.md` 是网站子树中唯一维护的 Markdown 文件。投影器测试会枚举所有已跟踪文件和未被忽略的未跟踪文件,并拒绝网站中的任何其他 Markdown,因此网站专用的 locale、路由、API 或生成源文件副本无法绕过发布 manifest。
diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml
index 33bffe92ff..aa3cabead0 100644
--- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md
-2026-07-21-serial-cross-platform-ci-reference.md: 07dc430e6fed3fe75a006ca03523bd6e4fc969d0
-2026-07-21-serial-cross-platform-ci-reference.zh.md: 8bbb60cdead2957069de22ecaddf01c6cd9fb305
+2026-07-21-serial-cross-platform-ci-reference.md: c2ed11d40f7f5487117b5c72f11bc1709042f68a
+2026-07-21-serial-cross-platform-ci-reference.zh.md: bef8c3b640cf43942e380e921d5f62d91723eff8
diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md
index 07dc430e6f..c2ed11d40f 100644
--- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md
+++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md
@@ -16,7 +16,7 @@ Real-kernel sandbox proofs require specific hosted operating systems and archite
## Decision
-[CI](../../../../.github/workflows/ci.yml) gives pull-request and master-push events complementary responsibilities. Pull requests run consolidated Linux and Wine-hosted Windows jobs plus the Node compatibility and Python contracts on standard GitHub-hosted capacity; an independent native Windows job reports the complete Windows inventory without participating in the required aggregate. On a push to `master`, the active reference is `serial / linux (self-hosted standby)` on the in-house `vm-backup` pool — the hot-standby drill that continuously re-proves the failover target described in the [failover runbook](2026-07-26-ci-failover-runbook.md). The standard-hosted `serial / linux`, `serial / macos`, and `serial / windows` definitions remain disabled under `TODO(hosted-serial-ci)` until their portable capacity can be restored. The separate job definitions intentionally keep their short checkout, runtime setup, and immutable install sequences visible instead of hiding operating systems behind a matrix or reusable workflow. `workflow_dispatch` is reserved for runner benchmarks.
+[CI](../../../../.github/workflows/ci.yml) gives pull-request and master-push events complementary responsibilities. Pull requests run consolidated Linux and Wine-hosted Windows jobs plus the Node compatibility and Python contracts on standard GitHub-hosted capacity; an independent native Windows job reports the complete Windows inventory without participating in the required aggregate. On a push to `master`, the active references are `serial / linux (self-hosted standby)` on the in-house `vm-backup` pool and `serial / windows (self-hosted standby)` on the in-house `dsh-win-ci` pool — the hot-standby drills that continuously re-prove the failover targets described in the [failover runbook](2026-07-26-ci-failover-runbook.md). The standard-hosted `serial / linux`, `serial / macos`, and `serial / windows` definitions remain disabled under `TODO(hosted-serial-ci)` until their portable capacity can be restored. The separate job definitions intentionally keep their short checkout, runtime setup, and immutable install sequences visible instead of hiding operating systems behind a matrix or reusable workflow. `workflow_dispatch` is reserved for runner benchmarks.
Each reference job runs `pnpm run check:ci` without any shard selector. `DSH_GATE_CONCURRENCY=1` makes the top-level aggregate execute one ready gate at a time; coverage, snapshot replay, built-bin smoke, and publication validation also receive worker counts of one. The reference jobs may run beside one another, but each host's repository gates are serial and complete. Linux installs bubblewrap before replaying snapshots, and Windows enables Developer Mode before installing the symlinked workspace.
@@ -28,7 +28,7 @@ The standalone [Sandbox](../../../../.github/workflows/sandbox.yml) workflow bel
Master reference jobs are diagnostic and do not participate in the pull request's required `all checks passed` result. The CI and Sandbox workflows keep their cross-platform references on master pushes. Performance is evaluated from completed hosted-job timestamps and reported as a measurement; it is not encoded as a `timeout-minutes` value.
-The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. The required pull-request Windows job runs under Wine on `ubuntu-latest`, while the independent pull-request native job uses standard `windows-2025` under the [dual Windows decision](2026-08-08-native-windows-pull-request-ci.md); when enabled, `serial / windows` remains a second complete, unsharded native-kernel oracle. Required pull-request jobs use portable standard capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration.
+The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. The required pull-request Windows job runs under Wine on `ubuntu-latest`, while the independent pull-request native job uses the hosted `dsh-windows-2025-16core` runner under normal operation and the self-hosted `[self-hosted, dsh-win-ci, windows]` pool under failover (see the [failover runbook](2026-07-26-ci-failover-runbook.md)), and is absent from the required aggregate under the [dual Windows decision](2026-08-08-native-windows-pull-request-ci.md); when enabled, `serial / windows` remains a second complete, unsharded native-kernel oracle. Required pull-request jobs use portable standard capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration.
## Alternatives considered
diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md
index 8bbb60cdea..bef8c3b640 100644
--- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md
+++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md
@@ -16,7 +16,7 @@ Status: implemented
## 决策
-[CI](../../../../.github/workflows/ci.yml) 为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求在 GitHub 标准托管容量上运行合并后的 Linux 和由 Wine 承载的 Windows 作业,以及 Node 兼容性与 Python 约定;一个独立的原生 Windows 作业会报告完整的 Windows 清单,但不参与必需聚合流程。向 `master` 推送时,当前启用的参考作业是公司自有 `vm-backup` 池上的 `serial / linux (self-hosted standby)`——该热备演练持续验证[故障切换手册](2026-07-26-ci-failover-runbook.md)所描述的切换目标。标准托管的 `serial / linux`、`serial / macos` 和 `serial / windows` 定义仍处于禁用状态,并由 `TODO(hosted-serial-ci)` 标记,直到其可移植容量恢复。各自独立的作业定义有意显式保留简短的代码检出、运行时设置和依赖锁定的安装步骤,而不是用矩阵或可复用工作流隐藏操作系统差异。`workflow_dispatch` 仅用于运行器基准测试。
+[CI](../../../../.github/workflows/ci.yml) 为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求在 GitHub 标准托管容量上运行合并后的 Linux 和由 Wine 承载的 Windows 作业,以及 Node 兼容性与 Python 约定;一个独立的原生 Windows 作业会报告完整的 Windows 清单,但不参与必需聚合流程。向 `master` 推送时,当前启用的参考作业是公司自有 `vm-backup` 池上的 `serial / linux (self-hosted standby)` 和 `dsh-win-ci` 池上的 `serial / windows (self-hosted standby)`——这些热备演练持续验证[故障切换手册](2026-07-26-ci-failover-runbook.md)所描述的切换目标。标准托管的 `serial / linux`、`serial / macos` 和 `serial / windows` 定义仍处于禁用状态,并由 `TODO(hosted-serial-ci)` 标记,直到其可移植容量恢复。各自独立的作业定义有意显式保留简短的代码检出、运行时设置和依赖锁定的安装步骤,而不是用矩阵或可复用工作流隐藏操作系统差异。`workflow_dispatch` 仅用于运行器基准测试。
每个参考作业均在不设置任何分片选择器的情况下运行 `pnpm run check:ci`。`DSH_GATE_CONCURRENCY=1` 使顶层聚合每次只执行一个已经就绪的门禁;覆盖率、快照回放、built-bin 冒烟测试和发布验证的 worker 数量也设为 1。各参考作业可以彼此并行,但每台主机上的仓库门禁都串行运行且完整执行。Linux 在回放快照前安装 bubblewrap,Windows 则在安装采用符号链接的工作区前启用开发人员模式。
@@ -28,7 +28,7 @@ macOS 参考流程使用 fork 进程运行常规 Vitest 项目。macOS arm64 上
master 分支的参考作业仅用于诊断,不参与拉取请求所要求的 `all checks passed` 结果。CI 与 Sandbox 工作流把跨平台参考流程保留在 master 推送上。系统根据已完成托管作业的时间戳评估性能,并将其报告为测量结果,而不是写成 `timeout-minutes` 值。
-可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。拉取请求必需的 Windows 作业在 `ubuntu-latest` 上通过 Wine 运行,而独立的拉取请求原生作业依据[双 Windows 决策](2026-08-08-native-windows-pull-request-ci.md)使用标准 `windows-2025`;`serial / windows` 启用时,仍作为第二个完整且未分片的原生内核标尺。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用可移植的标准容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。
+可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。拉取请求必需的 Windows 作业在 `ubuntu-latest` 上通过 Wine 运行,而独立的拉取请求原生作业在正常运行下使用托管的 `dsh-windows-2025-16core` 运行器,故障切换时使用自托管 `[self-hosted, dsh-win-ci, windows]` 池(参见[故障切换手册](2026-07-26-ci-failover-runbook.md)),依据[双 Windows 决策](2026-08-08-native-windows-pull-request-ci.md)不参与必需聚合流程;`serial / windows` 启用时,仍作为第二个完整且未分片的原生内核标尺。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用可移植的标准容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。
## 曾考虑的替代方案
diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml
index f4fe0c74d2..8615d248fc 100644
--- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
-2026-07-26-ci-failover-runbook.md: 72261f95ea74b61e3915a1a6419b2c2e616efbd9
-2026-07-26-ci-failover-runbook.zh.md: 1ee679eabc296ab31d71b87945409788539425bd
+2026-07-26-ci-failover-runbook.md: 47901844f4ec581dff8500cc429c54a076b7642b
+2026-07-26-ci-failover-runbook.zh.md: 88a793e144a4d06718ffe323a94f8e81f8e62b82
diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
index 72261f95ea..47901844f4 100644
--- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
+++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
@@ -6,25 +6,29 @@ English | [中文](2026-07-26-ci-failover-runbook.zh.md)
## Problem
-The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.yml) (`node 24 / static`, `node 24 / coverage`, `node 24 / snapshots and artifacts`) run on the hosted enterprise 32-core pools; the required verdict job that aggregates them (`all checks passed`) runs on standard `ubuntu-latest`. When the enterprise pools degrade — jobs queue indefinitely or the enterprise labels vanish — every open pull request becomes unmergeable, and the ordinary recovery of merging a fix is itself deadlocked behind the very required checks that cannot run. **Scope: this switch recovers an enterprise Linux-pool outage.** The verdict's other required dependencies (`node-compat`, `python-sdk`, `windows`) stay on standard hosted runners by design (the portable boundary); in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block `all checks passed`, and only the Windows leg has no in-house substitute at all — during the 2026-07-27 outage the standard pools recovered first, which is the ordering this design bets on. An outage therefore needs a switch any responder with repository write access can throw without merging anything.
+The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.yml) (`node 24 / static`, `node 24 / coverage`, `node 24 / snapshots and artifacts`) run on the hosted enterprise 32-core pools; the required verdict job that aggregates them (`all checks passed`) runs on standard `ubuntu-latest`; the independent native Windows job (`windows node 24 / native complete`) runs on the hosted `dsh-windows-2025-16core` larger runner. When the enterprise pools degrade — jobs queue indefinitely or the enterprise labels vanish — every open pull request becomes unmergeable, and the ordinary recovery of merging a fix is itself deadlocked behind the very required checks that cannot run. **Scope: this switch recovers an enterprise Linux-pool outage AND a hosted Windows-pool outage.** The verdict's other required dependencies (`node-compat`, `python-sdk`, `windows`) stay on standard hosted runners by design (the portable boundary); in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block `all checks passed`. An outage therefore needs a switch any responder with repository write access can throw without merging anything.
## Decision
-Each of the three required Linux worker jobs — and the `all checks passed` verdict job, which would otherwise stay queued on the failed pool even after every worker passed — resolves its runner pool through the `DSH_CI_FAILOVER` repository variable. Unset (normal), they run on the hosted enterprise pools. Set to `selfhosted` by any repository writer, all four retarget onto the in-house self-hosted `vm-backup` pool, coverage and snapshot concurrency drop to shared-VM bounds, and the hosted-path pnpm cache restores are skipped. The switch is writer-manageable repository state, not a merge, so it works while every check is red. The in-house pool's readiness is continuously re-proven by the `serial / linux (self-hosted standby)` lane, which runs the complete unsharded aggregate on every master push.
+Each of the three required Linux worker jobs, the independent native Windows job, and the `all checks passed` verdict job — which would otherwise stay queued on the failed pool even after every worker passed — resolves its runner pool through the `DSH_CI_FAILOVER` repository variable. Unset (normal), they run on the hosted enterprise pools. Set to `selfhosted` by any repository writer, all five retarget onto the in-house self-hosted pools: the Linux jobs and verdict onto the `vm-backup` pool, coverage and snapshot concurrency drop to shared-VM bounds, and the hosted-path pnpm cache restores are skipped; the native Windows job onto the `dsh-win-ci` pool. The switch is writer-manageable repository state, not a merge, so it works while every check is red. The in-house pools' readiness is continuously re-proven by the `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes, which run the complete unsharded aggregates on every master push.
### What the in-house pool is
`vm-backup`: one 64-core VM, six always-on systemd-managed runner instances. Its image must preinstall Playwright Chromium's Linux system packages; CI downloads the lockfile-selected browser but never runs `apt` on this persistent shared host. Check the latest `serial / linux (self-hosted standby)` run before switching: its aggregate includes browser replay, so a green standby verifies both ordinary capacity and this browser prerequisite.
+#### Windows pool
+
+`dsh-win-ci`: 32 always-on runner instances (scheduled tasks `GH-Runner-01`…`GH-Runner-32`) on the in-house Windows CI server (one 96-core / 580 GB machine). Labels: `[self-hosted, dsh-win-ci, windows]`. The image must preinstall Node 24, pnpm, Git (with Git Bash on `PATH`, i.e. `C:\Program Files\Git\bin` — the `bash` tool spawns `bash` by name), PowerShell 7, and enable Developer Mode for symlink support. Check the latest `serial / windows (self-hosted standby)` run before switching: a green standby verifies the pool can execute `check:ci:windows-complete` end-to-end.
+
### Switch (any repository writer, ~1 minute, no merge)
1. Repository **Settings → Secrets and variables → Actions → Variables → New repository variable**: name `DSH_CI_FAILOVER`, value `selfhosted`.
2. Retrigger the required jobs so they re-resolve their pool. Jobs already **queued** for the hosted labels do not retarget and cannot be re-run in place, so for the documented indefinite-queue outage, cancel the stuck run and re-run all jobs, or push a new commit; "Re-run failed jobs" only helps once a job has actually failed rather than queued.
3. That is the entire switch. Under failover the workflow also, automatically: drops `DSH_COVERAGE_MAX_WORKERS` to 8 and `DSH_SNAPSHOT_MAX_CONCURRENCY` to 12 (sized for six always-on instances: worst case 6 × 8 = 48 coverage workers on the 64-core VM) (shared-VM contention bounds), and skips the hosted-path pnpm cache restores (the VM's persistent store serves warm installs).
-#**Dependabot exception.** All four selectors deliberately exclude `dependabot[bot]`: under failover, Dependabot PRs stay queued for the hosted pool rather than executing dependency-supplied code on the persistent VM. A Dependabot PR that remains queued during an outage is expected behavior, not a failed switch; it completes when the hosted pool recovers.
+#**Dependabot exception.** All five selectors deliberately exclude `dependabot[bot]`: under failover, Dependabot PRs stay queued for the hosted pool rather than executing dependency-supplied code on the persistent VMs. A Dependabot PR that remains queued during an outage is expected behavior, not a failed switch; it completes when the hosted pool recovers.
-**Who can flip the variable.** GitHub's API lets any collaborator with write access manage repository variables, so the switch is writer-level, not strictly admin-only. In this repository's trust model that is not an escalation: the runner group admits all workflows of this private, fork-disabled repository (a deliberate trade to make PR-ref failover possible at all), so any writer could already reach the VM by pushing a branch workflow. The boundary against untrusted code is repository membership; the variable only routes work for members.
+**Who can flip the variable.** GitHub's API lets any collaborator with write access manage repository variables, so the switch is writer-level, not strictly admin-only. In this repository's trust model that is not an escalation: the runner groups admit all workflows of this private, fork-disabled repository (a deliberate trade to make PR-ref failover possible at all), so any writer could already reach the VMs by pushing a branch workflow. The boundary against untrusted code is repository membership; the variable only routes work for members.
## Capacity during failover
diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md
index 1ee679eabc..88a793e144 100644
--- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md
+++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md
@@ -6,23 +6,27 @@ Status: implemented
## 问题
-[CI](../../../../.github/workflows/ci.yml) 中三个必需的 Linux 工作作业(`node 24 / static`、`node 24 / coverage`、`node 24 / snapshots and artifacts`)运行在托管的企业级 32 核池上;聚合它们的必需判定作业(`all checks passed`)运行在标准 `ubuntu-latest` 上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:本切换恢复的是企业级 Linux 池故障。**判定作业的其余必需依赖(`node-compat`、`python-sdk`、`windows`)按设计留在标准托管运行器上(可移植边界);若更大范围的 GitHub 托管容量故障连标准池一并击倒,这些依赖仍会阻塞 `all checks passed`,且只有 Windows 这条腿完全没有自有替代——2026-07-27 的故障中标准池率先恢复,本设计押注的正是这一顺序。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。
+[CI](../../../../.github/workflows/ci.yml) 中三个必需的 Linux 工作作业(`node 24 / static`、`node 24 / coverage`、`node 24 / snapshots and artifacts`)运行在托管的企业级 32 核池上;聚合它们的必需判定作业(`all checks passed`)运行在标准 `ubuntu-latest` 上;独立的原生 Windows 作业(`windows node 24 / native complete`)运行在托管的 `dsh-windows-2025-16core` 大型运行器上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:本切换恢复的是企业级 Linux 池故障与托管 Windows 池故障。**判定作业的其余必需依赖(`node-compat`、`python-sdk`、`windows`)按设计留在标准托管运行器上(可移植边界);若更大范围的 GitHub 托管容量故障连标准池一并击倒,这些依赖仍会阻塞 `all checks passed`。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。
## 决策
-三个必需的 Linux 工作作业——以及 `all checks passed` 判定作业(若不随切换,即使全部工作作业通过,它仍会滞留在故障池的队列中)——各自通过仓库变量 `DSH_CI_FAILOVER` 解析运行器池。变量不存在(正常)时它们运行在托管企业池上;由任何具备写权限的协作者设为 `selfhosted` 时,四者全部切换到公司自有的自托管 `vm-backup` 池,覆盖率与快照的并发降到共享虚拟机上限,并跳过托管路径的 pnpm 缓存恢复。这个开关是写者可管理的仓库状态而非一次合并,因此在所有检查都是红色时仍然有效。自有池的就绪状态由 `serial / linux (self-hosted standby)` 通道持续验证——每次 master 推送都在其上运行完整的未分片聚合流程。
+三个必需的 Linux 工作作业、独立的原生 Windows 作业,以及 `all checks passed` 判定作业(若不随切换,即使全部工作作业通过,它仍会滞留在故障池的队列中)——各自通过仓库变量 `DSH_CI_FAILOVER` 解析运行器池。变量不存在(正常)时它们运行在托管企业池上;由任何具备写权限的协作者设为 `selfhosted` 时,五个作业全部切换到公司自有的自托管池:Linux 作业与判定作业切到 `vm-backup` 池,覆盖率与快照的并发降到共享虚拟机上限,并跳过托管路径的 pnpm 缓存恢复;原生 Windows 作业切到 `dsh-win-ci` 池。这个开关是写者可管理的仓库状态而非一次合并,因此在所有检查都是红色时仍然有效。自有池的就绪状态由 `serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)` 通道持续验证——每次 master 推送都在其上运行完整的未分片聚合流程。
### 自有池是什么
`vm-backup`:一台 64 核虚拟机,6 个常驻 systemd 管理的运行器实例。其镜像必须预装 Playwright Chromium 的 Linux 系统软件包;CI 会下载锁文件选定的浏览器,但绝不在这台持久化共享主机上运行 `apt`。切换前先看 `serial / linux (self-hosted standby)` 最近一次运行:其聚合流程包含浏览器回放,因此绿色热备同时验证常规容量和这项浏览器先决条件。
+#### Windows 池
+
+`dsh-win-ci`:公司内部 Windows CI 服务器(一台 96 核 / 580 GB 机器)上 32 个常驻运行器实例(计划任务 `GH-Runner-01`…`GH-Runner-32`)。标签:`[self-hosted, dsh-win-ci, windows]`。镜像必须预装 Node 24、pnpm、Git(Git Bash 在 `PATH` 上,即 `C:\Program Files\Git\bin`——`bash` 工具按名称 spawn `bash`)、PowerShell 7,并为符号链接支持启用开发人员模式。切换前先看 `serial / windows (self-hosted standby)` 最近一次运行:绿色热备验证该池能端到端执行 `check:ci:windows-complete`。
+
### 切换步骤(任何具备写权限的协作者,约 1 分钟,无需合并)
1. 仓库 **Settings → Secrets and variables → Actions → Variables → New repository variable**:名称 `DSH_CI_FAILOVER`,值 `selfhosted`。
2. 重新触发必需作业,使其重新解析运行器池。已经为托管标签**排队**的作业不会重定向,也无法原地 re-run,因此对于本手册所述的无限排队故障,应取消卡住的运行并 re-run all jobs,或推送一个新提交;“Re-run failed jobs”只有在作业真正失败(而非仍在排队)时才有用。
3. 切换到此完成。故障切换状态下工作流还会自动:把 `DSH_COVERAGE_MAX_WORKERS` 降为 8、`DSH_SNAPSHOT_MAX_CONCURRENCY` 降为 12(按 6 个常驻实例定容:最坏情况下,6 × 8 = 48 个覆盖率工作进程运行在 64 核虚拟机上)(共享虚拟机的争抢上限),并跳过托管路径的 pnpm 缓存恢复(虚拟机的持久 store 直接提供热安装)。
-#**Dependabot 例外。**四个选择器都刻意排除了 `dependabot[bot]`:故障切换期间,Dependabot 拉取请求继续在托管池排队,而不是把依赖项提供的代码放到持久化虚拟机上执行。故障期间 Dependabot PR 持续排队是预期行为而非切换失败;托管池恢复后它会自行完成。
+#**Dependabot 例外。**五个选择器都刻意排除了 `dependabot[bot]`:故障切换期间,Dependabot 拉取请求继续在托管池排队,而不是把依赖项提供的代码放到持久化虚拟机上执行。故障期间 Dependabot PR 持续排队是预期行为而非切换失败;托管池恢复后它会自行完成。
**谁能扳动这个变量。**GitHub 的 API 允许任何具有写权限的协作者管理仓库变量,因此该开关实际是写者级而非严格的管理员级。在本仓库的信任模型下这并不构成升权:runner group 接纳本私有、禁 fork 仓库的全部工作流(这是让 PR 引用的故障切换得以成立的刻意取舍),因此任何写者本就可以通过推送分支工作流触达这台虚拟机。抵御不可信代码的边界是仓库成员资格;变量只是为成员路由工作。
diff --git a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml
index b60f3ae3ed..6f6823e3c3 100644
--- a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md
-2026-07-30-generated-third-party-notices.md: 58e3fd5007a16d6391956f423d1ec91e851df295
-2026-07-30-generated-third-party-notices.zh.md: 834ec4ebc349bdee37b0873109de566223bb7d48
+2026-07-30-generated-third-party-notices.md: d13eef8412ecd5c8387a7239b812c835671b6bce
+2026-07-30-generated-third-party-notices.zh.md: 0af6a6bcd665197acd6a0b5e9e6f51940a0267e7
diff --git a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md
index 58e3fd5007..d13eef8412 100644
--- a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md
+++ b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md
@@ -20,9 +20,9 @@ One trigger gap is accepted rather than worked around: lefthook inspects only fi
The file discloses **direct** dependencies by default. The complete npm closure with pinned versions already lives in `pnpm-lock.yaml` (`pnpm licenses list` renders it) and the Python closure in `python/sdk/uv.lock`; re-materializing either as prose would be a second, worse copy. The one explicit transitive disclosure is the official Claude platform payload set declared by `@anthropic-ai/claude-agent-sdk` through `optionalDependencies`, because those packages carry the distributed Claude Code executable rather than ordinary library implementation detail.
-**Tiering is by declaring area, not by manifest section.** A package is a runtime dependency when any manifest outside `DEV_ONLY_AREAS` — the root manifest, `packages/support/`, `packages/client/test-runtime/`, `website/`, `examples/`, `native/` — names it under `dependencies` or `optionalDependencies`. Section names alone are wrong in both directions: a test-support package declares `vitest` under `dependencies` without shipping it, and the `bin/dsh` launcher execs through `tsx`, which no manifest declares as a runtime dependency at all (the generator marks it runtime explicitly).
+**Tiering is by declaring area, not by manifest section.** A package is a runtime dependency when any manifest outside `DEV_ONLY_AREAS` — the root manifest, `packages/support/`, `packages/client/test-runtime/`, `website/`, `examples/`, `native/` — names it under `dependencies` or `optionalDependencies`. Section names alone are wrong in both directions: a test-support package declares `vitest` under `dependencies` without shipping it, and the root source-run scripts execute through `tsx`, which no manifest declares as a runtime dependency at all (the generator marks it runtime explicitly).
-The runtime tier deliberately covers **every mountable plugin**, not just what the CLI, Web UI, and Python runtime load by default. `scripts/install.sh` installs the repository itself, so a user's `cordis.yml` can mount any plugin package; `@modelcontextprotocol/sdk` and the OpenTelemetry packages reach real users even though no default assembly imports them. Under-disclosure is the costly direction for a legal notice.
+The runtime tier deliberately covers **every mountable plugin**, not just what the CLI, Web UI, and Python runtime load by default. Source execution can mount any plugin package from a user's `cordis.yml`; `@modelcontextprotocol/sdk` and the OpenTelemetry packages therefore reach real users even though no default assembly imports them. Under-disclosure is the costly direction for a legal notice.
The manifest set is derived from the `packages:` members the root `pnpm-workspace.yaml` declares, including the Landlock workspace and its public packages, so a new member area is read the day it is declared rather than the day someone remembers to extend a list. License and repository metadata come from the root workspace's installed pnpm store and package-local link farms, so the generator requires an installed tree and fails loud when a package resolves to neither, rather than emitting an empty cell. `OVERRIDES` carries the packages whose published manifest cannot answer — Rust-built npm bins that omit `license`, and the `modelcontextprotocol/servers` packages whose repository is mid MIT→Apache-2.0 relicensing, so their effective terms are per-contribution. A runtime dependency whose license is not on the permissive list is a hard error: shipping copyleft is a distribution decision, not something a regenerated table may absorb silently. Vendored packages are cross-checked against `vendor/README.md` and rejected if any is not MIT, and `pnpm-workspace.yaml`'s `patchedDependencies` are listed under the runtime table because pnpm applies those patches at install time — shipped artifacts carry modified copies of `@earendil-works/pi-tui` and `node-pty`, and the patch files are the record of what changed.
diff --git a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md
index 834ec4ebc3..0af6a6bcd6 100644
--- a/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md
+++ b/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md
@@ -20,9 +20,9 @@ Status: implemented
文件默认只披露**直接**依赖。完整的 npm 闭包连同锁定版本已记录在 `pnpm-lock.yaml`(`pnpm licenses list` 可渲染),Python 闭包记录在 `python/sdk/uv.lock`;再用散文誊一遍只会得到一份更差的副本。唯一明确披露的传递依赖,是 `@anthropic-ai/claude-agent-sdk` 通过 `optionalDependencies` 声明的官方 Claude 平台载荷集合,因为这些包承载随产品分发的 Claude Code 可执行文件,而非普通的库实现细节。
-**分层依据是声明方所在区域,而非 manifest 字段名。** 只要 `DEV_ONLY_AREAS` 之外的任一 manifest——即根 manifest、`packages/support/`、`packages/client/test-runtime/`、`website/`、`examples/`、`native/` 之外——在 `dependencies` 或 `optionalDependencies` 里点名某个包,它就是运行时依赖。单看字段名在两个方向上都会出错:测试支撑包把 `vitest` 写在 `dependencies` 里却并不交付它;而 `bin/dsh` 启动器通过 `tsx` 执行,根本没有任何 manifest 把它声明为运行时依赖,只能由生成器显式标记。
+**分层依据是声明方所在区域,而非 manifest 字段名。** 只要 `DEV_ONLY_AREAS` 之外的任一 manifest——即根 manifest、`packages/support/`、`packages/client/test-runtime/`、`website/`、`examples/`、`native/` 之外——在 `dependencies` 或 `optionalDependencies` 里点名某个包,它就是运行时依赖。单看字段名在两个方向上都会出错:测试支撑包把 `vitest` 写在 `dependencies` 里却并不交付它;而根目录的源码运行脚本通过 `tsx` 执行,根本没有任何 manifest 把它声明为运行时依赖,只能由生成器显式标记。
-运行时层刻意覆盖**所有可挂载的插件**,而不止 CLI、Web UI 与 Python 运行时默认加载的那些。`scripts/install.sh` 安装的就是仓库本身,用户的 `cordis.yml` 可以挂载任何插件包;`@modelcontextprotocol/sdk` 与 OpenTelemetry 系列即使没有任何默认装配引入,也会触达真实用户。对法务披露而言,披露不足才是代价更高的那个方向。
+运行时层刻意覆盖**所有可挂载的插件**,而不止 CLI、Web UI 与 Python 运行时默认加载的那些。从源码运行时,用户可以通过 `cordis.yml` 挂载任何插件包;因此,`@modelcontextprotocol/sdk` 与 OpenTelemetry 系列即使没有任何默认装配引入,也会触达真实用户。对法务披露而言,披露不足才是代价更高的那个方向。
manifest 集合由根 `pnpm-workspace.yaml` 声明的 `packages:` 成员派生,其中包括 Landlock 工作区及其公开包,因此新增成员区域在声明当天就会被读取,而不必等谁想起来去补一份列表。许可证与仓库地址取自根工作区已安装的 pnpm store 和包本地链接场;某个包两处都解析不到时直接失败,而不是留下空单元格。`OVERRIDES` 收录已发布 manifest 答不上来的包:用 Rust 构建、发布时省略 `license` 字段的 npm 可执行包,以及 `modelcontextprotocol/servers` 系列——该仓库正处在 MIT 向 Apache-2.0 的重新许可过程中,实际条款按贡献逐条而定。运行时依赖的许可证若不在宽松清单内即为硬失败:交付 copyleft 是一项分发决策,不该被一次重新生成悄悄吸收。被源码收编的包会与 `vendor/README.md` 交叉核对,出现非 MIT 即报错;`pnpm-workspace.yaml` 的 `patchedDependencies` 列入运行时表格,因为 pnpm 在安装期就会打上这些补丁——交付产物携带的是改动过的 `@earendil-works/pi-tui` 与 `node-pty`,补丁文件本身就是改动的完整记录。
diff --git a/.agents/notes/implemented/process/2026-08-10-event-directed-pr-review-status.i18n.yaml b/.agents/notes/implemented/process/2026-08-10-event-directed-pr-review-status.i18n.yaml
new file mode 100644
index 0000000000..08607d5317
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-10-event-directed-pr-review-status.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-10-event-directed-pr-review-status.md
+2026-08-10-event-directed-pr-review-status.md: 9db9c64fc87c1701028ae825357c3cbd7fef44d1
+2026-08-10-event-directed-pr-review-status.zh.md: 381a3f64a62930a584f48cfbc3571679bbcbcef7
diff --git a/.agents/notes/implemented/process/2026-08-10-event-directed-pr-review-status.md b/.agents/notes/implemented/process/2026-08-10-event-directed-pr-review-status.md
new file mode 100644
index 0000000000..9db9c64fc8
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-10-event-directed-pr-review-status.md
@@ -0,0 +1,41 @@
+# Agent Note: Event-directed PR review status commands
+
+Status: implemented
+
+English | [中文](2026-08-10-event-directed-pr-review-status.zh.md)
+
+## Problem
+
+The Issue Project status records who owns the next step of resolving work. Aggregate pull-request review state answers whether GitHub considers the pull request mergeable, but it cannot represent that handoff: an earlier `CHANGES_REQUESTED` review can remain effective after the author fixes the code and requests review again.
+
+A monotonic projection also cannot return an automation-owned Issue from `In review` to `In progress` when a reviewer requests changes. Reconstructing review rounds or reviewer blockers would add state that the required two-event contract does not need.
+
+## Decision
+
+The Issue lifecycle workflow treats review webhooks as commands. `pull_request.review_requested`, including a repeated request, targets `In review`. `pull_request_review.submitted` targets `In progress` only when `review.state` is `changes_requested`; the submitted event remains necessary because a reviewer can request changes without an earlier review-request event. Approved and commented submissions skip their lifecycle job before it creates a Project token, while dismissed reviews are not subscribed.
+
+Ordinary subscribed pull-request events remain forward-only implementation signals: they can move `Inbox`, `Backlog`, or `Ready` to `In progress`, but they cannot move `In review` backward. Review-request commands can move any earlier active status to `In review`. Changes-requested commands can move earlier active statuses forward to `In progress` and can move `In review` back only when the latest status event for the target Project was written by the configured lifecycle actor. A human or unknown latest actor preserves the current status.
+
+The handler resolves only exact same-repository `Fixes`, `Closes`, or `Resolves` references. It does not alter terminal statuses, add an Issue with no Project status, depend on PR metadata validity, query `reviewDecision`, reconstruct review rounds, look up pull requests from Issues, or run a scheduled reconciler.
+
+[Issue lifecycle](../../../../.github/workflows/issue-lifecycle.yml) remains unsubscribed from `pull_request.ready_for_review`; neither event command depends on that action. [Issue policy](../../../../.github/workflows/issue-policy.yml) retains `ready_for_review` because it owns required-check enforcement when a human pull request enters review.
+
+## Verification
+
+[Issue-management tests](../../../../.github/issue-management/policy.test.mjs) pin the event-to-command mapping, the repeated-review-request transition after a changes-requested command, the changes-requested regression, terminal protection, and human override preservation. [Workflow tests](../../../../scripts/ci-workflow.spec.ts) pin the subscribed events, the changes-requested job condition, and the separate `ready_for_review` policy trigger.
+
+## Alternatives considered
+
+**Derive status from `reviewDecision` or a reconstructed review round.** GitHub's aggregate can remain `CHANGES_REQUESTED` after a repeated review request, while a round reducer introduces reviewer and ordering semantics beyond the two explicit handoffs.
+
+**Keep the forward-only projection.** Monotonic advancement protects later statuses, but it leaves an Issue in `In review` while the author is implementing requested changes.
+
+**Apply every review command unconditionally.** This is the smallest event handler, but it lets automation overwrite a human-owned Project status. The latest target-Project status actor therefore guards the only backward transition.
+
+**Restore `ready_for_review` or add a debounce queue.** Ready status carries neither review handoff, while another queue adds latency and control-plane state without changing either command.
+
+## Consequences
+
+A repeated review request moves an automation-managed resolving Issue to `In review` even while GitHub still reports an older blocking review. A later changes-requested review returns it to `In progress`; approval, comments, dismissal, pushes, and reviewer removal leave the most recent command's status unchanged.
+
+The projection remains event-driven and does not repair an event that never runs. Replaying an old workflow run can replay its old command, and ProjectV2 still provides no atomic compare-and-swap between the latest-state read and mutation. Per-pull-request workflow concurrency and the human-ownership guard reduce these races without introducing durable lifecycle state.
diff --git a/.agents/notes/implemented/process/2026-08-10-event-directed-pr-review-status.zh.md b/.agents/notes/implemented/process/2026-08-10-event-directed-pr-review-status.zh.md
new file mode 100644
index 0000000000..381a3f64a6
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-10-event-directed-pr-review-status.zh.md
@@ -0,0 +1,41 @@
+# Agent Note: 由事件直接指定的 PR 评审状态命令
+
+Status: implemented
+
+[English](2026-08-10-event-directed-pr-review-status.md) | 中文
+
+## 问题
+
+Issue 所在 Project 中的状态记录了解决工作的下一步由谁负责。PR(Pull Request)的汇总评审状态可以回答 GitHub 是否认为该 PR 可合并,却无法表示这次交接:作者修复代码并重新请求评审后,先前的 `CHANGES_REQUESTED` 评审仍可能继续生效。
+
+单调投影也无法在评审人提出修改要求时,将由自动化管理的 Issue 从 `In review` 退回 `In progress`。重建评审轮次或评审人阻塞项会引入既定双事件约定并不需要的状态。
+
+## 决策
+
+Issue 生命周期工作流把评审 webhook 视为命令。`pull_request.review_requested`(包括重复请求)将目标状态指定为 `In review`。`pull_request_review.submitted` 将目标状态指定为 `In progress`,但仅在 `review.state` 为 `changes_requested` 时生效;submitted 事件仍不可省略,因为评审人即使没有先触发 review-request 事件,也可以直接提出修改要求。对于 approved 和 commented 提交,工作流会在生命周期作业创建 Project token 前跳过该作业;dismissed 评审则不在订阅范围内。
+
+工作流订阅的普通 PR 事件仍是只向前推进的实现信号:它们可以将 `Inbox`、`Backlog` 或 `Ready` 推进至 `In progress`,但不能让 `In review` 倒退。请求评审命令可将任意较早的活跃状态推进至 `In review`。请求修改命令可将较早的活跃状态推进至 `In progress`;它也可以让 `In review` 状态回退,但仅在目标 Project 的最新状态事件由配置的生命周期执行主体写入时进行。若最新状态事件的执行主体是人工用户或未知主体,则保留当前状态。
+
+处理器仅解析同一仓库内严格匹配的 `Fixes`、`Closes` 或 `Resolves` 引用。它不会更改终态、将没有 Project 状态的 Issue 添加到 Project、依赖 PR 元数据是否有效、查询 `reviewDecision`、重建评审轮次、从 Issue 反向查找 PR,或运行定时协调器。
+
+[Issue 生命周期](../../../../.github/workflows/issue-lifecycle.yml)仍不订阅 `pull_request.ready_for_review`;两条事件命令均不依赖该动作。[Issue 策略](../../../../.github/workflows/issue-policy.yml)保留 `ready_for_review`,因为人工提交的 PR 进入评审时,该工作流负责执行必需检查门禁。
+
+## 验证
+
+[Issue 管理测试](../../../../.github/issue-management/policy.test.mjs)锁定事件到命令的映射、请求修改命令后重复请求评审所触发的状态转换、请求修改后的状态回退、终态保护,以及保留人工覆盖状态。[工作流测试](../../../../scripts/ci-workflow.spec.ts)锁定订阅事件、请求修改作业的条件,以及独立的 `ready_for_review` 策略触发器。
+
+## 考虑过的替代方案
+
+**根据 `reviewDecision` 或重建的评审轮次派生状态。** GitHub 的汇总状态在重复请求评审后仍可能保持为 `CHANGES_REQUESTED`,而轮次归约器会引入超出两个显式交接动作所需范围的评审人语义和顺序语义。
+
+**保留只向前推进的投影。** 单调推进可保护较后的状态不被回退,但作者正在按要求修改代码时,Issue 会一直停留在 `In review`。
+
+**无条件应用每条评审命令。** 这是最精简的事件处理器,但会让自动化覆盖由人工管理的 Project 状态。因此,处理器通过目标 Project 最新状态事件的执行主体保护唯一允许的回退转换。
+
+**恢复 `ready_for_review` 或添加防抖队列。** Ready 状态并不表示两种评审交接中的任何一种;新增队列只会增加延迟和控制平面状态,不会改变任一命令。
+
+## 后果
+
+即使 GitHub 仍报告一个较早的阻塞性评审,重复请求评审也会将正由当前 PR 解决且由自动化管理的 Issue 推进至 `In review`。后续提出修改要求的评审会将其退回 `In progress`;批准、评论、撤销评审、推送和移除评审人都不会改变最近一条命令设定的状态。
+
+投影仍由事件驱动;如果某个事件从未触发工作流运行,投影不会自行修复。回放旧的工作流运行可能会再次执行其中的旧命令;ProjectV2 仍不提供在读取最新状态与执行变更之间进行原子比较并交换(compare-and-swap)的能力。以单个 PR 为粒度的工作流并发控制和人工状态所有权保护机制可减少这些竞态,而无需引入持久化生命周期状态。
diff --git a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml
new file mode 100644
index 0000000000..52d911bb5f
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-10-npm-release-sequences.md
+2026-08-10-npm-release-sequences.md: df81756ab84163b21996b5e2f12c5c8db994d9a5
+2026-08-10-npm-release-sequences.zh.md: 03269aeb987509034564bd0cac92f5d26c7f9b58
diff --git a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md
new file mode 100644
index 0000000000..df81756ab8
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md
@@ -0,0 +1,162 @@
+# Agent Note: Private npm publication as three independent sequences
+
+Status: implemented
+
+English | [中文](2026-08-10-npm-release-sequences.zh.md)
+
+## Problem
+
+This repository held three unrelated groups of publishable packages and no channel that sent any of them to a registry.
+
+`packages/*/*` and `apps/*` form the runtime surface of `@deepseek-ai/dsh`; `vendor/*` holds nine rescoped Cordis framework packages, each carrying its upstream version; `native/landlock-run/packages/*` holds Linux platform packages with their own workflow. The three differ in version baseline, change rate, and build requirements: dsh moves with the product, vendor moves only when upstream is re-synced or a local modification changes, and native needs a musl toolchain and one build per architecture. Forcing them through one pipeline means every product release republishes the framework and the native binaries.
+
+Two hard blockers sat in the way. All 217 workspace manifests set `private: true`, which npm refuses to publish. The subtler one was 933 hand-written `peerDependencies: "^0.0.1"` entries between sibling dsh packages: `pnpm pack` substitutes the `workspace:` protocol but leaves semver ranges alone, and `^0.0.1` means `>=0.0.1 <0.0.2` — it excludes `0.0.2`, and semver excludes prereleases from a range without a prerelease of its own, so it excluded `0.0.1-rc.1` too. Those entries never failed only because the version never left `0.0.1`.
+
+`scripts/publish-npm-baseline.ts` is a local publication script: it packs and publishes in one process, needs a human to authenticate and retry on their own machine, and excludes vendor from its release set. It cannot be the basis for CI publication, though its tarball payload validation and installed-artifact probes are verified parts.
+
+## Decision
+
+### Three independent sequences
+
+`packages/`, `vendor/`, and `native/` each have one bump sequence and one publication, sharing no version, no trigger, and no waiting. Releasing dsh does not republish vendor; releasing vendor does not republish native.
+
+| Sequence | Members | Version baseline | Tag | Workflow |
+|---|---|---|---|---|
+| dsh | `packages/*/*` + `apps/*` (`@deepseek-ai/dsh` and `@deepseek-ai/dsh-frontend`) | one version for the family and the workspace root, `0.0.x` | `dsh-v` | `release.yml` |
+| vendored framework | the nine `vendor/*` packages | each package on its own version line | `vendor--v` (one per package) | `release-vendor.yml` |
+| native | `native/landlock-run/packages/*` | its own `0.0.x` | `landlock-run-v` | `landlock-run-release.yml` |
+
+All three publish privately to the `@deepseek-ai` scope on npmjs.com. `publishConfig.access` in each manifest is `restricted` and no workflow passes `--access`, because a command-line flag overrides the manifest.
+
+### Versions land in the repository from a local command; CI only checks and uploads
+
+Each sequence has one bump-and-commit command: it derives the target version, writes it into the relevant manifests, runs `pnpm install --lockfile-only`, and commits the manifests with the lockfile. The published version is therefore readable from the repository. A human creates the tag after the commit merges to master; CI never writes to the repository and needs no write permission.
+
+`release:dsh` accepts `major`, `minor`, `patch`, or an explicit version, and writes one version across the family **and the workspace root** — the workspace constraint requires every member's version to equal the root's, so the root carries the family version, and the root check accepts a prerelease segment. A prerelease such as `0.0.1-rc.1` drives pack, the installed-artifact probe, and one real private publication before numbered versions follow. The dist-tag decision is the one `landlock-run-release.yml` already made: a version with a prerelease segment publishes under `--tag next`, anything else takes `latest`.
+
+### vendor: publish what changed, and let tags be the ledger
+
+The vendored packages are decoupled from upstream by their scope but keep their own version lines. The published version is the higher of the manifest version and the last published version, with the patch incremented — which also drops an upstream prerelease segment. The first published versions:
+
+| Package | Upstream version | First published version |
+|---|---|---|
+| `@deepseek-ai/cordis` | 4.0.0-rc.7 | 4.0.1 |
+| `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | 1.0.1 |
+| `@deepseek-ai/cosmokit` | 1.8.1 | 1.8.2 |
+| `@deepseek-ai/schemastery` | 3.18.0 | 3.18.1 |
+| `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | 1.0.16 |
+| `@deepseek-ai/cordis-plugin-include` | 1.0.4 | 1.0.5 |
+| `@deepseek-ai/cordis-plugin-timer` | 1.1.2 | 1.1.3 |
+| `@deepseek-ai/cordis-plugin-group` | 1.0.0 | 1.0.1 |
+| `@deepseek-ai/cordis-plugin-logger-console` | 1.0.0 | 1.0.1 |
+
+Taking the last published version as the baseline is what survives a re-sync: upstream restoring `4.0.0-rc.8` after this repository published `4.0.1` would otherwise compute `4.0.1` again and collide. `--prerelease rc.1` publishes a rehearsal instead, which takes `--tag next` and leaves the release numbers free: a prerelease has lower precedence than the release it precedes, so `4.0.1` still follows `4.0.1-rc.1`. That ordering is computed here rather than read from `git tag --sort=v:refname`, which places a prerelease above its release.
+
+Only changed packages publish, and the change judgement adds no state file: **each package has its own tag, and that tag records the commit it last published from**. For each package, bump reads the newest `vendor--v*` tag and diffs the package directory against it. A path counts when the manifest's `files` selects it, when npm publishes it regardless (`package.json`, `README*`, `LICENSE*`), or — for a package whose `files` selects `lib/` — when it is a build input (`src/**`, `tsconfig*.json`, a build config). That last rule exists because a built payload is not tracked by git: without it, a real source change reads as "nothing changed" and the next publication fails on a version whose bytes moved.
+
+A tag is a commit pointer, not proof of publication. Bump asks the registry whether the version its newest tag names exists and fails for a human to resolve when it does not, because a tag pushed for a publication that then failed would otherwise read as "already published" and skip the package indefinitely. Querying a private package needs credentials, so an unauthenticated machine reports the gap instead of failing.
+
+`vendor/cordis` publishes `src` as well. Its export map declares `"./src/*"`, so a tarball without those files points consumers at absent paths, and `files` selecting only build output left the change judgement with no tracked path to match.
+
+### Publication runs only on GitHub, and the registry decides what goes out
+
+Publication runs only from GitHub Actions; there is no local publication path. Publish reads no tag and no manifest of "what this release includes". For each packed tarball it compares the version against the registry, in three states:
+
+| State | Action |
+|---|---|
+| the registry does not have that version | publish |
+| the registry has it, and the tarball's sha512 equals the recorded `dist.integrity` | skip: this is a re-run over one artifact |
+| the registry has it, and the integrity differs | fail, reporting content changed without a version bump |
+
+The third state catches code that changed without a version bump. The first two provide idempotence — re-running publish over one artifact republishes nothing and needs no manual selection of packages. The same rule resolves the tension between one vendor release carrying several tags and a workflow that can only run from one ref: the workflow never infers which packages to publish from the tag it ran from.
+
+### Workspace-internal references use the `workspace:` protocol
+
+Every reference to a workspace member uses `workspace:^`, so `pnpm pack` substitutes a range matching the target version: sibling `peerDependencies` follow the family version, and a reference to a vendored package follows that package's own line. The Landlock platform packages keep `workspace:*`, which publishes the exact version, because a platform package and its entry must agree exactly.
+
+`scripts/check-workspace-constraints.ts` requires the protocol, so a new package cannot reintroduce a hand-written range; the invariant-companion rule requires `workspace:^` for `@deepseek-ai/dsh-invariants` for the same reason.
+
+### Release family objects
+
+The entity in this domain is a **release family**: a set of packages sharing one version baseline and tag naming that publishes as a unit. Adding a family means adding a subclass and a workflow lane, not changing the core.
+
+| Object | Responsibility |
+|---|---|
+| `ReleaseFamily` | a family's identity: member discovery, version baseline, tag prefix, packed-payload rule, installed entry |
+| `ReleaseMember` | one publishable package: directory, name, version, manifest |
+| `publishOrder` | topological order over runtime dependencies, ties broken by package name; a cycle is reported rather than resolved arbitrarily |
+| `pack` | packs a whole family into one directory and records the upload order |
+| `verify` | the family's version baseline, and — when publishing — that the run comes from that family's tag and its members are publishable |
+| `verify-packed-install` | installs the tarballs of one or more pack directories into a throwaway consumer and drives the installed executable |
+| `publish` | the three registry states above |
+| `process` / `tarball` | the one home for spawning commands and for reading a packed tarball, including the entry guard that keeps every script importable |
+
+The dsh family applies the repository's publication payload policy, which rejects sources and declaration maps. The vendored family keeps upstream's payload, because those manifests export `./src/*` and dropping `src` would publish an export map pointing at absent files.
+
+### Workflow shape: pack everything at once, then publish as one set
+
+The `pack` job walks the whole release set once, packing each member into one directory, writes the upload order, and uploads that directory as one artifact; the `publish` job downloads that artifact and publishes each entry in order. The release set is one unit — half the packages can never reach the registry while the other half is still building.
+
+`pack` carries no credentials and runs on every pull request and master push, so a pull request proves the release set still packs. `publish` is a manual dispatch, sits behind the `npm-publish` environment for human approval, and neither builds nor rebuilds — it uploads the bytes pack produced. Pack runs are grouped per ref so concurrent pull requests do not displace each other; the publish job carries the global group, because dist-tags are shared registry state.
+
+A dsh verification installs the vendored family's pack output too. The harness packages declare the vendored framework as a peer, those packages live in another sequence, and the credential-free job cannot fetch them from a private registry — so `release.yml` packs the vendored family for verification while publishing only its own set.
+
+The verification also packs the Landlock entry, which `dsh-sandbox-local` declares as a plain dependency, and omits optional dependencies. The platform packages behind those optional entries need a musl toolchain and one build per architecture, so a job on one runner cannot produce them; a consumer that cannot install them must still start, which is what optional means here. The verification therefore reads a directory by its contents rather than a pack order, because a directory can hold tarballs packed only to satisfy a cross-sequence dependency.
+
+### Repository changes this carried
+
+| Item | Content |
+|---|---|
+| release-set manifests | `private: true` removed; `publishConfig.access: restricted` and `repository` with each package's `directory` added |
+| release-set boundary | every member of `packages/*/*`, `apps/*`, and `vendor/*` |
+| dependency protocol | workspace-internal references are `workspace:^`, with `check-workspace-constraints.ts` and the invariant-companion rule requiring it |
+| root `AGENTS.md` | the convention that vendored packages are `private: true` no longer holds |
+| `vendor/README.md` | records `src` joining `cordis`'s `files` as a local modification |
+| the three native packages | `publishConfig.access: restricted`, and their workflow no longer passes `--access` |
+
+### Relationship to the earlier proposal
+
+This Agent Note replaces the version scheme and the release-set boundary in [artifact-first npm baseline publication](../../proposed/process/2026-08-04-artifact-first-npm-baseline-publication.md): its `--` prerelease versions and `dev-` dist-tag are not adopted, and vendor is not excluded from the release set. What both agree on stands: pack and publish are separate, publish consumes only verified tarballs, and the payload and installed-artifact probes are release gates.
+
+## Alternatives considered
+
+**A `--` version.** Planned for continuous dev publication. It conflicts with keeping the published version in the repository: the version embeds a commit SHA, and writing the version back produces a new commit, so the SHA can only name the parent commit that was published and the link needs a convention to explain it. With numbered versions, a prerelease such as `0.0.1-rc.1` already covers "verify first, then release".
+
+**A `vendor/published.json` ledger recording each package's published version and commit.** This preceded the tag design. It adds a state file that must not drift from the registry. A per-package tag gives the same commit pointer, and the tag has to exist anyway, so it introduces no second copy of the state.
+
+**Event-level tags (`vendor-r1`, `vendor-r2`).** Prepared for one release event carrying several package versions. Once the registry decides what publishes, the workflow no longer infers the set from the tag, so per-package tags suffice — and each one names its own package's real version.
+
+**Putting the nine vendored packages on one `4.0.x` line.** It removes change detection, but cosmokit would jump from `1.8.1` to `4.0.1` and lose its upstream lineage; the upstream ranges inside the nine (`^1.8.1` and friends) would stop matching immediately, forcing a rewrite of the vendored manifests.
+
+**Incrementing every vendored package on every vendor release, with no change detection.** The least machinery, at the cost of new version numbers for packages whose content is byte-identical to the previous release. Tags reduce change detection to reading one tag and running one diff, which is not worth trading for inflated version numbers.
+
+**Deciding "already published" from the version alone, without comparing content.** The reference flow queries no registry: publish uploads each tarball and npm rejects a duplicate version. Skipping on the version alone misses code that changed without a bump, which is the only failure that quietly leaves stale bytes on the registry. The cost is a registry query and a dependency on reproducible builds.
+
+**Verifying only the packed install, with no local registry.** The reference flow unpacks tarballs into a tree and drives it with plain Node, which bypasses version-range resolution. Running a local registry in CI to cover that layer was rejected: artifact correctness is covered by existing tests, the publication path is exercised by the master rehearsal, and a pull request only needs to prove the release set packs. Installing from `file:` specifiers still exercises range resolution for every internal dependency.
+
+**Selecting a subset by entry closure.** Crawling `dependencies` from `@deepseek-ai/dsh` and `@deepseek-ai/dsh-frontend` yields 156 packages, 61 fewer than the whole set. But this repository's plugins are mounted by name from `cordis.yml` rather than imported: `vendor/cordis-plugin-group` and `vendor/cordis-plugin-logger-console` fall outside the dependency closure while being required at runtime. Selecting by code dependency fails as "the consumer installs it and it will not start", and it would need a standing proof that no mounted package was missed. Under a private scope the extra packages are invisible outside the organization. `python/`, the root `examples/`, `docs/`, and `website/` are not members.
+
+**Extending `scripts/publish-npm-baseline.ts`.** It is a local publication script that packs and publishes in one process, the opposite of separating credential-free packing from protected publication. Its verified parts — payload validation and installed-artifact probes — are reused so `pnpm run duplication` does not report clones.
+
+**One workflow with a `family` input.** Two version models in one file forks the concurrency group, the tag prefix, and the rehearsal triggers into conditional expressions. One file per family is both shorter and easier to read.
+
+**Rewriting dependency ranges at publication time.** Compared with the protocol, the rewrite runs only in CI, a local `pnpm install` cannot show whether it is correct, and it repeats on every release.
+
+**Running bump in CI and pushing the version back.** It needs repository write permission for the workflow, and a version commit on the release branch races human commits. Bump and commit stay local; CI checks and uploads.
+
+## Consequences
+
+The release scripts are importable modules behind a guarded entry point, and their judgements carry unit tests: tag naming, publish order and cycle reporting, version-baseline arithmetic, the payload change judgement, and each family's payload policy. Two defects the first draft carried — a publish command that ran the pack command on import, and a change judgement blind to `vendor/cordis` source edits — are exactly what a test at that seam catches.
+
+A pull request runs the full pack for both sequences without credentials and installs the packed dsh tarballs into a throwaway consumer, where plain Node drives `dsh --version`. That probe is deliberately one command: it proves `files` selected a complete payload and that the published ranges resolve, and says nothing about interactive behavior.
+
+What this costs:
+
+- **Tags can drift from the registry.** A tag pushed for a publication that then failed is caught by bump's registry check, but only where credentials exist; an unauthenticated machine reports the gap and continues.
+- **The change judgement depends on visible tags.** A shallow clone, or a checkout without tags, degrades the vendored judgement to "publish everything for the first time". `fetch-depth: 0` is a precondition, not an optimization.
+- **The protocol rewrite touched 1504 dependency declarations.** It does not change local resolution — pnpm already resolves from the workspace — but it changes the ranges that go out.
+- **Private packages need credentials to install.** Every consumer — CI, sandbox e2e, outside users — needs scope credentials, including for the Landlock packages, which have never been published and so cut off no existing anonymous path.
+- **`repository` names a different organization than the one running the workflows.** Token-based publication is unaffected; npm provenance (OIDC) requires the two to agree, so adopting it means either repointing `repository` or publishing from the organization it names.
+- **Byte reproducibility is assumed, not measured.** The skip-on-identical-integrity state rests on packing the same commit twice producing the same bytes. Nothing measures that yet: if the build embeds absolute paths or timestamps, a re-run reports a false failure. Measure it before the first publication a re-run might follow, and fall back to comparing per-file content hashes if it does not hold.
+- **Re-running publish over an older artifact can move `latest` backwards.** Publication is decided per version, so an older set republished after a newer one takes the stable dist-tag again. The rehearsals run from a prerelease version, which never takes `latest`.
+- **The first publication is one large step.** Nine vendored packages and the whole dsh set publish at once, so any payload defect surfaces in a single release, which is why a prerelease version drives the complete path first.
diff --git a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md
new file mode 100644
index 0000000000..03269aeb98
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md
@@ -0,0 +1,162 @@
+# Agent Note: 三条独立序列的私有 NPM 发布
+
+Status: implemented
+
+[English](2026-08-10-npm-release-sequences.md) | 中文
+
+## 问题
+
+这个仓库有三组互不相干的可发布包,却没有任何发布通道把它们送上 registry。
+
+`packages/*/*` 与 `apps/*` 组成 `@deepseek-ai/dsh` 的运行面;`vendor/*` 是九个 rescope 过的 Cordis 框架包,各自带着上游的版本号;`native/landlock-run/packages/*` 是 Linux 平台包,有自己的 workflow。三组的版本基线、变更节奏和构建要求都不同:dsh 随产品迭代,vendor 只在同步上游或改动本地修改时才动,native 需要 musl 工具链和逐架构构建。把它们塞进一条发布流水线,等于每次产品发版都要重发框架和原生二进制。
+
+挡路的还有两处硬门。全部 217 个 workspace manifest 都是 `private: true`,`npm publish` 直接拒绝。更隐蔽的是 933 条 dsh 兄弟包之间硬写的 `peerDependencies: "^0.0.1"`:`pnpm pack` 只替换 `workspace:` 协议,不动语义范围,而 `^0.0.1` 等于 `>=0.0.1 <0.0.2`——发 `0.0.2` 落不进去,发 `0.0.1-rc.1` 也落不进去(semver 规定不带预发布段的范围排除预发布版本)。这些条目至今没出事,只因为版本一直停在 `0.0.1`。
+
+`scripts/publish-npm-baseline.ts` 是本机发布脚本:它把 pack 与 publish 放进同一个进程,需要人工在本机完成认证与重试,且把 vendor 排除在发布集之外。它不能作为 CI 发布的基础,但其中的 tarball payload 校验与已安装产物探针是验证过的零件。
+
+## 决策
+
+### 三条独立序列
+
+`packages/`、`vendor/`、`native/` 各自一条 bump 序列、各自一次发布,不共享版本号、不共享触发、不互相等待。发 dsh 不重发 vendor,发 vendor 不重发 native。
+
+| 序列 | 成员 | 版本基线 | tag | workflow |
+|---|---|---|---|---|
+| dsh | `packages/*/*` + `apps/*`(`@deepseek-ai/dsh` 与 `@deepseek-ai/dsh-frontend`) | 全族与 workspace 根共用一个 `0.0.x` | `dsh-v<版本>` | `release.yml` |
+| vendored framework | `vendor/*` 九个包 | 每包各自一条版本线 | `vendor-<包名>-v<版本>`(每包一个) | `release-vendor.yml` |
+| native | `native/landlock-run/packages/*` | 自己的 `0.0.x` | `landlock-run-v<版本>` | `landlock-run-release.yml` |
+
+三组一律发到 npmjs.com 的 `@deepseek-ai` scope 下的私有包。每个 manifest 的 `publishConfig.access` 都是 `restricted`,且没有任何 workflow 传 `--access`——命令行选项会覆盖 manifest。
+
+### 版本由本地命令写进仓库,CI 只核对与上传
+
+每条序列有一条 bump-and-commit 命令:算出目标版本,写进相关 manifest,跑 `pnpm install --lockfile-only`,再把 manifest 连 lockfile 一起 commit。发布版本因此在仓库里查得到。tag 由人工在 commit 合入 master 后打;CI 不写仓库,也不需要写权限。
+
+`release:dsh` 接受 `major`、`minor`、`patch` 或显式版本号,把同一个版本写进全族**以及 workspace 根**——workspace 约束要求每个成员的版本等于根版本,所以根承载族版本,而根的检查接受预发布段。像 `0.0.1-rc.1` 这样的预发布号先把 pack、已安装产物探针和一次真实私有发布跑通,数字版本随后。dist-tag 沿用 `landlock-run-release.yml` 已有的判定:版本带预发布段就 `--tag next`,否则进 `latest`。
+
+### vendor:谁改了谁发版,tag 就是账本
+
+vendor 九包加了 scope 之后与上游脱钩,但保留各自的版本线。发布版本取「manifest 版本」与「上次发布版本」中较高的那个,再递增 patch——这一步同时去掉上游的预发布段。首发版本:
+
+| 包 | 上游版本 | 首发版本 |
+|---|---|---|
+| `@deepseek-ai/cordis` | 4.0.0-rc.7 | 4.0.1 |
+| `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | 1.0.1 |
+| `@deepseek-ai/cosmokit` | 1.8.1 | 1.8.2 |
+| `@deepseek-ai/schemastery` | 3.18.0 | 3.18.1 |
+| `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | 1.0.16 |
+| `@deepseek-ai/cordis-plugin-include` | 1.0.4 | 1.0.5 |
+| `@deepseek-ai/cordis-plugin-timer` | 1.1.2 | 1.1.3 |
+| `@deepseek-ai/cordis-plugin-group` | 1.0.0 | 1.0.1 |
+| `@deepseek-ai/cordis-plugin-logger-console` | 1.0.0 | 1.0.1 |
+
+以「上次发布版本」为基线才扛得住重同步:本仓发过 `4.0.1` 之后上游把版本恢复成 `4.0.0-rc.8`,只看 manifest 会再算出 `4.0.1` 并撞上已发版本。加 `--prerelease rc.1` 则发一次排练版:它进 `--tag next`,而且不占用那组数字——预发布的优先级低于它所先行的正式版,所以 `4.0.1` 仍然接在 `4.0.1-rc.1` 之后。这个次序由脚本自己算,不读 `git tag --sort=v:refname`——git 会把预发布排在正式版之前。
+
+只发改动过的包,而变更判据不引入新的状态文件:**每包一个 tag,tag 就是「上次发布到哪个 commit」的记录**。bump 对每个包取最新的 `vendor-<包名>-v*` tag,拿包目录与它做 diff。一条路径算命中的条件是:manifest 的 `files` 选中它,或 npm 无论如何都会发布它(`package.json`、`README*`、`LICENSE*`),或者——当该包的 `files` 选中 `lib/` 时——它是构建输入(`src/**`、`tsconfig*.json`、构建配置)。最后那条规则的存在理由是构建产物不在 git 里:没有它,真实的源码改动会读成「没变化」,而下一次发布会在一个字节已变的版本上失败。
+
+tag 只是 commit 指针,不是发布成功的证明。bump 会向 registry 核对「最新 tag 指向的版本是否真的存在」,不存在就明确失败交人处理——否则一个为失败发布而推的 tag 会被读成「已发布」,从此永远跳过该包。查询私有包需要凭据,因此未鉴权的机器只报告这道核对被跳过,不失败。
+
+`vendor/cordis` 现在也发布 `src`。它的 exports 声明了 `"./src/*"`,tarball 里没有这些文件就等于把消费方指向不存在的路径;而 `files` 只选构建产物,也让变更判据没有任何受 git 跟踪的路径可匹配。
+
+### 发布只在 GitHub 执行,由 registry 状态决定发什么
+
+发布只从 GitHub Actions 执行,没有本机发布路径。publish 不读 tag、不读任何「本次发布包含什么」的清单,而是对每个打包好的 tarball 拿版本与 registry 比对,分三态:
+
+| 状态 | 处置 |
+|---|---|
+| registry 上没有该版本 | 发布 |
+| 已有该版本,且 tarball 的 sha512 等于记录的 `dist.integrity` | 跳过:这是同一批产物的重跑 |
+| 已有该版本,但 integrity 不同 | 失败退出,报「内容已变但版本未 bump」 |
+
+第三态拦住「改了代码却没 bump 版本」。前两态给出幂等——同一个 artifact 重跑 publish 不会重复发布,也不需要人工挑拣包。同一条规则还解决了「一次 vendor 发布携带多个 tag,而 workflow 只能从一个 ref 触发」的矛盾:workflow 从不从触发它的 tag 去推断该发哪些包。
+
+### workspace 内部引用走 `workspace:` 协议
+
+所有指向 workspace 成员的引用都用 `workspace:^`,由 `pnpm pack` 替换成匹配目标版本的范围:兄弟包的 `peerDependencies` 跟随族版本,指向 vendored 包的引用跟随那个包自己的版本线。Landlock 平台包保留 `workspace:*`(发布成精确版本),因为平台包与它的入口必须版本完全一致。
+
+`scripts/check-workspace-constraints.ts` 要求这个协议,所以新包无法再引入硬写的范围;同理,invariant companion 规则要求 `@deepseek-ai/dsh-invariants` 用 `workspace:^`。
+
+### 发布族对象
+
+这个领域里的实体是**发布族**:一组共享版本基线与 tag 命名、可整体发布的包。新增一族等于加一个子类和一条 workflow lane,不改核心。
+
+| 对象 | 职责 |
+|---|---|
+| `ReleaseFamily` | 一族的身份:成员发现、版本基线、tag 前缀、打包 payload 规则、已安装入口 |
+| `ReleaseMember` | 一个可发布包:目录、包名、版本、manifest |
+| `publishOrder` | 按运行时依赖的拓扑序,同层按包名排;遇到环是报错而不是随意定序 |
+| `pack` | 把整族打进一个目录并记录上传顺序 |
+| `verify` | 族的版本基线;发布时还要求本次运行来自该族的 tag、且成员可发布 |
+| `verify-packed-install` | 把一个或多个 pack 目录的 tarball 装进一次性 consumer,并驱动已安装的可执行入口 |
+| `publish` | 上面那三态 |
+| `process` / `tarball` | 启动命令、读取打包 tarball 的唯一正家,其中的入口守卫让每个脚本都可被 import |
+
+dsh 族套用仓库的发布 payload 策略(拒绝源码与声明映射)。vendored 族保留上游 payload,因为那些 manifest 导出 `./src/*`,去掉 `src` 会发出一个导出映射指向不存在文件的包。
+
+### workflow 形状:一次性 pack 全部,再统一 publish
+
+`pack` job 一趟遍历整个发布集,把每个成员打进同一个目录,写出上传顺序,整个目录作为一份 artifact 上传;`publish` job 下载那一份 artifact,按顺序逐个发布。发布集是一个整体——绝不会出现一半的包已经上了 registry、另一半还在构建。
+
+`pack` 无凭据,在每个 pull request 和每次 master push 上跑,所以一个 pull request 就能证明发布集仍能完整打出来。`publish` 是手动 dispatch,挂在 `npm-publish` environment 后面等人工审批,且既不构建也不重建——它上传的就是 pack 产出的字节。pack 的 run 按 ref 分组,并发的 pull request 不会互相顶掉;全局分组落在 publish job 上,因为 dist-tag 是共享的 registry 状态。
+
+dsh 的验证会一并安装 vendored 族的 pack 产物。harness 的包把 vendored 框架声明成 peer,而那些包属于另一条序列,无凭据的 job 无法从私有 registry 取到——所以 `release.yml` 为验证而打包 vendored 族,发布的仍只有自己那一份。
+
+验证还会打一份 Landlock entry 的 tarball——`dsh-sandbox-local` 把它声明为普通 `dependencies`——同时略去可选依赖。那些可选项背后的平台包需要 musl 工具链且每个架构各构建一次,单台 runner 产不出来;而装不到它们的消费方也必须能起,这正是「可选」在这里的含义。因此验证按目录内容读取 tarball,而不是读发布顺序:一个目录可能只装着为满足跨序列依赖而打出来的包,任何发布顺序都不描述它。
+
+### 本次带出的仓库改动
+
+| 项 | 内容 |
+|---|---|
+| 发布集 manifest | 去掉 `private: true`;补 `publishConfig.access: restricted` 与带各自 `directory` 的 `repository` |
+| 发布集边界 | `packages/*/*`、`apps/*`、`vendor/*` 的全部成员 |
+| 依赖协议 | workspace 内部引用为 `workspace:^`,由 `check-workspace-constraints.ts` 与 invariant companion 规则强制 |
+| 根 `AGENTS.md` | 「vendored 包是 `private: true`」这条约定不再成立 |
+| `vendor/README.md` | 记录「`src` 加入 `cordis` 的 `files`」这条本地修改 |
+| native 三包 | `publishConfig.access: restricted`,且其 workflow 不再传 `--access` |
+
+### 与先前提案的关系
+
+本 Note 取代 [以产物为先的 NPM 基线发布](../../proposed/process/2026-08-04-artifact-first-npm-baseline-publication.md) 中的版本方案与发布集边界:那篇的 `-<时间戳>-<短 SHA>` 预发布版本与 `dev-` dist-tag 不再采用,vendor 也不排除在发布集之外。两篇一致的部分保留:pack 与 publish 分离、publish 只消费已验证的 tarball、payload 与安装后探针作为发布门。
+
+## 曾考虑的替代方案
+
+**`-<时间戳>-<短 SHA>` 版本号。** 曾计划用于持续 dev 发布。它与「把发布版本留在仓库里」冲突:版本内嵌 commit SHA,而把版本写回会产生新的 commit,于是 SHA 只能指向被发布的父 commit,这条链要靠约定解释。改用数字版本后,`0.0.1-rc.1` 这类预发布号已经覆盖「先验证再正式发」。
+
+**用 `vendor/published.json` 账本记录每包的已发版本与 commit。** 这是 tag 方案之前的设计。它新增一份必须与 registry 不漂移的状态文件;per-package tag 提供同样的 commit 指针,而 tag 本来就要打,不引入第二处状态。
+
+**事件级 tag(`vendor-r1`、`vendor-r2`)。** 为「一次发布事件携带多个包版本」准备。既然由 registry 决定发什么,workflow 就不再从 tag 推断集合,per-package tag 够用,而且每个 tag 携带的是它自己那个包的真实版本。
+
+**把九个 vendored 包统一到一条 `4.0.x` 版本线。** 省掉变更检测,但 cosmokit 会从 `1.8.1` 跳到 `4.0.1`、丢失上游血缘;九包内部的上游范围(`^1.8.1` 之类)会立刻失配,必须改写 vendored manifest。
+
+**每次 vendor 发布把九包全部 patch+1,不做变更检测。** 机制最少,代价是内容与上一版逐字节相同的包也拿到新版本号。tag 把变更检测的成本压到「读一个 tag、跑一次 diff」,不值得为省这点让版本号虚涨。
+
+**只按版本号判断「是否已发布」,不比对内容。** 参照流程根本不查 registry:publish 逐个上传,重复版本由 npm 拒绝。只按版本号跳过会漏掉「改了代码没 bump」,而这是唯一会安静地把旧字节留在 registry 上的错误。代价是引入一次 registry 查询和对构建可复现性的依赖。
+
+**只做打包后安装验证,不起本地 registry。** 参照流程是把 tarball 解包成一棵树、用普通 Node 驱动,这绕过了版本范围解析。曾提议在 CI 里起本地 registry 补这一层,被否:产物正确性已由既有测试覆盖,发布路径由 master 的排练覆盖,而 pull request 只需证明发布集能打出来。用 `file:` 说明符安装依然会对每个内部依赖走一遍范围解析。
+
+**按入口闭包挑一部分包发。** 从 `@deepseek-ai/dsh` 与 `@deepseek-ai/dsh-frontend` 沿 `dependencies` 爬得到 156 个包,比全量少 61 个。但本仓的插件是 `cordis.yml` 按名字挂载的、不是被 import 的:`vendor/cordis-plugin-group` 与 `vendor/cordis-plugin-logger-console` 落在依赖闭包之外,却是运行时必需。照代码依赖挑的失败形态是「消费方装完起不来」,而且要额外持续证明「没漏任何挂载项」。私有 scope 下多出来的包对组织外不可见。`python/`、根 `examples/`、`docs/` 与 `website/` 不是成员。
+
+**在 `scripts/publish-npm-baseline.ts` 上扩展。** 它是本机发布脚本,把 pack 与 publish 放在同一进程,与「无凭据 pack、受保护 publish」的分离相反。它验证过的零件——payload 校验与已安装产物探针——被搬运复用,以免 `pnpm run duplication` 判重复。
+
+**一个 workflow 用 `family` 输入选择序列。** 两套版本模型塞进一个文件,会让 concurrency 组、tag 前缀、排练触发条件全部分叉成条件表达式。一族一个文件更短也更好读。
+
+**在发布期改写依赖范围。** 与协议相比,改写逻辑只在 CI 执行过,本机 `pnpm install` 看不出它是否正确,而且每次发布都要重来一遍。
+
+**在 CI 里执行 bump 并把版本推回仓库。** 需要给 workflow 仓库写权限,且发布分支上的版本 commit 会与人的 commit 竞争。bump 与 commit 留在本地,CI 只核对与上传。
+
+## 后果
+
+发布脚本是带入口守卫的可 import 模块,其判断都有单测覆盖:tag 命名、发布顺序与环报告、版本基线运算、payload 变更判据,以及各族的 payload 策略。第一版带过的两个缺陷——publish 命令在 import 时执行了 pack 命令、变更判据对 `vendor/cordis` 的源码改动失明——正是这类测试在对应接缝上能抓住的。
+
+一个 pull request 会为两条序列跑完整的 pack(无凭据),并把打包好的 dsh tarball 装进一次性 consumer,用普通 Node 驱动 `dsh --version`。这个探针刻意只有一条命令:它证明 `files` 选出了完整 payload、发布出去的范围可解析,不涉及任何交互行为。
+
+代价:
+
+- **tag 可能与 registry 漂移。** 为失败发布而推的 tag 由 bump 的 registry 核对拦下,但只在有凭据的地方;未鉴权的机器只报告这道核对被跳过。
+- **变更判据依赖 tag 可见。** shallow clone 或未拉 tag 会把 vendored 族的判据退化成「全部首发」。`fetch-depth: 0` 是前提,不是优化。
+- **协议改写触及 1504 处依赖声明。** 它不改变本机解析(pnpm 本来就从 workspace 解析),但改变了发布出去的范围写法。
+- **私有包需要凭据才能安装。** 任何消费方——CI、沙箱 e2e、外部使用者——都要持有 scope 凭据,Landlock 三包也在其中;它们从未发布过,所以没有切断既有的匿名安装路径。
+- **`repository` 指向的组织与运行 workflow 的组织不同。** 用 token 发布不受影响;npm provenance(OIDC)要求二者一致,届时要么把 `repository` 改指过去,要么从它指向的组织发布。
+- **字节可复现性是假定的,没有实测。** 「integrity 相同则跳过」这一态建立在「同一 commit 两次 pack 得到相同字节」之上。目前没有任何东西测量过它:若构建嵌入了绝对路径或时间,重跑会误报失败。在第一次可能被重跑的发布之前实测,若不成立就退到比对 tarball 内逐文件内容哈希。
+- **用较旧的 artifact 重跑 publish 会把 `latest` 拉回旧版。** 发布是按版本决定的,所以在较新版本之后重发较旧的一批,会让稳定 dist-tag 再次指向旧版。排练用的是预发布版本,它永远不占 `latest`。
+- **首发是一次大步。** 九个 vendored 包与整个 dsh 集一次发出,任何 payload 缺陷都会集中在同一次发布里暴露——这正是先用预发布版本把完整链路走一遍的理由。
diff --git a/.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.i18n.yaml b/.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.i18n.yaml
new file mode 100644
index 0000000000..46715efdcb
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md
+2026-08-10-vendor-package-rescope.md: f2a142cec4e3c28fae54af8063cd730931fa738b
+2026-08-10-vendor-package-rescope.zh.md: 994064fc869e1ccd95609ec67010eae794ed1c48
diff --git a/.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md b/.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md
new file mode 100644
index 0000000000..f2a142cec4
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md
@@ -0,0 +1,51 @@
+# Agent Note: Rescope vendored Cordis into @deepseek-ai
+
+Status: implemented
+
+English | [中文](2026-08-10-vendor-package-rescope.zh.md)
+
+## Problem
+
+The nine packages under `vendor/` kept their upstream npm names (`cordis`, `cosmokit`, `schemastery`, `@cordisjs/plugin-*`). That premise does not survive publication: every harness package declares `cordis` as a peer dependency, so a consumer installing `@deepseek-ai/dsh-*` must resolve it from the registry, which means publishing the harness publishes this framework layer too. Publishing it under the upstream names squats them on the registry, and where that registry proxies npmjs, the same-name entries shadow the real upstream packages and install the wrong framework into unrelated projects.
+
+## Decision
+
+All nine packages move into the `@deepseek-ai` scope. Directory names, upstream version numbers, and dependency ranges stay untouched, so the `vendor/README.md` manifest still reads as an upstream snapshot. [docs/rescope.md](../../../../docs/rescope.md) restates this mapping for consumers.
+
+| Directory | npm name | Upstream name |
+|---|---|---|
+| `cordis/` | `@deepseek-ai/cordis` | `cordis` |
+| `cosmokit/` | `@deepseek-ai/cosmokit` | `cosmokit` |
+| `schemastery/` | `@deepseek-ai/schemastery` | `schemastery` |
+| `loader/` | `@deepseek-ai/cordis-plugin-loader` | `@cordisjs/plugin-loader` |
+| `include/` | `@deepseek-ai/cordis-plugin-include` | `@cordisjs/plugin-include` |
+| `group/` | `@deepseek-ai/cordis-plugin-group` | `@cordisjs/plugin-group` |
+| `timer/` | `@deepseek-ai/cordis-plugin-timer` | `@cordisjs/plugin-timer` |
+| `hmr/` | `@deepseek-ai/cordis-plugin-hmr` | `@cordisjs/plugin-hmr` |
+| `logger-console/` | `@deepseek-ai/cordis-plugin-logger-console` | `@cordisjs/plugin-logger-console` |
+
+The rewrite touches only **delimited, complete package-name tokens**: quoted or backticked specifiers (optionally with a `/subpath`), `package.json` names and dependency keys, `cordis.yml` `name:` values, and `tsconfig.base.json` `paths` keys. Identically spelled strings that are not package names therefore stayed as they were: the `cordis.yml` config-file family, the Loader's literal `cordis:` builtin prefix (`cordis:include`, `cordis:group` — see `vendor/loader/src/config/tree.ts`), kind strings like `cordis-config-entry`, `@deepseek-ai/dsh-tool-cordis`, Schemastery's upstream `Symbol.for('schemastery')` and `vendor:` metadata field, the `packages//` directory names in `GROUP_ORDER` (`scripts/gen-module-graph.ts`, `scripts/gen-doc-graphs.ts`), and the upstream install instructions in `vendor/*/README.md`.
+
+Two classes are invisible to a token rule and were renamed site by site. First, property access and unquoted object keys — `manifest.peerDependencies?.cordis`, and the manifest keys the scaffold generates in `npm-dependency-policy.ts` and `local-plugin-blueprint.ts` — where TypeScript cannot catch a stale `Record` key. Second, constants that carry the name as data: the vendored set in `check-workspace-constraints.ts`, the group/include names in `verify-cordis-config.ts`, the `declare module` target strings in `cordis-walk.ts`, `gen-scoped-events.ts`, and typert's `analyzer.ts`, and `alwaysBundle` in `app-boot/tsdown.config.ts`.
+
+Markdown splits along what a reader does with it. Every fence follows the rename regardless of its info string, because a fence is code they copy or configuration they mount — the `yaml` fences naming Loader plugins and the `ts ignore-check` fences beside compiled ones included. Prose follows it under `docs/`, where a tutorial sentence quoting a name teaches something this repository no longer resolves. Prose elsewhere — `vendor/*/README.md`, package READMEs, and `.agents/notes/` — keeps the names it was written with, both because it records what was true then and because the same spelling can mean something else: the Python SDK's `cordis` option, the unvendored `@cordisjs/plugin-http`, or an agent-preset id.
+
+## Consequences
+
+- No upstream name remains in the publication set. `publish-npm-baseline.ts` now requires every published package to be `@deepseek-ai/*` with no vendored exemption, so regressing the rename fails before packing.
+- The `vendor/README.md` manifest table gains an upstream-name column; `gen-third-party-notices` parses six columns and renders that name into `THIRD_PARTY_NOTICES.md`, keeping MIT attribution pointed at each fork's origin rather than our scope.
+- `pnpm-workspace.yaml` drops the `cordis` and `@cordisjs/plugin-loader` `minimumReleaseAgeExclude` entries, which can no longer be fetched from a registry, and `knip.json` drops the `@cordisjs/.+` ignore pattern that `@deepseek-ai/.+` already covers.
+- Upstream sync follows the procedure in `vendor/README.md` with one added obligation in step 3: re-apply the rename over the copied sources with `pnpm run rescope-vendor --apply`, whose mapping and the table's two name columns must agree.
+- **Returning to the official upstream packages** means applying that mapping in reverse — `pnpm run rescope-vendor --apply --reverse` — then restoring the two `minimumReleaseAgeExclude` entries and relaxing the publication-set assertion. It spans roughly 1300 files, so replay it with the script rather than by hand.
+
+`scripts/rescope-vendor.ts` owns the rename: the mapping, the delimited-token rule, the per-file exemptions where a name is a directory instead of a package, the exact edits above, and a `--check` mode asserting no residue, every exact edit landed, and idempotency, which the `hygiene` gate runs on every CI pass. A rebase replays it instead of resolving a 1300-file conflict, and an upstream change to one of the pinned sites fails the run loudly instead of being silently skipped.
+
+## Alternatives considered
+
+**Keep the upstream names and exclude `vendor/` from publication.** Rejected because every harness package declares `cordis` as a peer dependency, so an installed `@deepseek-ai/dsh-*` would have no resolvable framework.
+
+**Rename only at pack time.** Rejected because the published names would disagree with the source tree, every module specifier would have to be rewritten inside the publish path, and no local run could reproduce what was published.
+
+**Rename the `vendor/` directories and unify versions on the repository base version too.** Rejected because directory names are not publication identity — renaming them drags in project references, tsdown globs, and documentation paths for no gain — and a `0.0.1` version would no longer satisfy the preserved `^4.0.0-rc.7` ranges, so pnpm would look for a registry copy and `verify-vendored-links` would fail.
+
+**Rewrite prose outside `docs/` and historical Agent Notes as well.** Rejected because those record what was true when written, and a bare `cordis` there is as likely to be an SDK option name or a preset id as a package; `docs/rescope.md` carries the mapping for readers instead.
diff --git a/.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.zh.md b/.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.zh.md
new file mode 100644
index 0000000000..994064fc86
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.zh.md
@@ -0,0 +1,51 @@
+# Agent Note: 把 vendored Cordis 重命名进 @deepseek-ai scope
+
+Status: implemented
+
+[English](2026-08-10-vendor-package-rescope.md) | 中文
+
+## 问题
+
+`vendor/` 下的九个包此前保留上游 npm 名(`cordis`、`cosmokit`、`schemastery`、`@cordisjs/plugin-*`)。这个前提在发布时不成立:每个 harness 包都把 `cordis` 声明成 peer dependency,装了 `@deepseek-ai/dsh-*` 的消费者必须能从 registry 解析到它,所以发布 harness 必然连带发布这一层框架。用上游名发布就是在 registry 上占用别人的名字;若该 registry 对 npmjs 做上游代理,本名条目还会遮蔽真正的上游包,把错误的框架装进无关项目。
+
+## 决定
+
+九个包统一改名进 `@deepseek-ai` scope。目录名、上游版本号、依赖 range 一律不动,所以 `vendor/README.md` 的清单仍然读作一份上游快照。面向使用者的映射表见 [docs/rescope.md](../../../../docs/rescope.md)。
+
+| 目录 | npm 名 | 上游名 |
+|---|---|---|
+| `cordis/` | `@deepseek-ai/cordis` | `cordis` |
+| `cosmokit/` | `@deepseek-ai/cosmokit` | `cosmokit` |
+| `schemastery/` | `@deepseek-ai/schemastery` | `schemastery` |
+| `loader/` | `@deepseek-ai/cordis-plugin-loader` | `@cordisjs/plugin-loader` |
+| `include/` | `@deepseek-ai/cordis-plugin-include` | `@cordisjs/plugin-include` |
+| `group/` | `@deepseek-ai/cordis-plugin-group` | `@cordisjs/plugin-group` |
+| `timer/` | `@deepseek-ai/cordis-plugin-timer` | `@cordisjs/plugin-timer` |
+| `hmr/` | `@deepseek-ai/cordis-plugin-hmr` | `@cordisjs/plugin-hmr` |
+| `logger-console/` | `@deepseek-ai/cordis-plugin-logger-console` | `@cordisjs/plugin-logger-console` |
+
+改写只落在**带定界符的完整包名 token** 上:引号或反引号包裹的 specifier(可带 `/子路径`)、`package.json` 的 `name` 与依赖键、`cordis.yml` 的 `name:` 值、`tsconfig.base.json` 的 `paths` 键。因此以下同形串一律未改,它们不是包名:`cordis.yml` 及其家族文件名、Loader 的 `cordis:` 内建前缀(`cordis:include`、`cordis:group`,见 `vendor/loader/src/config/tree.ts`)、`cordis-config-entry` 这类 kind 串、`@deepseek-ai/dsh-tool-cordis`、Schemastery 上游的 `Symbol.for('schemastery')` 与 `vendor:` 元数据、`scripts/gen-module-graph.ts` 与 `gen-doc-graphs.ts` 里 `GROUP_ORDER` 的 `packages//` 目录名,以及 `vendor/*/README.md` 里的上游安装指引。
+
+Token 规则看不见两类点位,它们按名字逐处改:一是属性访问与未加引号的对象键(`manifest.peerDependencies?.cordis`、脚手架 `npm-dependency-policy.ts` 与 `local-plugin-blueprint.ts` 生成的清单键)——TypeScript 抓不到过期的 `Record` 键;二是把名字当数据的常量(`check-workspace-constraints.ts` 的 vendored 集合、`verify-cordis-config.ts` 的 group/include 名、`cordis-walk.ts` 与 `gen-scoped-events.ts` 与 typert `analyzer.ts` 里识别 `declare module` 目标的字符串、`app-boot/tsdown.config.ts` 的 `alwaysBundle`)。
+
+Markdown 按「读者拿它做什么」一分为二。围栏一律跟着改,不看 info string——围栏里是读者要照抄的代码或要挂载的配置,包括写着 Loader 插件名的 `yaml` 围栏和紧邻编译围栏的 `ts ignore-check` 围栏。散文只在 `docs/` 下跟着改:教程里引用某个名字的句子,教的是本仓已不解析的东西。`docs/` 之外的散文——`vendor/*/README.md`、各包 README、`.agents/notes/`——保留写作当时的名字:既因为它记录的是当时的事实,也因为同一个拼写可能指别的东西,比如 Python SDK 的 `cordis` 选项、我们没 vendor 的 `@cordisjs/plugin-http`,或某个 agent-preset 的 id。
+
+## 影响
+
+- 发布集里不再有任何上游名:`publish-npm-baseline.ts` 现在无条件要求每个待发包都是 `@deepseek-ai/*`,vendored 包不再豁免,改名一旦回退就会在打包前失败。
+- `vendor/README.md` 的清单表新增「上游名」列,`gen-third-party-notices` 随之解析六列并把上游名渲进 `THIRD_PARTY_NOTICES.md`;MIT 归属指向 fork 的来源,而不是我们的 scope。
+- `pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude` 删去 `cordis` 与 `@cordisjs/plugin-loader` 两条:改名后这两个名字永远不从 registry 取。`knip.json` 的 `@cordisjs/.+` 忽略模式同理删除,已被 `@deepseek-ai/.+` 覆盖。
+- 上游 sync 照 `vendor/README.md` 的流程走,第 3 步多一项:对拷进来的源码重跑 `pnpm run rescope-vendor --apply`,脚本里的映射与清单表两列名字必须一致。
+- **要回到官方上游包**时反着跑这份映射——`pnpm run rescope-vendor --apply --reverse`——再补回 `minimumReleaseAgeExclude` 两条、放开发布集对 `@deepseek-ai/*` 的断言。改写量约 1300 个文件,用脚本重放而不是手改。
+
+改名这件事由 `scripts/rescope-vendor.ts` 承载:映射、带定界符的 token 规则、名字其实是目录而非包时的逐文件豁免、上面那批精确改写,以及一个断言「零残留、每条精确改写都落上、幂等」的 `--check` 模式——它由 `hygiene` 门在每次 CI 上执行。rebase 时重放它,而不是去解一个 1300 文件的冲突;上游动了任一被钉住的点位,脚本会响亮失败而不是静默漏改。
+
+## 考虑过的替代方案
+
+**保留上游名,把 `vendor/` 排除在发布集之外。** 否决:每个 harness 包都声明 `cordis` 为 peer dependency,装好的 `@deepseek-ai/dsh-*` 会解析不到框架。
+
+**只在打包时改名。** 否决:发出去的名字与源码树不一致,所有模块 specifier 得在发布路径里现改,本地也没有任何一次运行能复现发布出去的东西。
+
+**目录名与版本号一并改。** 否决:目录名不是发布标识,改它会连带项目引用、tsdown glob 与文档路径,收益为零;版本号并入 `0.0.1` 后不再满足保留下来的 `^4.0.0-rc.7` range,pnpm 会转去 registry 找副本,`verify-vendored-links` 直接红。
+
+**`docs/` 之外的散文与历史 Agent Note 一起改。** 否决:它们记录的是写作当时的事实,而且那里的裸 `cordis` 同样可能是 SDK 选项名或某个 preset id,未必是包;面向读者的映射由 `docs/rescope.md` 承载。
diff --git a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.i18n.yaml
index dcef1b190f..b23fcae758 100644
--- a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.i18n.yaml
+++ b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md
-2026-07-20-remove-stdio-and-echo-agents.md: 256e626f4ff41016d0227eef7cc3e4e51e15058b
-2026-07-20-remove-stdio-and-echo-agents.zh.md: 013135eff5d1dbbc570561e70c751ff4de289989
+2026-07-20-remove-stdio-and-echo-agents.md: 23fcb90599c2ff96cd7ac0e6f7ee8fd508a6d1ad
+2026-07-20-remove-stdio-and-echo-agents.zh.md: 7aabf5612a54245327da9af804f745237472cb88
diff --git a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md
index 256e626f4f..23fcb90599 100644
--- a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md
+++ b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md
@@ -19,7 +19,7 @@ The stdio and Echo agents are removed without compatibility packages, modes, com
The remaining application roles are explicit:
- `@deepseek-ai/dsh-tui` owns terminal-interactive execution. It rejects non-TTY streams before Loader boot; `apps/cli/config/base.cordis.yml` plus the `tui.cordis.yml` overlay own the complete coding composition, with PTY plus terminal-snapshot coverage in `apps/cli/tests/`.
-- [`dsh run`](../../../../apps/cli/README.md) owns non-interactive execution. Its `headless` profile is the product composition; `examples/headless-agent` owns replay snapshots, generic real-agent suites, and an unexported keyless Loader driver.
+- [`dsh --profile headless`](../../../../apps/cli/README.md) owns non-interactive execution. Its `headless` profile is the product composition; `examples/headless-agent` owns replay snapshots, generic real-agent suites, and an unexported keyless Loader driver.
- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) and `@deepseek-ai/dsh-jsonrpc` own their framed protocol integrations.
The SDK project model and create/config workflows replace the `stdio` run-interface option with `tui`; generated TUI projects compose `@deepseek-ai/dsh-tui` and create or resume one exact session. Repository-facing demo documentation requires a DeepSeek API key and leads with the real Headless or TUI agents.
@@ -30,7 +30,7 @@ Keyless validation is test-owned. The Headless Loader smoke uses a fixture adapt
TUI and Headless Loader coverage run the real app packages in source and built modes. PTY-driven subprocess coverage is reserved for the TUI lifecycle; other entry-point smokes use the one-shot pipe protocol. Headless proves its task/result and tool-call contracts. Generated graphs and repository searches reject stale package, command, leaf, SDK-interface, `createStdioChat`, and `StdioRuntime` references.
-The built `dsh` bin rejects a piped TUI launch before Loader boot and points at `dsh run`; `apps/cli/tests/built-bin.e2e.ts` pins the product one-shot entry under plain Node, including output and invalid arguments. `examples/headless-agent/tests/headless.snapshot.ts` pins product persistence, while `apps/cli/tests/headless-shutdown.e2e.ts` owns bounded signal escalation. The headless example's test-only JSONL driver preserves assembled canonical-event snapshots without creating a second CLI contract. Code Mode has programmatic TUI snapshots and an ACP overlay demo. Time-context integration uses the explicit Headless test composition for two ordered turns, while its package tests own finer elapsed-time behavior.
+The built `dsh` bin rejects a piped TUI launch before Loader boot and points at `dsh --profile headless`; `apps/cli/tests/built-bin.e2e.ts` pins the product one-shot entry under plain Node, including output and invalid arguments. `examples/headless-agent/tests/headless.snapshot.ts` pins product persistence, while `apps/cli/tests/headless-shutdown.e2e.ts` owns bounded signal escalation. The headless example's test-only JSONL driver preserves assembled canonical-event snapshots without creating a second CLI contract. Code Mode has programmatic TUI snapshots and an ACP overlay demo. Time-context integration uses the explicit Headless test composition for two ordered turns, while its package tests own finer elapsed-time behavior.
## Alternatives considered
diff --git a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.zh.md b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.zh.md
index 013135eff5..7aabf5612a 100644
--- a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.zh.md
+++ b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.zh.md
@@ -19,7 +19,7 @@ DeepSeek Harness 在 TUI 和 Headless coding agent 之外,还提供了两个
保留的应用角色均有明确归属:
- `@deepseek-ai/dsh-tui` 负责终端交互式执行。它会在 Loader 启动前拒绝非 TTY 流;`apps/cli/config/base.cordis.yml` 与 `tui.cordis.yml` overlay 拥有完整 coding 组装,PTY 与终端快照覆盖则位于 `apps/cli/tests/`。
-- [`dsh run`](../../../../apps/cli/README.md) 负责非交互式执行。其 `headless` profile 是产品组装;`examples/headless-agent` 负责回放快照、通用真实 agent 测试套件和未导出的无密钥 Loader driver。
+- [`dsh --profile headless`](../../../../apps/cli/README.md) 负责非交互式执行。其 `headless` profile 是产品组装;`examples/headless-agent` 负责回放快照、通用真实 agent 测试套件和未导出的无密钥 Loader driver。
- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) 和 `@deepseek-ai/dsh-jsonrpc` 负责各自的分帧协议集成。
SDK 工程模型与 create/config 工作流将 `stdio` 运行接口选项替换为 `tui`;生成的 TUI 工程组合 `@deepseek-ai/dsh-tui`,并创建或恢复一个确切会话。仓库中的演示文档要求 DeepSeek API key,并优先引导到真实的 Headless 或 TUI agent。
@@ -30,7 +30,7 @@ SDK 工程模型与 create/config 工作流将 `stdio` 运行接口选项替换
TUI 与 Headless 的 Loader 覆盖以源码和构建产物两种模式运行真实 app 包。由 PTY 驱动的子进程覆盖仅用于 TUI 生命周期;其他入口冒烟测试使用单次管道协议。Headless 验证任务/结果约定和工具调用约定。生成图谱与仓库搜索会拒绝陈旧的包、命令、叶节点、SDK 接口、`createStdioChat` 和 `StdioRuntime` 引用。
-构建后的 `dsh` 可执行文件会在 Loader 启动前拒绝通过管道启动 TUI,并指向 `dsh run`;`apps/cli/tests/built-bin.e2e.ts` 在普通 Node 下固定产品的一次性入口,包括输出和无效参数。`examples/headless-agent/tests/headless.snapshot.ts` 固定产品持久化,`apps/cli/tests/headless-shutdown.e2e.ts` 则负责有界信号升级。headless 示例仅供测试的 JSONL driver 保留组装后的规范事件快照,而不会创建第二套 CLI(命令行界面)约定。Code Mode 由程序化 TUI 快照与 ACP overlay demo 覆盖。时间上下文集成通过显式的 Headless 测试组装执行两个有序轮次,而更细粒度的耗时行为由时间上下文的包级测试负责。
+构建后的 `dsh` 可执行文件会在 Loader 启动前拒绝通过管道启动 TUI,并指向 `dsh --profile headless`;`apps/cli/tests/built-bin.e2e.ts` 在普通 Node 下固定产品的一次性入口,包括输出和无效参数。`examples/headless-agent/tests/headless.snapshot.ts` 固定产品持久化,`apps/cli/tests/headless-shutdown.e2e.ts` 则负责有界信号升级。headless 示例仅供测试的 JSONL driver 保留组装后的规范事件快照,而不会创建第二套 CLI(命令行界面)约定。Code Mode 由程序化 TUI 快照与 ACP overlay demo 覆盖。时间上下文集成通过显式的 Headless 测试组装执行两个有序轮次,而更细粒度的耗时行为由时间上下文的包级测试负责。
## 曾考虑的替代方案
diff --git a/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.i18n.yaml
index 217265168d..e72d9beaf3 100644
--- a/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.i18n.yaml
+++ b/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md
-2026-08-08-remove-cli-demo.md: 403e01f94c976d2d17eb391830721b31675cd6a9
-2026-08-08-remove-cli-demo.zh.md: 7f11e0c17a15454b99b32d14ea6eda177f4b01f6
+2026-08-08-remove-cli-demo.md: f8bc09825797f2165c14a75e5841d74b2e1bc1af
+2026-08-08-remove-cli-demo.zh.md: 1044dd136888e33fc9935cf4e24cd3100b41b41f
diff --git a/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md b/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md
index 403e01f94c..f8bc098257 100644
--- a/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md
+++ b/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.md
@@ -6,29 +6,29 @@ English | [中文](2026-08-08-remove-cli-demo.zh.md)
## Problem
-After [`dsh run`](../feature/2026-08-08-dsh-run-headless-command.md) became the product one-shot command, `@deepseek-ai/dsh-cli-demo` remained a second application package for the same job. It carried another executable, argument grammar, app composition, cancellation lifecycle, text/JSON/stream-JSON output contract, built artifact, documentation surface, and test suite. The two entry points also assembled different trees, so a successful demo did not prove the shipped `headless` profile and users had to choose between overlapping commands.
+After [`dsh --profile headless`](../architecture/2026-08-06-app-owned-command-line.md) became the product one-shot command, `@deepseek-ai/dsh-cli-demo` remained a second application package for the same job. It carried another executable, argument grammar, app composition, cancellation lifecycle, text/JSON/stream-JSON output contract, built artifact, documentation surface, and test suite. The two entry points also assembled different trees, so a successful demo did not prove the shipped `headless` profile and users had to choose between overlapping commands.
The replay suites still need canonical session events to pin assembled backend behavior. That testing need does not require a published command or compatibility contract.
## Decision
-Delete `@deepseek-ai/dsh-cli-demo` completely: its package, bin, parser, app plugin, output formats, tests, workspace references, generated-catalog entries, and active documentation. No alias or compatibility package remains. The root `demo:headless` script is retained only as a direct alias of `dsh run`; the product command owns final-text stdout, the observation URL on stderr, persistence, exit status, and shutdown.
+Delete `@deepseek-ai/dsh-cli-demo` completely: its package, bin, parser, app plugin, output formats, tests, workspace references, generated-catalog entries, and active documentation. No alias or compatibility package remains. Source users invoke the product command through `pnpm dsh --profile headless`; it owns final-text stdout, failure diagnostics on stderr, persistence, exit status, and shutdown.
`examples/headless-agent` becomes an explicit test composition. Its Loader configs mount `@deepseek-ai/dsh-agent-spine-demo`, one root agent, JSONL persistence, and checkpoint policy as separate rows instead of hiding them behind an app bundle. The support-tier `@deepseek-ai/dsh-loader-smoke` package owns the shared direct-agent turn helper; unexported example-local drivers select their Loader configuration and render canonical events as JSONL. They are launched only by tests, have no bin, and do not define a supported product output format.
## Alternatives considered
-- **Keep `dsh-cli-demo` as an alias or wrapper around `dsh run`.** Rejected because a second bin and package would preserve two discoverable owners without adding capability.
-- **Move JSON and stream-JSON flags onto `dsh run`.** Rejected because no current product consumer requires them; adopting the old demo protocol would enlarge the canonical CLI contract solely to save test machinery.
+- **Keep `dsh-cli-demo` as an alias or wrapper around `dsh --profile headless`.** Rejected because a second bin and package would preserve two discoverable owners without adding capability.
+- **Move JSON and stream-JSON flags onto `dsh --profile headless`.** Rejected because no current product consumer requires them; adopting the old demo protocol would enlarge the canonical CLI contract solely to save test machinery.
- **Delete the canonical-event snapshots with the package.** Rejected because they pin model-visible assembled behavior that final-text product acceptance cannot observe.
- **Keep the app plugin but delete only its bin.** Rejected because the hidden composition would still duplicate the explicit headless profile and conceal which services the test leaf mounts.
## Consequences
-This is intentionally breaking. `dsh-cli-demo`, its `--output-format` choices, and imports from `@deepseek-ai/dsh-cli-demo/src/cli.ts` no longer resolve. There is no public event-stream replacement in this change; callers use `dsh run` for one-shot execution and must choose an existing protocol surface when they need structured automation.
+This is intentionally breaking. `dsh-cli-demo`, its `--output-format` choices, and imports from `@deepseek-ai/dsh-cli-demo/src/cli.ts` no longer resolve. There is no public event-stream replacement in this change; callers use `dsh --profile headless` for one-shot execution and must choose an existing protocol surface when they need structured automation.
-The repository retains backend replay coverage through test-only infrastructure, while product smoke and built-bin acceptance exercise `dsh run`. A separate one-shot package may return only if it owns a genuinely independent, versioned protocol that cannot belong to the product launcher; a second spelling or output shim is not enough.
+The repository retains backend replay coverage through test-only infrastructure, while product smoke and built-bin acceptance exercise `dsh --profile headless`. A separate one-shot package may return only if it owns a genuinely independent, versioned protocol that cannot belong to the product launcher; a second spelling or output shim is not enough.
## Verification
-Focused Loader smokes cover the explicit composition in source and plain-Node built modes, snapshot tests diff its canonical JSONL and persisted logs, product acceptance covers `dsh run`, and documentation plus generated graph/catalog gates reject live references to the removed package. The frozen Agent Note archive remains historical evidence and is not rewritten.
+Focused Loader smokes cover the explicit composition in source and plain-Node built modes, snapshot tests diff its canonical JSONL and persisted logs, product acceptance covers `dsh --profile headless`, and documentation plus generated graph/catalog gates reject live references to the removed package. The frozen Agent Note archive remains historical evidence and is not rewritten.
diff --git a/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.zh.md b/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.zh.md
index 7f11e0c17a..1044dd1368 100644
--- a/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.zh.md
+++ b/.agents/notes/implemented/simplification/2026-08-08-remove-cli-demo.zh.md
@@ -6,29 +6,29 @@ Status: implemented
## 问题
-在 [`dsh run`](../feature/2026-08-08-dsh-run-headless-command.md) 成为产品的一次性命令后,`@deepseek-ai/dsh-cli-demo` 仍是承担同一工作的第二个应用包。它另行拥有一套可执行文件、参数语法、应用组装、取消生命周期、文本/JSON/stream-JSON 输出约定、构建产物、配套文档和测试套件。两个入口组装的树也不相同,因此 demo 成功不能证明已交付的 `headless` profile 可用,用户还必须在功能重叠的命令之间作出选择。
+在 [`dsh --profile headless`](../architecture/2026-08-06-app-owned-command-line.md) 成为产品的一次性命令后,`@deepseek-ai/dsh-cli-demo` 仍是承担同一工作的第二个应用包。它另行拥有一套可执行文件、参数语法、应用组装、取消生命周期、文本/JSON/stream-JSON 输出约定、构建产物、配套文档和测试套件。两个入口组装的树也不相同,因此 demo 成功不能证明已交付的 `headless` profile 可用,用户还必须在功能重叠的命令之间作出选择。
回放套件仍需要规范会话事件来固定组装后的后端行为。这一测试需求不需要已发布命令或兼容性约定。
## 决策
-彻底删除 `@deepseek-ai/dsh-cli-demo`:包括它的包、bin、解析器、应用插件、输出格式、测试、workspace 引用、生成目录条目和现行文档。不保留别名或兼容包。根目录的 `demo:headless` 脚本仅作为 `dsh run` 的直接别名保留;stdout 上的最终文本、stderr 上的观察 URL、持久化、退出状态和关闭行为均由产品命令负责。
+彻底删除 `@deepseek-ai/dsh-cli-demo`:包括它的包、bin、解析器、应用插件、输出格式、测试、workspace 引用、生成目录条目和现行文档。不保留别名或兼容包。源码用户通过 `pnpm dsh --profile headless` 调用产品命令;stdout 上的最终文本、stderr 上的失败诊断、持久化、退出状态和关闭行为均由该命令负责。
`examples/headless-agent` 成为显式测试组装。其 Loader 配置把 `@deepseek-ai/dsh-agent-spine-demo`、一个根 agent(智能体)、JSONL 持久化和检查点策略挂载为独立配置行,不再将其隐藏在应用组合包之后。支持层的 `@deepseek-ai/dsh-loader-smoke` 包负责共享的直接 agent 轮次 helper;未导出的示例本地 driver 选择各自的 Loader 配置,并将规范事件渲染为 JSONL。这些 driver 只由测试启动,不提供 bin,也不定义受支持的产品输出格式。
## 考虑过的替代方案
-- **保留 `dsh-cli-demo` 作为 `dsh run` 的别名或包装层。** 不予采纳:第二个 bin 和包会让同一功能继续存在两个可发现的归属方,却没有增加任何能力。
-- **把 JSON 和 stream-JSON 标志移到 `dsh run`。** 不予采纳:当前没有产品消费方需要这些标志;沿用旧 demo 协议,只会为了保留测试机制而扩大规范 CLI(命令行界面)约定。
+- **保留 `dsh-cli-demo` 作为 `dsh --profile headless` 的别名或包装层。** 不予采纳:第二个 bin 和包会让同一功能继续存在两个可发现的归属方,却没有增加任何能力。
+- **把 JSON 和 stream-JSON 标志移到 `dsh --profile headless`。** 不予采纳:当前没有产品消费方需要这些标志;沿用旧 demo 协议,只会为了保留测试机制而扩大规范 CLI(命令行界面)约定。
- **随包一并删除规范事件快照。** 不予采纳:这些快照固定了模型可见的组装行为,而只检查最终文本的产品验收无法观察这些行为。
- **保留应用插件,只删除它的 bin。** 不予采纳:隐藏的组装仍会重复显式的 headless profile,并掩盖测试叶节点挂载了哪些服务。
## 后果
-这是有意为之的破坏性变更。`dsh-cli-demo`、它的 `--output-format` 选项以及对 `@deepseek-ai/dsh-cli-demo/src/cli.ts` 的导入都不再可解析。本变更不提供公开的事件流替代接口;调用方使用 `dsh run` 执行一次性任务,需要结构化自动化时则必须选择现有的协议接口。
+这是有意为之的破坏性变更。`dsh-cli-demo`、它的 `--output-format` 选项以及对 `@deepseek-ai/dsh-cli-demo/src/cli.ts` 的导入都不再可解析。本变更不提供公开的事件流替代接口;调用方使用 `dsh --profile headless` 执行一次性任务,需要结构化自动化时则必须选择现有的协议接口。
-仓库通过仅供测试的基础设施保留后端回放覆盖,产品冒烟测试和 built-bin 验收则运行 `dsh run`。只有当独立的一次性包负责一套真正独立、带版本且不能归产品启动器所有的协议时,它才可以重新引入;第二种命令写法或输出 shim 并不足以构成理由。
+仓库通过仅供测试的基础设施保留后端回放覆盖,产品冒烟测试和 built-bin 验收则运行 `dsh --profile headless`。只有当独立的一次性包负责一套真正独立、带版本且不能归产品启动器所有的协议时,它才可以重新引入;第二种命令写法或输出 shim 并不足以构成理由。
## 验证
-聚焦的 Loader 冒烟测试在源码模式和由普通 Node 启动的构建模式下覆盖显式组装,快照测试对比其规范 JSONL 和持久化日志,产品验收覆盖 `dsh run`,文档检查及生成图谱/目录门禁则拒绝对已移除包的活跃引用。冻结的 Agent Note 归档保留为历史证据,不会被重写。
+聚焦的 Loader 冒烟测试在源码模式和由普通 Node 启动的构建模式下覆盖显式组装,快照测试对比其规范 JSONL 和持久化日志,产品验收覆盖 `dsh --profile headless`,文档检查及生成图谱/目录门禁则拒绝对已移除包的活跃引用。冻结的 Agent Note 归档保留为历史证据,不会被重写。
diff --git a/.agents/notes/implemented/simplification/2026-08-09-remove-repository-plugin.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-09-remove-repository-plugin.i18n.yaml
new file mode 100644
index 0000000000..5317c7911e
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-09-remove-repository-plugin.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/simplification/2026-08-09-remove-repository-plugin.md
+2026-08-09-remove-repository-plugin.md: 8ac6fd18b756e227f8dabc82eb4926a51702c5a1
+2026-08-09-remove-repository-plugin.zh.md: 6e504a151a88b87b8093a91a91ede1cb919a0b45
diff --git a/.agents/notes/implemented/simplification/2026-08-09-remove-repository-plugin.md b/.agents/notes/implemented/simplification/2026-08-09-remove-repository-plugin.md
new file mode 100644
index 0000000000..8ac6fd18b7
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-09-remove-repository-plugin.md
@@ -0,0 +1,44 @@
+# Agent Note: Remove the dedicated repository Plugin path
+
+Status: implemented
+
+English | [中文](2026-08-09-remove-repository-plugin.zh.md)
+
+## Problem
+
+The repository Plugin path duplicated the profile bundle path for installing and composing third-party packages. It added a `.dsh-plugin` manifest, a generated wrapper, a preparation executable, a second Git/package cache, a Loader builtin, and repository-specific Skill and MCP adapters. Profile bundles already install npm or Git package specifications through the profile package manager, retain normal dependency and lifecycle semantics, and contribute an ordered `cordis.patch.yml` layer that can mount ordinary Cordis Plugins.
+
+The duplicate path also exposed less configuration than a bundle. Its `repositories` list selected source strings, but the generated wrapper mounted a code entry without a user-supplied Plugin config. Repository-specific preparation therefore added substantial code and CI work without becoming the general external-Plugin distribution mechanism.
+
+## Decision
+
+DeepSeek Harness has one standalone external-Plugin distribution path: installable profile bundles. `dsh plugin --profile add ` records the dependency in the profile package, and the installed package declares `dsh.bundle.patch` to contribute its patch layer. The package manager owns source acquisition, versions, dependencies, build lifecycles, and its lockfile. The bundle patch owns Cordis Plugin selection and complete Plugin config.
+
+The `@deepseek-ai/dsh-repository-plugin` package, `.dsh-plugin` authoring format, `dsh-plugin-prepare` executable, generated wrapper, immutable repository cache, base `repository-plugins` row, and dedicated GitHub acceptance lane are removed. The unused vendored `@cordisjs/plugin-loader/repository` subpath and its bundled pnpm dependency are removed with their only consumer. Existing repository cache directories are inert user data; DSH neither reads nor deletes them.
+
+Bundles compose existing owners directly. A bundle that contributes Skills mounts `@deepseek-ai/dsh-skill-local`; one that contributes MCP servers mounts `@deepseek-ai/dsh-mcp-client`; native behavior mounts an ordinary compiled Cordis Plugin. These packages retain their own validation, lifecycle, registration, and teardown contracts. No compatibility parser or migration from `.dsh-plugin` is retained under the pre-release compatibility policy.
+
+This note consolidates the removed repository cache, static format, config-only integration, npm-backed preparation, and trusted code-entry decisions. Their original motivation survives here: standalone users need package-manager-owned external composition, Git and npm dependencies may execute trusted lifecycle code, static Skill and MCP contributions should reuse their existing owners, and source identity belongs in the profile dependency specification and lockfile. Their implementation-specific wrappers, cache generations, and preparation protocol no longer constrain the product.
+
+## Alternatives considered
+
+**Keep repository Plugin as a convenience wrapper over bundles.** Rejected because it would preserve two install commands, two manifest formats, and two failure/cache identities for the same package. A convenience that cannot pass ordinary Plugin config also remains less capable than the mechanism it wraps.
+
+**Teach the repository wrapper to load a bundle patch.** Rejected because the repository cache and preparation protocol would still duplicate profile dependency installation. Bundle packages are already accepted from npm, Git, file, and link specifications through pnpm.
+
+**Keep the generic Loader repository cache for possible future consumers.** Rejected because it has no current consumer after the package removal and carries a pinned package-manager runtime in a vendored browser-adjacent package. A dedicated cache is warranted again only if configuration-time activation without an explicit installation becomes a product requirement that profile dependencies cannot satisfy; that consumer can choose its cache contract then.
+
+**Disable repository Plugin but retain its on-disk format for migration.** Rejected under the pre-release stance. Retaining a parser or compatibility loader would keep the removed contract alive without an external compatibility obligation.
+
+## Consequences
+
+- Third-party packages use one installation and composition model, with ordinary dependency declarations and full patch-level Plugin config.
+- Installing or updating an external bundle is an explicit `dsh plugin` package-manager operation rather than a watched source-list edit. User patch HMR still configures rows contributed by installed bundles.
+- Profile installation requires `pnpm` on the host `PATH`. This is acceptable for an explicit package-management operation and avoids shipping the removed cache's pinned package-manager runtime solely for configuration-time activation.
+- `.dsh-plugin` packages and existing repository source-list patches stop working. Their cache files remain removable by the user but are not migrated or automatically deleted.
+- The dedicated pnpm runtime, preparation executable, wrapper generator, Git credential CI setup, repository cache, and repository-specific tests disappear.
+- Package-relative static assets need a bundle-owned path form so a declarative bundle can point `dsh-skill-local`, `dsh-mcp-client`, or another Plugin at files it ships without custom runtime glue. That capability is owned by the bundle format rather than a repository adapter.
+
+## Testing
+
+Static gates reject stale package, config, documentation, graph, and workspace references. The existing `dsh plugin` built-CLI acceptance covers profile initialization, package-manager installation, bundle discovery, and layer reconciliation. Declarative package-relative Skill and MCP bundle resources remain a named coverage gap in this removal layer.
diff --git a/.agents/notes/implemented/simplification/2026-08-09-remove-repository-plugin.zh.md b/.agents/notes/implemented/simplification/2026-08-09-remove-repository-plugin.zh.md
new file mode 100644
index 0000000000..6e504a151a
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-09-remove-repository-plugin.zh.md
@@ -0,0 +1,44 @@
+# Agent Note: 移除专用 repository 插件路径
+
+Status: implemented
+
+[English](2026-08-09-remove-repository-plugin.md) | 中文
+
+## 问题
+
+repository 插件路径与 profile 组合包路径重复实现了第三方包的安装和组合。它增加了 `.dsh-plugin` manifest(元数据清单)、生成的包装层、准备工作可执行文件、第二套 Git/包缓存、Loader 内置项,以及 repository 专用的 skill(技能)和 MCP 适配器。profile 组合包已经能通过 profile 包管理器安装 npm 或 Git 包说明符,保留正常的依赖与生命周期语义,并提供一个有序 `cordis.patch.yml` 层,其中可以挂载普通 Cordis 插件。
+
+重复的路径所能提供的配置也少于组合包。其 `repositories` 列表选择源字符串,但生成的包装层挂载代码入口时无法传入用户提供的插件配置。因此,repository 专用的准备流程增加了大量代码和 CI 工作,却没有成为通用的外部插件分发机制。
+
+## 决策
+
+DeepSeek Harness 只保留一种独立的外部插件分发路径:可安装的 profile 组合包。`dsh plugin --profile add ` 将依赖记录到 profile 包中,安装的包通过声明 `dsh.bundle.patch` 提供自己的 patch 层。包管理器负责获取源、管理版本和依赖、运行构建生命周期,并维护锁文件。组合包 patch 负责选择 Cordis 插件并提供完整的插件配置。
+
+移除 `@deepseek-ai/dsh-repository-plugin` 包、`.dsh-plugin` 编写格式、`dsh-plugin-prepare` 可执行文件、生成的包装层、不可变 repository 缓存、base 中的 `repository-plugins` 配置项,以及专用 GitHub 验收流水线。vendor 中未再使用的 `@cordisjs/plugin-loader/repository` 子路径及其随附的 pnpm 依赖,也随唯一消费方一并移除。现有 repository 缓存目录只是不会再产生作用的用户数据;DSH 既不会读取,也不会删除这些目录。
+
+组合包直接组合现有归属方。提供 skill 的组合包挂载 `@deepseek-ai/dsh-skill-local`;提供 MCP 服务器的组合包挂载 `@deepseek-ai/dsh-mcp-client`;原生行为则挂载普通的已编译 Cordis 插件。这些包继续保有各自的校验、生命周期、注册和 teardown 契约。根据预发布兼容政策,不保留针对 `.dsh-plugin` 的兼容解析器或迁移机制。
+
+本说明整合了已移除的 repository 缓存、静态格式、纯配置集成、由 npm 支持的准备流程和受信任代码入口决策。其原始动机保留于此:独立用户需要由包管理器负责的外部组合方式;Git 和 npm 依赖可以执行受信任的生命周期代码;静态 skill 与 MCP 贡献应复用现有归属方;来源标识应位于 profile 的依赖说明符和锁文件中。相应实现特有的包装层、缓存 generation 和准备协议不再约束产品。
+
+## 曾考虑的替代方案
+
+**保留 repository 插件,将其作为组合包的便利包装层。** 不予采纳,因为这会为同一个包保留两条安装命令、两种 manifest 格式,以及两套失败/缓存标识。如果一层便利包装不能传递普通的插件配置,其能力仍然不及它所包装的机制。
+
+**让 repository 包装层加载组合包 patch。** 不予采纳,因为 repository 缓存和准备协议仍会重复 profile 依赖安装。组合包已经可以通过 pnpm 接受 npm、Git、file 和 link 说明符。
+
+**为未来可能出现的消费方保留通用 Loader repository 缓存。** 不予采纳,因为在移除相关包后,它已无当前消费方,却仍让一个 vendor 中与浏览器相邻的包携带固定版本的包管理器运行时。只有当无需显式安装即可在配置阶段激活这一能力成为 profile 依赖无法满足的产品需求时,才有理由重新引入专用缓存;届时该消费方可以选择自己的缓存约定。
+
+**禁用 repository 插件,但保留其磁盘格式以供迁移。** 根据预发布方针,不予采纳。保留解析器或兼容 loader 会在没有外部兼容义务的情况下,让已移除的契约继续存在。
+
+## 后果
+
+- 第三方包统一使用一种安装与组合模型,采用普通依赖声明和完整的 patch 层插件配置。
+- 安装或更新外部组合包时,必须显式通过 `dsh plugin` 执行包管理器操作,而不是编辑受监听的源列表。用户 patch 的 HMR(热模块替换)仍可配置已安装组合包所提供的配置项。
+- 安装 profile 时,宿主机的 `PATH` 中必须提供 `pnpm`。对于显式的包管理操作,这一要求可以接受,并且可避免仅为配置阶段激活而随产品交付已移除缓存所使用的固定版本包管理器运行时。
+- `.dsh-plugin` 包和现有 repository 源列表 patch 停止工作。用户仍可自行删除其缓存文件,但系统不会迁移或自动删除这些文件。
+- 专用 pnpm 运行时、准备工作可执行文件、包装层生成器、Git 凭据 CI 设置、repository 缓存和 repository 专用测试全部消失。
+- 静态资源需要一种由组合包拥有、可相对于包解析的路径形式,使声明式组合包可以将 `dsh-skill-local`、`dsh-mcp-client` 或其他插件指向它随包交付的文件,而无需定制运行时代码。该能力归组合包格式所有,而不是 repository 适配器。
+
+## 测试
+
+静态门禁会拒绝残留的包、配置、文档、图和 workspace 引用。现有 `dsh plugin` 已构建 CLI(命令行界面)验收测试覆盖 profile 初始化、包管理器安装、组合包发现和层调和。声明式、相对于包解析的 skill 与 MCP 组合包资源仍是本移除层中已明确记录的覆盖缺口。
diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml
new file mode 100644
index 0000000000..e67d7df745
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md
+2026-08-10-default-presets-single-editor.md: 82f254079080aeb88d76f4e7cc2c7ab195646ec4
+2026-08-10-default-presets-single-editor.zh.md: 22bbc19feadfb71fe3a9480bc60b8777e1fb50c2
diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md
new file mode 100644
index 0000000000..82f2540790
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md
@@ -0,0 +1,25 @@
+# Agent Note: One editor family in general-purpose presets
+
+Status: implemented
+
+English | [中文](2026-08-10-default-presets-single-editor.zh.md)
+
+## Problem
+
+The `standard`, `code`, and `cordis` presets exposed both the `read`/`write`/`edit` filesystem tools and `str_replace_editor`. The two interfaces overlap for ordinary file inspection and editing, so every request carried an additional tool schema without adding a distinct default capability. The `minimal` preset has a different composition contract: its exact two-tool roster intentionally includes `str_replace_editor` beside persistent `bash`.
+
+## Decision
+
+The `standard`, `code`, and `cordis` preset configurations mount `dsh-tool-fs` and `dsh-tool-fs-search`, but do not mount `dsh-tool-str-replace-editor`. Code Mode therefore omits `str_replace_editor` from both its registry and generated SDK. The `minimal` preset continues to mount `dsh-tool-str-replace-editor`, and deployments or user-authored presets may still mount the plugin explicitly.
+
+This decision narrows the preset roster rather than removing the tool package or its Python runtime support. The earlier [shared-roster decision](../feature/2026-07-31-even-out-shipped-tool-rosters.md) continues to own why surface-neutral tools live in preset composition; this note owns the editor exception.
+
+## Alternatives considered
+
+**Keep both editing interfaces in the general-purpose presets.** Rejected because the overlapping model-visible schemas increase tool choice without supplying a separate default operation.
+
+**Remove `str_replace_editor` from every shipped composition.** Rejected because the `minimal` preset intentionally exposes that schema as one of its two tools, and explicit deployments remain valid consumers of the standalone plugin.
+
+## Consequences
+
+General-purpose agents use `read`, `write`, and `edit` for filesystem mutations, while the minimal agent retains `str_replace_editor`. Preset composition tests pin its absence from the standard roster, the Cordis roster, and the Code Mode SDK, while the minimal assertions continue to pin its presence.
diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md
new file mode 100644
index 0000000000..22bbc19fea
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md
@@ -0,0 +1,25 @@
+# Agent Note: 通用 preset 只提供一套编辑工具
+
+Status: implemented
+
+[English](2026-08-10-default-presets-single-editor.md) | 中文
+
+## 问题
+
+`standard`、`code` 和 `cordis` preset 同时提供 `read`/`write`/`edit` 文件系统工具与 `str_replace_editor`。两套接口在常规文件查看和编辑上重叠,导致每次请求都携带额外的工具 schema,却没有增加独立的默认能力。`minimal` preset 具有不同的组合约定:它固定的双工具清单有意在持久 `bash` 之外提供 `str_replace_editor`。
+
+## 决策
+
+`standard`、`code` 和 `cordis` preset 配置挂载 `dsh-tool-fs` 与 `dsh-tool-fs-search`,但不挂载 `dsh-tool-str-replace-editor`。因此 Code Mode 的注册表和生成的 SDK 均不包含 `str_replace_editor`。`minimal` preset 继续挂载 `dsh-tool-str-replace-editor`,部署配置或用户自定义 preset 仍可显式挂载该插件。
+
+此决策收窄 preset 工具清单,不移除工具包及其 Python 运行时支持。较早的[共享清单决策](../feature/2026-07-31-even-out-shipped-tool-rosters.md)继续说明与 surface 无关的工具为何归 preset 组合所有;本记录说明编辑器例外。
+
+## 曾考虑的替代方案
+
+**在通用 preset 中保留两套编辑接口。** 不予采用,因为重叠的模型可见 schema 增加了工具选择,却没有提供不同的默认操作。
+
+**从所有交付组合中移除 `str_replace_editor`。** 不予采用,因为 `minimal` preset 有意将该 schema 作为两个工具之一,显式部署仍是该独立插件的有效消费方。
+
+## 后果
+
+通用 agent 使用 `read`、`write` 和 `edit` 完成文件系统修改,minimal agent 保留 `str_replace_editor`。preset 组合测试固定其不会出现在 standard 清单、Cordis 清单及 Code Mode SDK 中,同时 minimal 断言继续固定其存在。
diff --git a/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.i18n.yaml
new file mode 100644
index 0000000000..9920c09c11
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.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/simplification/2026-08-10-source-run-without-managed-installer.md
+2026-08-10-source-run-without-managed-installer.md: ac506b72acab0dd6c92ce6111487d091b2bf4a73
+2026-08-10-source-run-without-managed-installer.zh.md: 86e44a90a9e7b34ad37b6a4bfd3ad14de40868f5
diff --git a/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.md b/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.md
new file mode 100644
index 0000000000..ac506b72ac
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.md
@@ -0,0 +1,31 @@
+# Agent Note: Source run without a managed installer
+
+Status: implemented
+
+English | [中文](2026-08-10-source-run-without-managed-installer.zh.md)
+
+## Problem
+
+A repository-owned source installer can provide a stable launcher, isolated staging worktrees, atomic upgrades, rollback storage, and shared maintenance workflows for personal customizations. It also makes the repository responsible for a second lifecycle beside the package manager: host dependency installation, credential prompting, checkout adoption, symlink ownership, staging branch coordination, upgrade recovery, and continued compatibility between the installer and bundled maintenance skills.
+
+That lifecycle is not required to run or develop DeepSeek Harness from a source checkout. Maintaining it expands the supported filesystem and Git state space without improving the repository-native execution path.
+
+## Decision
+
+The repository supports source execution through its root `pnpm` scripts. The `dsh` entry in `package.json` runs `pnpm run build`, then launches `apps/cli/src/bin.ts` through `node --import tsx/esm`; build output remains visible before the CLI output. The package script forwards arguments and inherits the caller's environment, including `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. Users select Web with `pnpm dsh web` and headless execution with `pnpm dsh --profile headless "task"`. The independent ACP example remains available through `pnpm run demo:acp`.
+
+The repository does not distribute a source installer, an installer test suite, or skills that assume a managed `current` symlink and timestamped staging worktrees. Users own source checkout placement, Git updates, and any launcher they create outside the repository.
+
+## Alternatives considered
+
+**Keep the installer but document `pnpm run` as another path.** This retains the managed launcher and rollback capability but keeps both lifecycle contracts active, including the installer tests and staging-aware skills.
+
+**Keep generic customization and upstream-publication skills.** Their safety rules can apply beyond the staging layout, but the shipped workflows form one coupled maintenance system: customization discovers the installed staging checkout, upgrade performs the cutover, and upstream publication is selected from those personal changes. General Git contribution guidance already belongs to repository instructions and does not require product-bundled skills.
+
+**Replace the installer with a smaller launcher-link script.** This reduces setup behavior but still makes the repository responsible for host PATH mutation and launcher ownership. Source scripts provide the entry points without that state.
+
+## Consequences
+
+Source users invoke repository scripts rather than an installed `dsh` command. The repository provides no atomic upgrade cutover or preserved staging rollback checkout, and it does not automate the integration or upstream publication of personal source modifications. A future distribution mechanism must justify its ownership of installation and upgrade state, define recovery behavior, and add tests and user documentation without making the source-run path depend on it. Any future publication workflow must isolate one approved feature and obtain explicit approval before its first push and draft PR.
+
+Verification covers repository-wide references to the removed entry points, documentation links, generated third-party-notice freshness, the build-first `package.json` command, and a source CLI smoke through the exact `node --import tsx/esm` runtime vector.
diff --git a/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.zh.md b/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.zh.md
new file mode 100644
index 0000000000..86e44a90a9
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.zh.md
@@ -0,0 +1,31 @@
+# Agent Note: 无需托管安装器的源码运行
+
+Status: implemented
+
+[English](2026-08-10-source-run-without-managed-installer.md) | 中文
+
+## 问题
+
+仓库自带的源码安装器可以提供稳定的启动器、相互隔离的 staging worktree、原子升级、回滚存储,以及用于个人定制的共享维护工作流。与此同时,仓库还必须在包管理器之外负责第二套生命周期:安装宿主依赖、提示输入凭证、接管检出、管理符号链接归属、协调 staging 分支、处理升级恢复,以及持续保持安装器与随附维护 skill(技能)的兼容性。
+
+从源码检出运行或开发 DeepSeek Harness 并不需要这套生命周期。维护它会扩大需要支持的文件系统和 Git 状态空间,却无法改进仓库原生的执行路径。
+
+## 决策
+
+仓库通过根目录的 `pnpm` 脚本支持从源码运行。`package.json` 中的 `dsh` 项先执行 `pnpm run build`,再通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`;构建输出会显示在 CLI(命令行界面)输出之前。该包脚本会转发参数并继承调用方环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY` 和 `HTTPS_PROXY` 时,调用方可设置 `NODE_USE_ENV_PROXY=1`。用户使用 `pnpm dsh web` 选择 Web,使用 `pnpm dsh --profile headless "task"` 选择无头执行。独立的 ACP(Agent Client Protocol)示例仍可通过 `pnpm run demo:acp` 运行。
+
+仓库不分发源码安装器、安装器测试套件,也不分发依赖受管理的 `current` 符号链接和带时间戳 staging worktree 的 skill。源码检出的存放位置、Git 更新,以及用户在仓库外创建的任何启动器均由用户负责。
+
+## 考虑过的备选方案
+
+**保留安装器,但将 `pnpm run` 记作另一条路径。**这样可以保留受管理的启动器和回滚能力,但两套生命周期约定仍会同时生效,其中包括安装器测试和依赖 staging 布局的 skill。
+
+**保留通用的定制与上游发布 skill。**其中的安全规则也能用于 staging 布局之外,但现有工作流共同构成了一套耦合的维护系统:定制工作流查找已安装的 staging 检出,升级工作流执行切换,上游发布工作流则从这些个人修改中选择发布内容。通用 Git 贡献指南已经属于仓库指令,无需以产品随附 skill 的形式提供。
+
+**用更小的启动器链接脚本替换安装器。**这样可以简化设置过程,但仓库仍需负责修改宿主 PATH 和管理启动器归属。源码脚本无需引入这类状态即可提供入口点。
+
+## 影响
+
+源码用户通过仓库脚本运行程序,而非使用已安装的 `dsh` 命令。仓库不提供原子升级切换,也不保留 staging 回滚检出;仓库同样不会自动集成个人源码修改或将其发布到上游。未来的分发机制必须说明为何应由其管理安装和升级状态,定义恢复行为,并补充测试与用户文档,同时不得让源码运行路径依赖该机制。未来任何发布工作流都必须隔离出一项获批功能,并在首次推送和创建草稿 PR(Pull Request)前取得明确批准。
+
+验证范围包括仓库内对已移除入口点的所有引用、文档链接、生成的第三方声明文件的新鲜度、`package.json` 中的先构建后启动命令,以及通过准确的 `node --import tsx/esm` 运行方式对源码 CLI 进行的冒烟测试。
diff --git a/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.i18n.yaml
similarity index 52%
rename from .agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.i18n.yaml
rename to .agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.i18n.yaml
index df68353802..945a68054c 100644
--- a/.agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.i18n.yaml
+++ b/.agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.i18n.yaml
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-package-manager-native-repository-cache.md
-2026-07-30-package-manager-native-repository-cache.md: 38e7356d4abfc8eba8854f0a96700da448ff4ac4
-2026-07-30-package-manager-native-repository-cache.zh.md: 6833eeb4279c9feb9ac1860e780b8ed58b09d334
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.md
+2026-08-11-remove-empty-experimental-package-group.md: e5e81e3e3763f216921b3f3b74709b64be3dee37
+2026-08-11-remove-empty-experimental-package-group.zh.md: d44d0daaf346a5fea317f8c8c6a23f26eaa3cec0
diff --git a/.agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.md b/.agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.md
new file mode 100644
index 0000000000..e5e81e3e37
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.md
@@ -0,0 +1,37 @@
+# Agent Note: Remove the empty experimental package group
+
+Status: implemented
+
+English | [中文](2026-08-11-remove-empty-experimental-package-group.zh.md)
+
+## Problem
+
+The package hierarchy reserves `packages/experimental/` for prototypes and internal-only plugins, but no package has used the group. The empty group adds placement, dependency, promotion, and release rules without a current package or release mechanism that needs them.
+
+The original group aimed to let the team share prototypes against the real plugin graph without implying product support. That need remains possible, but it does not justify a permanent repository category before a concrete package exists.
+
+## Decision
+
+The package hierarchy has no reserved experimental or internal-only group. Packages continue to live in groups selected for their current product role.
+
+A concrete package that needs different release, stability, or dependency treatment requires a decision based on its actual consumers and release mechanism. That decision may reintroduce a dedicated group when it can also define and enforce the exclusion rules.
+
+This note consolidates and supersedes the experimental-package-group decision, whose active triplet is removed with the empty directory.
+
+## Alternatives considered
+
+**Keep the empty group.** It provides an obvious future incubation location, but it also keeps repository rules with no current owner, package, or enforcement mechanism.
+
+**Move the experimental rules into the general package instructions.** This preserves the policy without an empty directory, but makes every package change carry rules for a hypothetical package class.
+
+**Put concrete experimental packages in product-role groups with README labels.** This preserves product-role colocation, but labels alone cannot enforce release and runtime-dependency rules. A future package can evaluate this option against its actual release mechanism.
+
+**Treat every package as experimental until the first tagged release.** This applies a broad temporary status without providing durable treatment for packages that remain experimental after releases begin.
+
+**Require prototypes to stay outside the repository.** This would lose access to the real plugin graph, examples, snapshots, and lifecycle checks. Removing the reserved group does not impose that restriction; a concrete prototype can establish the placement it needs.
+
+## Consequences
+
+The hierarchy loses an unused group and its special release and dependency policy. It also gives up a predeclared location for team discovery and a ready-made promotion path.
+
+The first package that needs experimental or internal-only treatment must define where it lives, how releases exclude it, which runtime dependencies are allowed, and what condition promotes or removes it. A dedicated group can return when those rules have a current consumer and enforceable mechanism.
diff --git a/.agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.zh.md b/.agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.zh.md
new file mode 100644
index 0000000000..d44d0daaf3
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-11-remove-empty-experimental-package-group.zh.md
@@ -0,0 +1,37 @@
+# Agent Note: 移除空的实验性包分组
+
+Status: implemented
+
+[English](2026-08-11-remove-empty-experimental-package-group.md) | 中文
+
+## 问题
+
+包层级结构预留 `packages/experimental/` 用于原型和内部专用插件,但从未有包使用该分组。这个空分组添加了放置、依赖、提升和发布规则,却没有需要这些规则的现有包或发布机制。
+
+原分组旨在让团队基于真实插件图共享原型,同时不暗示产品会提供支持。这项需求将来可能出现,但在具体包出现前,不足以支持一个永久的仓库类别。
+
+## 决策
+
+包层级结构不再预留实验性或内部专用分组。包继续按照当前产品职责放入对应分组。
+
+如果具体包需要不同的发布、稳定性或依赖处理,必须根据其实际消费方和发布机制做出决策。只要该决策同时定义并强制执行排除规则,就可以重新引入专用分组。
+
+本 Agent Note 整合并取代实验性包分组决策;该旧决策的活跃三文件组随空目录一并移除。
+
+## 考虑过的替代方案
+
+**保留空分组。** 它为未来孵化工作提供明确位置,但也会保留没有当前负责人、包或强制执行机制的仓库规则。
+
+**将实验性规则移入通用包指令。** 这可以在不保留空目录的情况下延续政策,但会让每次包变更都携带针对假设包类别的规则。
+
+**将具体实验性包放入产品职责分组,并用 README 标注。** 这会保持产品职责共置,但仅靠标注无法强制执行发布和运行时依赖规则。未来的包可以根据实际发布机制评估此选项。
+
+**在首个带标签的版本发布前,将每个包都视为实验性。** 这会施加宽泛的临时状态,却无法为发布开始后仍处于实验状态的包提供持久处理方式。
+
+**要求原型留在仓库外。** 这会失去真实插件图、示例、快照和生命周期检查。移除预留分组并不施加这项限制;具体原型可以建立自身所需的放置规则。
+
+## 后果
+
+包层级结构移除了未使用的分组及其特殊发布和依赖政策,同时也放弃了预先声明的团队发现位置和现成的提升路径。
+
+第一个需要实验性或内部专用处理的包必须定义其存放位置、发布版本如何排除它、允许哪些运行时依赖,以及包在何种条件下获得提升或被移除。当这些规则具有当前消费方和可强制执行的机制时,可以恢复专用分组。
diff --git a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml
index cc081c72c4..7f86295344 100644
--- a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml
+++ b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md
-2026-07-24-web-gui-browser-e2e-lane.md: e572929ae6762da6adc2e77e1dba19361beaf670
-2026-07-24-web-gui-browser-e2e-lane.zh.md: 99f86f40ba006c4024f367b73ce52f8679b8d2fd
+2026-07-24-web-gui-browser-e2e-lane.md: 6e52d96a8adb5486e8666d65a3425bf5a0aad4a9
+2026-07-24-web-gui-browser-e2e-lane.zh.md: 7598a8edc530a34261799e57ce953edafe44e70e
diff --git a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md
index e572929ae6..6e52d96a8a 100644
--- a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md
+++ b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md
@@ -88,7 +88,7 @@ Surveyed AI-chat/agent web UIs and mocking layers (LibreChat, vercel/ai-chatbot
- **Follow-up-prompt-after-resume scenario**: the history/live stitch path over the real wire; add as its own scenario when that code changes or regresses.
- **Composer steering gesture**: the input locks while running (stop-or-wait), so the steering scenario steers over the wire from the page; `TODO(web-steer-composer)` upgrades the drive step to a real composer gesture when the product grows one.
- **Drag session reorder**: `workspace.insertSessionBefore` has no browser scenario; it needs two sessions materialized in one workspace plus synthesized HTML5 drag events. Add it when that surface changes or regresses. The inert session Rename/Fork/Delete and workspace Delete menu rows get scenarios when they gain behavior.
-- **Long-history Chat-to-Trajectory Inspect**: the independent inspection source exhausts history after the view opens, while the selected record is addressed by a derived table index that can move as older pages prepend. Short-history Inspect remains covered; the long-history interaction contract excludes this handoff until selection has a stable semantic identity.
+- **Long-history Chat-to-Trajectory Inspect**: both views share Session paging, while the selected Trajectory record is addressed by a derived table index that can move as older pages prepend. Short-history Inspect remains covered; the long-history interaction contract excludes this handoff until selection has a stable semantic identity.
## Consequences
diff --git a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md
index 99f86f40ba..7598a8edc5 100644
--- a/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md
+++ b/.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md
@@ -88,7 +88,7 @@ Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bu
- **恢复后追问场景**:真实 wire 上的历史/实时缝合路径;当该代码变更或回归时作为独立场景补充。
- **输入框 steering 手势**:输入在运行期间锁定(只能停止或等待),因此 steering 场景从页面走 wire 做 steer;`TODO(web-steer-composer)` 待产品长出真实的输入框手势后,把驱动步骤升级为该手势。
- **拖拽会话重排**:`workspace.insertSessionBefore` 尚无浏览器场景;它需要在同一个工作区里物化两个会话,并合成 HTML5 拖拽事件。当该表面变更或回归时再补充。无行为的会话 Rename/Fork/Delete 和工作区 Delete 菜单行待获得行为后再补充场景。
-- **长历史 Chat 到 Trajectory 的 Inspect**:独立的检查数据源会在视图打开后穷尽历史,而所选记录由一个派生的表格索引定位;随着较早页面前插,该索引可能移动。短历史 Inspect 仍有覆盖;在选中项具有稳定的语义身份之前,长历史交互约定不包含这项交接。
+- **长历史 Chat 到 Trajectory 的 Inspect**:两个视图共用 Session 分页,而所选 Trajectory 记录由一个派生的表格索引定位;随着较早页面前插,该索引可能移动。短历史 Inspect 仍有覆盖;在选中项具有稳定的语义身份之前,长历史交互约定不包含这项交接。
## 后果
diff --git a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml
index a0a0dc998d..7084931b1d 100644
--- a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml
+++ b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md
-2026-07-17-sdk-follow-up-capabilities.md: 9a17f07139014f95666789e41cacb7af180ef5d8
-2026-07-17-sdk-follow-up-capabilities.zh.md: 2431c5c3d9e45605223d9bb1843e48a3c711759e
+2026-07-17-sdk-follow-up-capabilities.md: f14d46a61f5fd3e64067441c2f8340cf94746a79
+2026-07-17-sdk-follow-up-capabilities.zh.md: 2e5efba340d503d2445e408bfc43ee0d6c6bec3c
diff --git a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md
index 9a17f07139..f14d46a61f 100644
--- a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md
+++ b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md
@@ -47,7 +47,7 @@ The repository ships a thin `SKILL.md` that teaches an agent to construct the st
The package manager owns source parsing, version or commit resolution, integrity data, lockfile updates, and any build policy. The SDK does not download or unpack a second copy through giget or pacote. An external plugin remains a dependency under `node_modules`; local plugin scaffolding remains a separate project-creation concern.
-This proposal concerns dependencies of developer-owned SDK projects. Standalone app repository caching, its bundled-pnpm policy, and its explicit preparation trust boundary are owned by the [package-manager-native repository cache](../../implemented/architecture/2026-07-30-package-manager-native-repository-cache.md).
+This proposal concerns dependencies of developer-owned SDK projects. Standalone apps install external packages as [profile bundles](../../implemented/simplification/2026-08-09-remove-repository-plugin.md), with their profile package manager and lockfile owning acquisition and lifecycle policy.
## Launcher telemetry
diff --git a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md
index 2431c5c3d9..2e5efba340 100644
--- a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md
+++ b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md
@@ -47,7 +47,7 @@ Create 和 config 使用相同的功能计划形状。create 通过上述命令
包管理器负责来源解析、版本或 commit 解析、`integrity` 数据、lockfile 更新和构建策略。SDK 不再通过 giget 或 pacote 下载、解压第二份副本。外部插件是 `node_modules` 下的依赖;本地插件脚手架仍属于独立的工程创建问题。
-本提案只涉及开发者自有 SDK 工程的依赖。独立应用的仓库缓存、随应用捆绑 pnpm 的政策和显式的准备流程信任边界,均由[包管理器原生仓库缓存](../../implemented/architecture/2026-07-30-package-manager-native-repository-cache.md)负责。
+本提案只涉及开发者自有 SDK 工程的依赖。独立应用将外部包安装为 [profile 组合包](../../implemented/simplification/2026-08-09-remove-repository-plugin.md),由 profile 的包管理器与 lockfile 负责获取和生命周期策略。
## Launcher 遥测
diff --git a/.github/AGENTS.md b/.github/AGENTS.md
index c18f5b5948..3e879db8ae 100644
--- a/.github/AGENTS.md
+++ b/.github/AGENTS.md
@@ -1,3 +1,3 @@
# AGENTS.md — GitHub Actions
-Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. The pull-request `windows` job is the deliberate exception: it runs Windows Node under Wine on hosted Linux and blocks `all checks passed`; `windows-native` runs automatically on `windows-2025` but reports independently — see the [dual-lane Agent Note](../.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md).
+Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. The pull-request `windows` job is the deliberate exception: it runs Windows Node under Wine on hosted Linux and blocks `all checks passed`; `windows-native` runs automatically on `windows-2025` (or the self-hosted `[self-hosted, dsh-win-ci, windows]` pool under `DSH_CI_FAILOVER=selfhosted`) but reports independently. The master `serial-windows` standby continuously validates the self-hosted failover target — see the [failover runbook](../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md).
diff --git a/.github/issue-management/config.json b/.github/issue-management/config.json
index 41019f0aa2..5dc925f245 100644
--- a/.github/issue-management/config.json
+++ b/.github/issue-management/config.json
@@ -3,6 +3,7 @@
"repository": "deepseek-harness",
"projectNumber": 1,
"projectTitle": "DSH Issue Management",
+ "lifecycleActor": "dsh-issue-management",
"priorityField": "Priority",
"allowUnassignedOwner": true,
"statuses": [
diff --git a/.github/issue-management/policy.mjs b/.github/issue-management/policy.mjs
index 2125ba2f36..24a82cf15f 100644
--- a/.github/issue-management/policy.mjs
+++ b/.github/issue-management/policy.mjs
@@ -37,10 +37,21 @@ const LEGACY_LABELS = new Set([
])
const TERMINAL_STATUSES = new Set(['Done', 'No action'])
const ACTIVE_STATUS_ORDER = config.statuses.filter((status) => !TERMINAL_STATUSES.has(status))
+const IMPLEMENTATION_PULL_REQUEST_ACTIONS = new Set([
+ 'opened',
+ 'edited',
+ 'synchronize',
+ 'reopened',
+ 'labeled',
+ 'unlabeled',
+])
for (const status of ['In progress', 'In review']) {
if (!ACTIVE_STATUS_ORDER.includes(status)) throw new Error(`config.statuses 缺少 ${status}`)
}
+if (typeof config.lifecycleActor !== 'string' || !config.lifecycleActor) {
+ throw new Error('config.lifecycleActor 未设置')
+}
/**
* Return Markdown outside balanced details elements.
@@ -159,18 +170,48 @@ export function requiresPullRequestPolicy({
}
/**
- * Derive a forward-only Issue status from the current PR phase.
- * @param {string|null} currentStatus Current Project status.
- * @param {{isDraft: boolean, reviewRequestCount: number, reviewCount: number}} pull PR phase.
- * @returns {string|null} Status to write, or null when no forward transition exists.
+ * Translate a repository event into one resolving-Issue lifecycle command.
+ * @param {string} eventName GitHub event name.
+ * @param {{action?: string, review?: {state?: string}}} event GitHub event payload.
+ * @returns {'implementation'|'review-requested'|'changes-requested'|null} Lifecycle command.
*/
-export function nextResolvingIssueStatus(currentStatus, pull) {
- const target =
- !pull.isDraft && (pull.reviewRequestCount > 0 || pull.reviewCount > 0)
- ? 'In review'
- : 'In progress'
+export function resolvingIssueStatusCommand(eventName, event) {
+ if (eventName === 'pull_request') {
+ if (event.action === 'review_requested') return 'review-requested'
+ return IMPLEMENTATION_PULL_REQUEST_ACTIONS.has(event.action) ? 'implementation' : null
+ }
+ if (
+ eventName === 'pull_request_review' &&
+ event.action === 'submitted' &&
+ event.review?.state?.toLowerCase() === 'changes_requested'
+ ) {
+ return 'changes-requested'
+ }
+ return null
+}
+
+/**
+ * Plan one event-directed resolving-Issue status transition.
+ * @param {string|null} currentStatus Current Project status.
+ * @param {'implementation'|'review-requested'|'changes-requested'} command Lifecycle command.
+ * @param {string|null} currentStatusActor Actor that last set the current Project status.
+ * @returns {string|null} Status to write, or null when no permitted transition exists.
+ */
+export function nextResolvingIssueStatus(currentStatus, command, currentStatusActor = null) {
+ let target
+ if (command === 'review-requested') target = 'In review'
+ else if (command === 'implementation' || command === 'changes-requested') target = 'In progress'
+ else throw new Error(`未知 lifecycle command:${command}`)
+
const currentIndex = ACTIVE_STATUS_ORDER.indexOf(currentStatus)
const targetIndex = ACTIVE_STATUS_ORDER.indexOf(target)
+ if (
+ command === 'changes-requested' &&
+ currentStatus === 'In review' &&
+ currentStatusActor === config.lifecycleActor
+ ) {
+ return target
+ }
return currentIndex >= 0 && currentIndex < targetIndex ? target : null
}
@@ -396,9 +437,15 @@ async function issueSnapshot(number, status = undefined) {
}
}
-async function projectContext(number) {
+async function projectContext(number, includeStatusActor = false) {
const data = await graphql(
- `query($organization: String!, $repository: String!, $number: Int!, $project: Int!) {
+ `query(
+ $organization: String!
+ $repository: String!
+ $number: Int!
+ $project: Int!
+ $includeStatusActor: Boolean!
+ ) {
organization(login: $organization) {
projectV2(number: $project) {
id
@@ -413,6 +460,16 @@ async function projectContext(number) {
repository(owner: $organization, name: $repository) {
issue(number: $number) {
id
+ timelineItems(last: 100, itemTypes: [PROJECT_V2_ITEM_STATUS_CHANGED_EVENT])
+ @include(if: $includeStatusActor) {
+ nodes {
+ ... on ProjectV2ItemStatusChangedEvent {
+ actor { login }
+ project { id }
+ status
+ }
+ }
+ }
projectItems(first: 20, includeArchived: true) {
nodes {
id
@@ -430,6 +487,7 @@ async function projectContext(number) {
repository: config.repository,
number,
project: config.projectNumber,
+ includeStatusActor,
},
)
const project = data.organization?.projectV2
@@ -439,7 +497,14 @@ async function projectContext(number) {
const statusField = project.fields.nodes.find((field) => field?.name === 'Status')
if (!statusField) throw new Error('Project 缺少 Status 字段')
const item = issue.projectItems.nodes.find((candidate) => candidate.project.id === project.id)
- return { project, issue, statusField, item }
+ const latestStatusEvent = issue.timelineItems?.nodes
+ ?.filter((event) => event?.project?.id === project.id)
+ .at(-1)
+ const statusActor =
+ latestStatusEvent?.status === item?.fieldValueByName?.name
+ ? (latestStatusEvent.actor?.login ?? null)
+ : null
+ return { project, issue, statusField, item, statusActor }
}
async function projectStatus(number) {
@@ -530,12 +595,7 @@ async function auditIssue(number, extraErrors = [], status = undefined) {
return errors
}
-async function pullRequestSnapshot(number) {
- const pull = await api(`/repos/${config.organization}/${config.repository}/pulls/${number}`)
- const [reviewRequests, reviews] = await Promise.all([
- api(`/repos/${config.organization}/${config.repository}/pulls/${number}/requested_reviewers`),
- api(`/repos/${config.organization}/${config.repository}/pulls/${number}/reviews?per_page=100`),
- ])
+async function resolvingReferencesSnapshot(number, pull) {
const references = parseReferences({
body: pull.body ?? '',
repository: `${config.organization}/${config.repository}`,
@@ -547,20 +607,41 @@ async function pullRequestSnapshot(number) {
}
return {
number,
- isDraft: pull.draft,
- authorType: pull.user?.type ?? 'User',
- reviewRequestCount: reviewRequests.users.length + reviewRequests.teams.length,
- reviewCount: reviews.length,
- labels: pull.labels.map((label) => label.name),
references: retainIssueReferences(references, issues),
issues,
}
}
-async function advanceResolvingIssues(pull) {
+async function pullRequestSnapshot(number) {
+ const [pull, reviewRequests, reviews] = await Promise.all([
+ api(`/repos/${config.organization}/${config.repository}/pulls/${number}`),
+ api(`/repos/${config.organization}/${config.repository}/pulls/${number}/requested_reviewers`),
+ api(`/repos/${config.organization}/${config.repository}/pulls/${number}/reviews?per_page=100`),
+ ])
+ const resolving = await resolvingReferencesSnapshot(number, pull)
+ return {
+ ...resolving,
+ isDraft: pull.draft,
+ authorType: pull.user?.type ?? 'User',
+ reviewRequestCount: reviewRequests.users.length + reviewRequests.teams.length,
+ reviewCount: reviews.length,
+ labels: pull.labels.map((label) => label.name),
+ }
+}
+
+async function lifecyclePullRequestSnapshot(number) {
+ const pull = await api(`/repos/${config.organization}/${config.repository}/pulls/${number}`)
+ return resolvingReferencesSnapshot(number, pull)
+}
+
+async function transitionResolvingIssues(pull, command) {
for (const number of pull.references.resolving) {
- const context = await projectContext(number)
- const target = nextResolvingIssueStatus(context.item?.fieldValueByName?.name ?? null, pull)
+ const context = await projectContext(number, command === 'changes-requested')
+ const target = nextResolvingIssueStatus(
+ context.item?.fieldValueByName?.name ?? null,
+ command,
+ context.statusActor,
+ )
if (!target) continue
// TODO: Replace this latest-state guard with per-Issue serialization or a
// conditional ProjectV2 update; GraphQL currently has no compare-and-swap.
@@ -598,8 +679,10 @@ async function runLifecycle(eventName, event) {
}
if (eventName === 'pull_request' || eventName === 'pull_request_review') {
- const pull = await pullRequestSnapshot(event.pull_request.number)
- await advanceResolvingIssues(pull)
+ const command = resolvingIssueStatusCommand(eventName, event)
+ if (!command) return
+ const pull = await lifecyclePullRequestSnapshot(event.pull_request.number)
+ await transitionResolvingIssues(pull, command)
}
}
diff --git a/.github/issue-management/policy.test.mjs b/.github/issue-management/policy.test.mjs
index 8a9b0f91e6..c03a7c3513 100644
--- a/.github/issue-management/policy.test.mjs
+++ b/.github/issue-management/policy.test.mjs
@@ -6,6 +6,7 @@ import {
nextResolvingIssueStatus,
parseReferences,
retainIssueReferences,
+ resolvingIssueStatusCommand,
requiresPullRequestPolicy,
validateBody,
validateIssue,
@@ -243,32 +244,73 @@ test('requires policy only after a human PR enters review', () => {
)
})
-test('advances resolving Issues to the live PR phase', () => {
- const draft = { isDraft: true, reviewRequestCount: 1, reviewCount: 4 }
- const open = { isDraft: false, reviewRequestCount: 0, reviewCount: 0 }
- const requestedReview = { isDraft: false, reviewRequestCount: 1, reviewCount: 0 }
- const submittedReview = { isDraft: false, reviewRequestCount: 0, reviewCount: 1 }
-
- for (const status of ['Inbox', 'Backlog', 'Ready']) {
- assert.equal(nextResolvingIssueStatus(status, draft), 'In progress')
- assert.equal(nextResolvingIssueStatus(status, open), 'In progress')
- assert.equal(nextResolvingIssueStatus(status, requestedReview), 'In review')
- assert.equal(nextResolvingIssueStatus(status, submittedReview), 'In review')
+test('maps only explicit review handoffs to review status commands', () => {
+ assert.equal(
+ resolvingIssueStatusCommand('pull_request', {
+ action: 'review_requested',
+ }),
+ 'review-requested',
+ )
+ assert.equal(
+ resolvingIssueStatusCommand('pull_request_review', {
+ action: 'submitted',
+ review: { state: 'changes_requested' },
+ }),
+ 'changes-requested',
+ )
+ for (const state of ['approved', 'commented']) {
+ assert.equal(
+ resolvingIssueStatusCommand('pull_request_review', {
+ action: 'submitted',
+ review: { state },
+ }),
+ null,
+ )
}
- assert.equal(nextResolvingIssueStatus('In progress', requestedReview), 'In review')
- assert.equal(nextResolvingIssueStatus('In progress', submittedReview), 'In review')
+ assert.equal(
+ resolvingIssueStatusCommand('pull_request_review', {
+ action: 'dismissed',
+ review: { state: 'changes_requested' },
+ }),
+ null,
+ )
})
-test('never regresses or reopens a resolving Issue', () => {
- const implementation = { isDraft: false, reviewRequestCount: 0, reviewCount: 0 }
- const review = { isDraft: false, reviewRequestCount: 0, reviewCount: 1 }
+test('keeps ordinary pull request events as forward-only implementation signals', () => {
+ for (const action of ['opened', 'edited', 'synchronize', 'reopened', 'labeled', 'unlabeled']) {
+ assert.equal(resolvingIssueStatusCommand('pull_request', { action }), 'implementation')
+ }
+ assert.equal(
+ resolvingIssueStatusCommand('pull_request', { action: 'review_request_removed' }),
+ null,
+ )
+})
- assert.equal(nextResolvingIssueStatus('In progress', implementation), null)
- assert.equal(nextResolvingIssueStatus('In review', implementation), null)
- assert.equal(nextResolvingIssueStatus('In review', review), null)
- assert.equal(nextResolvingIssueStatus('Done', review), null)
- assert.equal(nextResolvingIssueStatus('No action', review), null)
- assert.equal(nextResolvingIssueStatus(null, review), null)
+test('toggles automation-owned work on request changes and repeated review request', () => {
+ for (const status of ['Inbox', 'Backlog', 'Ready']) {
+ assert.equal(nextResolvingIssueStatus(status, 'implementation'), 'In progress')
+ assert.equal(nextResolvingIssueStatus(status, 'review-requested'), 'In review')
+ assert.equal(nextResolvingIssueStatus(status, 'changes-requested'), 'In progress')
+ }
+ let status = nextResolvingIssueStatus(
+ 'In review',
+ 'changes-requested',
+ 'dsh-issue-management',
+ )
+ assert.equal(status, 'In progress')
+ status = nextResolvingIssueStatus(status, 'review-requested')
+ assert.equal(status, 'In review')
+})
+
+test('preserves human review status and terminal Issues', () => {
+ assert.equal(nextResolvingIssueStatus('In progress', 'implementation'), null)
+ assert.equal(nextResolvingIssueStatus('In review', 'implementation'), null)
+ assert.equal(nextResolvingIssueStatus('In review', 'review-requested'), null)
+ assert.equal(nextResolvingIssueStatus('In review', 'changes-requested', 'tianyicui'), null)
+ assert.equal(nextResolvingIssueStatus('In review', 'changes-requested'), null)
+ assert.equal(nextResolvingIssueStatus('Done', 'review-requested'), null)
+ assert.equal(nextResolvingIssueStatus('No action', 'changes-requested'), null)
+ assert.equal(nextResolvingIssueStatus(null, 'review-requested'), null)
})
test('keeps lifecycle projection independent of PR metadata enforcement', () => {
@@ -283,7 +325,7 @@ test('keeps lifecycle projection independent of PR metadata enforcement', () =>
}
assert.ok(validatePullRequest(pull).length > 0)
- assert.equal(nextResolvingIssueStatus('Inbox', pull), 'In review')
+ assert.equal(nextResolvingIssueStatus('Inbox', 'review-requested'), 'In review')
})
test('exempts Draft, Bot, and App PRs', () => {
diff --git a/.github/workflows/build-exe-for-python-sdk.yml b/.github/workflows/build-exe-for-python-sdk.yml
index 017b77ee75..0924da5b35 100644
--- a/.github/workflows/build-exe-for-python-sdk.yml
+++ b/.github/workflows/build-exe-for-python-sdk.yml
@@ -92,7 +92,7 @@ jobs:
sdk-wheel:
needs: plan
- name: deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
+ name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
@@ -113,8 +113,8 @@ jobs:
- uses: actions/upload-artifact@v7
with:
- name: deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
- path: dist-python/deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
+ name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
+ path: dist-python/deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
if-no-files-found: error
build:
@@ -197,7 +197,7 @@ jobs:
- uses: actions/download-artifact@v8
with:
- name: deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
+ name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
path: dist-python
- name: Install only the SDK into a clean venv and run zero-config
@@ -208,7 +208,7 @@ jobs:
python -m venv "$RUNNER_TEMP/dsh-sdk-smoke"
"$RUNNER_TEMP/dsh-sdk-smoke/bin/python" -m pip install \
--find-links dist-python \
- deepseek-harness=="$VERSION"
+ deepseek-harness-sdk=="$VERSION"
"$RUNNER_TEMP/dsh-sdk-smoke/bin/python" scripts/smoke-python-runtime.py \
--scenario sdk-default
@@ -238,7 +238,7 @@ jobs:
esac
docker run --rm -e VERSION -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c '
/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk
- /tmp/dsh-sdk/bin/python -m pip install --find-links /work/dist-python deepseek-harness=="$VERSION"
+ /tmp/dsh-sdk/bin/python -m pip install --find-links /work/dist-python deepseek-harness-sdk=="$VERSION"
/tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default
'
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index eebdafaa48..1249439fde 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -175,8 +175,6 @@ jobs:
DSH_NODE_COMPAT_SKIP_TYPECHECK: '1'
DSH_OXLINT_THREADS: '8'
DSH_PUBLINT_CONCURRENCY: '8'
- DSH_GITHUB_REPOSITORY_PLUGIN_SOURCE: >-
- github:${{ github.event.pull_request.head.repo.full_name }}#${{ github.event.pull_request.head.sha }}&path:/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin
# Failover halves snapshot concurrency for the shared 64-core VM.
DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '12' || '32' }}
steps:
@@ -242,17 +240,6 @@ jobs:
if: vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]'
run: pnpm --filter @deepseek-ai/dsh-frontend exec playwright install chromium
- - name: Configure private GitHub repository Plugin access
- env:
- DSH_GITHUB_SOURCE_TOKEN: ${{ github.token }}
- run: |
- source_config="$RUNNER_TEMP/dsh-github-source.gitconfig"
- basic_auth=$(printf 'x-access-token:%s' "$DSH_GITHUB_SOURCE_TOKEN" | base64 | tr -d '\n')
- git config --file "$source_config" url.https://github.com/.insteadOf git@github.com:
- git config --file "$source_config" --add url.https://github.com/.insteadOf ssh://git@github.com/
- git config --file "$source_config" http.https://github.com/.extraheader "AUTHORIZATION: basic $basic_auth"
- echo "GIT_CONFIG_GLOBAL=$source_config" >> "$GITHUB_ENV"
-
- name: Run compatibility, snapshot, and artifact gates
run: pnpm run check:ci:consumers
@@ -318,7 +305,7 @@ jobs:
# Windows Node under Wine on standard hosted Linux. The independent
# windows-native job below keeps the complete native-kernel inventory —
# including the observational portability gates this lane does not run —
- # on real windows-2025. This job only provisions runner state (caches,
+ # on real Windows. This job only provisions runner state (caches,
# apt); scripts/wine-windows-gates.sh owns the gate logic and is the same
# script the optional local gate `pnpm run check:windows-wine` runs.
# Current topology and fidelity limits live in
@@ -424,12 +411,18 @@ jobs:
# Every pull request also gets a real Windows-kernel signal. This job keeps
# its own unmasked conclusion but is deliberately absent from
- # all-checks-passed.needs, so it never delays or changes that required verdict.
- # See the dual Wine/native pull-request CI decision:
- # .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
+ # all-checks-passed.needs, so it never delays or changes that required
+ # verdict. Under normal operation it runs on the hosted larger runner; under
+ # failover (DSH_CI_FAILOVER=selfhosted) it retargets onto the in-house
+ # self-hosted Windows pool. Dependabot PRs are excluded from the self-hosted
+ # pool and stay queued for the hosted runner — see the failover runbook.
windows-native:
if: github.event_name == 'pull_request'
- runs-on: dsh-windows-2025-16core
+ runs-on: >-
+ ${{ vars.DSH_CI_FAILOVER == 'selfhosted'
+ && github.event.pull_request.user.login != 'dependabot[bot]'
+ && fromJSON('["self-hosted", "dsh-win-ci", "windows"]')
+ || 'dsh-windows-2025-16core' }}
name: windows node 24 / native complete
timeout-minutes: 60
env:
@@ -455,8 +448,10 @@ jobs:
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
- # Extracting the many-file pnpm store cache is slower than a clean install,
- # and saving it adds more latency after the gates.
+ # Extracting the many-file pnpm store cache is slower than a clean
+ # install on hosted Windows runners, and saving it adds latency after
+ # the gates. The self-hosted VM's persistent store makes caching
+ # redundant.
- name: Install (immutable)
shell: pwsh
run: pnpm install --frozen-lockfile
@@ -621,10 +616,22 @@ jobs:
DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
run: pnpm run check:ci
+ # Hot-standby drill for the in-house self-hosted Windows pool: every master
+ # move re-runs the complete unsharded Windows gate inventory on the persistent
+ # VM, continuously proving that environment can take over the required
+ # `windows` lane if the hosted pool degrades (the switch is setting the
+ # writer-manageable DSH_CI_FAILOVER variable — see the failover runbook, no
+ # merge required). Push-triggered, so this lane always executes the base
+ # branch's own workflow definition. Non-blocking for pull requests; absent
+ # from all-checks-passed.needs by design — the required `windows` job owns
+ # the PR verdict. No cache steps because the VM's persistent pnpm store
+ # and tool caches make them redundant (and saving here would poison the
+ # hosted cache namespace with self-hosted paths).
serial-windows:
- if: false
- name: serial / windows
- runs-on: windows-2025
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ name: serial / windows (self-hosted standby)
+ runs-on: [self-hosted, dsh-win-ci, windows]
+ timeout-minutes: 60
steps:
- uses: actions/checkout@v6
@@ -642,20 +649,23 @@ jobs:
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
+ - name: Configure persistent pnpm store
+ shell: pwsh
+ run: |
+ $storeRoot = "$env:LOCALAPPDATA\pnpm\store"
+ echo "PNPM_CONFIG_STORE_DIR=$storeRoot" >> $env:GITHUB_ENV
+
- name: Install (immutable)
shell: pwsh
run: pnpm install --frozen-lockfile
- - name: Run complete unsharded primary Node CI serially
+ - name: Run complete unsharded Windows gate inventory serially
shell: pwsh
env:
DSH_COVERAGE_MAX_WORKERS: '1'
- DSH_E2E_MAX_WORKERS: '1'
DSH_GATE_CONCURRENCY: '1'
- DSH_OXLINT_THREADS: '1'
DSH_PUBLINT_CONCURRENCY: '1'
- DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
- run: pnpm run check:ci
+ run: pnpm run check:ci:windows-complete
# Manual, bounded comparison of the actual critical Linux and Windows lanes.
# The named pools are restricted at the organization level to this repository.
diff --git a/.github/workflows/issue-lifecycle.yml b/.github/workflows/issue-lifecycle.yml
index 7a25b5223d..e324cfefc2 100644
--- a/.github/workflows/issue-lifecycle.yml
+++ b/.github/workflows/issue-lifecycle.yml
@@ -36,6 +36,7 @@ concurrency:
jobs:
lifecycle:
name: Issue lifecycle
+ if: ${{ github.event_name != 'pull_request_review' || (github.event.action == 'submitted' && github.event.review.state == 'changes_requested') }}
runs-on: ubuntu-latest
steps:
- name: Check out trusted policy
diff --git a/.github/workflows/landlock-run-release.yml b/.github/workflows/landlock-run-release.yml
index 7d78e98bc6..8448af1ad1 100644
--- a/.github/workflows/landlock-run-release.yml
+++ b/.github/workflows/landlock-run-release.yml
@@ -172,5 +172,7 @@ jobs:
tag_args=()
case "$version" in *-*) tag_args=(--tag next);; esac
while IFS= read -r tarball; do
- npm publish "dist/npm/${tarball}" --access public "${tag_args[@]}"
+ # No --access: publishConfig.access in each manifest decides, and a
+ # command-line flag would override it.
+ npm publish "dist/npm/${tarball}" "${tag_args[@]}"
done < dist/npm/publish-order.txt
diff --git a/.github/workflows/release-vendor.yml b/.github/workflows/release-vendor.yml
new file mode 100644
index 0000000000..c8af47251e
--- /dev/null
+++ b/.github/workflows/release-vendor.yml
@@ -0,0 +1,132 @@
+# Pack and publish the vendored framework sequence: the nine rescoped Cordis
+# packages under vendor/, each on its own version line. This sequence releases
+# independently of dsh and of the native packages.
+#
+# Pack runs without credentials on every pull request and master push.
+# Publication is a manual dispatch from a vendor-* tag; a vendor release can
+# carry several versions, so each package has its own tag.
+name: Release (vendor)
+
+on:
+ pull_request:
+ push:
+ branches: [master]
+ workflow_dispatch:
+ inputs:
+ publish:
+ description: Publish the packed tarballs to npm. Must run from a vendor-* tag.
+ required: true
+ type: boolean
+ default: false
+
+permissions:
+ contents: read
+
+concurrency:
+ # Pack runs per ref so concurrent pull requests never displace each
+ # other; the publish job below serializes the shared dist-tag state.
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: false
+
+env:
+ PRIMARY_NODE_VERSION: '24'
+ DSH_TELEMETRY_DISABLED: '1'
+
+jobs:
+ pack:
+ name: Pack npm tarballs
+ runs-on: ubuntu-24.04
+ steps:
+ # Complete history: the release scripts read tags.
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - uses: pnpm/action-setup@v4
+ with:
+ dest: ${{ runner.temp }}/setup-pnpm
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.PRIMARY_NODE_VERSION }}
+
+ - name: Configure pnpm store path
+ id: pnpm-store
+ run: |
+ store_root="$HOME/.local/share/pnpm/store"
+ echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
+ store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
+ echo "path=$store_path" >> "$GITHUB_OUTPUT"
+
+ - uses: actions/cache/restore@v4
+ with:
+ path: ${{ steps.pnpm-store.outputs.path }}
+ key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
+ restore-keys: |
+ ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
+
+ - name: Install (immutable)
+ run: pnpm install --frozen-lockfile
+
+ - name: Verify release version
+ env:
+ RELEASE_PUBLISH: ${{ inputs.publish }}
+ run: pnpm run release:verify --family vendor
+
+ # The vendored packages publish their own sources and build outputs; the
+ # host build produces what their manifests select.
+ - name: Build
+ run: pnpm run build:lib:host
+
+ - name: Pack release tarballs
+ run: pnpm run release:pack --family vendor --out dist/npm-vendor
+
+ - name: Verify packed install
+ run: pnpm run release:verify-packed-install --family vendor --from dist/npm-vendor
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: vendor-npm-tarballs
+ path: dist/npm-vendor/*
+ if-no-files-found: error
+ retention-days: 7
+
+ publish:
+ name: Publish to npm
+ if: inputs.publish
+ needs: pack
+ runs-on: ubuntu-24.04
+ environment: npm-publish
+ concurrency:
+ group: Release-publish
+ cancel-in-progress: false
+ permissions:
+ contents: read
+ steps:
+ # Checkout and install carry the release scripts only; no build step.
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+
+ - uses: pnpm/action-setup@v4
+ with:
+ dest: ${{ runner.temp }}/setup-pnpm
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.PRIMARY_NODE_VERSION }}
+ registry-url: https://registry.npmjs.org
+
+ - name: Install (immutable, no package scripts)
+ run: pnpm install --frozen-lockfile --ignore-scripts
+
+ - uses: actions/download-artifact@v4
+ with:
+ name: vendor-npm-tarballs
+ path: dist/npm-vendor
+
+ - name: Publish tarballs
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ run: pnpm run release:publish --family vendor --from dist/npm-vendor
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000000..cab178a3d2
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,147 @@
+# Pack and publish the dsh release sequence: every package under packages/ plus
+# the apps/ entries, all on one version. The vendored framework and the native
+# packages are separate sequences with their own workflows and version lines.
+#
+# Pack runs without credentials on every pull request and master push, so a
+# pull request proves the whole publish set still packs. Publication is a
+# manual dispatch from a dsh-v* tag and consumes exactly the packed bytes.
+name: Release (dsh)
+
+on:
+ pull_request:
+ push:
+ branches: [master]
+ workflow_dispatch:
+ inputs:
+ publish:
+ description: Publish the packed tarballs to npm. Must run from a dsh-v* tag.
+ required: true
+ type: boolean
+ default: false
+
+permissions:
+ contents: read
+
+concurrency:
+ # Pack runs per ref so concurrent pull requests never displace each
+ # other; the publish job below serializes the shared dist-tag state.
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: false
+
+env:
+ PRIMARY_NODE_VERSION: '24'
+ DSH_TELEMETRY_DISABLED: '1'
+
+jobs:
+ pack:
+ name: Pack npm tarballs
+ runs-on: ubuntu-24.04
+ steps:
+ # Complete history: the release scripts read tags.
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - uses: pnpm/action-setup@v4
+ with:
+ dest: ${{ runner.temp }}/setup-pnpm
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.PRIMARY_NODE_VERSION }}
+
+ - name: Configure pnpm store path
+ id: pnpm-store
+ run: |
+ store_root="$HOME/.local/share/pnpm/store"
+ echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
+ store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
+ echo "path=$store_path" >> "$GITHUB_OUTPUT"
+
+ - uses: actions/cache/restore@v4
+ with:
+ path: ${{ steps.pnpm-store.outputs.path }}
+ key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
+ restore-keys: |
+ ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
+
+ - name: Install (immutable)
+ run: pnpm install --frozen-lockfile
+
+ - name: Verify release version
+ env:
+ RELEASE_PUBLISH: ${{ inputs.publish }}
+ run: pnpm run release:verify --family dsh
+
+ - name: Build
+ run: pnpm run build
+
+ - name: Pack release tarballs
+ run: pnpm run release:pack --family dsh --out dist/npm
+
+ # The harness packages declare the vendored framework as a peer, and this
+ # job has no credentials for the private registry, so the verification
+ # installs that family's pack output too. Only dist/npm is published.
+ - name: Pack the vendored framework for verification
+ run: pnpm run release:pack --family vendor --out dist/npm-vendor
+
+ # dsh-sandbox-local declares the Landlock entry as a runtime dependency, so
+ # the verification needs its tarball. Its platform packages stay out: they
+ # are optional, and building them needs a musl toolchain per architecture.
+ - name: Pack the Landlock entry for verification
+ run: |
+ pnpm --dir native/landlock-run run build:ts
+ pnpm --dir native/landlock-run/packages/entry pack --pack-destination "$PWD/dist/npm-landlock"
+
+ - name: Verify packed install
+ run: pnpm run release:verify-packed-install --family dsh --from dist/npm --from dist/npm-vendor --from dist/npm-landlock
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: dsh-npm-tarballs
+ path: dist/npm/*
+ if-no-files-found: error
+ retention-days: 7
+
+ publish:
+ name: Publish to npm
+ if: inputs.publish
+ needs: pack
+ runs-on: ubuntu-24.04
+ # Required reviewers and the allowed tags live on the environment; this is
+ # the only step in the sequence that can write to the registry.
+ environment: npm-publish
+ concurrency:
+ group: Release-publish
+ cancel-in-progress: false
+ permissions:
+ contents: read
+ steps:
+ # Checkout and install carry the release scripts only. There is no build
+ # step: publication uploads the bytes the pack job produced.
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+
+ - uses: pnpm/action-setup@v4
+ with:
+ dest: ${{ runner.temp }}/setup-pnpm
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.PRIMARY_NODE_VERSION }}
+ registry-url: https://registry.npmjs.org
+
+ - name: Install (immutable, no package scripts)
+ run: pnpm install --frozen-lockfile --ignore-scripts
+
+ - uses: actions/download-artifact@v4
+ with:
+ name: dsh-npm-tarballs
+ path: dist/npm
+
+ - name: Publish tarballs
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ run: pnpm run release:publish --family dsh --from dist/npm
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b4af9e3ecb..fd56278195 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -42,7 +42,7 @@ sdk-wheel:
- uv run --python 3.10 --group test --project python/sdk python scripts/smoke-python-runtime.py --scenario all --exe "$EXE"
- python scripts/build-python-release.py --package runtime --tag "$CI_COMMIT_TAG" --platform "$PLATFORM" --runtime-exe "$EXE" --output-dir "release/$PLATFORM"
- python -m venv .wheel-smoke
- - .wheel-smoke/bin/python -m pip install --find-links "release/$PLATFORM" --find-links release/sdk deepseek-harness=="$DSH_VERSION"
+ - .wheel-smoke/bin/python -m pip install --find-links "release/$PLATFORM" --find-links release/sdk deepseek-harness-sdk=="$DSH_VERSION"
- .wheel-smoke/bin/python scripts/smoke-python-runtime.py --scenario sdk-default
- |
if [ "${PLATFORM#linux-}" != "$PLATFORM" ]; then
@@ -55,7 +55,7 @@ sdk-wheel:
linux-arm64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
*) echo "Unsupported Linux platform $PLATFORM"; exit 1 ;;
esac
- docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c "/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk && /tmp/dsh-sdk/bin/python -m pip install --find-links /work/release/$PLATFORM --find-links /work/release/sdk deepseek-harness==$DSH_VERSION && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default"
+ docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c "/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk && /tmp/dsh-sdk/bin/python -m pip install --find-links /work/release/$PLATFORM --find-links /work/release/sdk deepseek-harness-sdk==$DSH_VERSION && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default"
fi
artifacts:
paths: [release/$PLATFORM/*.whl]
@@ -112,7 +112,7 @@ publish-python:
- python -m pip install twine==6.2.0
script:
- test "$(find release -name '*.whl' | wc -l | tr -d ' ')" = 4
- - test -f "release/sdk/deepseek_harness-${DSH_VERSION}-py3-none-any.whl"
+ - test -f "release/sdk/deepseek_harness_sdk-${DSH_VERSION}-py3-none-any.whl"
- test -f "release/linux-x64/deepseek_harness_runtime_bin-${DSH_VERSION}-py3-none-manylinux_2_28_x86_64.whl"
- test -f "release/linux-arm64/deepseek_harness_runtime_bin-${DSH_VERSION}-py3-none-manylinux_2_28_aarch64.whl"
- test -f "release/macos-arm64/deepseek_harness_runtime_bin-${DSH_VERSION}-py3-none-macosx_11_0_arm64.whl"
diff --git a/AGENTS.md b/AGENTS.md
index 9daf45a832..15d0863344 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -69,11 +69,11 @@ pnpm run typecheck
pnpm run lint
pnpm run duplication # cross-file TypeScript clone detection
pnpm run build # tsc emits lib/types, tsdown bundles runtime
-pnpm run check:windows-wine # ONLY when diagnosing a known Windows failure (needs wine); CI owns this signal
pnpm run hygiene # knip + publint + workspace constraints + NodeNext consumer check
+pnpm run check:windows-wine # ONLY when diagnosing a known Windows failure (needs wine); CI owns this signal
pnpm run doc-sync # all documentation gates; leaf list in scripts/run-gates.ts
pnpm run website:build # VitePress build (doubles as dead-link check)
-pnpm run demo:headless "task" # one-shot agent (needs DEEPSEEK_API_KEY)
+pnpm dsh --profile headless "task" # build, then run one task (needs DEEPSEEK_API_KEY)
pnpm run demo:cordis # the agent modifies its own runtime (needs key)
pnpm run demo:acp # ACP automation server (needs DEEPSEEK_API_KEY)
```
@@ -96,11 +96,11 @@ Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`,
## Conventions
-- Every npm package is `@deepseek-ai/dsh-`; vendored packages keep upstream names and are `private: true`. `cordis` is a peerDependency (+ dev) of every harness package.
+- Every npm package is `@deepseek-ai/dsh-`; vendored packages are rescoped ([mapping](docs/rescope.md)) and `private: true`. `@deepseek-ai/cordis` is a peerDependency (+ dev) of every harness package.
- ESM everywhere (`"type": "module"`). Use package names across packages and `.ts` in local relative imports. Config subprocesses run built `lib/` under plain Node; source regressions use their declared launcher ([testing policy](docs/testing.md#test-subprocess-launch-modes)). The `dsh` CLI source launch runs through tsx's ESM-only hook (`node --import tsx/esm`); modules it reaches must stay ESM (no CJS-only exports) — Node's native TypeScript modes are unavailable across the engines range ([source-launch contract](.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md)). Raw/Web `cordis.yml` bare plugins must appear in their resolver manifest's `dependencies`; `verify-cordis-config` enforces it.
- **Registrations are effects**: every contribution goes through `ctx.effect()` / `ctx.on()`; a registry's `register()` returns the disposer.
- **Runtime invariants assert owned relationships.** Check authoritative event streams or mutable data, not service or method presence, plugin metadata or effects, or fixed pure examples. Without a plausible relationship, an explained empty companion is correct ([package invariant rules](packages/AGENTS.md)).
-- **Typed events use declaration merging** and merge-extensible maps. Event JSDoc needs `@mode` and payload `@param`; scoped keys absent from payloads need `@dshScopeScan unsupported`. Public service methods document parameters and non-void returns.
+- **Typed events use declaration merging** and merge-extensible maps. Event JSDoc needs `@mode` and payload `@param`; scoped keys absent from payloads need `@dshScopeScan unsupported`. Public service methods document parameters and non-void returns. A `SessionEventMap` member is required-on-read by default — builds that do not know its type refuse the log unless the event carries the envelope's `ignorable: true`; only structural format changes bump `SESSION_FORMAT_VERSION` ([mechanism](.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md)).
- **Switch on discriminant tags.** Closed unions end in `assertNever`; merge-extensible unions fall through a documented default.
- **Waterfall listeners MUST call `next()`** to delegate; returning without it short-circuits the chain ([semantics](docs/cordis-primer.md#cordis-waterfall-semantics)).
- **Model-visible ⟺ logged**: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event.
diff --git a/BENCHMARK.md b/BENCHMARK.md
new file mode 100644
index 0000000000..6e8f466a1f
--- /dev/null
+++ b/BENCHMARK.md
@@ -0,0 +1,3 @@
+# Running benchmarks
+
+To run benchmark tasks with the minimal agent composition, follow [Get started with the Python SDK](docs/user/guide/python-sdk.md). The guide covers installation, running [`minimal.cordis.yml`](examples/jsonrpc-agent/minimal.cordis.yml), and isolating workspaces and session IDs between tasks.
diff --git a/README.i18n.yaml b/README.i18n.yaml
index 35cb82e776..552fb03a25 100644
--- a/README.i18n.yaml
+++ b/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write README.md
-README.md: 3174630d021b3868986d6ad9989d257fe8ac29fb
-README.zh.md: 377ea6372a9a531c1400d08b0ef33792b452dc65
+README.md: b2d84672275a4ca996b6ea596c104abfaa52432a
+README.zh.md: 747a3c88bc129ad5dd24f5fa150a1661c4db0b11
diff --git a/README.md b/README.md
index 3174630d02..b2d8467227 100644
--- a/README.md
+++ b/README.md
@@ -12,41 +12,34 @@ DeepSeek Harness is under internal testing. Features and interfaces may change.
The internal build uploads all Session Logs by default to help diagnose reported problems. Set `DSH_TELEMETRY_DISABLED=1` to disable telemetry. Send feedback through the internal WeChat group.
-## Install
+## Run from source
-Clone the repository, then run the installer:
+Clone this repo, complete the [dependency and API-key setup](docs/user/guide/quickstart.md#step-1-install-and-configure-the-api-key), then run:
```sh
-git clone
-cd deepseek-harness
-scripts/install.sh
+pnpm dsh web
```
-The installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, prompts for a DeepSeek API key, builds the required repository artifacts, and launches the Web UI.
-
-The default active checkout is `~/.dsh/source/current`, and the launcher is linked into `~/.local/bin`. Re-run the installer to update. [`scripts/install.sh`](scripts/install.sh) owns alternate locations, update mechanics, and recovery options.
-
## Use DeepSeek Harness
### Web UI
-For the recommended local interface, choose Web UI when the installer finishes. To start it later, or after updating the active checkout, build the repository and run:
+Start the recommended local interface from the repository root:
```sh
-(cd ~/.dsh/source/current && pnpm run build)
-dsh web
+pnpm dsh web
```
-The path above is the installer's default. If you set `DSH_SOURCE` or `DSH_CURRENT`, or reused an existing checkout, replace `~/.dsh/source/current` with that checkout path; see [`scripts/install.sh`](scripts/install.sh) for details. The Web UI is served at `http://127.0.0.1:3080` by default.
+The command builds the repository before starting the Web UI, which is served at `http://127.0.0.1:3080` by default.
### Profiles
-`dsh` boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/`:
+The source CLI boots profiles — ordered stacks of plugin-bundle patch layers under your own overrides in `$DSH_HOME/profiles/`:
```sh
-dsh --profile web # the browser UI (same as: dsh web)
-dsh plugin --profile tui add # install a plugin into a custom profile
-dsh --profile tui # boot it
+pnpm dsh --profile web # the browser UI
+pnpm dsh plugin --profile tui add # install a plugin into a custom profile
+pnpm dsh --profile tui # boot it
```
The [CLI reference](apps/cli/README.md#profiles) describes profile layout, layer semantics, and config dump commands.
@@ -56,7 +49,7 @@ The [CLI reference](apps/cli/README.md#profiles) describes profile layout, layer
Run one task, print the final answer, and exit:
```sh
-dsh run "summarize this workspace"
+pnpm dsh --profile headless "summarize this workspace"
```
### Automation and SDKs
diff --git a/README.zh.md b/README.zh.md
index 377ea6372a..747a3c88bc 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -12,41 +12,34 @@ DeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化
为帮助诊断上报的问题,内测版本默认上传所有会话日志。设置 `DSH_TELEMETRY_DISABLED=1` 可关闭遥测。请通过内部企业微信群反馈问题和建议。
-## 安装
+## 从源码运行
-克隆仓库,然后运行安装器:
+克隆本仓库,完成[依赖安装和 API 密钥配置](docs/user/guide/quickstart.md#step-1-install-and-configure-the-api-key),然后运行:
```sh
-git clone
-cd deepseek-harness
-scripts/install.sh
+pnpm dsh web
```
-安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥,然后构建所需的仓库产物并启动 Web UI。
-
-默认生效的检出位于 `~/.dsh/source/current`,启动器链接到 `~/.local/bin`。再次运行安装器即可更新。其他位置、更新机制和恢复选项由 [`scripts/install.sh`](scripts/install.sh) 负责。
-
## 使用 DeepSeek Harness
### Web UI
-推荐在本地使用 Web UI;安装结束时,选择 Web UI 即可。以后需要启动时,或更新当前生效的检出后,请构建仓库并运行:
+请从仓库根目录启动推荐的本地界面:
```sh
-(cd ~/.dsh/source/current && pnpm run build)
-dsh web
+pnpm dsh web
```
-上述路径是安装器的默认位置。如果你设置过 `DSH_SOURCE` 或 `DSH_CURRENT`,或者复用了已有检出,请把 `~/.dsh/source/current` 换成该检出路径;详情见 [`scripts/install.sh`](scripts/install.sh)。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。
+该命令会先构建仓库,再启动 Web UI。Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。
### Profile
-`dsh` 启动 profile:按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/` 中的自有覆盖层:
+源码 CLI(命令行界面)会启动 profile:按序叠放的插件组合包 patch 层,之上再叠加你在 `$DSH_HOME/profiles/` 中的自有覆盖层:
```sh
-dsh --profile web # the browser UI (same as: dsh web)
-dsh plugin --profile tui add # install a plugin into a custom profile
-dsh --profile tui # boot it
+pnpm dsh --profile web # the browser UI
+pnpm dsh plugin --profile tui add # install a plugin into a custom profile
+pnpm dsh --profile tui # boot it
```
profile 布局、层语义与配置输出命令详见 [CLI(命令行界面)参考](apps/cli/README.md#profiles)。
@@ -56,7 +49,7 @@ profile 布局、层语义与配置输出命令详见 [CLI(命令行界面)
运行一项任务,打印最终答案后退出:
```sh
-dsh run "summarize this workspace"
+pnpm dsh --profile headless "summarize this workspace"
```
### 自动化与 SDK
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index fec5de6128..e0c71e9903 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -11,23 +11,23 @@ The complete npm transitive closure, including the Landlock launcher workspace,
## Vendored source (`vendor/`)
-The Cordis framework and its foundation libraries are source-vendored into this repository rather than consumed from npm. All are MIT-licensed; each directory preserves its upstream `LICENSE` file. Exact upstream commits and local modifications are recorded in [`vendor/README.md`](vendor/README.md).
+The Cordis framework and its foundation libraries are source-vendored into this repository rather than consumed from npm, and republished under the `@deepseek-ai` scope. All are MIT-licensed; each directory preserves its upstream `LICENSE` file. Exact upstream commits and local modifications are recorded in [`vendor/README.md`](vendor/README.md).
-| Package | Upstream | License |
-| --- | --- | --- |
-| `cosmokit` | [github.com/deepseek-harness/cosmokit](https://github.com/deepseek-harness/cosmokit) | MIT |
-| `schemastery` | [github.com/deepseek-harness/schemastery](https://github.com/deepseek-harness/schemastery) | MIT |
-| `cordis` | [github.com/cordiverse/cordis](https://github.com/cordiverse/cordis) | MIT |
-| `@cordisjs/plugin-loader` | [github.com/cordiverse/cordis](https://github.com/cordiverse/cordis) | MIT |
-| `@cordisjs/plugin-include` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
-| `@cordisjs/plugin-group` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
-| `@cordisjs/plugin-timer` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
-| `@cordisjs/plugin-hmr` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
-| `@cordisjs/plugin-logger-console` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
+| Package | Upstream name | Upstream | License |
+| --- | --- | --- | --- |
+| `@deepseek-ai/cosmokit` | `cosmokit` | [github.com/deepseek-harness/cosmokit](https://github.com/deepseek-harness/cosmokit) | MIT |
+| `@deepseek-ai/schemastery` | `schemastery` | [github.com/deepseek-harness/schemastery](https://github.com/deepseek-harness/schemastery) | MIT |
+| `@deepseek-ai/cordis` | `cordis` | [github.com/cordiverse/cordis](https://github.com/cordiverse/cordis) | MIT |
+| `@deepseek-ai/cordis-plugin-loader` | `@cordisjs/plugin-loader` | [github.com/cordiverse/cordis](https://github.com/cordiverse/cordis) | MIT |
+| `@deepseek-ai/cordis-plugin-include` | `@cordisjs/plugin-include` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
+| `@deepseek-ai/cordis-plugin-group` | `@cordisjs/plugin-group` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
+| `@deepseek-ai/cordis-plugin-timer` | `@cordisjs/plugin-timer` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
+| `@deepseek-ai/cordis-plugin-hmr` | `@cordisjs/plugin-hmr` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
+| `@deepseek-ai/cordis-plugin-logger-console` | `@cordisjs/plugin-logger-console` | [github.com/deepseek-harness/cordis](https://github.com/deepseek-harness/cordis) | MIT |
## Runtime npm dependencies
-External packages that a workspace package resolves at runtime. `scripts/install.sh` installs this repository itself, so the tier covers every plugin a user can mount from `cordis.yml` — not only what the `dsh` CLI, Web UI, and Python SDK runtime load by default.
+External packages that a workspace package resolves at runtime. The tier covers every plugin a user can mount from `cordis.yml` — not only what the `dsh` CLI, Web UI, and Python SDK runtime load by default.
| Package | License |
| --- | --- |
@@ -59,6 +59,7 @@ External packages that a workspace package resolves at runtime. `scripts/install
| [`diff`](https://github.com/kpdecker/jsdiff) | BSD-3-Clause |
| [`e2b`](https://github.com/e2b-dev/e2b) | MIT |
| [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT |
+| [`fflate`](https://github.com/101arrowz/fflate) | MIT |
| [`handlebars`](https://github.com/handlebars-lang/handlebars.js) | MIT |
| [`immer`](https://github.com/immerjs/immer) | MIT |
| [`js-yaml`](https://github.com/nodeca/js-yaml) | MIT |
@@ -80,7 +81,6 @@ External packages that a workspace package resolves at runtime. `scripts/install
| [`node-addon-require-builtin`](https://www.npmjs.com/package/node-addon-require-builtin) | MIT |
| [`node-pty`](https://github.com/microsoft/node-pty) | MIT |
| [`picomatch`](https://github.com/micromatch/picomatch) | MIT |
-| [`pnpm`](https://github.com/pnpm/pnpm) | MIT |
| [`react`](https://github.com/facebook/react) | MIT |
| [`react-dom`](https://github.com/facebook/react) | MIT |
| [`sharp`](https://github.com/lovell/sharp) | Apache-2.0 |
@@ -181,7 +181,7 @@ Direct dependencies of the `pyproject.toml` manifests, plus `uv` as the developm
| Package | License | Role |
| --- | --- | --- |
| [`hatchling`](https://github.com/pypa/hatch) | MIT | build backend |
-| [`pydantic`](https://github.com/pydantic/pydantic) | MIT | runtime dependency of `deepseek-harness` |
+| [`pydantic`](https://github.com/pydantic/pydantic) | MIT | runtime dependency of `deepseek-harness-sdk` |
| [`pytest`](https://github.com/pytest-dev/pytest) | MIT | test-only |
| [`uv`](https://github.com/astral-sh/uv) | MIT / Apache-2.0 | development workflow tool |
diff --git a/apps/cli/README.i18n.yaml b/apps/cli/README.i18n.yaml
index 101332555d..191d6c8e68 100644
--- a/apps/cli/README.i18n.yaml
+++ b/apps/cli/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/cli/README.md
-README.md: dd29f7fc03a783079ea3194de99589c1f545be5b
-README.zh.md: 60e7aa1ec1ea2fad7e3f3d97a0f6bf42355adffc
+README.md: 98a856261bc632c97f350db8fc7bb0b10c22235d
+README.zh.md: 283e54138e24202ed6b88d1d309538c58cd66b3e
diff --git a/apps/cli/README.md b/apps/cli/README.md
index dd29f7fc03..98a856261b 100644
--- a/apps/cli/README.md
+++ b/apps/cli/README.md
@@ -9,18 +9,30 @@ The `dsh` command is the product launcher for profiles: ordered stacks of plugin
| Command | Purpose |
|---|---|
| `dsh --profile ` | Boot the named profile under `$DSH_HOME/profiles/`. |
-| `dsh run [--profile ] [--patch ...] "task"` | Run one fresh persisted session directly over core, print the final answer, and exit; the profile defaults to `headless` and mounts no Web server. |
-| `dsh web` | Alias of `--profile web` with the Web flag family (`--host`, `--port`, `--dev`, ...). |
+| `dsh --profile headless "task"` | Run one fresh persisted session, print the final answer, and exit. |
+| `dsh web` | Alias of `--profile web`. |
| `dsh plugin --profile ` | Manage a profile's plugins by forwarding to pnpm in the profile directory. |
-The invoking directory is the default workspace root. `dsh run` requires non-blank task text and the selected profile must mount the `headless-runner` row; `--profile` preserves custom one-shot profiles. The `web` and `headless` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`.
+The invoking directory is the default workspace root. The `web` and `headless` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`.
+
+## App arguments
+
+The launcher parses only its own flags and hands everything after them to the booted profile, where any injected app plugin may parse the shared immutable snapshot ([`dsh-cmdline`](../../packages/boot/cmdline/README.md)). Launcher flags therefore come first, and the first token the launcher does not recognize starts the app's arguments:
+
+```sh
+dsh --profile web --port 8080 # --port belongs to the web app
+dsh --profile tui --resume # --resume belongs to the terminal app
+dsh --profile headless "run the tests"
+dsh --profile web --help # the web app's flags, not the launcher's
+dsh --help # the launcher's own help
+```
## Profiles
-A profile directory holds a `package.json` (out-of-tree plugin dependencies plus the profile manifest `dsh.profile` with its ordered `bundles` list) and a `cordis.patch.yml` (the user's own patch layer, hot-reloaded on long-lived surfaces). The tree composes over an empty root: each bundle's patch in `dsh.profile.bundles` order, then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml`, then `--patch` overlays, then flag patches. Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it.
+A profile directory holds a `package.json` (out-of-tree plugin dependencies plus the profile manifest `dsh.profile` with its ordered `bundles` list) and a `cordis.patch.yml` (the user's own patch layer, hot-reloaded on long-lived surfaces). The tree composes over an empty root: each bundle's patch in `dsh.profile.bundles` order, then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml`, then `--patch` overlays. Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it.
-The [CLI behavior reference](reference/README.md) owns exact layer precedence, flags, shutdown behavior, deployment defaults, and the source launcher.
+The [CLI behavior reference](reference/README.md) owns exact layer precedence, flags, shutdown behavior, deployment defaults, and source execution.
## Development
-Production runs require built package and frontend artifacts. From a checkout, `pnpm run dsh` runs the TypeScript entry and forwards arguments; the [source-launcher reference](reference/README.md#source-launcher) describes the PATH symlink and module-resolution contract.
+Production runs require built package and frontend artifacts. From the repository root, `pnpm dsh ` builds those artifacts, runs the TypeScript entry, and forwards every argument; the [source-execution reference](reference/README.md#source-execution) owns the module-resolution contract.
diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md
index 60e7aa1ec1..283e54138e 100644
--- a/apps/cli/README.zh.md
+++ b/apps/cli/README.zh.md
@@ -9,18 +9,30 @@
| 命令 | 用途 |
|---|---|
| `dsh --profile ` | 启动位于 `$DSH_HOME/profiles/` 的指定 profile。 |
-| `dsh run [--profile ] [--patch ...] "task"` | 直接在 core 上运行一个新的持久化会话,打印最终答案并退出;profile 默认为 `headless`,且不挂载 Web server。 |
-| `dsh web` | `--profile web` 的别名,附带 Web flag 系列(`--host`、`--port`、`--dev` 等)。 |
+| `dsh --profile headless "task"` | 运行一个新的持久化会话,打印最终答案并退出。 |
+| `dsh web` | `--profile web` 的别名。 |
| `dsh plugin --profile ` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 |
-调用目录是默认 workspace 根目录。`dsh run` 要求任务文本非空白,且所选 profile 必须挂载 `headless-runner` 行;`--profile` 保留对自定义一次性 profile 的支持。`web` 和 `headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。
+调用目录是默认 workspace 根目录。`web` 和 `headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。
+
+## 应用参数
+
+启动器只解析属于自己的 flag,并把其后的一切交给启动起来的 profile,任何注入它的应用插件都可以解析这份共享的不可变快照([`dsh-cmdline`](../../packages/boot/cmdline/README.md))。因此启动器的 flag 必须写在前面,而启动器不认识的第一个 token 就是应用参数的起点:
+
+```sh
+dsh --profile web --port 8080 # --port belongs to the web app
+dsh --profile tui --resume # --resume belongs to the terminal app
+dsh --profile headless "run the tests"
+dsh --profile web --help # the web app's flags, not the launcher's
+dsh --help # the launcher's own help
+```
## Profile
-profile 目录包含一个 `package.json`(树外插件依赖,加上 profile manifest(元数据清单)`dsh.profile` 及其有序的 `bundles` 列表)和一个 `cordis.patch.yml`(用户自己的 patch 层,在长期运行的 surface 上热重载)。配置树在空根之上组合:先按 `dsh.profile.bundles` 顺序应用各组合包的 patch,然后是 profile 的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml`,然后是 `--patch` overlay,最后是 flag patch。`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自己的 `node_modules` 解析;pnpm 把树外插件安装在后者。使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。
+profile 目录包含一个 `package.json`(树外插件依赖,加上 profile manifest(元数据清单)`dsh.profile` 及其有序的 `bundles` 列表)和一个 `cordis.patch.yml`(用户自己的 patch 层,在长期运行的 surface 上热重载)。配置树在空根之上组合:先按 `dsh.profile.bundles` 顺序应用各组合包的 patch,然后是 profile 的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml`,然后是 `--patch` overlay。`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自己的 `node_modules` 解析;pnpm 把树外插件安装在后者。使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。
-[CLI(命令行界面)行为参考](reference/README.md)负责确切的层优先级、flag、关闭行为、部署默认值和源码启动器。
+[CLI(命令行界面)行为参考](reference/README.md)负责确切的层优先级、flag、关闭行为、部署默认值和源码执行。
## 开发
-生产运行需要已构建的包与前端产物。在 checkout 中,`pnpm run dsh` 会运行 TypeScript 入口并转发参数;[源码启动器参考](reference/README.md#source-launcher)说明 PATH 符号链接和模块解析约定。
+生产运行需要已构建的包与前端产物。从仓库根目录运行 `pnpm dsh ` 会先构建这些产物,再运行 TypeScript 入口并转发所有参数;模块解析约定由[源码执行参考](reference/README.md#source-execution)负责。
diff --git a/apps/cli/composition.md b/apps/cli/composition.md
index aef0dc6a7b..303903566b 100644
--- a/apps/cli/composition.md
+++ b/apps/cli/composition.md
@@ -8,12 +8,10 @@ The dsh-base bundle patch every profile applies first; mode bundles (dsh-web-app
```mermaid
flowchart LR
cfg["packages/bundle/base/cordis.patch.yml cordis.yml"]
- plugin_dsh_base_timer["timer @cordisjs/plugin-timer"]
+ plugin_dsh_base_timer["timer @deepseek-ai/cordis-plugin-timer"]
cfg --> plugin_dsh_base_timer
- plugin_dsh_base_hmr["hmr @cordisjs/plugin-hmr"]
+ plugin_dsh_base_hmr["hmr @deepseek-ai/cordis-plugin-hmr"]
cfg --> plugin_dsh_base_hmr
- plugin_dsh_base_repository_plugins["repository-plugins @deepseek-ai/dsh-repository-plugin"]
- cfg --> plugin_dsh_base_repository_plugins
plugin_dsh_base_llm["llm @deepseek-ai/dsh-llm"]
cfg --> plugin_dsh_base_llm
plugin_dsh_base_session["session @deepseek-ai/dsh-session"]
@@ -112,6 +110,10 @@ flowchart LR
cfg --> plugin_dsh_base_subagent_spawn
plugin_dsh_base_subagent_fork["subagent-fork @deepseek-ai/dsh-subagent-fork"]
cfg --> plugin_dsh_base_subagent_fork
+ plugin_dsh_base_subagent_codex["subagent-codex @deepseek-ai/dsh-subagent-codex"]
+ cfg --> plugin_dsh_base_subagent_codex
+ plugin_dsh_base_subagent_claude_code["subagent-claude-code @deepseek-ai/dsh-subagent-claude-code"]
+ cfg --> plugin_dsh_base_subagent_claude_code
plugin_dsh_base_tool_subagent_control["tool-subagent-control @deepseek-ai/dsh-tool-subagent-control"]
cfg --> plugin_dsh_base_tool_subagent_control
plugin_dsh_base_tool_subagent_list_agents["tool-subagent-list-agents @deepseek-ai/dsh-tool-subagent-control/list-agents"]
@@ -166,9 +168,8 @@ flowchart LR
| Plugin id | Package / module |
| --- | --- |
-| `timer` | `@cordisjs/plugin-timer` |
-| `hmr` | `@cordisjs/plugin-hmr` |
-| `repository-plugins` | `@deepseek-ai/dsh-repository-plugin` |
+| `timer` | `@deepseek-ai/cordis-plugin-timer` |
+| `hmr` | `@deepseek-ai/cordis-plugin-hmr` |
| `llm` | `@deepseek-ai/dsh-llm` |
| `session` | `@deepseek-ai/dsh-session` |
| `typert` | `@deepseek-ai/dsh-typert-registry` |
@@ -218,6 +219,8 @@ flowchart LR
| `subagent` | `@deepseek-ai/dsh-subagent` |
| `subagent-spawn` | `@deepseek-ai/dsh-subagent-spawn` |
| `subagent-fork` | `@deepseek-ai/dsh-subagent-fork` |
+| `subagent-codex` | `@deepseek-ai/dsh-subagent-codex` |
+| `subagent-claude-code` | `@deepseek-ai/dsh-subagent-claude-code` |
| `tool-subagent-control` | `@deepseek-ai/dsh-tool-subagent-control` |
| `tool-subagent-list-agents` | `@deepseek-ai/dsh-tool-subagent-control/list-agents` |
| `tool-subagent` | `@deepseek-ai/dsh-tool-subagent` |
diff --git a/apps/cli/config/agent-presets/code/agent.cordis.yml b/apps/cli/config/agent-presets/code/agent.cordis.yml
index 5e273bb955..c99e300f55 100644
--- a/apps/cli/config/agent-presets/code/agent.cordis.yml
+++ b/apps/cli/config/agent-presets/code/agent.cordis.yml
@@ -53,7 +53,7 @@
# ── filesystem ──────────────────────────────────────────────────────────────
-# All three register into the host `tools` registry and provide nothing, so
+# Both register into the host `tools` registry and provide nothing, so
# they need no realm. The `fs` service and its policy stay in the host.
- id: tool-fs
name: '@deepseek-ai/dsh-tool-fs'
@@ -63,11 +63,6 @@
config:
sampleOverCapGlobResults: false
-- id: tool-str-replace-editor
- name: '@deepseek-ai/dsh-tool-str-replace-editor'
- config:
- maxOutputChars: 16000
-
# ── background tasks ────────────────────────────────────────────────────────
# Only the model-facing controls. The task REGISTRY stays on the host plane:
@@ -134,17 +129,20 @@
# `compact-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
# share this realm rather than sit outside it.
+#
+# `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
+# plane, and the rows here resolve that one instance. It takes no configuration,
+# keys every fold by Session, and owns the context-meter projection units the
+# browser reads for every session — behind a realm those units would come and go
+# with whichever presets happen to be mounted. What a preset chooses is whether
+# its agent compacts at all, which is `compact-basic` below.
- id: compaction
name: cordis:group
group: true
isolate:
- tokenMeter: true
compact: true
toolResultPrune: true
config:
- - id: token-meter
- name: '@deepseek-ai/dsh-token-meter'
-
- id: compact-basic
name: '@deepseek-ai/dsh-compact-basic'
@@ -195,6 +193,27 @@
toolName: subagent_fork
backgroundMode: continuable
+ # Product providers are host-plane singletons. Copy this preset, then
+ # remove `disabled` from either ordinary tool row to expose that product
+ # only to agents composed from the copy.
+ - id: tool-subagent-codex
+ name: '@deepseek-ai/dsh-tool-subagent'
+ disabled: true
+ config:
+ provider: codex
+ toolName: subagent_codex
+ enableRunInBackground: false
+ maxDepth: provider-managed
+
+ - id: tool-subagent-claude-code
+ name: '@deepseek-ai/dsh-tool-subagent'
+ disabled: true
+ config:
+ provider: claude-code
+ toolName: subagent_claude_code
+ enableRunInBackground: false
+ maxDepth: provider-managed
+
- id: workflow-workerthread
name: '@deepseek-ai/dsh-workflow-workerthread'
config:
diff --git a/apps/cli/config/agent-presets/cordis/agent.cordis.yml b/apps/cli/config/agent-presets/cordis/agent.cordis.yml
index 1bc3d3bcf0..6fa6030c97 100644
--- a/apps/cli/config/agent-presets/cordis/agent.cordis.yml
+++ b/apps/cli/config/agent-presets/cordis/agent.cordis.yml
@@ -47,7 +47,7 @@
# ── filesystem ──────────────────────────────────────────────────────────────
-# All three register into the host `tools` registry and provide nothing, so
+# Both register into the host `tools` registry and provide nothing, so
# they need no realm. The `fs` service and its policy stay in the host.
- id: tool-fs
name: '@deepseek-ai/dsh-tool-fs'
@@ -57,11 +57,6 @@
config:
sampleOverCapGlobResults: false
-- id: tool-str-replace-editor
- name: '@deepseek-ai/dsh-tool-str-replace-editor'
- config:
- maxOutputChars: 16000
-
# ── background tasks ────────────────────────────────────────────────────────
# Only the model-facing controls. The task REGISTRY stays on the host plane:
@@ -115,17 +110,20 @@
# `compact-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
# share this realm rather than sit outside it.
+#
+# `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
+# plane, and the rows here resolve that one instance. It takes no configuration,
+# keys every fold by Session, and owns the context-meter projection units the
+# browser reads for every session — behind a realm those units would come and go
+# with whichever presets happen to be mounted. What a preset chooses is whether
+# its agent compacts at all, which is `compact-basic` below.
- id: compaction
name: cordis:group
group: true
isolate:
- tokenMeter: true
compact: true
toolResultPrune: true
config:
- - id: token-meter
- name: '@deepseek-ai/dsh-token-meter'
-
- id: compact-basic
name: '@deepseek-ai/dsh-compact-basic'
@@ -182,6 +180,27 @@
toolName: subagent_fork
backgroundMode: continuable
+ # Product providers are host-plane singletons. Copy this preset, then
+ # remove `disabled` from either ordinary tool row to expose that product
+ # only to agents composed from the copy.
+ - id: tool-subagent-codex
+ name: '@deepseek-ai/dsh-tool-subagent'
+ disabled: true
+ config:
+ provider: codex
+ toolName: subagent_codex
+ enableRunInBackground: false
+ maxDepth: provider-managed
+
+ - id: tool-subagent-claude-code
+ name: '@deepseek-ai/dsh-tool-subagent'
+ disabled: true
+ config:
+ provider: claude-code
+ toolName: subagent_claude_code
+ enableRunInBackground: false
+ maxDepth: provider-managed
+
- id: workflow-workerthread
name: '@deepseek-ai/dsh-workflow-workerthread'
config:
diff --git a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
index f6d9a6126c..ce751f039d 100644
--- a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
+++ b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
@@ -26,6 +26,34 @@ A preset is a directory holding one `agent.cordis.yml`, optionally beside a `pre
3. **Rewrite `preset.yml`**: give the copy its own `name` and `description`, and drop any `order` the source declared — that field sorts the shipped roster.
4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and realm rule above.
+### Native product subagents
+
+Codex and Claude Code providers already live in the host composition. A preset chooses either product by contributing the same ordinary delegation-tool row used for spawn and fork; never move a product provider into the preset and never add a product-specific settings field.
+
+Copy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested:
+
+```yaml
+- id: tool-subagent-codex
+ name: '@deepseek-ai/dsh-tool-subagent'
+ disabled: true
+ config:
+ provider: codex
+ toolName: subagent_codex
+ enableRunInBackground: false
+ maxDepth: provider-managed
+
+- id: tool-subagent-claude-code
+ name: '@deepseek-ai/dsh-tool-subagent'
+ disabled: true
+ config:
+ provider: claude-code
+ toolName: subagent_claude_code
+ enableRunInBackground: false
+ maxDepth: provider-managed
+```
+
+The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. The host must provide `codex` or `claude` on `PATH`; the preset does not install, authenticate, select a model for, or probe either product.
+
The shipped preset directories are off-limits: never edit or delete them, and never escalate the sandbox to reach them, even when a change there looks quicker — an upgrade overwrites the install, and corrupting the `cordis` preset disables preset authoring itself. Locally authored presets under the user root are yours to create, edit, and delete.
## The rule that catches people
diff --git a/apps/cli/config/agent-presets/minimal/agent.cordis.yml b/apps/cli/config/agent-presets/minimal/agent.cordis.yml
index 6ae88b9339..1ec0a6ea75 100644
--- a/apps/cli/config/agent-presets/minimal/agent.cordis.yml
+++ b/apps/cli/config/agent-presets/minimal/agent.cordis.yml
@@ -1,39 +1,73 @@
-# The `minimal` agent preset: the two-tool benchmark surface.
+# The `minimal` agent preset: a fixed-prompt, two-tool coding surface.
#
-# The native model surface is exactly persistent `bash` plus
-# `str_replace_editor`. Everything else a session could reach — skills, goals,
-# plan mode, delegation, workflows, todo, web — is simply absent rather than
-# disabled, because a preset composes what an agent has instead of subtracting
-# from a shared default.
-#
-# The host composition is unchanged: this agent still runs inside the same
-# sandbox, approval, persistence, and model routing as any other session.
+# The persona is the complete system prompt, so global identity, Web surface,
+# tool guidance, and later assembly listeners cannot add prompt text. The model
+# composes only the persistent `bash` and `str_replace_editor` tools.
- id: persona
name: '@deepseek-ai/dsh-persona'
config:
- text: >-
- You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
+ text: You are a helpful software engineer assistant.
+ complete: true
-# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
-# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
-# the criterion for host-plane ownership — injection resolves before any session
-# exists, so there is no agent to key by. Behind a preset realm those variables
-# never reached the model's shell at all. `tool-bash` consumes the host registry
-# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
-# sandbox policy owns it.
-#
-# `run_in_background` is off because this preset mounts no `tool-tasks`. The
-# host registry already refuses a start for an owner no attached control
-# surface serves, so this is not the safety boundary — it is the model-facing
-# one: an agent that could never collect a task should not be offered the
-# parameter at all, and disabling it drops the parameter from the schema.
-- id: tool-bash
- name: '@deepseek-ai/dsh-tool-bash'
+# The PTY registry is an agent-owned service, so it lives in an entry-local
+# realm. The backend still consumes the host sandbox policy and subprocess
+# implementation, while the tool registers into this agent's scoped catalog.
+- id: persistent-shell
+ name: cordis:group
+ group: true
+ isolate:
+ pty: true
config:
- enableRunInBackground: false
+ - id: pty
+ name: '@deepseek-ai/dsh-pty'
-- id: tool-str-replace-editor
+ - id: pty-local
+ name: '@deepseek-ai/dsh-pty-local'
+ config:
+ timeoutMs: 300000
+
+ - id: persistent-bash
+ name: '@deepseek-ai/dsh-tool-bash-persistent'
+ config:
+ timeoutMs: 300000
+ description: |-
+ Run commands in a bash shell
+ * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
+ * You don't have access to the internet via this tool.
+ * You do have access to a mirror of common linux and python packages via apt and pip.
+ * State is persistent across command calls and discussions with the user.
+ * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
+ * Please avoid commands that may produce a very large amount of output.
+ * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.
+
+# The editor requires absolute paths unconditionally.
+- id: str-replace-editor
name: '@deepseek-ai/dsh-tool-str-replace-editor'
config:
maxOutputChars: 16000
+
+# Model capacity comes from routed model metadata; this block states the
+# compaction policy explicitly.
+#
+# `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
+# plane, and the row here resolves that one instance. It takes no configuration,
+# keys every fold by Session, and owns the context-meter projection units the
+# browser reads for every session — behind a realm those units would come and go
+# with whichever presets happen to be mounted. What a preset chooses is whether
+# its agent compacts at all, which is `compact-basic` below.
+- id: compaction
+ name: cordis:group
+ group: true
+ isolate:
+ compact: true
+ config:
+ - id: compact-basic
+ name: '@deepseek-ai/dsh-compact-basic'
+ config:
+ thresholdRatio: 0.8
+ retainTokens: 20480
+ summarizationProvider: ''
+ summarizationModel: ''
+ maxTokens: 8192
+ compactionRetries: 1
diff --git a/apps/cli/config/agent-presets/minimal/preset.yml b/apps/cli/config/agent-presets/minimal/preset.yml
index 7160b51c43..97c714b72a 100644
--- a/apps/cli/config/agent-presets/minimal/preset.yml
+++ b/apps/cli/config/agent-presets/minimal/preset.yml
@@ -1,3 +1,3 @@
name: 极简模式
-description: 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
+description: 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
order: 3
diff --git a/apps/cli/config/agent-presets/standard/agent.cordis.yml b/apps/cli/config/agent-presets/standard/agent.cordis.yml
index 6442b12306..fbef791a22 100644
--- a/apps/cli/config/agent-presets/standard/agent.cordis.yml
+++ b/apps/cli/config/agent-presets/standard/agent.cordis.yml
@@ -46,7 +46,7 @@
# ── filesystem ──────────────────────────────────────────────────────────────
-# All three register into the host `tools` registry and provide nothing, so
+# Both register into the host `tools` registry and provide nothing, so
# they need no realm. The `fs` service and its policy stay in the host.
- id: tool-fs
name: '@deepseek-ai/dsh-tool-fs'
@@ -56,11 +56,6 @@
config:
sampleOverCapGlobResults: false
-- id: tool-str-replace-editor
- name: '@deepseek-ai/dsh-tool-str-replace-editor'
- config:
- maxOutputChars: 16000
-
# ── background tasks ────────────────────────────────────────────────────────
# Only the model-facing controls. The task REGISTRY stays on the host plane:
@@ -127,17 +122,20 @@
# `compact-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
# share this realm rather than sit outside it.
+#
+# `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
+# plane, and the rows here resolve that one instance. It takes no configuration,
+# keys every fold by Session, and owns the context-meter projection units the
+# browser reads for every session — behind a realm those units would come and go
+# with whichever presets happen to be mounted. What a preset chooses is whether
+# its agent compacts at all, which is `compact-basic` below.
- id: compaction
name: cordis:group
group: true
isolate:
- tokenMeter: true
compact: true
toolResultPrune: true
config:
- - id: token-meter
- name: '@deepseek-ai/dsh-token-meter'
-
- id: compact-basic
name: '@deepseek-ai/dsh-compact-basic'
@@ -194,6 +192,27 @@
toolName: subagent_fork
backgroundMode: continuable
+ # Product providers are host-plane singletons. Copy this preset, then
+ # remove `disabled` from either ordinary tool row to expose that product
+ # only to agents composed from the copy.
+ - id: tool-subagent-codex
+ name: '@deepseek-ai/dsh-tool-subagent'
+ disabled: true
+ config:
+ provider: codex
+ toolName: subagent_codex
+ enableRunInBackground: false
+ maxDepth: provider-managed
+
+ - id: tool-subagent-claude-code
+ name: '@deepseek-ai/dsh-tool-subagent'
+ disabled: true
+ config:
+ provider: claude-code
+ toolName: subagent_claude_code
+ enableRunInBackground: false
+ maxDepth: provider-managed
+
- id: workflow-workerthread
name: '@deepseek-ai/dsh-workflow-workerthread'
config:
diff --git a/apps/cli/config/core-web.cordis.yml b/apps/cli/config/core-web.cordis.yml
deleted file mode 100644
index 43860418c4..0000000000
--- a/apps/cli/config/core-web.cordis.yml
+++ /dev/null
@@ -1,113 +0,0 @@
-# Opt-in Web shell for the RL core agent contract. The model receives exactly
-# the configured persona plus the native `bash` and `str_replace_editor`
-# schemas; the Web host, browser shell, persistence, and permission stack stay.
-
-# Match the Claude SWE-compatible RL core prompt. Disabling the Web runtime's
-# surface context removes its GUI orientation, managed shell variables, and the
-# launcher's source-checkout section through one configuration contract.
-# Workspace instructions are model-visible user context rather than a system
-# section, but RL core disables them as part of the same prompt contract.
-- id: system-prompt
- config:
- includeHarnessIdentity: false
- persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.'
-
-- id: web-runtime
- config:
- surfaceContext: false
-
-- id: workspace-context
- disabled: true
-
-- id: tools
- config:
- mode: native
-
-# Disable every model-facing consumer in the base/Web tree. plan-mode owns the
-# always-registered exit_plan_mode tool even while the session is not planning.
-- id: tool-bash
- disabled: true
-
-- id: tool-tasks
- disabled: true
-
-- id: tool-fs
- disabled: true
-
-- id: tool-fs-search
- disabled: true
-
-- id: tool-web
- disabled: true
-
-- id: tool-skill
- disabled: true
-
-- id: plan-mode
- disabled: true
-
-- id: tool-subagent-control
- disabled: true
-
-- id: tool-subagent-list-agents
- disabled: true
-
-- id: tool-subagent
- disabled: true
-
-- id: tool-subagent-fork
- disabled: true
-
-- id: tool-workflow
- disabled: true
-
-- id: tool-todo
- disabled: true
-
-# These consumers are shared defaults on the ordinary shipped surfaces, but
-# this opt-in profile keeps exactly its two named tools.
-- id: tool-goal
- disabled: true
-
-- id: tool-ralph
- disabled: true
-
-- id: tool-str-replace-editor
- disabled: true
-
-# The matching browser controls must not offer surfaces whose tool this
-# overlay omits: the panels would render for a capability the model does not
-# have. Turning the row off no longer removes a tool — `ui-question`'s host
-# half is empty and `tool-ask-user` is composed per preset — so this is a UI
-# decision now, not a capability one.
-- id: ui-plan
- disabled: true
-
-- id: ui-question
- disabled: true
-
-- insert:
- - id: pty
- name: '@deepseek-ai/dsh-pty'
-
- # This backend consumes the existing Web sandbox and permission policy.
- # It loads only on Linux/macOS; Windows and other platforms fail at boot.
- # Its 300s send wait matches the persistent Bash command timeout instead of
- # pty-local's 30s default. An open persistent shell fences permission-mode
- # changes until it closes.
- - id: pty-local
- name: '@deepseek-ai/dsh-pty-local'
- config:
- timeoutMs: 300000
-
- - id: persistent-bash
- name: '@deepseek-ai/dsh-tool-bash-persistent'
- config:
- timeoutMs: 300000
-
- # The editor consumes the Web fs-sandbox provider and therefore retains
- # the selected session permission mode.
- - id: str-replace-editor
- name: '@deepseek-ai/dsh-tool-str-replace-editor'
- config:
- maxOutputChars: 16000
diff --git a/apps/cli/package.json b/apps/cli/package.json
index 03bd4c3db9..772ad7de32 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh",
"description": "dsh CLI: profile boot, plugin management, and the browser UI alias",
- "version": "0.0.1",
- "private": true,
+ "version": "0.0.1-rc.1",
+ "publishConfig": {
+ "access": "restricted"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
+ "directory": "apps/cli"
+ },
"type": "module",
"bin": {
"dsh": "lib/bin.js"
@@ -13,10 +20,10 @@
],
"license": "BSD-3-Clause",
"dependencies": {
- "@cordisjs/plugin-hmr": "workspace:*",
- "@cordisjs/plugin-include": "workspace:*",
- "@cordisjs/plugin-loader": "workspace:*",
- "@cordisjs/plugin-timer": "workspace:*",
+ "@deepseek-ai/cordis-plugin-hmr": "workspace:^",
+ "@deepseek-ai/cordis-plugin-include": "workspace:^",
+ "@deepseek-ai/cordis-plugin-loader": "workspace:^",
+ "@deepseek-ai/cordis-plugin-timer": "workspace:^",
"@deepseek-ai/dsh-agent-tool-mode": "workspace:^",
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@deepseek-ai/dsh-base": "workspace:^",
@@ -27,6 +34,8 @@
"@deepseek-ai/dsh-compact-tool-result-prune": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-goal-session": "workspace:^",
+ "@deepseek-ai/dsh-cmdline": "workspace:^",
+ "@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-headless": "workspace:^",
"@deepseek-ai/dsh-mcp-client": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
@@ -36,6 +45,7 @@
"@deepseek-ai/dsh-pty-local": "workspace:^",
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
"@deepseek-ai/dsh-pwsh-sandbox": "workspace:^",
+ "@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-skill-local": "workspace:^",
@@ -63,7 +73,7 @@
"@deepseek-ai/dsh-workflow-workerthread": "workspace:^",
"@deepseek-ai/dsh-workspace-context": "workspace:^",
"commander": "^15.0.0",
- "cordis": "^4.0.0-rc.7",
+ "@deepseek-ai/cordis": "workspace:^",
"js-yaml": "^4.2.0",
"node-addon-require-builtin": "^0.1.4"
},
diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml
index 4b5aed6cd2..a2cfed084a 100644
--- a/apps/cli/reference/README.i18n.yaml
+++ b/apps/cli/reference/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/cli/reference/README.md
-README.md: 0b5faf8993cd8065fffcfec5f240b0084508db91
-README.zh.md: b9c48c16dd4be186266d30a438329463c31aca70
+README.md: cef687dc392f97886ea18b162e8f668a44ce2284
+README.zh.md: f6721ec256d404e2b602fb2ed921b41c03633a82
diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md
index 0b5faf8993..cef687dc39 100644
--- a/apps/cli/reference/README.md
+++ b/apps/cli/reference/README.md
@@ -2,17 +2,32 @@
English | [中文](README.zh.md)
-This reference defines the profile, one-shot run, web-alias, plugin-management, and config-dump command modes. Argv is parsed once through [`src/args.ts`](../src/args.ts), and [`src/bin.ts`](../src/bin.ts) dynamically imports only the selected runner.
+This reference defines the profile, web-alias, plugin-management, and config-dump command modes. Argv is parsed once through [`src/args.ts`](../src/args.ts), and [`src/bin.ts`](../src/bin.ts) dynamically imports only the selected runner.
## Profile boot
-`dsh --profile ` boots the profile at `$DSH_HOME/profiles/`. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's `dsh.profile.bundles` list, the profile's own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml` (machine-local preferences shared by every profile, so it outranks the per-profile layer), each `--patch ` overlay in argv order, and launcher flag patches. Later layers win per row; a patch replaces the targeted row's complete `config` value rather than deep-merging keys, and may insert new rows. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit.
+`dsh --profile ` boots the profile at `$DSH_HOME/profiles/`. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's `dsh.profile.bundles` list, the profile's own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml` (machine-local preferences shared by every profile, so it outranks the per-profile layer), and each `--patch ` overlay in argv order. Later layers win per row; a patch replaces the targeted row's complete `config` value rather than deep-merging keys, and may insert new rows. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit.
Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent-walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch).
-The `web` and `headless` profiles auto-initialize from shipped templates on first use (`web`: base + web-app; `headless`: base + headless). On load, the exact installation-owned headless tuple (base + web-app + headless) normalizes to the shipped template; extra, missing, or reordered bundle lists are user-owned and remain untouched. Any other missing profile fails loud with a hint to run `dsh plugin --profile add `.
+The `web` and `headless` profiles auto-initialize from shipped templates on first use (`web`: base + web-app; `headless`: base + headless). Any other missing profile fails loud with a hint to run `dsh plugin --profile add `.
-Profile boot accepts no positional task. A profile that mounts the one-shot runner row (`headless-runner`) therefore fails loud with the canonical `dsh run --profile ""` command instead of reaching the row's raw required-field error.
+### App arguments
+
+The launcher's flags come first and end at the first token it does not recognize; everything from there on is handed to the booted profile verbatim through `ctx.cmdlineArgs`, where any injected app plugin may parse it ([`dsh-cmdline`](../../../packages/boot/cmdline/README.md)). `dsh --profile web --port 8080` therefore reaches the web app's `--port`, `dsh --profile web --help` prints that app's help and boots nothing, and `dsh --help` (no profile to hand it to) prints the launcher's own. `-V`/`--version` prints the launcher's version when it appears before the app-argument boundary.
+
+A composition mounts once. An ordinary plugin injects `cmdlineArgs`, parses this app's arguments, and provides what it resolved as a service; each row configured from flags injects that service, and Loader waits for it before evaluating the row's config (`port: !!js ctx.webStartup.port ?? 3080`). A flag therefore beats the value written beside it. This precedence requires the row to retain that expression; a user patch that replaces the whole `config` with literals removes the runtime read. Help and rejected arguments request exit — nonzero for a rejection, 0 for help — without activating rows that depend on the provider's service. A live `cordis.patch.yml` edit re-evaluates expressions against services that are still up, so it cannot reset a served port.
+
+Launcher flags must come before app arguments, and the launcher's parser consumes one `--`: an app argument that must arrive as a literal `--` needs `-- --`. A first app argument equal to `web` or `plugin` selects that subcommand instead. `ctx.cmdlineArgs.get()` is a shared immutable read: multiple plugins may parse the same snapshot, while a profile with no reader ignores its app arguments.
+
+The shipped apps own these command lines:
+
+| Profile | Arguments |
+|---|---|
+| `web` | `--host`, `--port`, `--dev`, repeatable `--trusted-host` |
+| `headless` | the task text, as the positional argument |
+
+A one-shot task (`dsh --profile headless "run the tests"`) creates one fresh persisted Agent through the core registry, submits the task, waits for quiescence, and flushes the Session before deriving the last non-empty assistant text and final `turn/end` reason from its durable interval. It prints the text on stdout and exits 0 for `completed`, else 1. An invocation with no task is a usage error from that app. The shipped headless profile mounts no ApiProxy, Host, HTTP server, Web runtime, or browser client; a successful run writes nothing to stderr and opens no listening port.
Inspect the composed tree without booting it:
@@ -21,13 +36,7 @@ dsh --profile web --dump-default-config
dsh --profile web --patch ./extra.yml --dump-config
```
-`--dump-default-config` prints only the bundle layers; `--dump-config` adds the profile's `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml`, and `--patch` overlays. Both print comments naming the file that supplied each row and every overlay that changed it; `!!js` expressions remain unevaluated, and unmatched patch targets are reported on stderr.
-
-## One-shot run
-
-`dsh run [--profile ] [--patch ...] ` joins the task arguments with spaces, rejects a missing or blank task, and defaults `--profile` to `headless`. Repeatable `--patch` overlays occupy the same layer position as profile-boot overlays. A custom selected profile must mount `headless-runner`; otherwise launch fails before boot with a diagnostic naming that missing row.
-
-The launcher patches the task text into the runner row. After Loader settlement, the runner reads the shared `ctx.agentDefaultModel` default, creates one fresh persisted Agent through `ctx.agents`, submits the task, waits for quiescence, and flushes the Session before deriving the last non-empty assistant text and final `turn/end` reason from its durable interval. It prints the text on stdout and exits 0 for `completed`, else 1. The shipped headless profile mounts no ApiProxy, Host, HTTP server, Web runtime, or browser client; a successful run writes nothing to stderr and opens no listening port.
+`--dump-default-config` prints only the bundle layers; `--dump-config` adds the profile's `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml`, and `--patch` overlays. Both print comments naming the file that supplied each row and every overlay that changed it; `!!js` expressions remain unevaluated, and unmatched patch targets are reported on stderr. A dump never runs app command-line providers, so it shows the composed tree before any app argument is resolved and rejects an invocation that carries app arguments.
## Plugin management
@@ -43,12 +52,13 @@ Git-hosted plugins that ship sources build during install through their `prepare
## Web alias
-`dsh web` is a hardcoded alias for `--profile web` that additionally accepts the Web flag family. `--host`, `--port`, and repeatable `--trusted-host` values become patches over the composed rows; their owning plugin schemas validate them at boot. `--dev` switches the web-runtime row to development mode and inserts the client-plugin HMR receiver; it expects a separate `pnpm run dev:web` watcher for no-refresh client bundle updates.
+`dsh web` is a hardcoded alias for `--profile web`; the flags after it belong to the web app, whose ordinary bundle provider parses them. `--host` and `--port` override the composed values of the rows that carry them, repeatable `--trusted-host` contributes invocation authorities through `ctx.webRuntime.trustedHosts` (a deployment expression concatenates its own authorities), and `--dev` switches the web-runtime row to development mode and enables the client-plugin HMR receiver the bundle ships disabled; it expects a separate `pnpm run dev:web` watcher for no-refresh client bundle updates.
```sh
dsh web
dsh web --patch ./extra.cordis.yml
dsh web --dump-config
+dsh web --help
```
The production Web runner needs built package and frontend artifacts (`pnpm run build`). It serves `http://127.0.0.1:3080` by default. Binding all interfaces also trusts the machine's discovered LAN IP literals; `--trusted-host` adds named authorities accepted by the `/api` browser-trust fence.
@@ -59,24 +69,16 @@ All modes treat the invoking directory as the default workspace root, load appli
New sessions default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads, network access, and process visibility are not confined. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one.
-`DSH_TOOLS_MODE` selects `native`, `code`, or `both` for the process; another value fails at boot. [`config/core-web.cordis.yml`](../config/core-web.cordis.yml) is an optional RL-compatible `--patch` overlay that pins native mode, renders only `DSH_SYSTEM_PROMPT` or `You are a helpful software engineer assistant.` as the system prompt, disables Workspace instructions and every Web runtime prompt contribution, and exposes only persistent `bash` and `str_replace_editor` while retaining the shipped host, browser, workspace, persistence, and permission composition.
-
-`DSH_SYSTEM_PROMPT` is passed as the system-prompt [`persona`](../../../packages/core/system-prompt/README.md#config): complete `{{…}}` groups use that contract's strict variable interpolation rules and have no literal-brace escape; any set value, including an empty string, is authoritative and an empty value therefore removes the system prompt, while only an unset variable selects the fallback.
+`DSH_TOOLS_MODE` selects `native`, `code`, or `both` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and composes only persistent `bash` plus `str_replace_editor`. Select 极简模式 when creating a Web session; every other prompt section and model-facing plugin remains absent from that agent while the shared browser, workspace, persistence, sandbox, and permission host stays in place.
## Shared deployment behavior
-The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search`, repository Plugin support, and session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`; `web_fetch` is disabled unless a patch layer inserts a provider and enables it.
+The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search`, and session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`; `web_fetch` is disabled unless a patch layer inserts a provider and enables it.
Session events stream as OTLP/HTTP logs by default. `DSH_TELEMETRY_OTLP_URL` selects another collector. Any non-empty `DSH_TELEMETRY_DISABLED` disables the telemetry row before boot. The shipped base has no telemetry redaction rule, so exported records can contain message text, tool arguments and results, and workspace paths; the [telemetry Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md) owns that deployment decision.
-The empty `repository-plugins` row lets profile patch layers mount prepared immutable repository Plugin generations. See the [repository Plugin contract](../../../packages/self-modification/repository-plugin/README.md#standalone-app-configuration). The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox.
+Install external plugin bundles through `dsh plugin --profile add `. The installed package owns its dependencies and contributes its declared `cordis.patch.yml` layer. The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox.
-## Source launcher
+## Source execution
-Link the source-running launcher onto PATH:
-
-```sh
-ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh
-```
-
-It resolves the checkout through its real path and launches `apps/cli/src/bin.ts` with `node --import tsx/esm`. `TSX_TSCONFIG_PATH` is pinned to the checkout root, so workspace package resolution is independent of the invoking directory. `pnpm run dsh` uses the same entry and forwards arguments. The built form is `apps/cli/lib/bin.js` after `pnpm run build`.
+From the repository root, use `pnpm dsh `. The `package.json` script runs the complete repository build, launches `apps/cli/src/bin.ts` with `node --import tsx/esm`, and forwards every argument. Build output appears before CLI output. The process inherits the launch environment; set `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. The installed form launches the built `apps/cli/lib/bin.js` without rebuilding the repository.
diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md
index b9c48c16dd..f6721ec256 100644
--- a/apps/cli/reference/README.zh.md
+++ b/apps/cli/reference/README.zh.md
@@ -2,17 +2,32 @@
[English](README.md) | 中文
-本参考定义 profile、一次性运行、web 别名、插件管理和配置 dump 命令模式。参数由 [`src/args.ts`](../src/args.ts) 统一解析,[`src/bin.ts`](../src/bin.ts) 只动态导入选中的运行器。
+本参考定义 profile、web 别名、插件管理和配置 dump 命令模式。参数由 [`src/args.ts`](../src/args.ts) 统一解析,[`src/bin.ts`](../src/bin.ts) 只动态导入选中的运行器。
## Profile 启动
-`dsh --profile ` 启动位于 `$DSH_HOME/profiles/` 的 profile。生效配置树在空根节点之上按以下顺序逐层组合:profile manifest(元数据清单)的 `dsh.profile.bundles` 列表所列的各个组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(各 profile 共享的机器本地偏好,因此优先级高于逐 profile 的层)、按 argv 顺序的各个 `--patch ` overlay,以及启动器 flag patch。后应用的层按行胜出;patch 替换目标行完整的 `config` 值,而不是深度合并各键,并且可以插入新行。配置解析、schema 校验、模块解析或插件启动失败会得到报告并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。
+`dsh --profile ` 启动位于 `$DSH_HOME/profiles/` 的 profile。生效配置树在空根节点之上按以下顺序逐层组合:profile manifest(元数据清单)的 `dsh.profile.bundles` 列表所列的各个组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(各 profile 共享的机器本地偏好,因此优先级高于逐 profile 的层)、以及按 argv 顺序的各个 `--patch ` overlay。后应用的层按行胜出;patch 替换目标行完整的 `config` 值,而不是深度合并各键,并且可以插入新行。配置解析、schema 校验、模块解析或插件启动失败会得到报告并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。
组合包名称先从 dsh 安装解析,再从 profile 目录解析。因此内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`)总是来自与正在运行的 `dsh` 相同的安装;树外组合包来自 profile 由 pnpm 管理的 `node_modules`。任何 patch 行中的裸插件 `name` 通过 profile 目录的 Node 父目录逐级查找解析,该查找可达到持续维护的安装后备目录 `$DSH_HOME/profiles/node_modules`(安装的应用和组合包所依赖的每个包对应一个符号链接,每次启动时修复)。
-`web` 和 `headless` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app;`headless`:base + headless)。加载时,与安装所管理的 headless 元组(base + web-app + headless)完全一致的列表会规范化为随附模板;包含额外项、缺少项或调整过顺序的组合包列表由用户拥有,保持不变。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile add `。
+`web` 和 `headless` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app;`headless`:base + headless)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile add `。
-Profile 启动不接受位置参数任务。因此,挂载了一次性运行器行(`headless-runner`)的 profile 会显式报错,并提示规范命令 `dsh run --profile ""`,而不会触发该行原始的必填字段错误。
+### 应用参数
+
+启动器自己的 flag 写在最前面,并在它不认识的第一个 token 处结束;从那里开始的一切都通过 `ctx.cmdlineArgs` 原样交给启动起来的 profile,任何注入它的应用插件都可以解析([`dsh-cmdline`](../../../packages/boot/cmdline/README.md))。因此 `dsh --profile web --port 8080` 到达的是 web 应用的 `--port`,`dsh --profile web --help` 打印的是该应用的 help 且什么也不启动,而 `dsh --help`(没有可以交付的 profile)打印的是启动器自己的 help。`-V`/`--version` 写在应用参数边界之前时会打印启动器的版本。
+
+一套组合只挂载一次。普通插件注入 `cmdlineArgs`、解析本应用参数,并把结果作为服务提供出去;由 flag 配置的每一行都会注入该服务,Loader 会等服务激活后再求值该行配置(`port: !!js ctx.webStartup.port ?? 3080`),因此 flag 胜过写在它旁边的值。该优先级要求配置行保留这一表达式;若用户 patch 用字面量替换整份 `config`,运行时读取也会随之消失。help 和被拒绝的参数会请求退出——拒绝时以非零状态,help 时以 0——且不会激活依赖提供方服务的行。在线编辑 `cordis.patch.yml` 会针对仍然在线的服务重新求值表达式,因此不会重置已在服务的端口。
+
+启动器的 flag 必须写在应用参数之前,且启动器的解析器会消耗掉一个 `--`:必须以字面量 `--` 送达应用的参数需要写成 `-- --`。如果应用的第一个参数恰好等于 `web` 或 `plugin`,会选择对应的子命令。`ctx.cmdlineArgs.get()` 是共享的不可变读取:多个插件可以解析同一份快照,没有读取方的 profile 则会忽略自己的应用参数。
+
+随附的各应用持有这些命令行:
+
+| Profile | 参数 |
+|---|---|
+| `web` | `--host`、`--port`、`--dev`、可重复的 `--trusted-host` |
+| `headless` | 任务文本,作为位置参数 |
+
+一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent(智能体),提交任务、等待完全停稳并对 Session 执行 flush,再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 stdout 打印文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载 ApiProxy、Host、HTTP 服务器、Web 运行时或浏览器客户端;成功运行不会向 stderr 写入任何内容,也不会打开监听端口。
可在不启动的情况下检查组合出的配置树:
@@ -21,13 +36,7 @@ dsh --profile web --dump-default-config
dsh --profile web --patch ./extra.yml --dump-config
```
-`--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml` 和 `--patch` overlay。两者都会打印注释,标明每行由哪个文件提供,以及哪些 overlay 修改过它;`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。
-
-## 一次性运行
-
-`dsh run [--profile ] [--patch ...] ` 会用空格拼接任务参数,拒绝缺失或空白任务,并让 `--profile` 默认为 `headless`。可重复使用的 `--patch` overlay 与 profile 启动的 overlay 位于同一层。所选的自定义 profile 必须挂载 `headless-runner`;否则启动器会在启动前失败,并在诊断中指明缺少该行。
-
-启动器把任务文本 patch 进运行器行。Loader 结算后,运行器读取共享的 `ctx.agentDefaultModel` 默认值,通过 `ctx.agents` 创建一个全新的持久化 Agent(智能体),提交任务、等待完全停稳并对 Session 执行 flush,再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 stdout 打印文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出。随附 headless profile 不挂载 ApiProxy、Host、HTTP 服务器、Web 运行时或浏览器客户端;成功运行不会向 stderr 写入任何内容,也不会打开监听端口。
+`--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml` 和 `--patch` overlay。两者都会打印注释,标明每行由哪个文件提供,以及哪些 overlay 修改过它;`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。dump 从不运行应用命令行提供方,因此它展示的是任何应用参数被解析之前的组合配置树,并拒绝携带应用参数的调用。
## 插件管理
@@ -43,12 +52,13 @@ Git 托管、随附源码的插件在安装期间通过其 `prepare` 脚本构
## Web 别名
-`dsh web` 是 `--profile web` 的硬编码别名,并额外接受 Web flag 系列。`--host`、`--port` 和可重复的 `--trusted-host` 值会成为作用在组合行之上的 patch;负责这些值的插件 schema 会在启动时验证它们。`--dev` 把 web-runtime 行切换到开发模式并插入客户端插件 HMR(热模块替换)接收器;若要无刷新更新客户端 bundle,还需单独运行 `pnpm run dev:web` watcher。
+`dsh web` 是 `--profile web` 的硬编码别名;写在它之后的 flag 属于 web 应用,由组合包中的普通提供方解析。`--host` 和 `--port` 覆盖承载它们的那些行的组合取值,可重复的 `--trusted-host` 通过 `ctx.webRuntime.trustedHosts` 提供本次调用的 authority(部署表达式会拼接自己的 authority),`--dev` 把 web-runtime 行切换到开发模式并启用组合包以禁用状态交付的客户端插件 HMR(热模块替换)接收器;若要无刷新更新客户端 bundle,还需单独运行 `pnpm run dev:web` watcher。
```sh
dsh web
dsh web --patch ./extra.cordis.yml
dsh web --dump-config
+dsh web --help
```
生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`。绑定所有接口时,还会信任机器自动发现的 LAN IP 字面量;`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。
@@ -59,24 +69,16 @@ dsh web --dump-config
新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取、网络访问和进程可见性不受限制。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。
-`DSH_TOOLS_MODE` 为进程选择 `native`、`code` 或 `both`;其他值会导致启动失败。[`config/core-web.cordis.yml`](../config/core-web.cordis.yml) 是可选的 RL 兼容 `--patch` overlay:它固定使用 `native` 模式,仅将 `DSH_SYSTEM_PROMPT` 或 `You are a helpful software engineer assistant.` 渲染为系统提示词,禁用 Workspace 指令与所有 Web 运行时提示词贡献,并且在保留随附宿主、浏览器、workspace、持久化和权限组合的同时,仅暴露持久 `bash` 和 `str_replace_editor`。
-
-`DSH_SYSTEM_PROMPT` 会传给系统提示词的 [`persona`](../../../packages/core/system-prompt/README.md#config):完整的 `{{…}}` 分组遵循该约定的严格变量插值规则,且无法转义为字面花括号;任何已设置的值(包括空字符串)都具有权威性,因此空值会移除系统提示词,只有未设置该变量时才会选择后备值。
+`DSH_TOOLS_MODE` 为进程选择 `native`、`code` 或 `both`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并且仅组合持久 `bash` 和 `str_replace_editor`。创建 Web 会话时请选择极简模式;该 agent 不包含任何其他提示词段落或面向模型的插件,而共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。
## 共享部署行为
-基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search`、repository Plugin 支持和会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`;只有 patch 层插入提供方并启用 `web_fetch` 后,该工具才可用。
+基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`;只有 patch 层插入提供方并启用 `web_fetch` 后,该工具才可用。
会话事件默认作为 OTLP/HTTP 日志流式发送。`DSH_TELEMETRY_OTLP_URL` 选择其他 collector。任何非空 `DSH_TELEMETRY_DISABLED` 都会在启动前禁用遥测配置行。随附基础配置没有遥测脱敏规则,因此导出的记录可能包含消息文本、工具参数与结果以及 workspace 路径;该部署决策由[遥测 Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md)负责。
-空 `repository-plugins` 行让 profile 的 patch 层能够挂载已准备的不可变 repository Plugin generation。参见 [repository Plugin 约定](../../../packages/self-modification/repository-plugin/README.md#standalone-app-configuration)。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent(智能体)沙箱之外的受信任可执行代码。
+通过 `dsh plugin --profile add ` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml` 层。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent(智能体)沙箱之外的受信任可执行代码。
-## 源码启动器
+## 源码执行
-把源码运行启动器链接到 PATH:
-
-```sh
-ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh
-```
-
-它通过 real path 解析 checkout,并使用 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`。`TSX_TSCONFIG_PATH` 固定到 checkout 根目录,因此 workspace 包解析不依赖调用目录。`pnpm run dsh` 使用同一入口并转发参数。运行 `pnpm run build` 后,构建形式为 `apps/cli/lib/bin.js`。
+请从仓库根目录使用 `pnpm dsh `。`package.json` 中的脚本会完成整个仓库的构建,通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。构建输出会显示在 CLI 输出之前。该进程会继承启动环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY` 和 `HTTPS_PROXY` 时,请设置 `NODE_USE_ENV_PROXY=1`。安装形式会直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
diff --git a/apps/cli/src/args.ts b/apps/cli/src/args.ts
index 97f5222398..27d92dcf66 100644
--- a/apps/cli/src/args.ts
+++ b/apps/cli/src/args.ts
@@ -1,31 +1,30 @@
/**
- * Commander adapter for the `dsh` command-line entry. The default command
- * boots a named profile (`--profile `), optionally with extra `--patch`
- * overlays. `run` owns one-shot task execution, defaulting to the headless
- * profile; `web` is a hardcoded alias for `--profile web` that adds the Web
- * flag family; `plugin` manages a profile's plugin dependencies by forwarding
- * to pnpm. Commander owns help, version, and parse errors.
+ * Commander adapter for the `dsh` command line.
+ *
+ * The launcher parses only what it owns — which profile to boot, which extra
+ * patch overlays to apply, and the config dumps — and hands **everything after
+ * its own flags** to the booted tree verbatim, where injected app plugins parse
+ * their own flag families and print their own `--help` (see
+ * `@deepseek-ai/dsh-cmdline`). Launcher flags therefore come first: the first
+ * token this parser does not recognize starts the inner arguments, so
+ * `dsh --profile tui --resume abc` boots the tui profile with `--resume abc`,
+ * and `dsh --profile web -h` prints the web app's help, not this one's.
+ *
+ * `web` is a hardcoded alias for `--profile web`; `plugin` manages a profile's
+ * plugin dependencies by forwarding to pnpm.
* @module @deepseek-ai/dsh/args
*/
import { Command, CommanderError } from 'commander'
-/** Boot a named profile. */
+/** Boot a named profile and hand it the invocation's inner arguments. */
interface ProfileInvocation {
mode: 'profile'
profile: string
/** Extra patch-list overlays applied after the profile's own layer, in argv order. */
patches: string[]
-}
-
-/** Run one task through a profile mounting the headless runner. */
-interface RunInvocation {
- mode: 'run'
- profile: string
- /** Extra patch-list overlays applied after the profile's own layer, in argv order. */
- patches: string[]
- /** Non-blank task text joined from the variadic positional arguments. */
- task: string
+ /** Everything after the launcher's own flags, verbatim, for injected app plugins. */
+ args: string[]
}
/** Print a composed profile tree and exit without booting. */
@@ -37,21 +36,6 @@ interface DumpConfigInvocation {
patches: string[]
}
-/**
- * Browser UI: `dsh web` (alias of `--profile web`). Host and port remain
- * unvalidated pass-throughs to the webserver schema; absent values leave the
- * shipped web bundle values intact.
- */
-interface WebInvocation {
- mode: 'web'
- patches: string[]
- host?: string
- port?: number
- dev: boolean
- /** Extra authorities for the /api browser-trust fence. */
- trustedHosts?: string[]
-}
-
/** Manage a profile's plugins: forward `args` to pnpm inside the profile directory. */
interface PluginInvocation {
mode: 'plugin'
@@ -61,31 +45,63 @@ interface PluginInvocation {
}
/** The resolved `dsh` invocation. Help, version, and errors exit inside {@link parseDshArgs}. */
-export type DshInvocation = ProfileInvocation | RunInvocation | DumpConfigInvocation | WebInvocation | PluginInvocation
+export type DshInvocation = ProfileInvocation | DumpConfigInvocation | PluginInvocation
-/** Raw web-subcommand options straight from Commander. */
-interface WebOptions {
+/** Launcher flags shared by the default command and the `web` alias. */
+interface BootOptions {
patch?: string[]
- host?: string
- port?: string
- dev?: boolean
- trustedHost?: string[]
dumpConfig?: boolean
dumpDefaultConfig?: boolean
}
-/** Raw run-subcommand options straight from Commander. */
-interface RunOptions {
- profile: string
- patch?: string[]
-}
-
/**
* Repeatable single-value collector: `--patch a.yml --patch b.yml`. Never
- * variadic — a variadic `--patch` would swallow a following positional task.
+ * variadic — a variadic `--patch` would swallow the inner arguments.
*/
const collect = (value: string, previous: string[] = []): string[] => [...previous, value]
+/** The launcher's own help text; each app prints its own. */
+const HELP_EXAMPLES = `
+Examples:
+ dsh --profile web boot the web profile (same as: dsh web)
+ dsh --profile headless "run the tests" answer one task, print the result, and exit
+ dsh --profile tui --patch ./extra.yml boot a custom profile with one extra overlay
+ dsh --profile tui --resume arguments after the launcher flags reach the app
+ dsh --profile web --help the web app's own flags and help
+ dsh plugin --profile tui add install a plugin into the tui profile
+`
+
+/**
+ * Resolve a boot or dump invocation from the launcher flags and the leftover
+ * inner arguments.
+ * @param program - the command whose options were parsed (the root, or the `web` alias).
+ * @param profile - the profile these flags boot.
+ * @param options - the launcher flags commander collected.
+ * @param args - the leftover arguments, in argv order.
+ * @returns the resolved invocation.
+ */
+function resolveBoot(program: Command, profile: string, options: BootOptions, args: string[]): DshInvocation {
+ const patches = options.patch ?? []
+ if (patches.includes('')) program.error('error: --patch needs a path')
+ if (options.dumpConfig !== true && options.dumpDefaultConfig !== true) {
+ return { mode: 'profile', profile, patches, args }
+ }
+ if (options.dumpConfig === true && options.dumpDefaultConfig === true) {
+ program.error('error: --dump-config and --dump-default-config are mutually exclusive')
+ }
+ // The dump is boot-free: it never runs app command-line providers, so it
+ // cannot show what those flags would decide, and printing a tree that differs
+ // from the same invocation's boot would mislead.
+ if (args.length > 0) {
+ program.error(`error: config dumps take no app arguments, got ${args.map(argument => JSON.stringify(argument)).join(' ')}`)
+ }
+ const defaultOnly = options.dumpDefaultConfig === true
+ if (defaultOnly && patches.length > 0) {
+ program.error('error: --dump-default-config prints the bundle layers and takes no --patch')
+ }
+ return { mode: 'dump-config', profile, defaultOnly, patches }
+}
+
/**
* Resolve argv into one invocation, or print and exit for help, version, or an
* error.
@@ -95,121 +111,61 @@ const collect = (value: string, previous: string[] = []): string[] => [...previo
*/
export function parseDshArgs(argv: readonly string[], version: string): DshInvocation {
let resolved: DshInvocation | undefined
- const program = new Command()
+ // Annotated, not inferred: the actions below call back into `program`, and an
+ // inferred type would be circular through its own chain.
+ const program: Command = new Command()
+ program
.name('dsh')
.version(version, '-V, --version', 'output the version number')
.description('dsh: boot a DeepSeek Harness profile — an ordered stack of plugin-bundle patch layers under your own overrides.')
- .addHelpText('after', `
-Examples:
- dsh --profile web boot the web profile (same as: dsh web)
- dsh run "run the tests" answer one task, print the result, and exit
- dsh run --profile custom "run the tests" run one task through a custom one-shot profile
- dsh --profile tui --patch ./extra.yml boot a custom profile with one extra overlay
- dsh plugin --profile tui add install a plugin into the tui profile
- dsh web --port 8080 the web alias with its flag family
-`)
+ .addHelpText('after', HELP_EXAMPLES)
.exitOverride()
+ // The launcher's flags come first and end at the first token it does not
+ // know; everything from there on belongs to the booted app, including
+ // its -h. `dsh -h` with no profile still prints this help, below.
+ .helpOption(false)
+ .allowUnknownOption()
+ .passThroughOptions()
.enablePositionalOptions()
+ .argument('[args...]', 'arguments for the booted profile\'s app (see: dsh --profile --help)')
.option('--profile ', 'the profile under $DSH_HOME/profiles to boot')
.option('--patch ', 'extra patch-list overlay applied after the profile layer (repeatable)', collect)
.option('--dump-config', 'print the composed profile tree and exit')
.option('--dump-default-config', 'print the profile tree without its user layer or --patch overlays and exit')
- .action((options: {
- profile?: string
- patch?: string[]
- dumpConfig?: boolean
- dumpDefaultConfig?: boolean
- }) => {
- const profile = options.profile ?? program.error('error: --profile is required')
- if (profile === '') program.error('error: --profile needs a name')
- const patches = options.patch ?? []
- if (patches.includes('')) program.error('error: --patch needs a path')
- if (options.dumpConfig === true || options.dumpDefaultConfig === true) {
- if (options.dumpConfig === true && options.dumpDefaultConfig === true) {
- program.error('error: --dump-config and --dump-default-config are mutually exclusive')
- }
- const defaultOnly = options.dumpDefaultConfig === true
- if (defaultOnly && patches.length > 0) {
- program.error('error: --dump-default-config prints the bundle layers and takes no --patch')
- }
- resolved = { mode: 'dump-config', profile, defaultOnly, patches }
- return
+ .action((args: string[], options: BootOptions & { profile?: string }) => {
+ // With the app owning -h, the launcher's own help is what a bare
+ // `dsh -h` (no profile to hand it to) must print.
+ if (options.profile === undefined) {
+ if (args.some(argument => argument === '-h' || argument === '--help')) program.help()
+ program.error('error: --profile is required')
}
- resolved = { mode: 'profile', profile, patches }
+ const profile = options.profile
+ if (profile === '') program.error('error: --profile needs a name')
+ resolved = resolveBoot(program, profile, options, args)
})
/** Reject parent options supplied before a subcommand. */
const rejectParentOptions = (command: string): void => {
- const parent = program.opts<{
- profile?: string
- patch?: string[]
- dumpConfig?: boolean
- dumpDefaultConfig?: boolean
- }>()
+ const parent = program.opts()
if (parent.profile !== undefined || parent.patch !== undefined
|| parent.dumpConfig !== undefined || parent.dumpDefaultConfig !== undefined) {
program.error(`error: ${command} takes none of parent --profile, --patch, --dump-config, or --dump-default-config`)
}
}
- const run = program.command('run').description('run one task through a profile mounting the headless runner')
- run
- .option('--profile ', 'one-shot profile under $DSH_HOME/profiles', 'headless')
- .option('--patch ', 'extra patch-list overlay applied after the profile layer (repeatable)', collect)
- .argument('', 'task text')
- .action((task: string[], options: RunOptions) => {
- rejectParentOptions('run')
- const profile = options.profile
- if (profile === '') program.error('error: --profile needs a name')
- const patches = options.patch ?? []
- if (patches.includes('')) program.error('error: --patch needs a path')
- const joined = task.join(' ')
- if (joined.trim() === '') program.error('error: run needs a non-blank task')
- resolved = { mode: 'run', profile, patches, task: joined }
- })
-
- const web = program.command('web').description('serve the browser UI (alias of --profile web) on the configured host and port')
+ const web = program.command('web').description('boot the web profile (alias of --profile web); the web app\'s own flags follow')
web
+ .helpOption(false)
+ .allowUnknownOption()
+ .passThroughOptions()
+ .enablePositionalOptions()
+ .argument('[args...]', 'arguments for the web app (see: dsh web --help)')
.option('--patch ', 'extra patch-list overlay applied after the profile layer (repeatable)', collect)
- .option('--host ', 'bind host; pass 0.0.0.0 to reach it from another machine')
- .option('--port ', 'listen port; pass 0 to let the OS pick a free one')
- .option('--dev', 'mount the client-plugin HMR receiver (run pnpm run dev:web separately to rebuild bundles)')
- .option('--trusted-host ', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)')
.option('--dump-config', 'print the composed web-profile tree (with the user layer and any --patch) and exit')
.option('--dump-default-config', 'print the web profile\'s bundle layers (no user layer) and exit')
- .action((options: WebOptions) => {
+ .action((args: string[], options: BootOptions) => {
rejectParentOptions('web')
- const patches = options.patch ?? []
- if (patches.includes('')) program.error('error: --patch needs a path')
- if (options.dumpConfig === true || options.dumpDefaultConfig === true) {
- if (options.dumpConfig === true && options.dumpDefaultConfig === true) {
- program.error('error: --dump-config and --dump-default-config are mutually exclusive')
- }
- const defaultOnly = options.dumpDefaultConfig === true
- if (defaultOnly && patches.length > 0) {
- program.error('error: --dump-default-config prints the bundle layers and takes no --patch')
- }
- // The dump is boot-free and does not derive flag patches; silently
- // dropping them would print a tree that differs from the same
- // invocation's boot.
- if (options.host !== undefined || options.port !== undefined || options.dev === true
- || options.trustedHost !== undefined) {
- program.error('error: config dumps take no web flags (--host/--port/--dev/--trusted-host)')
- }
- resolved = { mode: 'dump-config', profile: 'web', defaultOnly, patches }
- return
- }
- if (options.port !== undefined && !/^\d+$/.test(options.port)) {
- program.error(`error: --port must be a number, got ${JSON.stringify(options.port)}`)
- }
- resolved = {
- mode: 'web',
- patches,
- ...options.host !== undefined && { host: options.host },
- ...options.port !== undefined && { port: Number(options.port) },
- dev: options.dev === true,
- ...options.trustedHost !== undefined && { trustedHosts: options.trustedHost },
- }
+ resolved = resolveBoot(web, 'web', options, args)
})
const plugin = program.command('plugin').description('manage a profile\'s plugins by forwarding the remaining arguments to pnpm in the profile directory')
diff --git a/apps/cli/src/bin.ts b/apps/cli/src/bin.ts
index b332a64615..9aa44f8b22 100644
--- a/apps/cli/src/bin.ts
+++ b/apps/cli/src/bin.ts
@@ -33,24 +33,10 @@ switch (invocation.mode) {
environment: loadLayeredEnv('dsh'),
profile: invocation.profile,
patchFiles: invocation.patches,
+ args: invocation.args,
})
break
}
- case 'run': {
- const { runProfile } = await import('./profile-boot.ts')
- await runProfile({
- environment: loadLayeredEnv('dsh'),
- profile: invocation.profile,
- patchFiles: invocation.patches,
- task: invocation.task,
- })
- break
- }
- case 'web': {
- const { runWeb } = await import('./web.ts')
- await runWeb(invocation, loadLayeredEnv('dsh'))
- break
- }
case 'plugin': {
const { runPlugin } = await import('./plugin.ts')
process.exit(runPlugin(invocation.profile, invocation.args))
diff --git a/apps/cli/src/profile-boot.ts b/apps/cli/src/profile-boot.ts
index e4a719379e..f3c356f199 100644
--- a/apps/cli/src/profile-boot.ts
+++ b/apps/cli/src/profile-boot.ts
@@ -1,18 +1,22 @@
/**
* Shared profile boot for every `dsh` surface: resolve the profile, stack its
- * patch layers (bundle layers in `dsh.profile.bundles` order, the profile's own
- * `cordis.patch.yml`, `--patch` overlays, flag-derived patches, the telemetry
- * switch), mount the tree over the profile's empty root config, keep the
- * profile patch layer live, and wire fail-loud plus bounded shutdown.
+ * patch layers (bundle layers in `dsh.profile.bundles` order, the profile's
+ * own `cordis.patch.yml`, `--patch` overlays, the telemetry switch), mount the
+ * tree over the profile's empty root config, keep the profile patch layer
+ * live, and wire fail-loud plus bounded shutdown.
+ *
+ * App flags are not the launcher's business: the invocation's inner arguments
+ * are provided to the tree through `ctx.cmdlineArgs`, where any injected app
+ * plugin may read the same immutable snapshot.
* @module @deepseek-ai/dsh/profile-boot
*/
import { writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
-import { FiberState, type Context } from 'cordis'
-import type { PatchOptions } from '@cordisjs/plugin-include'
-import { dshHomePath } from '@deepseek-ai/dsh-paths'
+import { FiberState, type Context } from '@deepseek-ai/cordis'
+import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
+import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
import {
boot,
composeEntries,
@@ -25,7 +29,7 @@ import {
watchUserPatches,
type Profile,
} from '@deepseek-ai/dsh-app-boot'
-import { resolveDshHome } from '@deepseek-ai/dsh-paths'
+import { dshHomePath, resolveDshHome } from '@deepseek-ai/dsh-paths'
/** Shipped agent-preset root: beside this app's own config, in both source and built layouts. */
const SHIPPED_PRESET_ROOT = fileURLToPath(new URL('../config/agent-presets/', import.meta.url))
@@ -33,6 +37,7 @@ const SHIPPED_PRESET_ROOT = fileURLToPath(new URL('../config/agent-presets/', im
/** Harness-home directory holding locally authored agent presets. */
const USER_PRESET_DIR = '.agent-presets'
import { DSH_ENVIRONMENT_KEY, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
+import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
import type { HeadlessIo } from '@deepseek-ai/dsh-headless'
import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts'
import { resolveWindowsShellLayer } from './windows-shell.ts'
@@ -55,7 +60,7 @@ export const INSTALL_ANCHOR = fileURLToPath(new URL('../package.json', import.me
/** The session-telemetry row id the DSH_TELEMETRY_DISABLED switch targets. */
const TELEMETRY_ROW_ID = 'telemetry-otel'
-/** The one-shot runner row a `dsh run` task requires and configures. */
+/** The one-shot runner row: its presence means this composition exits by itself. */
const HEADLESS_ROW_ID = 'headless-runner'
/** The empty root entry list every profile tree patches over. */
@@ -104,9 +109,6 @@ export function prepareProfile(name: string, userLayer = true): Profile {
return profile
}
-/** Read-only row index of a profile composition before launcher flag patches. */
-export type ProfileRows = ReadonlyMap
-
/** One profile's patch layers (application order) and the row index of its pre-flag composition. */
interface ComposedProfile {
profile: Profile
@@ -116,14 +118,13 @@ interface ComposedProfile {
windowsShellPatches: PatchOptions[]
/** The home-level user layer (`$DSH_HOME/cordis.patch.yml`), applied after the profile's own. */
homePatches: PatchOptions[]
- /** Layers above the user layers on a live reload: --patch overlays, flag patches, the telemetry switch. */
- overlayAndFlags: PatchOptions[]
+ /** Layers above the user layers on a live reload: `--patch` overlays and the telemetry switch. */
+ overlays: PatchOptions[]
/**
- * id → row of the pre-flag composition (bundles + user layers + overlays),
- * for flag merges and row checks. Flag patches must not insert rows the
- * launcher consults here (they only override values and insert dev glue).
+ * id → row of the composed tree (bundles + user layers + overlays), for the
+ * launcher's own row checks.
*/
- rows: ProfileRows
+ rows: ReadonlyMap
}
/** The full patch stack of one composed profile, in application order. */
@@ -133,7 +134,7 @@ function allPatches(composed: ComposedProfile): PatchOptions[] {
...composed.windowsShellPatches,
...composed.profile.patches,
...composed.homePatches,
- ...composed.overlayAndFlags,
+ ...composed.overlays,
]
}
@@ -143,36 +144,28 @@ function allPatches(composed: ComposedProfile): PatchOptions[] {
* is Windows), the profile's user layer, the home-level user layer
* (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply to
* every profile, so it outranks the per-profile layer), `--patch` overlays,
- * then flag patches derived from the composed rows, then the telemetry
- * switch.
+ * then the telemetry switch.
* @param name - the profile name.
* @param patchFiles - `--patch` overlay paths, in argv order.
- * @param deriveFlagPatches - launcher hook turning composed rows into flag patches.
* @returns the profile, its patch layers, and the composed row index.
*/
function composeProfile(
name: string,
patchFiles: readonly string[],
- deriveFlagPatches: (rows: ComposedProfile['rows']) => PatchOptions[] = () => [],
): ComposedProfile {
const profile = prepareProfile(name)
const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
const bundlePatches = profile.layers.flatMap(layer => layer.patches)
const windowsShellPatches = resolveWindowsShellLayer(process.platform, profile.layers, NAME)?.patches ?? []
- const rows = new Map()
+ const rows = new Map()
for (const row of composeEntries([bundlePatches, windowsShellPatches, profile.patches, homePatches, overlays])) {
if (typeof row.id === 'string') rows.set(row.id, row)
}
- const overlayAndFlags = [...overlays, ...deriveFlagPatches(rows)]
- // The agent-preset roots are an assembly fact of every dsh launcher, not a
- // patch author's choice: the shipped set sits beside this app's config and
- // the user's own under the Harness home. Resolved per boot ($DSH_HOME may
- // differ per run) and only patched when the composed tree actually mounts
- // the roster — a one-shot `dsh run` composes agents from the same roster
- // `dsh web` offers.
+ const composedOverlays = [...overlays]
+ // Preset roots belong to every dsh composition that mounts the roster.
if (rows.has('agent-presets')) {
- overlayAndFlags.push({
+ composedOverlays.push({
id: 'agent-presets',
config: {
...(rows.get('agent-presets')?.config ?? {}) as Record,
@@ -184,24 +177,20 @@ function composeProfile(
})
}
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
- if (telemetryPatch !== undefined) overlayAndFlags.push(telemetryPatch)
- return { profile, bundlePatches, windowsShellPatches, homePatches, overlayAndFlags, rows }
+ if (telemetryPatch !== undefined) composedOverlays.push(telemetryPatch)
+ return { profile, bundlePatches, windowsShellPatches, homePatches, overlays: composedOverlays, rows }
}
/** Options for {@link runProfile}. */
export interface RunProfileOptions {
+ /** This run's frozen environment snapshot, provided before any entry mounts. */
+ environment: EnvironmentSnapshot
/** The profile name to boot. */
profile: string
/** `--patch` overlay paths, in argv order. */
patchFiles: readonly string[]
- /** Launcher hook turning the pre-flag composed rows into flag patches (the web alias's flag family). */
- deriveFlagPatches?: (rows: ProfileRows) => PatchOptions[]
- /** `dsh run` task text; requires the composition to mount the headless runner row. */
- task?: string
- /** Surface setup registered after Loader installation and before any config-tree entry mounts. */
- prepare?: (ctx: Context, rows: ProfileRows) => Promise | void
- /** This run's frozen environment snapshot, provided to the tree before any entry mounts. */
- environment: EnvironmentSnapshot
+ /** The invocation's inner arguments, handed to the tree through `ctx.cmdlineArgs`. */
+ args: readonly string[]
}
/** Re-throw setup failures unless this invocation's signal already owns shutdown. */
@@ -211,29 +200,16 @@ function suppressSignalShutdownError(signal: AbortSignal, error: unknown): void
/**
* Boot one profile invocation end to end and leave process lifetime to the
- * mounted plugins (or to the one-shot runner when `task` is present).
- * @param options - profile name, overlays, flag patches, and the optional task.
+ * mounted plugins (or to a one-shot runner the composition mounts).
+ * @param options - environment snapshot, profile name, overlays, and the booted app's own arguments.
* @returns the settled root context and the shutdown controller.
*/
export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Context; shutdown: ProcessShutdown }> {
- const composed = composeProfile(options.profile, options.patchFiles, options.deriveFlagPatches)
- if (options.task !== undefined) {
- if (!composed.rows.has(HEADLESS_ROW_ID)) {
- throw new Error(
- `dsh: profile ${JSON.stringify(options.profile)} takes no task — its composition mounts no "${HEADLESS_ROW_ID}" row `
- + '(the headless profile does)',
- )
- }
- composed.overlayAndFlags.push({ id: HEADLESS_ROW_ID, config: { task: options.task } })
- } else if (composed.rows.has(HEADLESS_ROW_ID)) {
- // The inverse misuse: a one-shot composition booted without its task
- // would otherwise die in the runner row's schema with a raw "required"
- // error naming no fix.
- throw new Error(
- `dsh: profile ${JSON.stringify(options.profile)} mounts the one-shot runner and needs a task: `
- + `dsh run --profile ${options.profile} ""`,
- )
- }
+ const composed = composeProfile(options.profile, options.patchFiles)
+ // A one-shot composition ends by itself, which changes what a signal means
+ // and makes watching the user's patch layer pointless.
+ const headlessRow = composed.rows.get(HEADLESS_ROW_ID)
+ const oneShot = headlessRow !== undefined && headlessRow.disabled !== true
const app: { current?: Context } = {}
const shutdown = createProcessShutdown(async () => { await app.current?.fiber.dispose() })
@@ -243,9 +219,8 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
shutdown.interrupt(code)
}
// Signals own teardown throughout the startup window, not only after boot()
- // settles: an inserted entry point can publish readiness before sibling rows
- // finish mounting.
- process.on('SIGTERM', () => { interrupt(options.task === undefined ? 0 : 143) })
+ // settles: an inserted provider can publish before sibling rows finish mounting.
+ process.on('SIGTERM', () => { interrupt(oneShot ? 143 : 0) })
process.on('SIGINT', () => { interrupt(130) })
installFailLoud(NAME, process, async () => {
await app.current?.fiber.dispose()
@@ -253,7 +228,9 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
const rootConfig = join(composed.profile.dir, PROFILE_ROOT_FILENAME)
// Recomposition for the live user layers: bundle layers below, overlays
- // and flag patches above, so a user edit can never displace them. BOTH
+ // above, so a user edit can never displace them. Parsed app arguments are
+ // not in here at all — they live in app-provided services that survive a
+ // recomposition. BOTH
// user files are re-read per generation (the HMR watcher hands us only the
// changed file's patches, which one of the reads duplicates — fresh reads
// keep the two watchers from stitching in each other's stale copy).
@@ -267,19 +244,25 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
...composed.windowsShellPatches,
...loadOptionalPatches(NAME, composed.profile.patchPath) ?? [],
...loadOptionalPatches(NAME, homePatchPath()) ?? [],
- ...composed.overlayAndFlags,
+ ...composed.overlays,
])
// One-shot runs exit through the runner; watching would only hold the
// process open after its exit request.
- const watchProfilePatch = options.task === undefined
+ const watchProfilePatch = !oneShot
// Cloned for the same insert-aliasing reason as composeLive: the boot
// application must not mutate the objects later reloads recompose from.
- const ctx = await boot(NAME, rootConfig, structuredClone(allPatches(composed)), async (hostCtx) => {
+ const ctx = await boot(NAME, rootConfig, structuredClone(allPatches(composed)), (hostCtx) => {
app.current = hostCtx
- // Before any config-tree entry mounts, so a plugin that resolves a
- // user-facing value at construction already sees this run's layers.
+ // Before any config-tree entry mounts, so plugins resolve all launch-time
+ // environment values from the same immutable provenance snapshot.
hostCtx.provide(DSH_ENVIRONMENT_KEY, options.environment)
- if (options.task !== undefined) {
+ // The command line and bounded exit request are launcher facts available
+ // to every app plugin that injects the argument snapshot.
+ provideCmdline(hostCtx, {
+ args: options.args,
+ exit: code => void shutdown.shutdown(code),
+ })
+ if (oneShot) {
const io: HeadlessIo = {
stdout: process.stdout,
stderr: process.stderr,
@@ -287,11 +270,10 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
}
hostCtx.provide('headlessIo', io)
}
- await options.prepare?.(hostCtx, composed.rows)
})
app.current = ctx
- // A surface can dispose the whole tree while startup or this post-boot
- // watcher setup is still in flight. Loader presence and fiber state own
+ // A surface can dispose the whole tree while boot or this post-boot watcher
+ // setup is still in flight. Loader presence and fiber state own
// liveness; the local signal fact distinguishes that expected exit race
// from a real HMR error.
if (watchProfilePatch
@@ -308,9 +290,9 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
// bare custom profile may not mount either.
if (ctx.get('hmr') === undefined) {
if (ctx.get('timer') === undefined) {
- await ctx.loader.create({ name: '@cordisjs/plugin-timer' })
+ await ctx.loader.create({ name: '@deepseek-ai/cordis-plugin-timer' })
}
- await ctx.loader.create({ name: '@cordisjs/plugin-hmr', config: { root: [] } })
+ await ctx.loader.create({ name: '@deepseek-ai/cordis-plugin-hmr', config: { root: [] } })
}
await watchUserPatches(ctx, {
binName: NAME,
diff --git a/apps/cli/src/web.ts b/apps/cli/src/web.ts
deleted file mode 100644
index e438c7da7c..0000000000
--- a/apps/cli/src/web.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-/**
- * `dsh web` — the browser-surface alias over the profile boot: `--profile web`
- * plus the Web flag family (`--host/--port/--dev/--trusted-host`), each flag
- * becoming a patch over the composed profile
- * tree. All web runtime glue (dist serving, prompt section, URL line) lives
- * in the `@deepseek-ai/dsh-web-app` bundle; this launcher only derives
- * flag patches and the LAN-trust snapshot.
- * @module @deepseek-ai/dsh/web
- */
-
-import { networkInterfaces } from 'node:os'
-import { fileURLToPath } from 'node:url'
-import type { Context } from 'cordis'
-import type { PatchOptions } from '@cordisjs/plugin-include'
-import { addHarnessSourceSection } from '@deepseek-ai/dsh-app-boot'
-import type { EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
-import { runProfile, type ProfileRows } from './profile-boot.ts'
-
-const SOURCE_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
-
-/** The webserver schema's all-interfaces bind literal: gates LAN-authority derivation. */
-const ALL_INTERFACES_HOST = '0.0.0.0'
-
-/**
- * Non-internal IPv4 interface addresses of this machine — the IP-literal
- * authorities an all-interfaces bind is reachable by on the LAN.
- * @returns the addresses in interface order (possibly empty).
- */
-function lanIPv4Addresses(): string[] {
- return Object.values(networkInterfaces()).flat()
- .filter((iface): iface is NonNullable => iface !== undefined && iface.family === 'IPv4' && !iface.internal)
- .map(iface => iface.address)
-}
-
-/**
- * One LAN-trust resolution for one invocation, sampled exactly once: the
- * machine's LAN IP literals when the effective bind is all-interfaces, and
- * the `trustedHosts` value built from them plus the explicit extras. The
- * single sample is deliberate — display must advertise only addresses the
- * fence was configured with, so the web-app row receives this same snapshot.
- * Derived entries are port-less IP literals: DNS rebinding needs an
- * attacker-controlled name, so an IP-literal Host is safe on any port, and
- * the bound port may be OS-assigned, unknowable pre-boot.
- * @param bindHost - the effective webserver bind host (CLI flag, else the composed row value).
- * @param extra - `--trusted-host` values, in argv order.
- * @returns the sampled LAN addresses and the connection row's `trustedHosts` value (each possibly empty).
- */
-export function resolveLanTrust(
- bindHost: string | undefined,
- extra: readonly string[],
-): { lanAddresses: string[]; trustedHosts: string[] } {
- const lanAddresses = bindHost === ALL_INTERFACES_HOST ? lanIPv4Addresses() : []
- return { lanAddresses, trustedHosts: [...lanAddresses, ...extra] }
-}
-
-/** The `dsh web` flag family, already parsed by the argument adapter. */
-export interface WebFlags {
- patches: string[]
- host?: string
- port?: number
- dev: boolean
- trustedHosts?: string[]
-}
-
-/**
- * Derive the web alias's flag patches over an already-composed profile tree.
- * Patches replace a row's whole config, so each patched row's composed values
- * are re-read and merged under the overrides.
- * @param rows - the composed row index from {@link composeProfile}.
- * @param flags - the parsed flag family.
- * @returns the flag patch list, in application order.
- */
-function deriveWebFlagPatches(
- rows: ProfileRows,
- flags: WebFlags,
-): PatchOptions[] {
- const overrides = new Map>()
- const put = (entryId: string, key: string, value: unknown): void => {
- const bag = overrides.get(entryId) ?? {}
- bag[key] = value
- overrides.set(entryId, bag)
- }
- if (flags.host !== undefined) put('webserver', 'host', flags.host)
- if (flags.port !== undefined) put('webserver', 'port', flags.port)
- const composedHost = (rows.get('webserver')?.config as { host?: string } | undefined)?.host
- const { lanAddresses, trustedHosts } = resolveLanTrust(flags.host ?? composedHost, flags.trustedHosts ?? [])
- if (trustedHosts.length > 0) {
- // Additive over the composed value: a cordis.patch.yml-configured fence
- // authority must survive the derived LAN literals and flag extras — a
- // silent drop of security-relevant fence configuration.
- const composedTrusted = (rows.get('connection')?.config as { trustedHosts?: string[] } | undefined)?.trustedHosts ?? []
- put('connection', 'trustedHosts', [...composedTrusted, ...trustedHosts])
- }
- // mode and lanAddresses are launcher-derived on every boot (--dev also
- // inserts the client-hmr row), never pass-throughs of composed values.
- put('web-runtime', 'mode', flags.dev ? 'development' : 'production')
- put('web-runtime', 'lanAddresses', lanAddresses)
- // The agent-preset roots are patched by the shared profile boot: they are
- // an assembly fact of every dsh launcher, and `dsh run` composes agents
- // from the same roster this alias offers.
- const patches = [...overrides.entries()].map(([id, bag]): PatchOptions => {
- const composed = rows.get(id)
- if (composed === undefined) throw new Error(`dsh: patch target row "${id}" not found in the web profile composition`)
- return { id, config: { ...(composed.config ?? {}) as Record, ...bag } }
- })
- if (flags.dev) patches.push({ insert: [{ id: 'client-hmr', name: '@deepseek-ai/dsh-client-hmr' }] })
- return patches
-}
-
-/**
- * Whether the composed Web runtime keeps its model- and shell-visible surface
- * context. The bundle schema defaults the field to true, so only an explicit
- * false suppresses both the bundle contributions and the launcher-owned
- * source-checkout section.
- * @param rows - the composed Web profile rows before launcher flag patches.
- * @returns true unless the web-runtime row explicitly disables surface context.
- */
-export function webSurfaceContextEnabled(rows: ProfileRows): boolean {
- return (rows.get('web-runtime')?.config as { surfaceContext?: boolean } | undefined)?.surfaceContext !== false
-}
-
-/**
- * Serve the browser UI from the web profile. Host/port flags are passed
- * through only when given (absent, the composed profile values
- * stand); `web-runtime.mode` and `lanAddresses` are launcher-derived on
- * every boot. The URL line is printed by the web-app bundle's runtime row
- * after Loader settlement.
- * @param flags - the parsed `dsh web` flag family.
- * @param environment - this run's frozen environment snapshot.
- */
-export async function runWeb(flags: WebFlags, environment: EnvironmentSnapshot): Promise {
- await runProfile({
- environment,
- profile: 'web',
- patchFiles: flags.patches,
- deriveFlagPatches: rows => deriveWebFlagPatches(rows, flags),
- prepare: (ctx: Context, rows: ProfileRows) => {
- if (!webSurfaceContextEnabled(rows)) return
- ctx.inject(['systemPrompt'], (promptCtx) => {
- addHarnessSourceSection(promptCtx, SOURCE_ROOT)
- })
- },
- })
-}
diff --git a/apps/cli/src/windows-shell.ts b/apps/cli/src/windows-shell.ts
index fbb3d13194..1a9ca719f8 100644
--- a/apps/cli/src/windows-shell.ts
+++ b/apps/cli/src/windows-shell.ts
@@ -11,7 +11,7 @@
*/
import { join } from 'node:path'
-import type { PatchOptions } from '@cordisjs/plugin-include'
+import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { loadOverlayPatches, type ProfileLayer } from '@deepseek-ai/dsh-app-boot'
/** The base bundle whose package carries the Windows shell patch. */
diff --git a/apps/cli/tests/args.spec.ts b/apps/cli/tests/args.spec.ts
index 603d8e2dd5..89a16921b2 100644
--- a/apps/cli/tests/args.spec.ts
+++ b/apps/cli/tests/args.spec.ts
@@ -21,22 +21,28 @@ function exitCode(argv: string[]): number {
afterEach(() => { vi.restoreAllMocks() })
describe('parseDshArgs', () => {
- it('routes profile boots, one-shot runs, and the web alias', () => {
- expect(parse(['--profile', 'tui'])).toEqual({ mode: 'profile', profile: 'tui', patches: [] })
+ it('routes profile boots and the web alias, handing the rest to the app', () => {
+ expect(parse(['--profile', 'tui'])).toEqual({ mode: 'profile', profile: 'tui', patches: [], args: [] })
expect(parse(['--profile', 'tui', '--patch', 'a.yml', '--patch', 'b.yml']))
- .toEqual({ mode: 'profile', profile: 'tui', patches: ['a.yml', 'b.yml'] })
- expect(parse(['run', 'run', 'the', 'tests']))
- .toEqual({ mode: 'run', profile: 'headless', patches: [], task: 'run the tests' })
- expect(parse(['run', '--profile', 'custom', '--patch', 'a.yml', '--patch', 'b.yml', 'run', 'the', 'tests']))
- .toEqual({ mode: 'run', profile: 'custom', patches: ['a.yml', 'b.yml'], task: 'run the tests' })
- expect(parse(['run', '--', '--profile', 'is', 'task', 'text']))
- .toEqual({ mode: 'run', profile: 'headless', patches: [], task: '--profile is task text' })
- expect(parse(['web'])).toEqual({ mode: 'web', dev: false, patches: [] })
- expect(parse(['web', '--patch', 'web.yml'])).toEqual({ mode: 'web', dev: false, patches: ['web.yml'] })
+ .toEqual({ mode: 'profile', profile: 'tui', patches: ['a.yml', 'b.yml'], args: [] })
+ expect(parse(['web'])).toEqual({ mode: 'profile', profile: 'web', patches: [], args: [] })
+ expect(parse(['web', '--patch', 'web.yml']))
+ .toEqual({ mode: 'profile', profile: 'web', patches: ['web.yml'], args: [] })
+ })
+
+ it('ends the launcher flags at the first token it does not own', () => {
+ // App flags, including its -h, and positionals reach the app verbatim.
+ expect(parse(['--profile', 'tui', '--resume', 'abc']))
+ .toEqual({ mode: 'profile', profile: 'tui', patches: [], args: ['--resume', 'abc'] })
+ expect(parse(['--profile', 'web', '-h']))
+ .toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['-h'] })
expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev']))
- .toEqual({ mode: 'web', host: '0.0.0.0', port: 8080, dev: true, patches: [] })
- expect(parse(['web', '--trusted-host', 'harness.internal:3080', 'lab.internal', '--trusted-host', '10.0.0.9']))
- .toEqual({ mode: 'web', dev: false, patches: [], trustedHosts: ['harness.internal:3080', 'lab.internal', '10.0.0.9'] })
+ .toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['--host', '0.0.0.0', '--port', '8080', '--dev'] })
+ expect(parse(['--profile', 'headless', 'run', 'the', 'tests']))
+ .toEqual({ mode: 'profile', profile: 'headless', patches: [], args: ['run', 'the', 'tests'] })
+ // Launcher flags placed after that boundary belong to the app too.
+ expect(parse(['--profile', 'tui', '--patch', 'a.yml', '--resume', 'b', '--patch', 'late.yml']))
+ .toEqual({ mode: 'profile', profile: 'tui', patches: ['a.yml'], args: ['--resume', 'b', '--patch', 'late.yml'] })
})
it('routes the plugin pnpm forwarder', () => {
@@ -44,8 +50,8 @@ describe('parseDshArgs', () => {
.toEqual({ mode: 'plugin', profile: 'tui', args: ['add', 'turtle-ui'] })
expect(parse(['plugin', '--profile', 'tui', 'remove', 'turtle-ui']))
.toEqual({ mode: 'plugin', profile: 'tui', args: ['remove', 'turtle-ui'] })
- expect(parse(['plugin', '--profile', 'tui', 'why', 'cordis']))
- .toEqual({ mode: 'plugin', profile: 'tui', args: ['why', 'cordis'] })
+ expect(parse(['plugin', '--profile', 'tui', 'why', '@deepseek-ai/cordis']))
+ .toEqual({ mode: 'plugin', profile: 'tui', args: ['why', '@deepseek-ai/cordis'] })
// Unknown pnpm flags forward verbatim.
expect(parse(['plugin', '--profile', 'tui', 'add', '--save-dev', 'x']))
.toEqual({ mode: 'plugin', profile: 'tui', args: ['add', '--save-dev', 'x'] })
@@ -64,18 +70,12 @@ describe('parseDshArgs', () => {
.toEqual({ mode: 'dump-config', profile: 'web', defaultOnly: true, patches: [] })
})
- it('rejects missing profile, flags outside the current grammar, and contradictory inputs', () => {
+ it('rejects missing profile, removed flags, and contradictory inputs', () => {
expect(exitCode([])).toBe(1)
- expect(exitCode(['tui'])).toBe(1) // a bare word is a task without --profile
- expect(exitCode(['--config', 'c.yml'])).toBe(1) // outside the current grammar
- expect(exitCode(['-p', 'task'])).toBe(1) // outside the current grammar
- expect(exitCode(['--profile', 'headless', 'task'])).toBe(1) // tasks belong to `run`
- expect(exitCode(['run'])).toBe(1)
- expect(exitCode(['run', ''])).toBe(1)
- expect(exitCode(['run', '--profile', '', 'task'])).toBe(1)
- expect(exitCode(['run', '--patch=', 'task'])).toBe(1)
- expect(exitCode(['--profile', 'headless', 'run', 'task'])).toBe(1)
- expect(exitCode(['--patch', 'parent.yml', 'run', 'task'])).toBe(1)
+ expect(exitCode(['tui'])).toBe(1) // an app argument without --profile has no app to reach
+ expect(exitCode(['--config', 'c.yml'])).toBe(1) // removed
+ expect(exitCode(['-p', 'task'])).toBe(1) // removed
+ expect(exitCode(['run', 'task'])).toBe(1) // app-owned task replaced the launcher subcommand
expect(exitCode(['--profile', ''])).toBe(1)
expect(exitCode(['--profile', 'x', '--patch='])).toBe(1)
expect(exitCode(['--dump-config'])).toBe(1)
@@ -87,21 +87,20 @@ describe('parseDshArgs', () => {
expect(exitCode(['web', '--dump-config', '--dump-default-config'])).toBe(1)
expect(exitCode(['web', '--dump-default-config', '--patch', 'w.yml'])).toBe(1)
expect(exitCode(['web', '--patch='])).toBe(1)
- // Boot-free dumps derive no flag patches; silently dropping the flags
- // would print a tree that differs from the same invocation's boot.
+ // A dump never runs app command-line providers, so it cannot show what
+ // those flags would decide; printing a tree that differs from the same
+ // invocation's boot would mislead.
expect(exitCode(['web', '--dump-config', '--port', '8080'])).toBe(1)
- expect(exitCode(['web', '--dump-config', '--dev'])).toBe(1)
- // A non-numeric port fails at the flag, not deep in the webserver schema.
- expect(exitCode(['web', '--port', 'abc'])).toBe(1)
+ expect(exitCode(['--profile', 'web', '--dump-config', '-h'])).toBe(1)
expect(exitCode(['plugin', 'add', 'x'])).toBe(1) // --profile required
expect(exitCode(['plugin', '--profile', 'tui'])).toBe(1) // nothing to forward
expect(exitCode(['plugin', '--profile', ''])).toBe(1)
expect(exitCode(['--profile', 'x', 'plugin', 'add', 'y'])).toBe(1)
})
- it('exits 0 for help and version', () => {
+ it('keeps its own help for an invocation with no app to hand it to', () => {
expect(exitCode(['--help'])).toBe(0)
- expect(exitCode(['run', '--help'])).toBe(0)
+ expect(exitCode(['-h'])).toBe(0)
expect(exitCode(['--version'])).toBe(0)
})
})
diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts
index b81f837cf8..81acefadc5 100644
--- a/apps/cli/tests/built-bin.e2e.ts
+++ b/apps/cli/tests/built-bin.e2e.ts
@@ -8,8 +8,10 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
/** Published-entry acceptance for argument errors, profile lifecycle, and boot-free config dumps. */
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
+// The release version, including a prerelease such as 0.0.1-rc.1: `--version`
+// prints what this manifest carries, so no test may pin it to a literal.
+const cliVersion = (JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string }).version
const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
-const coreWebOverlay = fileURLToPath(new URL('../config/core-web.cordis.yml', import.meta.url))
const invalidProvider = fileURLToPath(new URL('./fixtures/invalid-provider.cordis.yml', import.meta.url))
async function runBuiltBin(
@@ -129,8 +131,8 @@ function createProfileLifecycleFixture(): ProfileLifecycleFixture {
return { home, ready, settled, disposed, interrupt }
}
-function startProfileLifecycle(fixture: ProfileLifecycleFixture) {
- return execa(process.execPath, [dshBin, '--profile', 'lifecycle'], {
+function startProfileLifecycle(fixture: ProfileLifecycleFixture, args: readonly string[] = []) {
+ return execa(process.execPath, [dshBin, '--profile', 'lifecycle', ...args], {
cwd: fixture.home,
input: '',
reject: false,
@@ -145,8 +147,8 @@ function startProfileLifecycle(fixture: ProfileLifecycleFixture) {
}
function requestProfileShutdown(
- child: ReturnType,
- fixture: ProfileLifecycleFixture,
+ child: Pick, 'kill'>,
+ fixture: Pick,
): void {
if (process.platform === 'win32') {
writeFileSync(fixture.interrupt, 'interrupt')
@@ -194,8 +196,121 @@ function createEnvironmentProbeProfile(home: string, project: string): void {
].join('\n'))
}
+interface StartupFixture {
+ home: string
+ ready: string
+ echo: string
+ interrupt: string
+ /** An always-running row's echo, used to observe that a user patch reload landed. */
+ witness: string
+}
+
+/**
+ * A custom profile whose ordinary provider plugin injects `cmdlineArgs`, plus
+ * a row that reads its app-owned service through a `!!js` config expression.
+ * Both plugin modules resolve
+ * `@deepseek-ai/dsh-cmdline` and `commander` through the profile module
+ * fallback, exactly as an installed out-of-tree bundle does.
+ */
+function createStartupFixture(): StartupFixture {
+ const home = mkdtempSync(join(tmpdir(), 'dsh-profile-startup-'))
+ const profileDir = join(home, 'profiles', 'startup')
+ // Written straight into the installed location: a row module resolves its
+ // own imports from where it is installed, and only inside the profile does
+ // Node's parent walk reach the installation fallback these plugins need.
+ const bundleDir = join(profileDir, 'node_modules', 'dsh-startup-bundle')
+ mkdirSync(bundleDir, { recursive: true })
+ writeFileSync(join(bundleDir, 'startup.mjs'), [
+ "import { Command } from 'commander'",
+ "import { parseCmdline } from '@deepseek-ai/dsh-cmdline'",
+ "export const name = 'fixture-startup'",
+ "export const inject = ['cmdlineArgs']",
+ 'export function apply(ctx) {',
+ " const program = new Command().name('fixture').option('--generation ', 'echoed generation')",
+ ' const values = parseCmdline(ctx, program, parsed => ({ generation: parsed.opts().generation }))',
+ ' if (values !== undefined) ctx.provide(\'fixtureStartup\', values)',
+ '}',
+ '',
+ ].join('\n'))
+ writeFileSync(join(bundleDir, 'waiting.mjs'), [
+ "import { existsSync, writeFileSync } from 'node:fs'",
+ "import { join } from 'node:path'",
+ "export const name = 'startup-fixture'",
+ 'export function apply(ctx, config = {}) {',
+ ' let interrupted = false',
+ ' const heartbeat = setInterval(() => {',
+ ' if (interrupted || !existsSync(process.env.RAW_INTERRUPT_FILE)) return',
+ ' interrupted = true',
+ " process.emit('SIGTERM')",
+ ' }, 20)',
+ " writeFileSync(join(process.env.DSH_HOME, 'config-echo'), String(config.generation ?? 'bundle-default'))",
+ " writeFileSync(process.env.RAW_READY_FILE, 'ready')",
+ ' ctx.effect(() => () => { clearInterval(heartbeat) })',
+ '}',
+ '',
+ ].join('\n'))
+ writeFileSync(join(bundleDir, 'witness.mjs'), [
+ "import { writeFileSync } from 'node:fs'",
+ "import { join } from 'node:path'",
+ "export const name = 'reload-witness'",
+ 'export function apply(ctx, config = {}) {',
+ " writeFileSync(join(process.env.DSH_HOME, 'witness'), String(config.generation ?? 'bundle-default'))",
+ '}',
+ '',
+ ].join('\n'))
+ writeFileSync(join(bundleDir, 'cordis.patch.yml'), [
+ '- insert:',
+ ' - id: startup-fixture',
+ ` name: ${pathToFileURL(join(bundleDir, 'waiting.mjs')).href}`,
+ ' inject: [fixtureStartup]',
+ ' config:',
+ // Lazy interpolation runs only after the provider's service is injected.
+ " generation: !!js ctx.fixtureStartup.generation ?? 'bundle-default'",
+ ' - id: fixture-startup',
+ ` name: ${pathToFileURL(join(bundleDir, 'startup.mjs')).href}`,
+ ' - id: reload-witness',
+ ` name: ${pathToFileURL(join(bundleDir, 'witness.mjs')).href}`,
+ '',
+ ].join('\n'))
+ writeFileSync(join(bundleDir, 'package.json'), JSON.stringify({
+ name: 'dsh-startup-bundle',
+ version: '0.0.0',
+ type: 'module',
+ dsh: { bundle: { patch: './cordis.patch.yml' } },
+ }, undefined, 2))
+ writeFileSync(join(profileDir, 'package.json'), JSON.stringify({
+ name: 'dsh-profile-startup',
+ private: true,
+ dependencies: {},
+ dsh: { profile: { bundles: ['dsh-startup-bundle'] } },
+ }, undefined, 2))
+ writeFileSync(join(profileDir, 'cordis.patch.yml'), '[]\n')
+ return {
+ home,
+ ready: join(home, 'ready'),
+ echo: join(home, 'config-echo'),
+ interrupt: join(home, 'interrupt'),
+ witness: join(home, 'witness'),
+ }
+}
+
+function startStartupProfile(fixture: StartupFixture, args: readonly string[]) {
+ return execa(process.execPath, [dshBin, '--profile', 'startup', ...args], {
+ cwd: fixture.home,
+ input: '',
+ reject: false,
+ timeout: 25_000,
+ killSignal: 'SIGKILL',
+ env: {
+ DSH_HOME: fixture.home,
+ RAW_READY_FILE: fixture.ready,
+ RAW_INTERRUPT_FILE: fixture.interrupt,
+ },
+ })
+}
+
describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', () => {
- it('requires --profile and rejects inputs outside the current grammar', async () => {
+ it('requires --profile and rejects removed commands', async () => {
const bare = await runBuiltBin()
expect(bare.code).toBe(1)
expect(bare.stdout).toBe('')
@@ -203,46 +318,63 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
const help = await runBuiltBin(['--help'])
expect(help.code).toBe(0)
expect(help.stdout).toContain('dsh --profile web')
- expect(help.stdout).toContain('dsh run "run the tests"')
expect(help.stdout).toContain('dsh plugin --profile')
expect(help.stdout).not.toMatch(/^\s+(?:tui|meta|upgrade)\b/mu)
- for (const outsideGrammar of [['tui'], ['--config', 'x.yml'], ['-p', 'task'], ['--profile', 'headless', 'task']]) {
- const result = await runBuiltBin(outsideGrammar)
+ for (const removed of [['tui'], ['--config', 'x.yml'], ['-p', 'task'], ['run', 'task']]) {
+ const result = await runBuiltBin(removed)
expect(result.code).toBe(1)
}
}, 30_000)
- it('prints run help without initializing the selected profile', async () => {
- const parent = mkdtempSync(join(tmpdir(), 'dsh-run-help-'))
- const home = join(parent, 'not-created')
+ it('routes help and usage errors without activating startup-dependent rows', async () => {
+ const home = mkdtempSync(join(tmpdir(), 'dsh-app-help-'))
try {
- const result = await runBuiltBin(['run', '--help'], { DSH_HOME: home })
- expect(result.code).toBe(0)
- expect(result.stderr).toBe('')
- expect(result.stdout).toContain('Usage: dsh run [options] ')
- expect(existsSync(home)).toBe(false)
- } finally {
- rmSync(parent, { recursive: true, force: true })
- }
- })
+ const web = await runBuiltBin(['--profile', 'web', '--help'], {
+ DSH_HOME: home,
+ DSH_TELEMETRY_DISABLED: '1',
+ })
+ expect(web.code).toBe(0)
+ expect(web.stderr).toBe('')
+ expect(web.stdout).toContain('Usage: dsh --profile web')
+ expect(web.stdout).toContain('--port ')
+ expect(web.stdout).not.toContain('dsh web: http://')
- it('runs the default headless profile through the published run command', async () => {
- const apiKey = 'built-dsh-run-key'
+ const headlessHelp = await runBuiltBin(['--profile', 'headless', '--help'], {
+ DSH_HOME: home,
+ DSH_TELEMETRY_DISABLED: '1',
+ })
+ expect(headlessHelp.code).toBe(0)
+ expect(headlessHelp.stderr).toBe('')
+ expect(headlessHelp.stdout).toContain('Usage: dsh --profile headless')
+
+ const missingTask = await runBuiltBin(['--profile', 'headless'], {
+ DSH_HOME: home,
+ DSH_TELEMETRY_DISABLED: '1',
+ })
+ expect(missingTask.code).toBe(1)
+ expect(missingTask.stderr).toContain('a task is required')
+ } finally {
+ rmSync(home, { recursive: true, force: true })
+ }
+ }, 30_000)
+
+ it('runs the headless profile through its app-owned task positional', async () => {
+ const apiKey = 'built-dsh-headless-key'
const server = await startMockLlmServer({
sequence: ['success'],
apiKey,
- successText: 'published dsh run reached the mock',
+ successText: 'published headless profile reached the mock',
})
- const home = mkdtempSync(join(tmpdir(), 'dsh-built-run-'))
+ const home = mkdtempSync(join(tmpdir(), 'dsh-built-headless-'))
try {
- const result = await runBuiltBin(['run', 'answer', 'from', 'the', 'published', 'entry'], {
+ const result = await runBuiltBin(['--profile', 'headless', 'answer', 'from', 'the', 'published', 'entry'], {
DSH_HOME: home,
DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: apiKey,
DEEPSEEK_BASE_URL: server.baseURL,
})
expect(result.code, result.stderr).toBe(0)
- expect(result.stdout).toBe('published dsh run reached the mock')
+ expect(result.stdout).toBe('published headless profile reached the mock')
expect(result.stderr).toBe('')
expect(server.requests.length).toBeGreaterThan(0)
expect(server.requests.every(request => request.path === '/chat/completions')).toBe(true)
@@ -258,7 +390,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
writeFileSync(join(project, '.env'), 'PATH=/project-only-path\n')
try {
const result = await runBuiltBin(['--version'], {}, project)
- expect(result).toEqual({ code: 0, stdout: '0.0.1', stderr: '' })
+ expect(result).toEqual({ code: 0, stdout: cliVersion, stderr: '' })
} finally {
rmSync(project, { recursive: true, force: true })
}
@@ -318,9 +450,9 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
}, 30_000)
it('reports a patch-overlay boot failure without hanging', async () => {
- // An HMR main-watcher initial scan that refreshes the include
- // mid-initial-apply deadlocks the failing apply's rollback against the
- // refresh drain: dsh exits 13 with no diagnostic instead of settling
+ // The HMR main watcher's initial scan once refreshed the include
+ // mid-initial-apply, deadlocking the failing apply's rollback against the
+ // refresh drain: dsh exited 13 with no diagnostic instead of settling
// ([Agent Note](../../../.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.md)).
const home = mkdtempSync(join(tmpdir(), 'dsh-invalid-patch-'))
try {
@@ -337,9 +469,9 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
}
}, 30_000)
- it('applies a custom profile bundle and disposes it on a startup-time signal', async () => {
+ it('lets a profile without a parser ignore app arguments and dispose on a startup-time signal', async () => {
const fixture = createProfileLifecycleFixture()
- const child = startProfileLifecycle(fixture)
+ const child = startProfileLifecycle(fixture, ['--unclaimed'])
try {
await waitForFile(fixture.ready)
requestProfileShutdown(child, fixture)
@@ -405,6 +537,83 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
}
}, 30_000)
+ it('hands the app arguments to the profile, which applies them before its rows start', async () => {
+ const fixture = createStartupFixture()
+ const child = startStartupProfile(fixture, ['--generation', 'flagged'])
+ try {
+ await waitForFile(fixture.ready)
+ // The consumer started once, already carrying the flag value: the
+ // launcher never saw --generation, and the app provider resolved it first.
+ expect(readFileSync(fixture.echo, 'utf8')).toBe('flagged')
+ requestProfileShutdown(child, fixture)
+ expect((await child).exitCode).toBe(0)
+ } finally {
+ child.kill('SIGKILL')
+ rmSync(fixture.home, { recursive: true, force: true })
+ }
+ }, 30_000)
+
+ it('starts a consumer on its composed value when the invocation carries no app arguments', async () => {
+ const fixture = createStartupFixture()
+ const child = startStartupProfile(fixture, [])
+ try {
+ await waitForFile(fixture.ready)
+ expect(readFileSync(fixture.echo, 'utf8')).toBe('bundle-default')
+ requestProfileShutdown(child, fixture)
+ expect((await child).exitCode).toBe(0)
+ } finally {
+ child.kill('SIGKILL')
+ rmSync(fixture.home, { recursive: true, force: true })
+ }
+ }, 30_000)
+
+ it('keeps the app arguments across a user patch reload', async () => {
+ // A live edit recomposes every row while the provider service remains
+ // active, so each config expression reads the same invocation value (a
+ // served port does not move back to its composed fallback).
+ const fixture = createStartupFixture()
+ const profilePatch = join(fixture.home, 'profiles', 'startup', 'cordis.patch.yml')
+ const child = startStartupProfile(fixture, ['--generation', 'flagged'])
+ try {
+ // Both rows: the waiting one carries the flag value, and the witness is
+ // what a reload will re-mount. They start independently, so neither
+ // marker implies the other.
+ await waitForFile(fixture.ready)
+ await waitForFile(fixture.witness)
+ expect(readFileSync(fixture.echo, 'utf8')).toBe('flagged')
+ // An edit to an unrelated row: the witness re-mounts, which is how this
+ // test knows the whole tree was recomposed.
+ rmSync(fixture.witness)
+ writeFileSync(profilePatch, [
+ '- id: reload-witness',
+ ' config:',
+ ' generation: reloaded',
+ '',
+ ].join('\n'))
+ await waitForFile(fixture.witness)
+ expect(readFileSync(fixture.witness, 'utf8')).toBe('reloaded')
+ expect(readFileSync(fixture.echo, 'utf8')).toBe('flagged')
+ requestProfileShutdown(child, fixture)
+ expect((await child).exitCode).toBe(0)
+ } finally {
+ child.kill('SIGKILL')
+ rmSync(fixture.home, { recursive: true, force: true })
+ }
+ }, 30_000)
+
+ it("prints the app's own help, starts none of its rows, and exits", async () => {
+ const fixture = createStartupFixture()
+ try {
+ const result = await startStartupProfile(fixture, ['--help'])
+ expect(result.exitCode).toBe(0)
+ expect(result.stdout).toContain('Usage: fixture')
+ expect(result.stdout).toContain('--generation')
+ expect(existsSync(fixture.ready)).toBe(false)
+ } finally {
+ rmSync(fixture.home, { recursive: true, force: true })
+ }
+ }, 30_000)
+
it('anchors a relative add spec to the invoking directory, not the profile', async () => {
// `dsh plugin --profile x add .` from a plugin checkout must install THAT
// checkout — pnpm's cwd is the profile directory, so an un-anchored `.`
@@ -491,18 +700,17 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
expect(stdout).toContain("name: '@deepseek-ai/dsh-host-webserver'")
}, 30_000)
- it('prints a headless profile with no Host, HTTP, or browser rows', async () => {
+ it('prints the headless profile without Host or browser layers', async () => {
const { stdout, code, stderr } = await runBuiltBin(
['--profile', 'headless', '--dump-default-config'],
{ DSH_HOME: home },
)
expect(code).toBe(0)
expect(stderr).toBe('')
- expect(stdout).toContain("name: '@deepseek-ai/dsh-agent-default-model'")
expect(stdout).toContain("name: '@deepseek-ai/dsh-headless'")
- expect(stdout).not.toContain("name: '@deepseek-ai/dsh-host-")
+ expect(stdout).not.toMatch(/name: '@deepseek-ai\/dsh-host-/)
expect(stdout).not.toContain("name: '@deepseek-ai/dsh-web-app'")
- expect(stdout).not.toContain("name: '@deepseek-ai/dsh-client-")
+ expect(stdout).not.toMatch(/name: '@deepseek-ai\/dsh-client-/)
}, 30_000)
it('composes the profile user layer and a --patch overlay in order', async () => {
@@ -543,16 +751,5 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
expect(stdout).toContain(`patched by ${profilePatch}, ${overlay}`)
expect(stderr).toContain('patch: entry "absent-row" not found')
}, 30_000)
-
- it('shows the RL Web patch disabling runtime surface context', async () => {
- const { stdout, code, stderr } = await runBuiltBin(
- ['web', '--patch', coreWebOverlay, '--dump-config'],
- { DSH_HOME: home },
- )
- expect(code).toBe(0)
- expect(stderr).toBe('')
- expect(stdout).toContain("name: '@deepseek-ai/dsh-web-app'")
- expect(stdout).toContain('surfaceContext: false')
- }, 30_000)
})
})
diff --git a/apps/cli/tests/dsh-badge.snapshot.ts b/apps/cli/tests/dsh-badge.snapshot.ts
index 00d5278f82..28e418d6fc 100644
--- a/apps/cli/tests/dsh-badge.snapshot.ts
+++ b/apps/cli/tests/dsh-badge.snapshot.ts
@@ -85,14 +85,14 @@ describe('dsh badge assembled snapshot', () => {
- Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20
- Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\`
- - Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\`
+ - Project URL: \`https://github.com/deepseek-ai/deepseek-harness\`
## Markdown
Use this linked badge in Markdown:
\`\`\`markdown
- [](https://github.com/deepseek-ai/deepseek-harness-sdk)
+ [](https://github.com/deepseek-ai/deepseek-harness)
\`\`\`
If attribution should not be linked, use:
@@ -124,14 +124,14 @@ describe('dsh badge assembled snapshot', () => {
- Local PNG: [\`dsh-badge.png\`](dsh-badge.png), 726×120 source image; render at 121×20
- Shields.io image URL: \`https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white\`
- - Project URL: \`https://github.com/deepseek-ai/deepseek-harness-sdk\`
+ - Project URL: \`https://github.com/deepseek-ai/deepseek-harness\`
## Markdown
Use this linked badge in Markdown:
\`\`\`markdown
- [](https://github.com/deepseek-ai/deepseek-harness-sdk)
+ [](https://github.com/deepseek-ai/deepseek-harness)
\`\`\`
If attribution should not be linked, use:
diff --git a/apps/cli/tests/fixtures/dsh-badge/snapshot.ts b/apps/cli/tests/fixtures/dsh-badge/snapshot.ts
index 8d6019ab53..98f3fd7578 100644
--- a/apps/cli/tests/fixtures/dsh-badge/snapshot.ts
+++ b/apps/cli/tests/fixtures/dsh-badge/snapshot.ts
@@ -1,5 +1,5 @@
import { fileURLToPath } from 'node:url'
-import { Context } from 'cordis'
+import { Context } from '@deepseek-ai/cordis'
import { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent'
import { CallId } from '@deepseek-ai/dsh-llm'
import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
diff --git a/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/.mcp.json b/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/.mcp.json
deleted file mode 100644
index 4851f9f1f8..0000000000
--- a/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/.mcp.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "mcpServers": {
- "github_repository": {
- "command": "node",
- "args": [
- "lib/mcp-server.mjs"
- ]
- }
- }
-}
diff --git a/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/package.json b/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/package.json
deleted file mode 100644
index 6871a4d053..0000000000
--- a/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/package.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "name": "dsh-github-repository-plugin-e2e-fixture",
- "version": "0.0.0",
- "private": true,
- "type": "module",
- "files": [
- "lib",
- "dsh-plugin.mjs",
- "dsh-plugin-assets"
- ],
- "scripts": {
- "prepack": "tsc --noEmit && tsdown src/plugin.ts src/mcp-server.ts --no-config --tsconfig tsconfig.json --out-dir lib --platform node --target es2024 --clean && dsh-plugin-prepare"
- },
- "dsh": {
- "skills": [
- "../skills"
- ],
- "mcpServers": "./.mcp.json",
- "entry": "./lib/plugin.mjs"
- },
- "dependencies": {
- "@modelcontextprotocol/sdk": "1.29.0"
- },
- "devDependencies": {
- "@deepseek-ai/dsh-repository-plugin": "0.0.1",
- "cordis": "4.0.0-rc.7",
- "tsdown": "0.22.2",
- "typescript": "6.0.3"
- }
-}
diff --git a/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/src/mcp-server.ts b/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/src/mcp-server.ts
deleted file mode 100644
index 78a3e008f4..0000000000
--- a/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/src/mcp-server.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
-import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
-
-// The repository root's linter cannot resolve this independently installed
-// Git-package dependency; the package's prepack tsc validates the SDK types.
-/* oxlint-disable typescript/no-unsafe-assignment, typescript/no-unsafe-call, typescript/no-unsafe-member-access */
-const server = new McpServer({
- name: 'github-repository-plugin-e2e',
- version: '0.0.0',
-})
-
-server.registerTool('proof', {
- description: 'Proves that an MCP server compiled from the exact GitHub repository package is active.',
- inputSchema: {},
-}, async () => ({
- content: [{ type: 'text', text: 'MCP_FROM_GITHUB_REPOSITORY' }],
-}))
-
-await server.connect(new StdioServerTransport())
diff --git a/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/src/plugin.ts b/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/src/plugin.ts
deleted file mode 100644
index 5106f71b2f..0000000000
--- a/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/src/plugin.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import type { Context } from 'cordis'
-
-const PROOF_TOOL_NAME = 'mcp__github_repository__proof'
-
-interface TextBlock {
- readonly type: 'text'
- readonly text: string
-}
-
-interface ToolExecution {
- readonly name: string
-}
-
-interface ToolResult {
- readonly isError: boolean
- readonly content: readonly TextBlock[]
-}
-
-type PostDecision =
- | { readonly kind: 'accept'; readonly content?: readonly TextBlock[]; readonly value?: unknown; readonly additionalContexts?: readonly unknown[] }
- | { readonly kind: 'block'; readonly feedback: readonly TextBlock[] }
-
-type PostListener = (
- execution: ToolExecution,
- result: ToolResult,
- next: () => Promise,
-) => Promise
-
-type DshContext = Context & {
- on(event: 'tools/post-execute', listener: PostListener): () => void
-}
-
-/** Cordis plugin name used by the repository acceptance fixture. */
-export const name = 'github-repository-typescript-proof'
-
-/** DSH tool registry required by the post-execute contribution. */
-export const inject = ['tools']
-
-/**
- * Append a marker after the repository MCP proof tool succeeds.
- * @param ctx - trusted DSH Cordis context supplied to the repository package.
- */
-export function apply(ctx: Context): void {
- const dsh = ctx as DshContext
- dsh.on('tools/post-execute', async (execution, result, next): Promise => {
- const decision = await next()
- if (execution.name !== PROOF_TOOL_NAME || result.isError || decision.kind !== 'accept' || Object.hasOwn(decision, 'value')) {
- return decision
- }
- return {
- kind: 'accept',
- content: [
- ...(decision.content ?? result.content),
- { type: 'text', text: 'TS_PLUGIN_FROM_GITHUB_REPOSITORY' },
- ],
- ...decision.additionalContexts === undefined ? {} : { additionalContexts: decision.additionalContexts },
- }
- })
-}
diff --git a/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/tsconfig.json b/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/tsconfig.json
deleted file mode 100644
index 22d9301cfb..0000000000
--- a/apps/cli/tests/fixtures/github-repository-plugin/.dsh-plugin/tsconfig.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2024",
- "module": "ESNext",
- "moduleResolution": "Bundler",
- "strict": true,
- "skipLibCheck": true,
- "noEmit": true
- },
- "include": [
- "src/**/*.ts"
- ]
-}
diff --git a/apps/cli/tests/fixtures/github-repository-plugin/skills/github-source-proof/SKILL.md b/apps/cli/tests/fixtures/github-repository-plugin/skills/github-source-proof/SKILL.md
deleted file mode 100644
index eae668a4f9..0000000000
--- a/apps/cli/tests/fixtures/github-repository-plugin/skills/github-source-proof/SKILL.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-name: github-source-proof
-description: Proves that dsh installed a private repository Plugin from an exact GitHub source.
----
-
-This skill exists only in the GitHub repository source fixture.
diff --git a/apps/cli/tests/fixtures/never-dispose.mjs b/apps/cli/tests/fixtures/never-dispose.mjs
index 5dc8510550..9e81b32738 100644
--- a/apps/cli/tests/fixtures/never-dispose.mjs
+++ b/apps/cli/tests/fixtures/never-dispose.mjs
@@ -4,7 +4,7 @@ import { existsSync } from 'node:fs'
/**
* Register a disposer that keeps process shutdown pending until it is forced.
- * @param {import('cordis').Context} ctx - loader-mounted test plugin context.
+ * @param {import('@deepseek-ai/cordis').Context} ctx - loader-mounted test plugin context.
*/
export function apply(ctx) {
const keepAlive = setInterval(() => {}, 60_000)
diff --git a/apps/cli/tests/github-repository-plugin.built.e2e.ts b/apps/cli/tests/github-repository-plugin.built.e2e.ts
deleted file mode 100644
index 7bb4706dd7..0000000000
--- a/apps/cli/tests/github-repository-plugin.built.e2e.ts
+++ /dev/null
@@ -1,256 +0,0 @@
-import { createHash } from 'node:crypto'
-import { cpSync, existsSync, globSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
-import { createServer } from 'node:http'
-import { createRequire } from 'node:module'
-import { tmpdir } from 'node:os'
-import { delimiter, join } from 'node:path'
-import { fileURLToPath } from 'node:url'
-import { startMockLlmServer } from '@deepseek-ai/dsh-llm-mock-server'
-import { execa } from 'execa'
-import { describe, expect, it } from 'vitest'
-
-const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
-const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
-const repositoryPluginPackage = join(repoRoot, 'packages/self-modification/repository-plugin')
-const releasePackageNames = new Set(globSync([
- 'vendor/*/package.json',
- 'packages/*/*/package.json',
- 'apps/*/package.json',
-], { cwd: repoRoot }).map((filename) => {
- const manifest = JSON.parse(readFileSync(join(repoRoot, filename), 'utf8')) as Record
- if (typeof manifest.name !== 'string') throw new Error(`workspace package name is missing: ${filename}`)
- return manifest.name
-}))
-const source = process.env.DSH_GITHUB_REPOSITORY_PLUGIN_SOURCE
-const required = process.env.DSH_REQUIRE_GITHUB_REPOSITORY_PLUGIN_E2E === '1'
-const enabled = required || source !== undefined
-
-interface PublishedPackageRegistry {
- url: string
- requests: string[]
- close(): Promise
-}
-
-function publishedManifest(): Record {
- const manifest = JSON.parse(readFileSync(join(repositoryPluginPackage, 'package.json'), 'utf8')) as Record
- const version = manifest.version
- if (typeof version !== 'string') throw new Error('repository Plugin package version is missing')
- Reflect.deleteProperty(manifest, 'private')
- for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
- const dependencies = manifest[field]
- if (typeof dependencies !== 'object' || dependencies === null || Array.isArray(dependencies)) continue
- const entries = dependencies as Record
- for (const name of Object.keys(entries)) {
- if (releasePackageNames.has(name)) {
- entries[name] = version
- }
- }
- }
- return manifest
-}
-
-async function startPublishedPackageRegistry(root: string): Promise {
- const staging = join(root, 'published-repository-plugin')
- const artifacts = join(root, 'npm-registry-artifacts')
- mkdirSync(staging)
- mkdirSync(artifacts)
- cpSync(join(repositoryPluginPackage, 'lib'), join(staging, 'lib'), { recursive: true })
- for (const filename of ['README.md', 'README.zh.md', 'README.i18n.yaml']) {
- cpSync(join(repositoryPluginPackage, filename), join(staging, filename))
- }
- cpSync(join(repoRoot, 'LICENSE'), join(staging, 'LICENSE'))
- const manifest = publishedManifest()
- writeFileSync(join(staging, 'package.json'), `${JSON.stringify(manifest, undefined, 2)}\n`)
- const packed = await execa('pnpm', ['pack', '--pack-destination', artifacts], {
- cwd: staging,
- reject: false,
- })
- if (packed.exitCode !== 0) {
- throw new Error(`failed to pack the simulated published prepare package:\n${packed.stderr}\n${packed.stdout}`)
- }
- const tarballs = readdirSync(artifacts).filter(filename => filename.endsWith('.tgz'))
- if (tarballs.length !== 1) throw new Error(`expected one simulated published tarball, found ${tarballs.length}`)
- const tarball = readFileSync(join(artifacts, tarballs[0]!))
- const name = manifest.name as string
- const version = manifest.version as string
- const requests: string[] = []
- let registryUrl = ''
- const server = createServer((request, response) => {
- const path = decodeURIComponent(new URL(request.url ?? '/', registryUrl).pathname)
- requests.push(`${request.method ?? 'GET'} ${path}`)
- if (path === `/${name}`) {
- const metadata = {
- name,
- 'dist-tags': { latest: version },
- versions: {
- [version]: {
- ...manifest,
- dist: {
- tarball: `${registryUrl}${name}/-/${name.split('/').at(-1)}-${version}.tgz`,
- shasum: createHash('sha1').update(tarball).digest('hex'),
- integrity: `sha512-${createHash('sha512').update(tarball).digest('base64')}`,
- },
- },
- },
- }
- response.writeHead(200, { 'content-type': 'application/json' })
- response.end(JSON.stringify(metadata))
- return
- }
- if (path === `/${name}/-/${name.split('/').at(-1)}-${version}.tgz`) {
- response.writeHead(200, {
- 'content-type': 'application/octet-stream',
- 'content-length': String(tarball.length),
- })
- response.end(tarball)
- return
- }
- response.writeHead(404, { 'content-type': 'application/json' })
- response.end(JSON.stringify({ error: 'not found' }))
- })
- await new Promise((resolve, reject) => {
- server.once('error', reject)
- server.listen(0, '127.0.0.1', resolve)
- })
- const address = server.address()
- if (address === null || typeof address === 'string') throw new Error('simulated npm registry did not expose a TCP address')
- registryUrl = `http://127.0.0.1:${address.port}/`
- return {
- url: registryUrl,
- requests,
- close: () => new Promise((resolve, reject) => {
- server.close((error) => { if (error === undefined) resolve(); else reject(error) })
- }),
- }
-}
-
-describe.skipIf(!enabled)('dsh run GitHub repository Plugin installation', () => {
- it('installs the published prepare dependency, then builds and runs skill, MCP, and TypeScript Plugin contributions from a private exact GitHub source', async () => {
- expect(existsSync(dshBin), 'the repository Plugin acceptance must run the built dsh entry').toBe(true)
- expect(source, 'DSH_GITHUB_REPOSITORY_PLUGIN_SOURCE is required by this CI lane').toMatch(
- /^github:[^/\s#&]+\/[^/\s#&]+#[0-9a-f]{40}&path:\/.*\/\.dsh-plugin$/u,
- )
-
- const apiKey = 'github-repository-plugin-e2e-key'
- const server = await startMockLlmServer({
- sequence: ['tool_call_success', 'success'],
- apiKey,
- toolName: 'mcp__github_repository__proof',
- toolArguments: '{}',
- successText: 'trusted GitHub repository package reached dsh run',
- })
- const home = mkdtempSync(join(tmpdir(), 'dsh-github-repository-plugin-'))
- const registry = await startPublishedPackageRegistry(home)
- const npmrc = join(home, 'npmrc')
- writeFileSync(npmrc, `@deepseek-ai:registry=${registry.url}\n`)
- const hostBin = join(home, 'host-bin')
- mkdirSync(hostBin)
- writeFileSync(join(hostBin, 'dsh-plugin-prepare'), [
- '#!/bin/sh',
- 'echo "host PATH supplied dsh-plugin-prepare instead of the declared npm dependency" >&2',
- 'exit 91',
- '',
- ].join('\n'), { mode: 0o700 })
- const patch = join(home, 'github-repository-plugin.cordis.patch.yml')
- writeFileSync(patch, [
- '- id: repository-plugins',
- ' config:',
- ' repositories:',
- ` - ${JSON.stringify(source)}`,
- '- id: session-title-llm',
- ' disabled: true',
- '',
- ].join('\n'))
-
- try {
- const result = await execa(process.execPath, [
- dshBin,
- 'run',
- '--patch',
- patch,
- 'prove the private GitHub repository Plugin is active',
- ], {
- cwd: repoRoot,
- input: '',
- timeout: 180_000,
- killSignal: 'SIGKILL',
- reject: false,
- env: {
- ...process.env,
- DSH_HOME: home,
- DSH_TELEMETRY_DISABLED: '1',
- DEEPSEEK_API_KEY: apiKey,
- DEEPSEEK_BASE_URL: server.baseURL,
- NPM_CONFIG_USERCONFIG: npmrc,
- // A warm runner cache could satisfy the exact tarball without
- // contacting this test's registry, which would stop proving the
- // unpublished package was installed through the simulated release.
- PNPM_CONFIG_CACHE_DIR: join(home, 'pnpm-cache'),
- PNPM_CONFIG_STORE_DIR: join(home, 'pnpm-store'),
- PATH: process.env.PATH === undefined ? hostBin : `${hostBin}${delimiter}${process.env.PATH}`,
- },
- })
- if (result.timedOut) {
- throw new Error(`dsh GitHub repository Plugin run did not exit within 180s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
- }
- expect(result.exitCode, `${result.stderr}\nstdout:\n${result.stdout}`).toBe(0)
- expect(result.stdout).toBe('trusted GitHub repository package reached dsh run')
- expect(server.requests).toHaveLength(2)
- const runtimeDiagnostic = `${result.stderr}\nstdout:\n${result.stdout}`
- expect(registry.requests, runtimeDiagnostic).toContain('GET /@deepseek-ai/dsh-repository-plugin')
- expect(registry.requests, runtimeDiagnostic).toContain('GET /@deepseek-ai/dsh-repository-plugin/-/dsh-repository-plugin-0.0.1.tgz')
- const firstRequest = JSON.stringify(server.requests[0]!.body)
- const secondRequest = JSON.stringify(server.requests[1]!.body)
- expect(firstRequest, runtimeDiagnostic).toContain(
- 'Proves that dsh installed a private repository Plugin from an exact GitHub source.',
- )
- expect(firstRequest, runtimeDiagnostic).toContain('mcp__github_repository__proof')
- expect(firstRequest, runtimeDiagnostic).toContain('Proves that an MCP server compiled from the exact GitHub repository package is active.')
- expect(secondRequest, runtimeDiagnostic).toContain('MCP_FROM_GITHUB_REPOSITORY')
- expect(secondRequest, runtimeDiagnostic).toContain('TS_PLUGIN_FROM_GITHUB_REPOSITORY')
-
- const cacheRoot = join(home, 'cache', 'repository-plugins')
- const generations = readdirSync(cacheRoot, { withFileTypes: true }).filter(entry => entry.isDirectory())
- expect(generations).toHaveLength(1)
- const installed = join(cacheRoot, generations[0]!.name, 'node_modules', 'repository')
- const manifest = JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8')) as Record
- expect(manifest).toMatchObject({
- name: 'dsh-github-repository-plugin-e2e-fixture',
- private: true,
- scripts: {
- prepack: 'tsc --noEmit && tsdown src/plugin.ts src/mcp-server.ts --no-config --tsconfig tsconfig.json --out-dir lib --platform node --target es2024 --clean && dsh-plugin-prepare',
- },
- dsh: {
- skills: ['../skills'],
- mcpServers: './.mcp.json',
- entry: './lib/plugin.mjs',
- },
- dependencies: {
- '@modelcontextprotocol/sdk': '1.29.0',
- },
- devDependencies: {
- '@deepseek-ai/dsh-repository-plugin': '0.0.1',
- cordis: '4.0.0-rc.7',
- tsdown: '0.22.2',
- typescript: '6.0.3',
- },
- })
- expect(readFileSync(join(installed, 'dsh-plugin-assets/skills/0/github-source-proof/SKILL.md'), 'utf8'))
- .toContain('This skill exists only in the GitHub repository source fixture.')
- expect(readFileSync(join(installed, 'dsh-plugin-assets/.mcp.json'), 'utf8')).toContain('lib/mcp-server.mjs')
- expect(readFileSync(join(installed, 'lib/plugin.mjs'), 'utf8')).toContain('TS_PLUGIN_FROM_GITHUB_REPOSITORY')
- expect(readFileSync(join(installed, 'lib/mcp-server.mjs'), 'utf8')).toContain('MCP_FROM_GITHUB_REPOSITORY')
- expect(existsSync(join(installed, 'src'))).toBe(false)
- const installedRequire = createRequire(join(installed, 'lib/mcp-server.mjs'))
- expect(existsSync(installedRequire.resolve('@modelcontextprotocol/sdk/server/mcp.js'))).toBe(true)
- const wrapper = readFileSync(join(installed, 'dsh-plugin.mjs'), 'utf8')
- expect(wrapper).toContain('dsh-repository-plugin')
- expect(wrapper).toContain('await import(manifest.entry)')
- expect(wrapper).toContain('"entry":"./lib/plugin.mjs"')
- } finally {
- await server.close()
- await registry.close()
- rmSync(home, { recursive: true, force: true })
- }
- }, 190_000)
-})
diff --git a/apps/cli/tests/headless-shutdown.e2e.ts b/apps/cli/tests/headless-shutdown.e2e.ts
index b0bcbb7cae..42f2aad709 100644
--- a/apps/cli/tests/headless-shutdown.e2e.ts
+++ b/apps/cli/tests/headless-shutdown.e2e.ts
@@ -83,7 +83,7 @@ async function runHeadlessPtySmoke(): Promise {
].join('\n'))
const launch = resolveExampleLaunch({
srcBin: dshBinScript,
- configArgs: ['run', 'never complete'],
+ configArgs: ['--profile', 'headless', 'never complete'],
tsconfigPath,
env: {
DSH_HOME: home,
diff --git a/apps/cli/tests/install-script.spec.ts b/apps/cli/tests/install-script.spec.ts
deleted file mode 100644
index 871f72d056..0000000000
--- a/apps/cli/tests/install-script.spec.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-import { chmodSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
-import { mkdtemp, rm } from 'node:fs/promises'
-import { tmpdir } from 'node:os'
-import { join } from 'node:path'
-import { fileURLToPath } from 'node:url'
-import { execa } from 'execa'
-import { afterEach, describe, expect, it } from 'vitest'
-
-const installer = fileURLToPath(new URL('../../../scripts/install.sh', import.meta.url))
-const fixtures: string[] = []
-
-const PTY_DRIVER = String.raw`
-import errno, json, os, pty, select, signal, sys, time
-script, cwd, env_json, actions_json = sys.argv[1:]
-env = os.environ.copy()
-env.update(json.loads(env_json))
-actions = json.loads(actions_json)
-pid, fd = pty.fork()
-if pid == 0:
- os.chdir(cwd)
- os.execvpe("sh", ["sh", script], env)
-
-output = bytearray()
-action_index = 0
-deadline = time.monotonic() + 15
-status = None
-while time.monotonic() < deadline:
- ready, _, _ = select.select([fd], [], [], 0.05)
- if ready:
- try:
- chunk = os.read(fd, 65536)
- except OSError as error:
- if error.errno != errno.EIO:
- raise
- chunk = b""
- output.extend(chunk)
- while action_index < len(actions) and actions[action_index]["waitFor"].encode() in output:
- os.write(fd, actions[action_index]["send"].encode())
- action_index += 1
- waited, candidate = os.waitpid(pid, os.WNOHANG)
- if waited == pid:
- status = candidate
- break
-
-if status is None:
- os.kill(pid, signal.SIGKILL)
- _, status = os.waitpid(pid, 0)
-sys.stdout.buffer.write(output)
-if action_index != len(actions):
- sys.stderr.write(f"completed {action_index}/{len(actions)} PTY actions\n")
- sys.exit(124)
-sys.exit(os.waitstatus_to_exitcode(status))
-`
-
-interface Action {
- readonly waitFor: string
- readonly send: string
-}
-
-interface Fixture {
- readonly binDirectory: string
- readonly launchLog: string
- readonly pnpmLog: string
- readonly root: string
- readonly script: string
-}
-
-afterEach(async () => {
- await Promise.all(fixtures.splice(0).map(async (fixture) => { await rm(fixture, { force: true, recursive: true }) }))
-})
-
-function executable(path: string, content: string): void {
- writeFileSync(path, content)
- chmodSync(path, 0o755)
-}
-
-async function createFixture(): Promise {
- const root = await mkdtemp(join(tmpdir(), 'dsh-install-'))
- fixtures.push(root)
- const checkoutDirectory = join(root, 'checkout')
- const scriptsDirectory = join(checkoutDirectory, 'scripts')
- const sourceBinDirectory = join(checkoutDirectory, 'bin')
- const fakeBinDirectory = join(root, 'fake-bin')
- const binDirectory = join(root, 'path-bin')
- for (const directory of [scriptsDirectory, sourceBinDirectory, fakeBinDirectory, binDirectory, join(root, 'home/.dsh')]) {
- mkdirSync(directory, { recursive: true })
- }
- const script = join(scriptsDirectory, 'install.sh')
- copyFileSync(installer, script)
- const launchLog = join(root, 'launch.log')
- const pnpmLog = join(root, 'pnpm.log')
- executable(join(sourceBinDirectory, 'dsh'), '#!/bin/sh\nprintf \'%s\\n\' "$*" >"$DSH_TEST_LAUNCH_LOG"\n')
- executable(join(fakeBinDirectory, 'pnpm'), `#!/bin/sh
-if [ "\${1:-}" = --version ]; then printf '11.7.0\\n'; exit 0; fi
-printf '%s\\n' "$*" >>"$DSH_TEST_PNPM_LOG"
-`)
- await execa('git', ['init', '-q'], { cwd: checkoutDirectory })
- await execa('git', ['add', 'bin/dsh', 'scripts/install.sh'], { cwd: checkoutDirectory })
- await execa('git', [
- '-c', 'user.name=dsh-test',
- '-c', 'user.email=dsh-test@example.invalid',
- 'commit', '-qm', 'fixture',
- ], { cwd: checkoutDirectory })
- writeFileSync(join(root, 'home/.dsh/.env'), 'DEEPSEEK_API_KEY=test\n')
- return { binDirectory, launchLog, pnpmLog, root, script }
-}
-
-async function runInstaller(fixture: Fixture, actions: readonly Action[]): Promise {
- const result = await execa('python3', [
- '-c',
- PTY_DRIVER,
- fixture.script,
- fixture.root,
- JSON.stringify({
- DSH_BIN_DIR: fixture.binDirectory,
- DSH_HOME: join(fixture.root, 'home/.dsh'),
- DSH_TEST_LAUNCH_LOG: fixture.launchLog,
- DSH_TEST_PNPM_LOG: fixture.pnpmLog,
- HOME: join(fixture.root, 'home'),
- PATH: `${join(fixture.root, 'fake-bin')}:${fixture.binDirectory}:${process.env.PATH ?? ''}`,
- }),
- JSON.stringify(actions),
- ], { reject: false, stripFinalNewline: false, timeout: 20_000 })
- expect(result.exitCode, result.stderr).toBe(0)
- return result.stdout
-}
-
-describe.runIf(process.platform !== 'win32')('one-line installer launch', { timeout: 25_000 }, () => {
- it('builds and launches the Web UI', async () => {
- const fixture = await createFixture()
-
- const output = await runInstaller(fixture, [
- { waitFor: 'Replace it?', send: '\n' },
- ])
-
- expect(output).toContain('launching Web UI')
- expect(readFileSync(fixture.pnpmLog, 'utf8')).toBe('install\nrun build\n')
- expect(readFileSync(fixture.launchLog, 'utf8')).toBe('web\n')
- })
-})
diff --git a/apps/cli/tests/memory-mcp-configs.spec.ts b/apps/cli/tests/memory-mcp-configs.spec.ts
index 818b8f4561..ee8905c3b6 100644
--- a/apps/cli/tests/memory-mcp-configs.spec.ts
+++ b/apps/cli/tests/memory-mcp-configs.spec.ts
@@ -8,8 +8,8 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
-import type { Context } from 'cordis'
-import type { PatchOptions } from '@cordisjs/plugin-include'
+import type { Context } from '@deepseek-ai/cordis'
+import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
diff --git a/apps/cli/tests/source-launch.compat.spec.ts b/apps/cli/tests/source-launch.compat.spec.ts
index bd2268231b..597c3cff26 100644
--- a/apps/cli/tests/source-launch.compat.spec.ts
+++ b/apps/cli/tests/source-launch.compat.spec.ts
@@ -1,11 +1,12 @@
+import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { execa } from 'execa'
import { describe, expect, it } from 'vitest'
/**
- * Keyless smoke for the SOURCE `dsh` launcher: run `apps/cli/src/bin.ts`
- * with the exact production launch vector (`node --import tsx/esm`, the same
- * executable and arguments as `bin/dsh` and the root `dsh`/`demo:web` scripts) and assert the
+ * Keyless smoke for SOURCE `dsh` execution: run `apps/cli/src/bin.ts`
+ * with the exact production runtime vector (`node --import tsx/esm`, the
+ * vector the root `dsh` script invokes after building) and assert the
* required-config diagnostic. The Node compatibility matrix runs this
* WHOLE file, so a Node release changing module hooks or TypeScript handling
* breaks this gate instead of every developer's `pnpm dsh`; the built-bin
@@ -16,6 +17,13 @@ const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
const dshSourceBin = 'apps/cli/src/bin.ts'
describe('dsh SOURCE launcher (node --import tsx/esm)', () => {
+ it('builds before launching the source CLI', async () => {
+ const rootPackage = JSON.parse(await readFile(new URL('../../../package.json', import.meta.url), 'utf8')) as {
+ readonly scripts?: Record
+ }
+ expect(rootPackage.scripts?.dsh).toBe('pnpm run build && node --import tsx/esm apps/cli/src/bin.ts')
+ })
+
it('boots the source entry and requires a profile', async () => {
const result = await execa(process.execPath, ['--import', 'tsx/esm', dshSourceBin], {
cwd: repoRoot,
diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts
index 004de203c8..7947fb0807 100644
--- a/apps/cli/tests/web-agent-presets.e2e.ts
+++ b/apps/cli/tests/web-agent-presets.e2e.ts
@@ -3,18 +3,23 @@ import { mkdir, mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
-import { Context } from 'cordis'
+import { Context } from '@deepseek-ai/cordis'
import { boot, healProfilesModuleFallback, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
+import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { Agent } from '@deepseek-ai/dsh-agent'
-import type { PatchOptions } from '@cordisjs/plugin-include'
-import { beforeAll, describe, expect, it } from 'vitest'
+import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
+import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { resolveSessionPreset, SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-presets'
import { applyChildComposition, childSessionMeta } from '@deepseek-ai/dsh-subagent'
import { CallId } from '@deepseek-ai/dsh-llm'
+import type { BasicCompactService } from '@deepseek-ai/dsh-compact-basic'
import type {} from '@deepseek-ai/dsh-skill'
import type {} from '@deepseek-ai/dsh-tools'
+// Type-only: resolves `ctx.get('sessionProjections')` and `ctx.get('tokenMeter')`.
+import type {} from '@deepseek-ai/dsh-session-projection'
+import type {} from '@deepseek-ai/dsh-token-meter'
const CONFIG_DIR = fileURLToPath(new URL('../config/', import.meta.url))
const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
@@ -23,6 +28,15 @@ const BASE_PATCH = join(REPO_ROOT, 'packages/bundle/base/cordis.patch.yml')
const WEB_PATCH = join(REPO_ROOT, 'packages/bundle/web-app/cordis.patch.yml')
/** The installation anchor whose dependency surface the preset module fallback mirrors. */
const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json')
+const MINIMAL_PROMPT = 'You are a helpful software engineer assistant.'
+const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
+* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
+* You don't have access to the internet via this tool.
+* You do have access to a mirror of common linux and python packages via apt and pip.
+* State is persistent across command calls and discussions with the user.
+* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
+* Please avoid commands that may produce a very large amount of output.
+* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.`
/**
* Boot the shipped Web composition, minus the rows that would bind a port,
@@ -90,12 +104,26 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
await mkdir(profileDir, { recursive: true })
const rootConfig = join(profileDir, 'cordis.yml')
await writeFile(rootConfig, '[]\n')
- return await boot('dsh-test', rootConfig, patches)
+ return await boot('dsh-test', rootConfig, patches, (bootCtx) => {
+ provideCmdline(bootCtx, { args: [], exit: () => {} })
+ })
}
const toolNames = (ctx: Context, agent?: Agent): string[] =>
ctx.tools.schemas(agent).map(schema => schema.name).sort()
+function enablePresetTool(composition: string, id: string): string {
+ const row = ` - id: ${id}\n`
+ const start = composition.indexOf(row)
+ if (start < 0) throw new Error(`missing preset row ${id}`)
+ const end = composition.indexOf('\n - id:', start + row.length)
+ const disabled = composition.indexOf(' disabled: true\n', start)
+ if (disabled < 0 || (end >= 0 && disabled > end)) {
+ throw new Error(`preset row ${id} is not disabled`)
+ }
+ return composition.slice(0, disabled) + composition.slice(disabled + ' disabled: true\n'.length)
+}
+
let ctx: Context
beforeAll(async () => {
const settingsFile = join(await mkdtemp(join(tmpdir(), 'dsh-web-presets-')), 'settings.yaml')
@@ -113,6 +141,33 @@ describe('the shipped Web composition', () => {
expect(toolNames(ctx)).toEqual([])
})
+ it('keeps the token meter and its context-meter projections on the host plane', async () => {
+ // Read before any preset in this file mounts, which is what makes this an
+ // ownership assertion rather than a mount-order coincidence: a preset-side
+ // meter sits behind an `isolate` realm and is invisible to `ctx.get`.
+ //
+ // The projection registry is process-wide rather than scope-layered, so a
+ // preset-side meter would also make the browser's context meter appear for
+ // a `minimal` session the moment some OTHER session mounted a preset that
+ // carries one, and vanish entirely in a process that only ever ran
+ // `minimal`. Host ownership is what makes the meter a per-session fact.
+ expect(ctx.get('tokenMeter')).toBeDefined()
+ const projections = ctx.get('sessionProjections')
+ if (projections === undefined) throw new Error('the Web composition must compose a projection registry')
+ const handle = await ctx.agents.create({
+ sessionId: SessionId('preset-minimal-meter'),
+ setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
+ })
+ try {
+ // A subset assertion: `tasks`, `goal`, and the rest register into the
+ // same process-wide table, and this is about the meter's three units.
+ expect(Object.keys(projections.snapshot(handle.agent.session).values))
+ .toEqual(expect.arrayContaining(['contextBreakdown', 'contextPressure', 'tokenUsage']))
+ } finally {
+ await handle.dispose()
+ }
+ })
+
it('supplies both shipped presets, and only those, from the system root', async () => {
const listed = await ctx.agentPresets.list()
@@ -134,8 +189,8 @@ describe('the shipped Web composition', () => {
// depend on ripgrep being present on the machine.
expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([
'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
- 'get_goal', 'interrupt_agent', 'list_agents', 'ralph', 'read', 'send_message', 'skill',
- 'str_replace_editor', 'subagent', 'subagent_fork', 'task_kill',
+ 'get_goal', 'interrupt_agent', 'list_agents', 'ralph', 'read', 'read_image', 'send_message', 'skill',
+ 'subagent', 'subagent_fork', 'task_kill',
'task_list', 'task_output', 'todo_write', 'update_goal', 'web_search',
'workflow', 'write',
])
@@ -144,14 +199,30 @@ describe('the shipped Web composition', () => {
}
})
- it('composes exactly two tools from `minimal`', async () => {
+ it('composes the exact RL prompt and two tools from `minimal`', async () => {
const handle = await ctx.agents.create({
sessionId: SessionId('preset-minimal'),
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
})
try {
- // Exactly what the preset lists — nothing arrives from the host.
- expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'str_replace_editor'])
+ const assembly = await ctx.systemPrompt.assemble({ scope: handle.agent })
+ expect(assembly.sections).toEqual([
+ { name: 'deployment:persona', text: MINIMAL_PROMPT },
+ ])
+ expect(assembly.tools.map(tool => tool.name)).toEqual(['bash', 'str_replace_editor'])
+ expect(assembly.tools.find(tool => tool.name === 'bash')?.description).toBe(MINIMAL_BASH_DESCRIPTION)
+ expect(JSON.stringify(assembly.tools.find(tool => tool.name === 'str_replace_editor')?.parameters))
+ .toContain('Absolute path')
+ const compact = ctx.agentPresets.serviceFor(handle.agent, 'compact')
+ expect(compact).toBeDefined()
+ expect((compact as BasicCompactService).config).toMatchObject({
+ thresholdRatio: 0.8,
+ retainTokens: 20480,
+ summarizationProvider: '',
+ summarizationModel: '',
+ maxTokens: 8192,
+ compactionRetries: 1,
+ })
} finally {
await handle.dispose()
}
@@ -191,6 +262,7 @@ describe('the shipped Web composition', () => {
expect(tools).toEqual(expect.arrayContaining(['cordis_inspect', 'cordis_mount', 'cordis_unmount']))
// And it keeps the standard agent's own tools rather than replacing them.
expect(tools).toEqual(expect.arrayContaining(['bash', 'read', 'edit', 'skill']))
+ expect(tools).not.toContain('str_replace_editor')
// The preset's own authoring skill registers into ITS layer of the host
// registry: the cordis agent's view carries it, the global view does not.
@@ -217,9 +289,9 @@ describe('the shipped Web composition', () => {
// the capabilities — so the assembly is what carries the claim.
const assembly = await ctx.systemPrompt.assemble({ scope: coded.agent })
expect(assembly.tools.map(tool => tool.name)).toEqual(['run_code'])
- expect(toolNames(ctx, coded.agent)).toContain('str_replace_editor')
+ expect(toolNames(ctx, coded.agent)).not.toContain('str_replace_editor')
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? ''
- expect(sdk).toContain('str_replace_editor')
+ expect(sdk).not.toContain('str_replace_editor')
expect(sdk).toContain('web_search')
// The presentation is this agent's alone: the deployment default is
@@ -340,18 +412,96 @@ describe('the shipped Web composition', () => {
expect(await readFile(path, 'utf8')).toBe(before)
})
+})
- it('gives each session its own persona', async () => {
- const handle = await ctx.agents.create({
- sessionId: SessionId('preset-persona'),
- setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
+describe('product subagent rows in user presets', () => {
+ let productCtx: Context
+ const ids = ['products-none', 'products-codex', 'products-claude', 'products-both'] as const
+
+ beforeAll(async () => {
+ const root = await mkdtemp(join(tmpdir(), 'dsh-product-presets-'))
+ const userRoot = join(root, 'presets')
+ const settingsFile = join(root, 'settings.yaml')
+ const standard = await readFile(join(CONFIG_DIR, 'agent-presets', 'standard', 'agent.cordis.yml'), 'utf8')
+ await writeFile(settingsFile, '{}\n')
+ for (const id of ids) {
+ let composition = standard
+ if (id === 'products-codex' || id === 'products-both') {
+ composition = enablePresetTool(composition, 'tool-subagent-codex')
+ }
+ if (id === 'products-claude' || id === 'products-both') {
+ composition = enablePresetTool(composition, 'tool-subagent-claude-code')
+ }
+ const directory = join(userRoot, id)
+ await mkdir(directory, { recursive: true })
+ await writeFile(join(directory, 'agent.cordis.yml'), composition)
+ }
+ productCtx = await bootWeb(settingsFile, [{
+ id: 'agent-presets',
+ config: {
+ default: 'standard',
+ roots: [
+ { path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' },
+ { path: userRoot, trust: 'user' },
+ ],
+ },
+ }])
+ }, 120_000)
+
+ afterAll(async () => {
+ await productCtx.fiber.dispose()
+ })
+
+ it('composes none, either product, or both without changing the shared host registry', async () => {
+ const expected = new Map([
+ ['products-none', []],
+ ['products-codex', ['subagent_codex']],
+ ['products-claude', ['subagent_claude_code']],
+ ['products-both', ['subagent_claude_code', 'subagent_codex']],
+ ])
+ expect(productCtx.subagents.list()).toEqual(expect.arrayContaining([
+ 'spawn', 'fork', 'codex', 'claude-code',
+ ]))
+
+ for (const [id, productTools] of expected) {
+ const handle = await productCtx.agents.create({
+ sessionId: SessionId(`preset-${id}`),
+ setup: agentCtx => productCtx.agentPresets.mount(agentCtx, id).then(() => undefined),
+ })
+ try {
+ const tools = toolNames(productCtx, handle.agent)
+ expect(tools.filter(name => name === 'subagent_codex' || name === 'subagent_claude_code'))
+ .toEqual(productTools)
+ } finally {
+ await handle.dispose()
+ }
+ }
+ })
+
+ it('applies a product-row edit only to later sessions on the preset', async () => {
+ const preset = await productCtx.agentPresets.resolve('products-none')
+ const original = await readFile(preset.path, 'utf8')
+ const existing = await productCtx.agents.create({
+ sessionId: SessionId('preset-product-generation-existing'),
+ setup: agentCtx => productCtx.agentPresets.mount(agentCtx, 'products-none').then(() => undefined),
})
try {
- const assembly = await ctx.systemPrompt.assemble({ scope: handle.agent })
- expect(assembly.sections.find(section => section.name === 'deployment:persona')?.text)
- .toContain('You are a coding agent powered by')
+ expect(toolNames(productCtx, existing.agent)).not.toContain('subagent_codex')
+ await writeFile(preset.path, enablePresetTool(original, 'tool-subagent-codex'))
+
+ const later = await productCtx.agents.create({
+ sessionId: SessionId('preset-product-generation-later'),
+ setup: agentCtx => productCtx.agentPresets.mount(agentCtx, 'products-none').then(() => undefined),
+ })
+ try {
+ expect(toolNames(productCtx, existing.agent)).not.toContain('subagent_codex')
+ expect(toolNames(productCtx, later.agent)).toContain('subagent_codex')
+ } finally {
+ await later.dispose()
+ }
} finally {
- await handle.dispose()
+ await existing.dispose()
+ await writeFile(preset.path, original)
}
})
})
diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json
index d830e8fba6..cecf7a4bb9 100644
--- a/apps/cli/tsconfig.json
+++ b/apps/cli/tsconfig.json
@@ -20,6 +20,9 @@
{
"path": "../../packages/boot/app-boot"
},
+ {
+ "path": "../../packages/boot/cmdline"
+ },
{
"path": "../../packages/bundle/base"
},
@@ -47,6 +50,9 @@
{
"path": "../../packages/core/tools"
},
+ {
+ "path": "../../packages/util/environment"
+ },
{
"path": "../../packages/util/paths"
},
diff --git a/apps/web/package.json b/apps/web/package.json
index c58e5b9682..aed4488355 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-frontend",
"description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web",
- "version": "0.0.1",
- "private": true,
+ "version": "0.0.1-rc.1",
+ "publishConfig": {
+ "access": "restricted"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
+ "directory": "apps/web"
+ },
"type": "module",
"exports": {
"./dist/*": "./dist/*",
@@ -23,11 +30,12 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
- "@cordisjs/plugin-group": "workspace:^",
+ "@deepseek-ai/cordis-plugin-group": "workspace:^",
"@deepseek-ai/dsh-client-modules": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-client-web-react": "workspace:^",
+ "@deepseek-ai/dsh-cmdline": "workspace:^",
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
"@types/node": "^22.0.0",
"@types/react": "~18.3.1",
@@ -36,6 +44,7 @@
"playwright": "^1.49.0",
"typescript": "^6.0.3",
"vite": "^6.0.0",
- "vitest": "^4.1.8"
+ "vitest": "^4.1.8",
+ "fflate": "^0.8.2"
}
}
diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg
index 8a8fc56752..c92f15d43b 100644
--- a/apps/web/public/favicon.svg
+++ b/apps/web/public/favicon.svg
@@ -1,3 +1,8 @@
\ No newline at end of file
+
diff --git a/apps/web/tests/agent-preset-authoring.e2e.ts b/apps/web/tests/agent-preset-authoring.e2e.ts
index e8ff5aa538..1a27f96c6e 100644
--- a/apps/web/tests/agent-preset-authoring.e2e.ts
+++ b/apps/web/tests/agent-preset-authoring.e2e.ts
@@ -161,7 +161,7 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
expect(composition).toBe(await readFile(join(SHIPPED_PRESETS, 'minimal', 'agent.cordis.yml'), 'utf8'))
const metadata = await readFile(join(userRoot, 'my-agent', 'preset.yml'), 'utf8')
expect(metadata).toContain('name: 我的模式')
- expect(metadata).toContain('description: 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。')
+ expect(metadata).toContain('description: 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。')
expect(metadata).not.toContain('order:')
}, 60_000)
@@ -176,8 +176,10 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
await expect.poll(async () => dialog.getByText('我的模式').count(), { timeout: 10_000 }).toBe(0)
expect(existsSync(join(userRoot, 'my-agent'))).toBe(false)
- // Custom group gone with its only member; the shipped set stands.
- expect(await dialog.getByRole('heading', { name: '自定义' }).count()).toBe(0)
+ // The custom group outlives its only member: the heading stays with the
+ // creator entry so the place to author a preset never disappears.
+ expect(await dialog.getByRole('heading', { name: '自定义' }).count()).toBe(1)
+ expect(await dialog.getByRole('button', { name: '用「创造模式」创作自定义预设' }).count()).toBe(1)
expect(await dialog.getByText('标准模式').count()).toBeGreaterThan(0)
}, 60_000)
@@ -194,18 +196,18 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
const dialog = settingsDialog()
await dialog.getByRole('button', { name: '通用设置' }).click()
await dialog.getByRole('button', { name: 'Agent 预设' }).click()
- await dialog.getByText('已损坏').first().waitFor({ timeout: 10_000 })
+ await dialog.getByText('加载失败').first().waitFor({ timeout: 10_000 })
const snapshot = withPresetRoot(
await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd))
await compareOrRefreshGolden(DAMAGED_EXPECTED, snapshot, MODE)
// Both damage shapes surface as marked, unselectable, uncopyable cards
// that still carry their metadata and the discovery-reported reason.
- expect(snapshot).toContain('已损坏: broken-yaml')
- expect(snapshot).toContain('已损坏: 幽灵预设')
+ expect(snapshot).toContain('加载失败: broken-yaml')
+ expect(snapshot).toContain('加载失败: 幽灵预设')
expect(snapshot).toContain('not valid YAML')
expect(snapshot).toContain('agent.cordis.yml is missing')
- expect(await dialog.getByRole('button', { name: '已损坏: broken-yaml' }).isDisabled()).toBe(true)
+ expect(await dialog.getByRole('button', { name: '加载失败: broken-yaml' }).isDisabled()).toBe(true)
expect(await dialog.getByRole('button', { name: '复制: 幽灵预设' }).isDisabled()).toBe(true)
// A broken card offers no "set default" affordance at all — the aria name
// IS the broken marking, so the picking name must not exist.
diff --git a/apps/web/tests/background-task-list.e2e.ts b/apps/web/tests/background-task-list.e2e.ts
new file mode 100644
index 0000000000..734d43131a
--- /dev/null
+++ b/apps/web/tests/background-task-list.e2e.ts
@@ -0,0 +1,133 @@
+// Web e2e scenario: the session-header background-task list over the real
+// host. No model call is involved — a genuine `run_in_background` bash call
+// registers with `ctx.tasks`, and the assertion chain is the whole delivery
+// path: registry change feed → api-proxy `session/tasks` frame → the client's
+// `tasksBySession` mirror → the header action.
+import { readFile } from 'node:fs/promises'
+import { fileURLToPath } from 'node:url'
+import { join } from 'node:path'
+import type { Browser, Page } from 'playwright'
+import { chromium } from 'playwright'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import type { Agent } from '@deepseek-ai/dsh-agent'
+import { CallId } from '@deepseek-ai/dsh-llm'
+import { SessionId } from '@deepseek-ai/dsh-session'
+import { TaskId } from '@deepseek-ai/dsh-tasks'
+import {
+ assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
+ launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
+} from './scaffold.ts'
+import { newEnglishPage, saveFailureShot } from './support.ts'
+
+const FIXTURE = fileURLToPath(new URL('./snapshots/fresh-round-trip/session.jsonl', import.meta.url))
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/background-task-list', import.meta.url))
+const RUNNING_EXPECTED = join(SNAPSHOT_DIR, 'running.expected.md')
+const SETTLED_EXPECTED = join(SNAPSHOT_DIR, 'settled.expected.md')
+const MODE = webSnapshotMode()
+const SEED_ID = 'background-task-list-web-e2e'
+// Long enough that the running assertions never race the process exiting on
+// their own; the test kills it explicitly to reach the settled state.
+const COMMAND = 'sleep 45'
+
+/**
+ * Wait for the Host to publish the live Agent that opening a session resumes.
+ * @param scaffold - the booted web scaffold.
+ * @param sessionId - the opened session's identity.
+ * @returns the registered Agent instance.
+ */
+async function liveAgent(scaffold: WebScaffold, sessionId: SessionId): Promise {
+ const deadline = Date.now() + 30_000
+ for (;;) {
+ const found = scaffold.ctx.agents.get(sessionId)
+ if (found !== undefined) return found
+ if (Date.now() > deadline) throw new Error(`opening session "${sessionId}" published no live Agent`)
+ await new Promise(resolve => setTimeout(resolve, 100))
+ }
+}
+
+describe.skipIf(MODE === 'record')('web e2e: background task list', () => {
+ let scaffold: WebScaffold
+ let browser: Browser
+ let page: Page
+ let tripwire: ReturnType
+ let agent: Agent
+ let taskId: TaskId
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold({})
+ await seedSession(scaffold, await readFile(FIXTURE, 'utf8'), SEED_ID)
+ browser = await chromium.launch()
+ page = await newEnglishPage(browser)
+ tripwire = watchConsole(page)
+ await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+
+ const groupRow = page.locator('[role="treeitem"]').first()
+ await groupRow.waitFor({ timeout: 15_000 })
+ await groupRow.click()
+ const sessionRow = page.locator('[role="treeitem"]').nth(1)
+ await sessionRow.waitFor({ timeout: 10_000 })
+ await sessionRow.click()
+
+ // Opening the session drives the Host's ordinary Agent resolution; the
+ // task owner must be that exact live instance, never a second one.
+ // `expect.poll` is test-scoped, so this hook polls by hand.
+ agent = await liveAgent(scaffold, SessionId(SEED_ID))
+ }, 120_000)
+
+ afterAll(async () => {
+ await browser?.close()
+ await scaffold?.close()
+ })
+
+ it('shows a running background task in the session header without a refresh', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-background-task-running'))
+ // Point assertion, not a poll: `expect.poll` retries until a predicate
+ // holds, so polling for zero passes at t=0 and proves nothing. The
+ // "renders nothing without a task" branch is owned by the component suite.
+ const trigger = page.getByRole('button', { name: '1 background task running' })
+ expect(await trigger.count()).toBe(0)
+
+ const started = await scaffold.ctx.tools.execute({
+ signal: new AbortController().signal,
+ callId: CallId('background-task-list-e2e'),
+ name: 'bash',
+ arguments: { command: COMMAND, description: 'Hold a background slot open', run_in_background: true },
+ agent,
+ })
+ const reported = started.content.map(block => block.type === 'text' ? block.text : '').join('')
+ const matched = /\bbash-\d+\b/.exec(reported)
+ if (matched === null) throw new Error(`background bash reported no task id: ${reported}`)
+ taskId = TaskId(matched[0])
+
+ await trigger.waitFor({ timeout: 15_000 })
+ await trigger.click()
+ const row = page.getByRole('list', { name: 'Background tasks' }).getByRole('listitem').first()
+ await row.waitFor({ timeout: 10_000 })
+ await expect.poll(() => row.textContent()).toContain(COMMAND)
+
+ const snapshot = await captureStableAria(page, '[class*="menu"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(RUNNING_EXPECTED, snapshot, MODE)
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ }, 60_000)
+
+ it('flips the open list to the cancelled outcome when the registry settles it', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-background-task-settled'))
+ expect(scaffold.ctx.tasks.kill(taskId, agent, 'web e2e cancellation')).toBe('requested')
+
+ // The trigger drops its live count once the task leaves running/stopping,
+ // which is also the proof that settlement reached the browser unprompted.
+ const idle = page.getByRole('button', { name: '1 background task' })
+ await idle.waitFor({ timeout: 20_000 })
+
+ const snapshot = await captureStableAria(page, '[class*="menu"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(SETTLED_EXPECTED, snapshot, MODE)
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ }, 60_000)
+
+ it('keeps its snapshot inventory closed', async () => {
+ await assertFixtureInventory(SNAPSHOT_DIR, ['running.expected.md', 'settled.expected.md'])
+ })
+})
diff --git a/apps/web/tests/core-web-profile.snapshot.ts b/apps/web/tests/core-web-profile.snapshot.ts
deleted file mode 100644
index 1178390837..0000000000
--- a/apps/web/tests/core-web-profile.snapshot.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { writeFile } from 'node:fs/promises'
-import { join } from 'node:path'
-import { fileURLToPath } from 'node:url'
-import { afterAll, beforeAll, describe, expect, it } from 'vitest'
-import type { AgentHandle } from '@deepseek-ai/dsh-agent'
-import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
-import { SessionId } from '@deepseek-ai/dsh-session'
-import { assertFixtureInventory, launchWebScaffold, type WebScaffold } from './scaffold.ts'
-
-const CORE_WEB_OVERLAY = fileURLToPath(new URL('../../cli/config/core-web.cordis.yml', import.meta.url))
-const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/core-web-profile', import.meta.url))
-const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
-const PROMPT = 'Reply exactly CORE_WEB_REQUEST_OK and stop.'
-
-describe('core Web profile', () => {
- let scaffold: WebScaffold
- let agentHandle: AgentHandle
-
- beforeAll(async () => {
- const systemPrompt = process.env.DSH_SYSTEM_PROMPT
- Reflect.deleteProperty(process.env, 'DSH_SYSTEM_PROMPT')
- try {
- scaffold = await launchWebScaffold({ extraOverlayPath: CORE_WEB_OVERLAY, replayFixture: FIXTURE })
- } finally {
- if (systemPrompt !== undefined) process.env.DSH_SYSTEM_PROMPT = systemPrompt
- }
- agentHandle = await scaffold.ctx.agents.create({
- sessionId: SessionId('core-web-profile-smoke'),
- meta: { cwd: scaffold.workspaceCwd },
- agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
- })
- })
-
- afterAll(async () => {
- const failures: unknown[] = []
- await agentHandle?.dispose().catch((error: unknown) => failures.push(error))
- await scaffold?.close().catch((error: unknown) => failures.push(error))
- if (failures.length === 1) throw failures[0]
- if (failures.length > 1) throw new AggregateError(failures, 'core Web profile smoke teardown failed')
- })
-
- it('sends the RL prompt and tool schemas through a real request, then executes both tools', async () => {
- agentHandle.agent.followup(createUserMessage({
- content: [{ type: 'text', text: PROMPT }],
- source: { kind: 'user' },
- }))
- await agentHandle.agent.whenIdle()
-
- const requestHeader = agentHandle.agent.session.requestHeader()
- if (requestHeader === undefined) throw new Error('the core Web agent issued no model request')
-
- const seedPath = join(scaffold.workspaceCwd, 'profile-smoke.txt')
- await writeFile(seedPath, 'CORE_WEB_EDITOR_OK\n')
- const signal = new AbortController().signal
- const bash = await scaffold.ctx.tools.execute({
- signal,
- callId: CallId('core-web-bash-smoke'),
- name: 'bash',
- arguments: { command: "printf 'CORE_WEB_BASH_OK\\n'" },
- agent: agentHandle.agent,
- })
- const editor = await scaffold.ctx.tools.execute({
- signal,
- callId: CallId('core-web-editor-smoke'),
- name: 'str_replace_editor',
- arguments: { command: 'view', path: seedPath },
- agent: agentHandle.agent,
- })
-
- const text = (result: typeof bash): string => result.content
- .filter(block => block.type === 'text')
- .map(block => block.text)
- .join('')
- .replaceAll(scaffold.workspaceCwd, '{{cwd}}')
- .trimEnd()
-
- expect({
- prompt: requestHeader.system,
- tools: requestHeader.tools?.map(tool => tool.name),
- bash: text(bash),
- editor: text(editor),
- }).toMatchInlineSnapshot(`
- {
- "bash": "CORE_WEB_BASH_OK",
- "editor": "Here's the content of {{cwd}}/profile-smoke.txt with line numbers (which has a total of 2 lines):
- 1 CORE_WEB_EDITOR_OK
- 2",
- "prompt": "You are a helpful software engineer assistant.",
- "tools": [
- "bash",
- "str_replace_editor",
- ],
- }
- `)
- expect(requestHeader.tools).toEqual(scaffold.ctx.tools.schemas(agentHandle.agent))
-
- const entries = [...scaffold.ctx.loader.entries()]
- expect(entries.find(entry => entry.options.id === 'persistent-bash')?.fiber).toBeDefined()
- expect(entries.find(entry => entry.options.id === 'pty-local')?.fiber).toBeDefined()
- expect(entries.find(entry => entry.options.id === 'str-replace-editor')?.fiber).toBeDefined()
- expect(entries.find(entry => entry.options.id === 'web-runtime')?.fiber).toBeDefined()
- expect(entries.find(entry => entry.options.id === 'workspace-context')?.fiber).toBeUndefined()
- await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl'])
- })
-
- it('uses DSH_SYSTEM_PROMPT as the complete prompt when configured', async () => {
- const previous = process.env.DSH_SYSTEM_PROMPT
- process.env.DSH_SYSTEM_PROMPT = 'RL prompt override'
- let overrideScaffold: WebScaffold | undefined
- let overrideAgent: AgentHandle | undefined
- try {
- overrideScaffold = await launchWebScaffold({ extraOverlayPath: CORE_WEB_OVERLAY, replayFixture: FIXTURE })
- overrideAgent = await overrideScaffold.ctx.agents.create({
- sessionId: SessionId('core-web-profile-override'),
- meta: { cwd: overrideScaffold.workspaceCwd },
- agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
- })
- overrideAgent.agent.followup(createUserMessage({
- content: [{ type: 'text', text: PROMPT }],
- source: { kind: 'user' },
- }))
- await overrideAgent.agent.whenIdle()
- expect(overrideAgent.agent.session.requestHeader()?.system).toBe('RL prompt override')
- } finally {
- try {
- await overrideAgent?.dispose()
- } finally {
- try {
- await overrideScaffold?.close()
- } finally {
- if (previous === undefined) Reflect.deleteProperty(process.env, 'DSH_SYSTEM_PROMPT')
- else process.env.DSH_SYSTEM_PROMPT = previous
- }
- }
- }
- })
-})
diff --git a/apps/web/tests/feedback-command.e2e.ts b/apps/web/tests/feedback-command.e2e.ts
new file mode 100644
index 0000000000..577e77a97a
--- /dev/null
+++ b/apps/web/tests/feedback-command.e2e.ts
@@ -0,0 +1,101 @@
+// Keyless assembled-browser coverage for the /feedback command over the
+// shipped Web bundles and the real host wire. The command plane settles
+// without a model turn: the host appends the log-only command/run +
+// feedback/record + command/done lifecycle, and the transcript renders the
+// acknowledgement — the recorded session id plus the session-sharing
+// disclosure — as a persistent command row. The scaffold mounts the shipped
+// telemetry row in FULL mode against a local dead endpoint (no record leaves
+// the process), so the golden pins the shipped default sentence
+// `Session sharing is enabled.`; the per-status sentences are pinned by the
+// package and OTel unit tests.
+import { readFile } from 'node:fs/promises'
+import { fileURLToPath } from 'node:url'
+import { join } from 'node:path'
+import type { Browser, Page } from 'playwright'
+import { chromium } from 'playwright'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import {
+ assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
+ launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
+} from './scaffold.ts'
+import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
+
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/feedback-command', import.meta.url))
+const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
+const ACK_EXPECTED = join(SNAPSHOT_DIR, 'ack.expected.md')
+const MODE = webSnapshotMode()
+// Discard port: loopback listener never binds, so FULL telemetry discloses
+// the shipped default policy without any record reaching a collector.
+const TELEMETRY_URL = 'http://127.0.0.1:9/v1/logs'
+
+const PROMPT = 'Reply with the single word LIGHTHOUSE and stop.'
+
+describe('web e2e: /feedback command acknowledgement', () => {
+ let scaffold: WebScaffold
+ let browser: Browser
+ let page: Page
+ let tripwire: ReturnType
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold({
+ telemetryUrl: TELEMETRY_URL,
+ ...(MODE === 'record' ? {} : { replayFixture: FIXTURE }),
+ })
+ browser = await chromium.launch()
+ page = await newEnglishPage(browser)
+ tripwire = watchConsole(page)
+ await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ // Fresh world: connecting a workspace births the blank session whose
+ // live composer accepts the slash line.
+ await connectFreshWorkspace(page, scaffold.workspaceCwd)
+ }, 120_000)
+
+ afterAll(async () => {
+ await browser?.close()
+ await scaffold?.close()
+ })
+
+ it('drives the recorded prompt to a settled turn (all modes)', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-feedback-drive'))
+ if (MODE !== 'record') {
+ // Drift guard: the committed fixture must carry exactly the drive prompt.
+ expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT])
+ }
+ const input = page.locator('textarea').first()
+ await input.waitFor({ timeout: 10_000 })
+ // Arm the turn-boundary waiter BEFORE sending, so a burst replay cannot
+ // miss the turn/end that settles the recorded turn.
+ const settled = scaffold.whenTurnSettled()
+ await input.fill(PROMPT)
+ await input.press('Enter')
+ const sessionId = await settled
+ if (MODE === 'record') {
+ await recordFixture(scaffold, sessionId, FIXTURE)
+ }
+ }, 60_000)
+
+ it.skipIf(MODE === 'record')('records feedback and renders the acknowledgement with session id and sharing status', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-feedback-command'))
+ // The drive test settled the recorded turn: the transcript is active (a
+ // command row does not render while a fresh session is still blank) and
+ // the replayed reply is on screen.
+ await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
+ const input = page.locator('textarea').first()
+ await input.fill('/feedback the diff view is unreadable')
+ await input.press('Enter')
+ // The command plane settles without a model turn: the ack row names the
+ // recorded session and the mounted FULL backend's disclosure.
+ await page.getByText(/Feedback recorded for session/).waitFor({ timeout: 10_000 })
+ expect(await page.getByText(/Session sharing is enabled/).count()).toBe(1)
+ const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(ACK_EXPECTED, snapshot, MODE)
+
+ expect(tripwire.pageErrors).toEqual([])
+ expect(tripwire.warnings).toEqual([])
+ }, 60_000)
+
+ it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
+ await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl', 'ack.expected.md'])
+ })
+})
diff --git a/apps/web/tests/hmr-live.e2e.ts b/apps/web/tests/hmr-live.e2e.ts
index cafd0fb474..57a3867187 100644
--- a/apps/web/tests/hmr-live.e2e.ts
+++ b/apps/web/tests/hmr-live.e2e.ts
@@ -6,8 +6,8 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { chromium } from 'playwright'
import { expect, it } from 'vitest'
-import { Context } from 'cordis'
-import type { Fiber } from 'cordis'
+import { Context } from '@deepseek-ai/cordis'
+import type { Fiber } from '@deepseek-ai/cordis'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import { REPO_ROOT } from './support.ts'
diff --git a/apps/web/tests/math-rendering.e2e.ts b/apps/web/tests/math-rendering.e2e.ts
index b183fe7df5..de24c1ca76 100644
--- a/apps/web/tests/math-rendering.e2e.ts
+++ b/apps/web/tests/math-rendering.e2e.ts
@@ -119,6 +119,10 @@ describe('web e2e: settled Markdown math rendering', () => {
await expect.poll(() => page.locator('.katex').count(), { timeout: 10_000 }).toBe(6)
await expect.poll(() => page.locator('.katex-display').count(), { timeout: 10_000 }).toBe(2)
expect(await page.locator('.katex-error').count()).toBe(0)
+ await expect.poll(
+ () => page.getByText('Input 0 tok · Output 0 tok', { exact: false }).count(),
+ { timeout: 10_000 },
+ ).toBe(1)
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
.split(SEED_ID).join('{{seededId}}')
diff --git a/apps/web/tests/message-actions.e2e.ts b/apps/web/tests/message-actions.e2e.ts
index 7555be866b..4284d3d71a 100644
--- a/apps/web/tests/message-actions.e2e.ts
+++ b/apps/web/tests/message-actions.e2e.ts
@@ -128,6 +128,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-actions-aria'))
await page.getByRole('button', { name: /^Select model, current/ })
.waitFor({ timeout: 10_000 })
+ await page.getByText(/Cache hit \d+%/u).first().waitFor({ timeout: 10_000 })
// Keep a footer focused so opacity-hidden actions stay in the a11y tree
// as an active/focused control during the capture.
await page.getByRole('button', { name: 'Copy' }).first().focus()
diff --git a/apps/web/tests/message-feedback-protocol.snapshot.ts b/apps/web/tests/message-feedback-protocol.snapshot.ts
new file mode 100644
index 0000000000..9fbc556f77
--- /dev/null
+++ b/apps/web/tests/message-feedback-protocol.snapshot.ts
@@ -0,0 +1,115 @@
+import { readFile } from 'node:fs/promises'
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { afterAll, beforeAll, describe, expect, it } from 'vitest'
+import {
+ assertFixtureInventory,
+ compareOrRefreshGolden,
+ launchWebScaffold,
+ seedSession,
+ type WebScaffold,
+} from './scaffold.ts'
+
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/message-feedback-protocol', import.meta.url))
+const SESSION_FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
+const PROTOCOL_EXPECTED = join(SNAPSHOT_DIR, 'protocol.expected.json')
+const SESSION_ID = 'message-feedback-protocol'
+const MESSAGE_ID = '11111111-1111-4111-8111-111111111111'
+
+interface ProtocolExchange {
+ readonly endpoint: string
+ readonly request: unknown
+ readonly status: number
+ readonly response: unknown
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null
+}
+
+/** Extract the opaque item version while keeping every surrounding wire field snapshot-owned. */
+function createdVersion(response: unknown): string {
+ if (!isRecord(response) || !isRecord(response.result) || response.result.ok !== true
+ || !isRecord(response.result.value) || response.result.value.ok !== true
+ || !isRecord(response.result.value.value)
+ || typeof response.result.value.value.version !== 'string') {
+ throw new Error('messageFeedback.put did not return a successful versioned item')
+ }
+ return response.result.value.value.version
+}
+
+/** Replace only run-owned UUID/time values; all protocol names and business fields stay exact. */
+function normalizeProtocol(exchanges: readonly ProtocolExchange[], version: string): string {
+ return JSON.stringify(exchanges, (key, value: unknown) => {
+ if ((key === 'version' || key === 'ifVersion') && value === version) return '{{version}}'
+ if ((key === 'createdAt' || key === 'updatedAt') && typeof value === 'number') return '{{timestamp}}'
+ return value
+ }, 2)
+}
+
+describe('message feedback Host Remote protocol', () => {
+ let scaffold: WebScaffold
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold()
+ await seedSession(scaffold, await readFile(SESSION_FIXTURE, 'utf8'), SESSION_ID)
+ })
+
+ afterAll(async () => {
+ await scaffold?.close()
+ })
+
+ it('snapshots strict list, put, conflict, and delete calls through the shipped Web Host', async () => {
+ const exchanges: ProtocolExchange[] = []
+ const invoke = async (rpcId: string, endpoint: string, request: unknown): Promise => {
+ const payload = { args: { request } }
+ const response = await fetch(`${scaffold.baseUrl}/api/${endpoint}`, {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({
+ type: 'client-request',
+ rpcId,
+ method: endpoint,
+ payload,
+ }),
+ })
+ const body: unknown = await response.json()
+ exchanges.push({ endpoint: `/api/${endpoint}`, request: payload, status: response.status, response: body })
+ return body
+ }
+
+ await invoke('feedback-invalid', 'messageFeedback/put', {
+ sessionId: SESSION_ID,
+ messageId: MESSAGE_ID,
+ rating: 'invalid-rating',
+ ifVersion: null,
+ })
+ await invoke('feedback-list-empty', 'messageFeedback/list', { sessionId: SESSION_ID })
+ const created = await invoke('feedback-put', 'messageFeedback/put', {
+ sessionId: SESSION_ID,
+ messageId: MESSAGE_ID,
+ rating: 'positive',
+ note: 'Useful answer',
+ ifVersion: null,
+ })
+ const version = createdVersion(created)
+ expect(version).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
+ await invoke('feedback-list-created', 'messageFeedback/list', { sessionId: SESSION_ID })
+ await invoke('feedback-conflict', 'messageFeedback/put', {
+ sessionId: SESSION_ID,
+ messageId: MESSAGE_ID,
+ rating: 'negative',
+ ifVersion: null,
+ })
+ await invoke('feedback-delete', 'messageFeedback/delete', {
+ sessionId: SESSION_ID,
+ messageId: MESSAGE_ID,
+ ifVersion: version,
+ })
+ await invoke('feedback-list-deleted', 'messageFeedback/list', { sessionId: SESSION_ID })
+
+ expect(exchanges.every(exchange => exchange.status === 200)).toBe(true)
+ await compareOrRefreshGolden(PROTOCOL_EXPECTED, normalizeProtocol(exchanges, version), scaffold.mode)
+ await assertFixtureInventory(SNAPSHOT_DIR, ['protocol.expected.json', 'session.jsonl'])
+ })
+})
diff --git a/apps/web/tests/minimal-preset.snapshot.ts b/apps/web/tests/minimal-preset.snapshot.ts
new file mode 100644
index 0000000000..0c8c6fa765
--- /dev/null
+++ b/apps/web/tests/minimal-preset.snapshot.ts
@@ -0,0 +1,115 @@
+import { mkdir, writeFile } from 'node:fs/promises'
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { afterAll, beforeAll, describe, expect, it } from 'vitest'
+import type { AgentHandle } from '@deepseek-ai/dsh-agent'
+import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
+import { SessionId } from '@deepseek-ai/dsh-session'
+import type {} from '@deepseek-ai/dsh-agent-presets'
+import type {} from '@deepseek-ai/dsh-system-prompt'
+import { assertFixtureInventory, launchWebScaffold, type WebScaffold } from './scaffold.ts'
+
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/minimal-preset', import.meta.url))
+const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
+const PROMPT = 'Reply exactly MINIMAL_PRESET_REQUEST_OK and stop.'
+
+describe('minimal agent preset', () => {
+ let scaffold: WebScaffold
+ let agentHandle: AgentHandle
+ let disposeInjectedPrompt: () => void
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold({ replayFixture: FIXTURE })
+ disposeInjectedPrompt = scaffold.ctx.systemPrompt.section({
+ name: 'test:injected-prompt',
+ order: 999,
+ text: 'THIS TEXT MUST NOT REACH THE MODEL.',
+ })
+ agentHandle = await scaffold.ctx.agents.create({
+ sessionId: SessionId('minimal-preset-smoke'),
+ meta: { cwd: scaffold.workspaceCwd, agentPreset: 'minimal' },
+ agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
+ setup: agentCtx => scaffold.ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
+ })
+ })
+
+ afterAll(async () => {
+ const failures: unknown[] = []
+ await agentHandle?.dispose().catch((error: unknown) => failures.push(error))
+ try {
+ disposeInjectedPrompt?.()
+ } catch (error: unknown) {
+ failures.push(error)
+ }
+ await scaffold?.close().catch((error: unknown) => failures.push(error))
+ if (failures.length === 1) throw failures[0]
+ if (failures.length > 1) throw new AggregateError(failures, 'minimal preset smoke teardown failed')
+ })
+
+ it('sends the exact RL prompt and schemas, then executes the persistent shell and editor', async () => {
+ agentHandle.agent.followup(createUserMessage({
+ content: [{ type: 'text', text: PROMPT }],
+ source: { kind: 'user' },
+ }))
+ await agentHandle.agent.whenIdle()
+
+ const requestHeader = agentHandle.agent.session.requestHeader()
+ if (requestHeader === undefined) throw new Error('the minimal agent issued no model request')
+
+ const stateDir = join(scaffold.workspaceCwd, 'persistent-state')
+ await mkdir(stateDir)
+ const signal = new AbortController().signal
+ await scaffold.ctx.tools.execute({
+ signal,
+ callId: CallId('minimal-bash-state-setup'),
+ name: 'bash',
+ arguments: { command: `cd ${JSON.stringify(stateDir)} && export DSH_MINIMAL_STATE=PERSISTED` },
+ agent: agentHandle.agent,
+ })
+ const bash = await scaffold.ctx.tools.execute({
+ signal,
+ callId: CallId('minimal-bash-state-read'),
+ name: 'bash',
+ arguments: { command: 'printf \'%s:%s\n\' "$DSH_MINIMAL_STATE" "$PWD"' },
+ agent: agentHandle.agent,
+ })
+ const seedPath = join(scaffold.workspaceCwd, 'preset-smoke.txt')
+ await writeFile(seedPath, 'MINIMAL_EDITOR_OK\n')
+ const editor = await scaffold.ctx.tools.execute({
+ signal,
+ callId: CallId('minimal-editor-smoke'),
+ name: 'str_replace_editor',
+ arguments: { command: 'view', path: seedPath },
+ agent: agentHandle.agent,
+ })
+
+ const text = (result: typeof bash): string => result.content
+ .filter(block => block.type === 'text')
+ .map(block => block.text)
+ .join('')
+ .replaceAll(scaffold.workspaceCwd, '{{cwd}}')
+ .trimEnd()
+
+ expect({
+ prompt: requestHeader.system,
+ tools: requestHeader.tools?.map(tool => tool.name),
+ bash: text(bash),
+ editor: text(editor),
+ }).toMatchInlineSnapshot(`
+ {
+ "bash": "PERSISTED:{{cwd}}/persistent-state",
+ "editor": "Here's the content of {{cwd}}/preset-smoke.txt with line numbers (which has a total of 2 lines):
+ 1 MINIMAL_EDITOR_OK
+ 2",
+ "prompt": "You are a helpful software engineer assistant.",
+ "tools": [
+ "bash",
+ "str_replace_editor",
+ ],
+ }
+ `)
+ expect(requestHeader.tools?.toSorted((left, right) => left.name.localeCompare(right.name)))
+ .toEqual(scaffold.ctx.tools.schemas(agentHandle.agent).toSorted((left, right) => left.name.localeCompare(right.name)))
+ await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl'])
+ })
+})
diff --git a/apps/web/tests/navigation-panes.e2e.ts b/apps/web/tests/navigation-panes.e2e.ts
index b96a1fa393..c8b6455296 100644
--- a/apps/web/tests/navigation-panes.e2e.ts
+++ b/apps/web/tests/navigation-panes.e2e.ts
@@ -11,6 +11,7 @@ import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import type { Browser, Page, Response } from 'playwright'
import { chromium } from 'playwright'
+import { strFromU8, unzipSync } from 'fflate'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, onTestFailed } from 'vitest'
import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
@@ -274,6 +275,23 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
await details.getByRole('button', { name: 'Close details' }).click()
}, 60_000)
+ it.skipIf(MODE === 'record')('downloads the session-log ZIP from the trajectory toolbar', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-export'))
+ await ensureSeedOpen(page)
+ await page.getByRole('tab', { name: 'Trajectory' }).click()
+ const downloadPromise = page.waitForEvent('download', { timeout: 30_000 })
+ await page.getByRole('button', { name: 'Export session log' }).click()
+ const download = await downloadPromise
+ expect(download.suggestedFilename()).toMatch(/^dsh-session-.+\.zip$/)
+ // The real host streamed the ZIP; its root entry is the persisted log
+ // text verbatim (the assembled seam: real route, real persistence read).
+ const files = unzipSync(await readFile(await download.path()))
+ expect(Object.keys(files)).toEqual(['session.jsonl'])
+ const content = strFromU8(files['session.jsonl'] as Uint8Array)
+ expect(content.split('\n')[0]).toContain(SEED_ID)
+ expect(content).toContain('FIRST_DONE')
+ }, 60_000)
+
it.skipIf(MODE === 'record')('focuses the ledger by dragging an overview interval', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-timeline'))
await ensureSeedOpen(page)
diff --git a/apps/web/tests/pwa-manifest.e2e.ts b/apps/web/tests/pwa-manifest.e2e.ts
index 696e1c7797..fe97e42da9 100644
--- a/apps/web/tests/pwa-manifest.e2e.ts
+++ b/apps/web/tests/pwa-manifest.e2e.ts
@@ -25,3 +25,11 @@ it('ships install metadata with the built web application', async () => {
}],
})
})
+
+it('ships a favicon that switches to a light mark under dark color scheme', async () => {
+ const favicon = await readFile(join(DIST_ROOT, 'favicon.svg'), 'utf8')
+ // The light fill must live inside the dark-scheme media query, so the icon
+ // stays black in light mode and only turns white under a dark scheme.
+ expect(favicon).toMatch(/@media \(prefers-color-scheme: dark\)\s*{\s*path\s*{[^}]*fill:\s*#fff/i)
+ expect(favicon).toContain('fill="#000"')
+})
diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts
index 945775678f..772bd4ae91 100644
--- a/apps/web/tests/scaffold.ts
+++ b/apps/web/tests/scaffold.ts
@@ -29,13 +29,12 @@ import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import type { Page } from 'playwright'
import { expect } from 'vitest'
-import { Context } from 'cordis'
-import Loader from '@cordisjs/plugin-loader'
-import Include, { type PatchOptions } from '@cordisjs/plugin-include'
-import Group from '@cordisjs/plugin-group'
+import { Context } from '@deepseek-ai/cordis'
+import Loader from '@deepseek-ai/cordis-plugin-loader'
+import Include, { type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
+import Group from '@deepseek-ai/cordis-plugin-group'
import { scrubRequestHeaders, stabilizeFixtureMessageIds } from '@deepseek-ai/dsh-acp-snapshot'
import {
- addHarnessSourceSection,
assertEntriesLoaded,
composeEntries,
healProfilesModuleFallback,
@@ -65,6 +64,7 @@ import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
// Empty type imports carry the httpServer/agents/sessionPersistence Context merges.
import type {} from '@deepseek-ai/dsh-host-webserver'
import type {} from '@deepseek-ai/dsh-agent'
+import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
import { REPO_ROOT, requireDist } from './support.ts'
/** Snapshot mode for the lane, from $DSH_SNAPSHOT (same vocabulary as the other snapshot suites). */
@@ -245,6 +245,13 @@ export interface LaunchOptions {
}
/** Leave the current welcome notice unacknowledged; ordinary scenarios publish it as complete before browser boot. */
welcomeNoticePending?: boolean
+ /**
+ * Mount the shipped telemetry row in FULL mode against this exporter URL
+ * instead of disabling it. Used to pin a real backend disclosure in
+ * assembled coverage; point the URL at a local dead endpoint so no record
+ * leaves the process.
+ */
+ telemetryUrl?: string
/**
* Browse through a trusted non-loopback hostname that the browser resolves
* to loopback (for example `*.localhost`). The test server stays bound to
@@ -334,6 +341,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise failures.push(cleanupError))
+ restoreSkillRootEnvironment()
if (failures.length > 1) throw new AggregateError(failures, 'web scaffold temp-root setup failed')
throw error
}
@@ -395,8 +403,11 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise {
+ throw new Error(`web e2e scaffold: the web app requested exit ${String(code)} with no arguments to reject`)
+ },
+ })
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
// `cordis:group` beside it, exactly as `boot()` registers it: a group row is
// how a preset gives one `isolate` realm to a provider and its consumers,
// and a preset resolving package names from its own directory cannot reach
- // `@cordisjs/plugin-group` by name.
+ // `@deepseek-ai/cordis-plugin-group` by name.
ctx.loader.builtins.group = Group
// The shipped CLI deliberately has no dependency on this opt-in package.
// Keep the Loader row real without broadening the product installation.
if (options.cordisTools === true) ctx.loader.builtins['tool-cordis'] = ToolCordis
- if (surfaceContext) {
- ctx.inject(['systemPrompt'], (promptCtx) => { addHarnessSourceSection(promptCtx, REPO_ROOT) })
- }
await ctx.loader.create({
name: 'cordis:include',
config: { path: pathToFileURL(rootConfig).href, patches },
diff --git a/apps/web/tests/seeded-history.e2e.ts b/apps/web/tests/seeded-history.e2e.ts
index a112933f9c..9ec978fb97 100644
--- a/apps/web/tests/seeded-history.e2e.ts
+++ b/apps/web/tests/seeded-history.e2e.ts
@@ -18,7 +18,6 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm'
import { deriveEventMessage, SessionId } from '@deepseek-ai/dsh-session'
-import type {} from '@deepseek-ai/dsh-agent-presets'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import type { TokenMeterService } from '@deepseek-ai/dsh-token-meter'
import { join } from 'node:path'
@@ -195,21 +194,11 @@ describe('web e2e: seeded history renders through cold resume', () => {
if (MODE !== 'record') {
const raw = await readFile(SEED, 'utf8')
expect(fixtureUserPrompts(raw), 'seed fixture must carry exactly the drive prompt').toEqual([PROMPT])
- // The meter belongs to an agent's preset, not to the process — token
- // accounting is per session. It is used here as a pure pricing function
- // over fixture content, so a throwaway composition is enough to reach one.
- const priced = await scaffold.ctx.agents.create({
- sessionId: SessionId('seeded-history-pricing'),
- setup: agentCtx => scaffold.ctx.agentPresets.mount(agentCtx).then(() => undefined),
- })
- let realizedWithCompaction: string
- try {
- const meter = scaffold.ctx.agentPresets.serviceFor(priced.agent, 'tokenMeter')
- if (meter === undefined) throw new Error('seeded-history requires the composed token meter')
- realizedWithCompaction = withCompaction(realizeSeedFixture(scaffold, raw, SEED_ID), meter)
- } finally {
- await priced.dispose()
- }
+ // The meter is host-plane — it takes no configuration and keys every
+ // fold by Session — so pricing fixture content needs no agent at all.
+ const meter = scaffold.ctx.get('tokenMeter')
+ if (meter === undefined) throw new Error('seeded-history requires the host token meter')
+ const realizedWithCompaction = withCompaction(realizeSeedFixture(scaffold, raw, SEED_ID), meter)
await seedSession(scaffold, realizedWithCompaction, SEED_ID)
}
browser = await chromium.launch()
@@ -468,9 +457,9 @@ describe('web e2e: seeded history renders through cold resume', () => {
if (done?.type !== 'command/done') throw new Error('feedback command did not settle')
const [sessionLine, userLine, extraLine] = done.data.text?.split('\n') ?? []
expect(sessionLine).toBe(`Feedback recorded for session ${SEED_ID}`)
- expect(userLine).toMatch(/^User: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
+ expect(userLine).toMatch(/^User: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\./i)
expect(extraLine).toBeUndefined()
- const userId = userLine?.slice('User: '.length)
+ const userId = userLine?.match(/^User: ([0-9a-f-]+)/i)?.[1]
if (userId === undefined) throw new Error('feedback command omitted the user id')
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts
index faf01ede62..ae0bab04c8 100644
--- a/apps/web/tests/shipped-composition.e2e.ts
+++ b/apps/web/tests/shipped-composition.e2e.ts
@@ -36,9 +36,9 @@ const EXPECTED_TOOLS = [
'list_agents',
'ralph',
'read',
+ 'read_image',
'send_message',
'skill',
- 'str_replace_editor',
'subagent',
'subagent_fork',
'task_kill',
diff --git a/apps/web/tests/smoke-real.e2e.ts b/apps/web/tests/smoke-real.e2e.ts
index b0acf074b4..24a6f269c3 100644
--- a/apps/web/tests/smoke-real.e2e.ts
+++ b/apps/web/tests/smoke-real.e2e.ts
@@ -478,17 +478,20 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
requireDist()
sessionsDir = mkdtempSync(join(tmpdir(), 'dsh-web-w5-'))
const port = await probeFreePort()
- // tsx boot mirrors demo:web — lib/ may be unbuilt in this worktree. Isolate
+ // tsx boot mirrors the runtime half of the root dsh script. Isolate
// the host-level Harness and shared-agent homes inside the temp world; tsx
// also needs the repo's loader and tsconfig paths pointed at explicitly.
const tsxLoader = pathToFileURL(createRequire(join(REPO_ROOT, 'package.json')).resolve('tsx')).href
child = spawn(
process.execPath,
[
- '--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', String(port),
+ '--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web',
+ // Launcher flags come first: the first token the launcher does not own
+ // starts the web app's own arguments.
// Pin the in-browser picker: the shipped `-auto` row would resolve to
// the native OS chooser on this bind, and no page can drive that.
'--patch', fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)),
+ '--port', String(port),
],
{
cwd: sessionsDir,
diff --git a/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md
index 395e517aa1..0ddc4dd77b 100644
--- a/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md
+++ b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md
@@ -40,7 +40,7 @@
- text: 复制
- listitem:
- 'button "设为默认: 极简模式"':
- - text: 极简模式 内置 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
+ - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
- code: minimal
- 'button "查看: 极简模式"':
- img
@@ -62,7 +62,7 @@
- list:
- listitem:
- 'button "设为默认: 我的模式"':
- - text: 我的模式 自定义 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
+ - text: 我的模式 自定义 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
- code: my-agent
- 'button "查看路径: 我的模式"':
- img
diff --git a/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md
index b40b848b5e..f853cacf24 100644
--- a/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md
+++ b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md
@@ -40,7 +40,7 @@
- text: 复制
- listitem:
- 'button "设为默认: 极简模式"':
- - text: 极简模式 内置 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
+ - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
- code: minimal
- 'button "查看: 极简模式"':
- img
@@ -61,8 +61,8 @@
- heading "自定义" [level=3]
- list:
- listitem:
- - 'button "已损坏: broken-yaml" [disabled]':
- - text: broken-yaml 已损坏 自定义 暂无描述。
+ - 'button "加载失败: broken-yaml" [disabled]':
+ - text: broken-yaml 加载失败 自定义 暂无描述。
- alert: "the composition is not valid YAML: unexpected end of the stream within a flow collection (3:1)"
- code: broken-yaml
- 'button "查看路径: broken-yaml"':
@@ -70,13 +70,13 @@
- text: 查看路径
- 'button "复制: broken-yaml" [disabled]':
- img
- - text: 预设已损坏,无法复制
+ - text: 预设加载失败,不能复制
- 'button "删除: broken-yaml"':
- img
- text: 删除
- listitem:
- - 'button "已损坏: 幽灵预设" [disabled]':
- - text: 幽灵预设 已损坏 自定义 composition 已被手动删除。
+ - 'button "加载失败: 幽灵预设" [disabled]':
+ - text: 幽灵预设 加载失败 自定义 composition 已被手动删除。
- alert: the composition file agent.cordis.yml is missing — the directory still occupies the id; delete it or restore the file
- code: ghost
- 'button "查看路径: 幽灵预设"':
@@ -84,7 +84,7 @@
- text: 查看路径
- 'button "复制: 幽灵预设" [disabled]':
- img
- - text: 预设已损坏,无法复制
+ - text: 预设加载失败,不能复制
- 'button "删除: 幽灵预设"':
- img
- text: 删除
diff --git a/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md
index dcbe72641c..ad2bf86389 100644
--- a/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md
+++ b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md
@@ -40,7 +40,7 @@
- text: 复制
- listitem:
- 'button "设为默认: 极简模式"':
- - text: 极简模式 内置 仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。
+ - text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
- code: minimal
- 'button "查看: 极简模式"':
- img
@@ -58,6 +58,7 @@
- 'button "复制: 创造模式"':
- img
- text: 复制
+ - heading "自定义" [level=3]
- button "用「创造模式」创作自定义预设":
- img
- text: 用「创造模式」创作自定义预设
diff --git a/apps/web/tests/snapshots/agent-preset-selection/menu.expected.md b/apps/web/tests/snapshots/agent-preset-selection/menu.expected.md
index 78ec056f56..7e0d1ae032 100644
--- a/apps/web/tests/snapshots/agent-preset-selection/menu.expected.md
+++ b/apps/web/tests/snapshots/agent-preset-selection/menu.expected.md
@@ -3,5 +3,5 @@
- text: Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.
- img
- menuitem "Code mode All Standard mode capabilities, with tools exposed through the Code Mode SDK so the model can combine multi-step operations in one TypeScript program."
- - menuitem "Minimal mode Two-tool coding agent with only bash and str_replace_editor, for benchmarks and minimal reproductions."
+ - menuitem "Minimal mode Two-tool coding agent with persistent bash and str_replace_editor."
- menuitem "Creator mode Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance."
diff --git a/apps/web/tests/snapshots/background-task-list/running.expected.md b/apps/web/tests/snapshots/background-task-list/running.expected.md
new file mode 100644
index 0000000000..1adaa965e2
--- /dev/null
+++ b/apps/web/tests/snapshots/background-task-list/running.expected.md
@@ -0,0 +1,2 @@
+- list "Background tasks":
+ - listitem: bash sleep 45 running {{duration}}
diff --git a/apps/web/tests/snapshots/background-task-list/settled.expected.md b/apps/web/tests/snapshots/background-task-list/settled.expected.md
new file mode 100644
index 0000000000..c97ff5026c
--- /dev/null
+++ b/apps/web/tests/snapshots/background-task-list/settled.expected.md
@@ -0,0 +1,2 @@
+- list "Background tasks":
+ - listitem: "bash sleep 45 signal: SIGTERM {{duration}}"
diff --git a/apps/web/tests/snapshots/feedback-command/ack.expected.md b/apps/web/tests/snapshots/feedback-command/ack.expected.md
new file mode 100644
index 0000000000..89d40acb3b
--- /dev/null
+++ b/apps/web/tests/snapshots/feedback-command/ack.expected.md
@@ -0,0 +1,39 @@
+- banner:
+ - navigation "Session hierarchy":
+ - button "Reply with the single word" [disabled]
+ - img
+ - text: Standard mode
+ - tablist:
+ - tab "Chat" [selected]
+ - tab "Trajectory"
+- text: Reply with the single word LIGHTHOUSE and stop. {{clock}}
+- button "Copy":
+ - img
+- button "Context injection @deepseek-ai/dsh-system-prompt":
+ - img
+ - img
+ - text: Context injection @deepseek-ai/dsh-system-prompt
+- button "Think The user wants me to reply with a single word. Let me comply.":
+ - img
+ - img
+ - text: Think The user wants me to reply with a single word. Let me comply.
+- paragraph: LIGHTHOUSE
+- button "Copy":
+ - img
+- button "Branch into a new conversation":
+ - img
+- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
+- 'button "feedback Feedback recorded for session session-{{uuid}} User: {{uuid}}. Session sharing is enabled."':
+ - img
+ - img
+ - text: "feedback Feedback recorded for session session-{{uuid}} User: {{uuid}}. Session sharing is enabled."
+- textbox "Message the agent"
+- button "Commands":
+ - img
+- 'button "Access mode, current: Workspace Write"': Workspace Write
+- button "Select model, current DeepSeek-V4-Flash":
+ - text: DeepSeek-V4-Flash
+ - img
+- button "6% of context used"
+- button "Send message" [disabled]
+- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 7.8K tok · Output 21 tok
diff --git a/apps/web/tests/snapshots/feedback-command/session.jsonl b/apps/web/tests/snapshots/feedback-command/session.jsonl
new file mode 100644
index 0000000000..d528f36c0e
--- /dev/null
+++ b/apps/web/tests/snapshots/feedback-command/session.jsonl
@@ -0,0 +1,17 @@
+{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785015039278,"cwd":"{{cwd}}/workspace"}
+{"type":"turn/start","seq":0,"time":1785015039291,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
+{"type":"user/message","seq":1,"time":1785015039292,"data":{"content":[{"type":"text","text":"Reply with the single word LIGHTHOUSE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
+{"type":"session/title","seq":2,"time":1785015039294,"data":{"title":"Reply with the single word","messageSeqs":[1],"source":{"kind":"fallback"}}}
+{"type":"step/start","seq":3,"time":1785015039362,"data":{"turn":1,"step":1}}
+{"type":"request/header","seq":4,"time":1785015039363,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
+{"type":"assistant/chunk","seq":5,"time":1785015039930,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","seq0":6,"time0":1785015039930,"data":{"turn":1,"step":1,"index":0,"dt":[162,28,1,0,0,46,1,0,0,0,11,0,0,30],"texts":["The"," user"," wants"," me"," to"," reply"," with"," a"," single"," word","."," Let"," me"," comply","."]}}
+{"type":"assistant/chunk","seq":21,"time":1785015040209,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"text-chunks","seq0":22,"time0":1785015040209,"data":{"turn":1,"step":1,"index":1,"dt":[1,0,30,1],"texts":["L","IGH","TH","O","USE"]}}
+{"type":"assistant/chunk","seq":27,"time":1785015040241,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with a single word. Let me comply."}}}}
+{"type":"assistant/chunk","seq":28,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"LIGHTHOUSE"}}}}
+{"type":"assistant/chunk","seq":29,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15}}}}
+{"type":"assistant/chunk","seq":30,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":31,"time":1785015040244,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with a single word. Let me comply."},{"type":"text","text":"LIGHTHOUSE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30],"surfaceOp":"append"}
+{"type":"step/end","seq":32,"time":1785015040246,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":33,"time":1785015040247,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/apps/web/tests/snapshots/message-feedback-protocol/protocol.expected.json b/apps/web/tests/snapshots/message-feedback-protocol/protocol.expected.json
new file mode 100644
index 0000000000..302e4876df
--- /dev/null
+++ b/apps/web/tests/snapshots/message-feedback-protocol/protocol.expected.json
@@ -0,0 +1,203 @@
+[
+ {
+ "endpoint": "/api/messageFeedback/put",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol",
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "invalid-rating",
+ "ifVersion": null
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-invalid",
+ "result": {
+ "ok": false,
+ "error": {
+ "code": "internal",
+ "message": "typert gateway: messageFeedback/put: wire field \"request\" failed boundary validation",
+ "details": {}
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/list",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol"
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-list-empty",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "items": []
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/put",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol",
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "positive",
+ "note": "Useful answer",
+ "ifVersion": null
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-put",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "positive",
+ "note": "Useful answer",
+ "version": "{{version}}",
+ "createdAt": "{{timestamp}}",
+ "updatedAt": "{{timestamp}}"
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/list",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol"
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-list-created",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "items": [
+ {
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "positive",
+ "note": "Useful answer",
+ "version": "{{version}}",
+ "createdAt": "{{timestamp}}",
+ "updatedAt": "{{timestamp}}"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/put",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol",
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "negative",
+ "ifVersion": null
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-conflict",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": false,
+ "error": {
+ "code": "version-conflict",
+ "current": {
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "rating": "positive",
+ "note": "Useful answer",
+ "version": "{{version}}",
+ "createdAt": "{{timestamp}}",
+ "updatedAt": "{{timestamp}}"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/delete",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol",
+ "messageId": "11111111-1111-4111-8111-111111111111",
+ "ifVersion": "{{version}}"
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-delete",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "absent": true
+ }
+ }
+ }
+ }
+ },
+ {
+ "endpoint": "/api/messageFeedback/list",
+ "request": {
+ "args": {
+ "request": {
+ "sessionId": "message-feedback-protocol"
+ }
+ }
+ },
+ "status": 200,
+ "response": {
+ "type": "server-response",
+ "rpcId": "feedback-list-deleted",
+ "result": {
+ "ok": true,
+ "value": {
+ "ok": true,
+ "value": {
+ "items": []
+ }
+ }
+ }
+ }
+ }
+]
diff --git a/apps/web/tests/snapshots/message-feedback-protocol/session.jsonl b/apps/web/tests/snapshots/message-feedback-protocol/session.jsonl
new file mode 100644
index 0000000000..d970fb8cb4
--- /dev/null
+++ b/apps/web/tests/snapshots/message-feedback-protocol/session.jsonl
@@ -0,0 +1,7 @@
+{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1786406400000,"cwd":"{{cwd}}"}
+{"type":"turn/start","seq":0,"time":1786406400001,"data":{"turn":1}}
+{"type":"user/message","seq":1,"time":1786406400002,"data":{"role":"user","content":[{"type":"text","text":"Give one useful answer."}],"source":{"kind":"user"},"id":"22222222-2222-4222-8222-222222222222"},"surfaceOp":"append"}
+{"type":"step/start","seq":2,"time":1786406400003,"data":{"turn":1,"step":1}}
+{"type":"assistant/message","seq":3,"time":1786406400004,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"A useful answer."}],"source":{"kind":"model","provider":"fixture","model":"fixture"},"id":"11111111-1111-4111-8111-111111111111"},"usage":{"inputTokens":4,"outputTokens":4}},"surfaceOp":"append"}
+{"type":"step/end","seq":4,"time":1786406400005,"data":{"turn":1,"step":1}}
+{"type":"turn/end","seq":5,"time":1786406400006,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/apps/web/tests/snapshots/core-web-profile/session.jsonl b/apps/web/tests/snapshots/minimal-preset/session.jsonl
similarity index 73%
rename from apps/web/tests/snapshots/core-web-profile/session.jsonl
rename to apps/web/tests/snapshots/minimal-preset/session.jsonl
index 04f0d62d15..49977be802 100644
--- a/apps/web/tests/snapshots/core-web-profile/session.jsonl
+++ b/apps/web/tests/snapshots/minimal-preset/session.jsonl
@@ -1,7 +1,7 @@
-{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785974400000,"cwd":"{{cwd}}"}
-{"type":"user/message","seq":0,"time":1785974400001,"data":{"content":[{"type":"text","text":"Reply exactly CORE_WEB_REQUEST_OK and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
+{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785974400000,"cwd":"{{cwd}}","agentPreset":"minimal"}
+{"type":"user/message","seq":0,"time":1785974400001,"data":{"content":[{"type":"text","text":"Reply exactly MINIMAL_PRESET_REQUEST_OK and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":1,"time":1785974400002,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":2,"time":1785974400003,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CORE_WEB_REQUEST_OK"}}}
-{"type":"assistant/chunk","seq":3,"time":1785974400004,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CORE_WEB_REQUEST_OK"}}}}
+{"type":"assistant/chunk","seq":2,"time":1785974400003,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"MINIMAL_PRESET_REQUEST_OK"}}}
+{"type":"assistant/chunk","seq":3,"time":1785974400004,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}}}}
{"type":"assistant/chunk","seq":4,"time":1785974400005,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}}}
{"type":"assistant/chunk","seq":5,"time":1785974400006,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
diff --git a/apps/web/tests/snapshots/navigation-panes/trajectory.expected.md b/apps/web/tests/snapshots/navigation-panes/trajectory.expected.md
index a9b5dbb982..3476255bab 100644
--- a/apps/web/tests/snapshots/navigation-panes/trajectory.expected.md
+++ b/apps/web/tests/snapshots/navigation-panes/trajectory.expected.md
@@ -2,6 +2,7 @@
- button "Use actual duration": Duration
- button "Collapse turns": Turns
- button "Collapse calls": Calls
+ - button "Export session log": Export
- img
- searchbox "Search trajectory"
- region "Trajectory timeline":
diff --git a/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md b/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md
index 87b763d37c..6928b95777 100644
--- a/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md
+++ b/apps/web/tests/snapshots/seeded-history/feedback-row.expected.md
@@ -38,10 +38,10 @@
- text: Context injection AGENTS.md
- img
- text: permission preset read-only
-- 'button "feedback Feedback recorded for session {{seededId}} User: {{uuid}}" [expanded]':
+- 'button "feedback Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured." [expanded]':
- img
- - text: "feedback Feedback recorded for session {{seededId}} User: {{uuid}}"
-- text: "Feedback recorded for session {{seededId}} User: {{uuid}}"
+ - text: "feedback Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured."
+- text: "Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured."
- textbox "Message the agent"
- button "Commands":
- img
diff --git a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md
index cf22f5b566..15ceebba6b 100644
--- a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md
+++ b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md
@@ -43,7 +43,7 @@
- textbox "Message the agent"
- button "Commands":
- img
-- 'button "Access mode, current: Workspace Write"': Workspace Write
+- 'button "Access mode, current: Custom"': Custom
- button "6% of context used"
- button "Send message" [disabled]
- text: 2 turns · 2 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 15.6K tok · Output 158 tok
diff --git a/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md b/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md
index 7fe41a532d..8f4554e47b 100644
--- a/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md
+++ b/apps/web/tests/snapshots/subagent-interrupt/offline-composer.expected.md
@@ -20,6 +20,6 @@
- textbox "Parent session offline; sending is unavailable but you can still stop the run" [disabled]
- button "Commands" [disabled]:
- img
-- 'button "Access mode, current: Workspace Write" [disabled]': Workspace Write
+- 'button "Access mode, current: Custom" [disabled]': Custom
- button "Stop generating"
- button "Send message" [disabled]
diff --git a/apps/web/tests/subagent-conversation.e2e.ts b/apps/web/tests/subagent-conversation.e2e.ts
index fa33e5cd3d..6756b093d2 100644
--- a/apps/web/tests/subagent-conversation.e2e.ts
+++ b/apps/web/tests/subagent-conversation.e2e.ts
@@ -395,7 +395,8 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
expect([
Math.round(clickAreaBox!.x - treeBox!.x),
Math.round(treeBox!.x + treeBox!.width - clickAreaBox!.x - clickAreaBox!.width),
- ]).toEqual([5, 5])
+ // Menu padding alone insets the rows now that the border is gone.
+ ]).toEqual([4, 4])
await compareOrRefreshGolden(
BRANCHLESS_EXPECTED,
await captureStableAria(page, '[role="tree"][aria-label="Subagent sessions"]', scaffold.workspaceCwd),
diff --git a/apps/web/tests/support.ts b/apps/web/tests/support.ts
index 40b9be39ca..ee2a1a1a62 100644
--- a/apps/web/tests/support.ts
+++ b/apps/web/tests/support.ts
@@ -57,9 +57,9 @@ export function probeFreePort(): Promise {
/**
* Drive the hero's workspace picker through the composed directory dialog
* until the live composer unlocks. A fresh world has no Workspace, so the boot
- * lands in the locked view state (startup auto-selection has nothing to
+ * lands in the Workspace-trigger view state (startup auto-selection has nothing to
* select); every scenario that types into the composer must connect one
- * first. With nothing to list, the chip gesture raises the dialog directly —
+ * first. With nothing to list, activating the textarea raises the dialog directly —
* adding a workspace is the picker's only entry. The directory is staged here
* and adopted through the path editor, which is idempotent across the repeated
* connects a scenario may make; creating a folder from inside the dialog (the
@@ -73,7 +73,7 @@ export function probeFreePort(): Promise {
*/
export async function connectFreshWorkspace(page: Page, root: string, name = 'workspace'): Promise {
mkdirSync(join(root, name), { recursive: true })
- await page.getByRole('button', { name: 'Choose workspace' }).click()
+ await page.getByRole('textbox', { name: 'Choose workspace' }).click()
const dialog = page.getByRole('dialog', { name: 'Select Workspace Directory' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: 'Edit path' }).click()
@@ -97,7 +97,7 @@ export async function connectFreshWorkspace(page: Page, root: string, name = 'wo
*/
export async function connectFreshWorkspaceZh(page: Page, root: string, name = 'workspace'): Promise {
mkdirSync(join(root, name), { recursive: true })
- await page.getByRole('button', { name: '选择工作区' }).click()
+ await page.getByRole('textbox', { name: '选择工作区' }).click()
const dialog = page.getByRole('dialog', { name: '选择工作区目录' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: '编辑路径' }).click()
diff --git a/apps/web/tests/workspace-management.e2e.ts b/apps/web/tests/workspace-management.e2e.ts
index c18a522d30..a435592da5 100644
--- a/apps/web/tests/workspace-management.e2e.ts
+++ b/apps/web/tests/workspace-management.e2e.ts
@@ -99,6 +99,19 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
}
}
+ /**
+ * Reveal and click a row action, re-hovering if a projection update replaces
+ * the row before its hover-only button becomes visible.
+ */
+ async function clickHoverAction(row: Locator, name: string): Promise {
+ const button = row.getByRole('button', { name })
+ await expect.poll(async () => {
+ await row.hover()
+ return await button.isVisible()
+ }, { timeout: 10_000 }).toBe(true)
+ await button.click()
+ }
+
beforeAll(async () => {
scaffold = await launchWebScaffold({})
// Seed one cold session (Ungrouped bucket) for the flat view + hover card.
@@ -137,10 +150,8 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
it('renames a workspace over the wire with a duplicate-name pre-check', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-ws-rename'))
- // The actions button is display:none until its row hovers — hover the
- // group row first, then the revealed button becomes actionable.
- await page.locator('[role="treeitem"]').filter({ hasText: 'alpha-ws' }).first().hover()
- await page.getByRole('button', { name: 'Workspace actions for alpha-ws' }).click()
+ const alphaRow = page.locator('[role="treeitem"]').filter({ hasText: 'alpha-ws' }).first()
+ await clickHoverAction(alphaRow, 'Workspace actions for alpha-ws')
await page.getByRole('menuitem', { name: 'Rename' }).click()
const dialog = page.getByRole('dialog', { name: 'Rename workspace' })
await dialog.waitFor({ timeout: 10_000 })
@@ -214,17 +225,19 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
// The header row is wrapped by its HoverCard anchor span, so the section
// is the nearest groupSection ancestor, not the immediate parent.
const groupSection = groupRow.locator('xpath=ancestor::*[contains(@class, "groupSection")][1]')
- if (await groupSection.locator('[role="treeitem"]').count() < 2) await groupRow.click()
- await expect.poll(
- () => groupSection.locator('[role="treeitem"]').count(),
- { timeout: 10_000 },
- ).toBeGreaterThanOrEqual(2)
+ await expect.poll(async () => {
+ const count = await groupSection.locator('[role="treeitem"]').count()
+ if (count < 2 && await groupRow.getAttribute('aria-expanded') !== 'true') {
+ await groupRow.click()
+ await page.waitForTimeout(50)
+ }
+ return await groupSection.locator('[role="treeitem"]').count()
+ }, { timeout: 10_000 }).toBeGreaterThanOrEqual(2)
const seededRow = groupSection.locator('[role="treeitem"]').nth(1)
await seededRow.click()
await expect.poll(() => seededRow.getAttribute('aria-selected'), { timeout: 10_000 }).toBe('true')
- await groupRow.hover()
- await page.getByRole('button', { name: `Workspace actions for ${workspace.title}` }).click()
+ await clickHoverAction(groupRow, `Workspace actions for ${workspace.title}`)
await page.getByRole('menuitem', { name: 'Delete workspace' }).click()
const dialog = page.getByRole('dialog', { name: 'Delete workspace' })
await dialog.waitFor({ timeout: 10_000 })
@@ -338,8 +351,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
if (oldWorkspace === undefined) throw new Error('old same-name Workspace was not registered')
const oldRow = page.locator('[role="treeitem"]').filter({ hasText: title }).first()
- await oldRow.hover()
- await page.getByRole('button', { name: `Workspace actions for ${title}` }).click()
+ await clickHoverAction(oldRow, `Workspace actions for ${title}`)
await page.getByRole('menuitem', { name: 'Delete workspace' }).click()
await page.getByRole('dialog', { name: 'Delete workspace' })
.getByRole('button', { name: 'Delete workspace' }).click()
@@ -509,9 +521,10 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
onTestFailed(() => saveFailureShot(page, 'web-e2e-ws-row-menu'))
const sessionRow = await seededSessionRow()
// The trigger is display:none until its row hovers.
- await sessionRow.hover()
const trigger = sessionRow.locator('button[aria-label^="Session actions for "]')
- await trigger.click()
+ const triggerName = await trigger.getAttribute('aria-label')
+ if (triggerName === null) throw new Error('seeded Session row has no actions label')
+ await clickHoverAction(sessionRow, triggerName)
const item = page.getByRole('menuitem', { name: 'Rename' })
await item.waitFor({ timeout: 5_000 })
// Into the list, then back up to the trigger across the 4px gap below it:
@@ -559,8 +572,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
const rowTitle = await sessionRow.locator('[class*="title"]').innerText()
// Row menu: hover reveals the actions button; Archive session commits
// without a confirmation dialog (non-destructive: log + accounting stay).
- await sessionRow.hover()
- await sessionRow.getByRole('button', { name: `Session actions for ${rowTitle}` }).click()
+ await clickHoverAction(sessionRow, `Session actions for ${rowTitle}`)
await page.getByRole('menuitem', { name: 'Archive session' }).click()
// The row disappears on the archive-set echo; with no other visible
// stray, the whole Ungrouped bucket withdraws.
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index da25f0cc59..f96a1f8dd1 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -24,7 +24,8 @@
"exclude": [
"tests/scaffold.ts",
"tests/scaffold-hermetic.e2e.ts",
- "tests/core-web-profile.snapshot.ts",
+ "tests/minimal-preset.snapshot.ts",
+ "tests/message-feedback-protocol.snapshot.ts",
"tests/live-interactions.e2e.ts",
"tests/question-composer.e2e.ts",
"tests/approval-composer.e2e.ts",
@@ -64,6 +65,7 @@
"tests/agent-preset-selection.e2e.ts",
"tests/agent-preset-authoring.e2e.ts",
"tests/shipped-composition.e2e.ts",
+ "tests/feedback-command.e2e.ts",
"tests/startup-auto-selection.e2e.ts",
"tests/produced-files.e2e.ts",
"tests/produced-file-mentions.e2e.ts",
@@ -72,6 +74,7 @@
"tests/subagent-interrupt.e2e.ts",
"tests/subagent-interrupt-ui.e2e.ts",
"tests/sidebar-subagent-activity.e2e.ts",
+ "tests/background-task-list.e2e.ts",
"tests/bash-abort-row.e2e.ts",
"tests/skill-tool-row.e2e.ts",
"tests/turn-tail-actions.e2e.ts",
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index a8323cdd78..2600d64acb 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -5,8 +5,8 @@ import react from '@vitejs/plugin-react'
const src = (rel: string): string => fileURLToPath(new URL(rel, import.meta.url))
const STANDALONE_ERROR = 'apps/web is not a standalone application: bare Vite cannot inject window.__DSH_BOOT__. '
- + 'Build with `pnpm run build && pnpm run build:web`, then run `dsh web` (repository checkout: `pnpm run dsh -- web`). '
- + 'For client-plugin HMR, run `pnpm run dsh -- web --dev` together with `pnpm run dev:web`.'
+ + 'From a repository checkout, run `pnpm dsh web`; an installed package uses `dsh web`. '
+ + 'For client-plugin HMR, run `pnpm dsh web --dev` together with `pnpm run dev:web`.'
/** Fail before a Vite dev or preview server can expose the boot-manifest-free shell. */
function rejectStandaloneServe(): Plugin {
diff --git a/bin/dsh b/bin/dsh
deleted file mode 100755
index c578d78e74..0000000000
--- a/bin/dsh
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/sh
-# dsh launcher: runs the apps/cli `dsh` bin FROM SOURCE through the tsx ESM
-# hook, so a symlink from anywhere (e.g. ~/.local/bin/dsh) always executes the
-# current working tree without a build step.
-set -eu
-
-# Resolve symlink chains without readlink -f (not on every macOS).
-script=$0
-while [ -L "$script" ]; do
- target=$(readlink "$script")
- case $target in
- /*) script=$target ;;
- *) script=$(dirname "$script")/$target ;;
- esac
-done
-root=$(CDPATH='' cd -- "$(dirname -- "$script")/.." && pwd)
-
-# The ESM-only tsx hook transforms TypeScript and projects this checkout's
-# tsconfig paths into Node resolution (the CJS hook stays off: the graph is
-# ESM-only and the CJS resolver costs ~0.4s of startup). Absolute paths keep
-# both the hook and the tsconfig anchored to this checkout when the launcher
-# runs from any cwd, where bare `tsx/esm` would not resolve.
-NODE_USE_ENV_PROXY=1 \
- TSX_TSCONFIG_PATH="$root/tsconfig.json" \
- exec node --import "$root/node_modules/tsx/dist/esm/index.mjs" \
- "$root/apps/cli/src/bin.ts" "$@"
diff --git a/docs/api-gateway.i18n.yaml b/docs/api-gateway.i18n.yaml
index 09204ccfff..a958569942 100644
--- a/docs/api-gateway.i18n.yaml
+++ b/docs/api-gateway.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 docs/api-gateway.md
-api-gateway.md: 81cd80893d53212edc74cc85e3e05731fa05f411
-api-gateway.zh.md: 692cf825f619f71e86ae801e04246e9feb4a4c36
+api-gateway.md: 3065f3f10861965b327a4412049878dc1ba7faec
+api-gateway.zh.md: aa9b726c33fd9f51fc0b2d2ed95c4c9658662796
diff --git a/docs/api-gateway.md b/docs/api-gateway.md
index 81cd80893d..3065f3f108 100644
--- a/docs/api-gateway.md
+++ b/docs/api-gateway.md
@@ -17,7 +17,7 @@ Services normally extend `GatewayService` so the constructor explicitly binds th
```ts
import type { Agent } from '@deepseek-ai/dsh-agent'
import { GatewayService, Remote, RemoteScope } from '@deepseek-ai/dsh-type-meta'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export interface CreateGoalRequest {
objective: string
@@ -60,7 +60,7 @@ The Client uses concrete functions on ordinary objects, not a JavaScript Proxy.
```ts ignore-check
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { AgentContext } from '@deepseek-ai/dsh-client-runtime/client'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-api-remotes/client'
export const inject = ['remote', 'remote.goals']
@@ -138,16 +138,10 @@ SRC solves only dispatch for a Host process running from source. The Client does
## Development mode
-A complete build generates Host contracts before compiling the Host, Client, and Web, so it is the deterministic entry for creating or refreshing all artifacts:
+The repository `dsh` script completes the Host, Client, and Web build before starting the source Host. Web development runs that command and the Client plugin watcher in separate terminals:
```sh
-pnpm run build
-```
-
-Web development normally starts the source Host after one complete build and runs the Client plugin watcher in another terminal:
-
-```sh
-pnpm run dsh -- web --dev
+pnpm dsh web --dev
pnpm run dev:web
```
diff --git a/docs/api-gateway.zh.md b/docs/api-gateway.zh.md
index 692cf825f6..aa9b726c33 100644
--- a/docs/api-gateway.zh.md
+++ b/docs/api-gateway.zh.md
@@ -17,7 +17,7 @@ Service 通常继承 `GatewayService`,让 Cordis service key 与默认 Remote
```ts
import type { Agent } from '@deepseek-ai/dsh-agent'
import { GatewayService, Remote, RemoteScope } from '@deepseek-ai/dsh-type-meta'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export interface CreateGoalRequest {
objective: string
@@ -60,7 +60,7 @@ Client 使用普通对象上的具体函数,不使用 JavaScript Proxy。直
```ts ignore-check
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { AgentContext } from '@deepseek-ai/dsh-client-runtime/client'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-api-remotes/client'
export const inject = ['remote', 'remote.goals']
@@ -138,16 +138,10 @@ SRC 只解决 Host 源码进程的分发问题。Client 不会从运行中的 Ho
## 开发模式
-完整构建会先生成 Host 约定,再编译 Host、Client 与 Web,因此是建立或刷新所有产物的确定性入口:
+仓库的 `dsh` 脚本会先完成 Host、Client 与 Web 构建,再启动源码 Host。Web 开发需要在两个终端中分别运行该命令和 Client plugin watcher:
```sh
-pnpm run build
-```
-
-Web 开发通常在完成一次构建后启动源码 Host,并在另一个终端运行 Client plugin watcher:
-
-```sh
-pnpm run dsh -- web --dev
+pnpm dsh web --dev
pnpm run dev:web
```
diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml
index 344bac8145..ea8b934c79 100644
--- a/docs/architecture.i18n.yaml
+++ b/docs/architecture.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/architecture.md
-architecture.md: 8d1c5a1be391e2455aefc69db89d96027aaf3efa
-architecture.zh.md: 53bf9f54a5503ae40aa62a348822f9d92162dda2
+architecture.md: aeda7f9674e75a1e97549f25c13d571b3b37ee8c
+architecture.zh.md: a25f20ba9babefeaab4636027e97d6f2d4ae8caf
diff --git a/docs/architecture.md b/docs/architecture.md
index 8d1c5a1be3..aeda7f9674 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -42,6 +42,7 @@ Harnesses are [Cordis](cordis-primer.md) contexts; packages contribute services,
| `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | background task registry, generic `task_*` controls |
| `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | script-driven multi-agent orchestration |
| `ctx.goals` | [`goal/`](../packages/goal/README.md) | persisted same-session goals |
+| `ctx.messageFeedback` | [`feedback/`](../packages/feedback/README.md) | lifecycle-bound editable feedback for individual assistant messages and its Host Remote contract |
| `ctx.sessionPersistence` | [`session/`](../packages/session/README.md) | durable session-log storage |
| `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | live-preferred exact/filter/trace queries over SQLite FTS, workspace-authorized model tools |
| `ctx.sessionTitle` | [`session/session-title`](../packages/session/README.md) | log-backed fallbacks, one optional asynchronous provider |
diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md
index 53bf9f54a5..a25f20ba9b 100644
--- a/docs/architecture.zh.md
+++ b/docs/architecture.zh.md
@@ -42,6 +42,7 @@
| `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | 后台任务注册表和通用 `task_*` 控制 |
| `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | 脚本驱动的多 agent 编排 |
| `ctx.goals` | [`goal/`](../packages/goal/README.md) | 持久化的同会话目标 |
+| `ctx.messageFeedback` | [`feedback/`](../packages/feedback/README.md) | 绑定生命周期的单条 assistant 消息可编辑反馈及其 Host Remote 契约 |
| `ctx.sessionPersistence` | [`session/`](../packages/session/README.md) | 会话日志的持久化存储 |
| `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | 基于 SQLite 全文搜索的实时优先精确检索/过滤/追踪、经工作区授权的模型工具 |
| `ctx.sessionTitle` | [`session/session-title`](../packages/session/README.md) | 基于日志的回退标题和单个可选异步提供方 |
diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml
index d6c4832447..c0091e400e 100644
--- a/docs/capability-seams.i18n.yaml
+++ b/docs/capability-seams.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 docs/capability-seams.md
-capability-seams.md: c102167aa76b9ba613b1b434cb0aa58765d26106
-capability-seams.zh.md: 7c4eab8d5a2d890bdf4513bb9acdc81f55414642
+capability-seams.md: 64d20b1bfb609aec3acb3673e4589516bb315bfa
+capability-seams.zh.md: 10b5116d12991319df55c551d51840bb566ac898
diff --git a/docs/capability-seams.md b/docs/capability-seams.md
index c102167aa7..64d20b1bfb 100644
--- a/docs/capability-seams.md
+++ b/docs/capability-seams.md
@@ -30,6 +30,7 @@ flowchart LR
pkg_session_query_sqlite["session-query-sqlite"]
pkg_subagent_inprocess["subagent-inprocess"]
pkg_invariants["invariants"]
+ pkg_message_feedback["message-feedback"]
svc_invariants["ctx.invariants Package-owned invariant registry"]
pkg_scope["scope"]
pkg_typert_registry["typert-registry"]
@@ -60,6 +61,7 @@ flowchart LR
pkg_storage_domain["storage-domain"]
svc_storageDomain["ctx.storageDomain Domain data facility"]
pkg_workspace["workspace"]
+ svc_messageFeedback["ctx.messageFeedback Lifecycle-bound message feedback"]
svc_workspace["ctx.workspace Workspace entity registry"]
svc_sessionQuery["ctx.sessionQuery Session reads, traces, filters, and search"]
pkg_session_reference["session-reference"]
@@ -218,6 +220,7 @@ flowchart LR
pkg_llm_deepseek --> svc_llm
pkg_llm_pi_ai --> svc_llm
pkg_llm_replay --> svc_llm
+ pkg_message_feedback --> svc_messageFeedback
pkg_modules --> svc_clientModuleHost
pkg_permission --> svc_permission
pkg_plan_mode --> svc_planMode
@@ -322,6 +325,7 @@ flowchart LR
svc_sessionPersistence --> pkg_agent_loop
svc_sessionPersistence --> pkg_hooks_claude
svc_sessionPersistence --> pkg_hooks_codex
+ svc_sessionPersistence --> pkg_message_feedback
svc_sessionPersistence --> pkg_session_query
svc_sessionPersistence --> pkg_session_query_sqlite
svc_sessionPersistence --> pkg_tool_bash
@@ -334,6 +338,7 @@ flowchart LR
svc_sessions --> pkg_agent
svc_sessions --> pkg_agent_loop
svc_sessions --> pkg_invariants
+ svc_sessions --> pkg_message_feedback
svc_sessions --> pkg_session_persistence
svc_sessions --> pkg_session_query
svc_sessions --> pkg_session_query_sqlite
@@ -344,6 +349,7 @@ flowchart LR
svc_skills --> pkg_tool_skill
svc_spillStore --> pkg_spill_policy
svc_storage --> pkg_storage_domain
+ svc_storageDomain --> pkg_message_feedback
svc_storageDomain --> pkg_workspace
svc_subagents --> pkg_tool_ralph
svc_subagents --> pkg_tool_subagent
@@ -392,16 +398,17 @@ flowchart LR
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compact-basic`](../packages/compact/compact-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. |
| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
-| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. |
+| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | Owns append-only Session instances and emits the durable session event feed. |
| `ctx.invariants` | `core` | [`invariants`](../packages/support/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. |
| `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges. |
| `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. |
-| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. |
+| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`message-feedback`](../packages/feedback/message-feedback) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. |
| `ctx.settings` | `seam` | [`settings`](../packages/settings/settings) | [`settings-local`](../packages/settings/settings-local) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | Plugins register namespace schemas and resolve layered values; providers store the raw document. The LLM adapters register their entry config as the composition base under the user section; the web gateway serves redacted layered descriptors and writes the user layer. |
| `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | Configuration carries references to secrets; providers own the values. Consumers resolve per operation, so a rotated credential reaches the very next request; the web gateway exposes value-free views and write-only storage. |
| `ctx.telemetry` | `seam` | [`session-telemetry`](../packages/session/session-telemetry) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | - | - | The seam captures, redacts, and hands session records to one backend; nothing else consumes the service — its output leaves the process. |
| `ctx.storage` | `seam` | [`storage`](../packages/storage/storage) | [`storage-json`](../packages/storage/storage-json), [`storage-sqlite`](../packages/storage/storage-sqlite) | [`storage-domain`](../packages/storage/storage-domain) | - | Backends register side by side under names; data forms (domain first) mount on the hub and translate typed operations into opaque KV-unit primitives. |
-| `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace) | - | Waits for every configured backend, then publishes the domain form as one lifecycle-bound service for typed durable state. |
+| `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace), [`message-feedback`](../packages/feedback/message-feedback) | - | Waits for every configured backend, then publishes the domain form as one lifecycle-bound service for typed durable state. |
+| `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | Owns local per-assistant-message feedback, lifecycle and target validation, per-item compare-and-set, and the Host unary Remote contract without entering Session history or telemetry. |
| `ctx.workspace` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | Owns WorkspaceId-branded records over the domain facility; stable sessionIds accounts drive Host RPC and GUI projections. |
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/session-query/tool-session-query) | - | The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations, while the model consumer owns workspace authority and cursor-free rendering. |
| `ctx.sessionReferences` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax. |
diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md
index 7c4eab8d5a..10b5116d12 100644
--- a/docs/capability-seams.zh.md
+++ b/docs/capability-seams.zh.md
@@ -32,6 +32,7 @@ flowchart LR
pkg_session_query_sqlite["session-query-sqlite"]
pkg_subagent_inprocess["subagent-inprocess"]
pkg_invariants["invariants"]
+ pkg_message_feedback["message-feedback"]
svc_invariants["ctx.invariants Package-owned invariant registry"]
pkg_scope["scope"]
pkg_typert_registry["typert-registry"]
@@ -62,6 +63,7 @@ flowchart LR
pkg_storage_domain["storage-domain"]
svc_storageDomain["ctx.storageDomain Domain data facility"]
pkg_workspace["workspace"]
+ svc_messageFeedback["ctx.messageFeedback Lifecycle-bound message feedback"]
svc_workspace["ctx.workspace Workspace entity registry"]
svc_sessionQuery["ctx.sessionQuery Session reads, traces, filters, and search"]
pkg_session_reference["session-reference"]
@@ -220,6 +222,7 @@ flowchart LR
pkg_llm_deepseek --> svc_llm
pkg_llm_pi_ai --> svc_llm
pkg_llm_replay --> svc_llm
+ pkg_message_feedback --> svc_messageFeedback
pkg_modules --> svc_clientModuleHost
pkg_permission --> svc_permission
pkg_plan_mode --> svc_planMode
@@ -324,6 +327,7 @@ flowchart LR
svc_sessionPersistence --> pkg_agent_loop
svc_sessionPersistence --> pkg_hooks_claude
svc_sessionPersistence --> pkg_hooks_codex
+ svc_sessionPersistence --> pkg_message_feedback
svc_sessionPersistence --> pkg_session_query
svc_sessionPersistence --> pkg_session_query_sqlite
svc_sessionPersistence --> pkg_tool_bash
@@ -336,6 +340,7 @@ flowchart LR
svc_sessions --> pkg_agent
svc_sessions --> pkg_agent_loop
svc_sessions --> pkg_invariants
+ svc_sessions --> pkg_message_feedback
svc_sessions --> pkg_session_persistence
svc_sessions --> pkg_session_query
svc_sessions --> pkg_session_query_sqlite
@@ -346,6 +351,7 @@ flowchart LR
svc_skills --> pkg_tool_skill
svc_spillStore --> pkg_spill_policy
svc_storage --> pkg_storage_domain
+ svc_storageDomain --> pkg_message_feedback
svc_storageDomain --> pkg_workspace
svc_subagents --> pkg_tool_ralph
svc_subagents --> pkg_tool_subagent
@@ -394,16 +400,17 @@ flowchart LR
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek)、[`llm-pi-ai`](../packages/llm/llm-pi-ai)、[`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop)、[`compact-basic`](../packages/compact/compact-basic) | - | 适配器注册提供方实现;agent loop(智能体循环)与压缩功能调用提供方无关的流服务。 |
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | 拥有按会话隔离的回放折叠区;压力消费方共享不可变且带修订版本的测量结果。 |
| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 |
-| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop)、[`agent`](../packages/core/agent)、[`session-persistence`](../packages/session/session-persistence)、[`session-query`](../packages/session-query/session-query)、[`session-query-sqlite`](../packages/session-query/session-query-sqlite)、[`subagent-inprocess`](../packages/subagent/subagent-inprocess)、[`invariants`](../packages/support/invariants) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 |
+| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop)、[`agent`](../packages/core/agent)、[`session-persistence`](../packages/session/session-persistence)、[`session-query`](../packages/session-query/session-query)、[`session-query-sqlite`](../packages/session-query/session-query-sqlite)、[`subagent-inprocess`](../packages/subagent/subagent-inprocess)、[`invariants`](../packages/support/invariants)、[`message-feedback`](../packages/feedback/message-feedback) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 |
| `ctx.invariants` | `core` | [`invariants`](../packages/support/invariants) | - | [`session`](../packages/core/session)、[`agent`](../packages/core/agent)、[`scope`](../packages/core/scope)、[`agent-loop`](../packages/core/agent-loop) | - | 配套子路径注册所属包本地的检查;该服务负责选择、唯一性、子 fiber,以及标明所属包的失败。 |
| `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader)、[`api-gateway`](../packages/api/gateway) | - | 插件直接或通过 dsh-typert-loader 注册实时 zod 贡献;API 网关消费调用描述符和提供方,其他运行时消费方则在各自边界查询 schema 与反射元数据。 |
| `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | 将生成的 Remote 描述符与实时 Cordis 服务关联,解析已注册的身份,并通过共享的 Connection RPC 载体提供一元调用。 |
-| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl)、[`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop)、[`tool-bash`](../packages/bash/tool-bash)、[`hooks-claude`](../packages/hooks/hooks-claude)、[`hooks-codex`](../packages/hooks/hooks-codex)、[`session-query`](../packages/session-query/session-query)、[`session-query-sqlite`](../packages/session-query/session-query-sqlite) | - | 各后端持久化同一套 SessionEvent 词汇;应用在组合时选择后端。 |
+| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl)、[`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop)、[`tool-bash`](../packages/bash/tool-bash)、[`hooks-claude`](../packages/hooks/hooks-claude)、[`hooks-codex`](../packages/hooks/hooks-codex)、[`session-query`](../packages/session-query/session-query)、[`session-query-sqlite`](../packages/session-query/session-query-sqlite)、[`message-feedback`](../packages/feedback/message-feedback) | - | 各后端持久化同一套 SessionEvent 词汇;应用在组合时选择后端。 |
| `ctx.settings` | `seam` | [`settings`](../packages/settings/settings) | [`settings-local`](../packages/settings/settings-local) | [`llm-deepseek`](../packages/llm/llm-deepseek)、[`llm-pi-ai`](../packages/llm/llm-pi-ai)、`apiproxy` | - | 插件注册命名空间 schema 并解析分层值;提供方存储原始文档。LLM(大语言模型)适配器在用户分区下将其入口配置注册为组合基础;Web 网关提供经过脱敏的分层描述符,并写入用户层。 |
| `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`llm-deepseek`](../packages/llm/llm-deepseek)、[`llm-pi-ai`](../packages/llm/llm-pi-ai)、`apiproxy` | - | 配置携带对机密信息的引用;提供方拥有实际值。消费方按操作解析,因此轮换后的凭据会在紧接着的下一次请求中生效;Web 网关提供不含实际值的视图和只写存储。 |
| `ctx.telemetry` | `seam` | [`session-telemetry`](../packages/session/session-telemetry) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | - | - | 该 seam 捕获会话记录、进行脱敏并交给一个后端;没有其他组件消费该服务,其输出会离开当前进程。 |
| `ctx.storage` | `seam` | [`storage`](../packages/storage/storage) | [`storage-json`](../packages/storage/storage-json)、[`storage-sqlite`](../packages/storage/storage-sqlite) | [`storage-domain`](../packages/storage/storage-domain) | - | 各后端以不同名称并列注册;数据形态(领域优先)挂载到枢纽上,并将类型化操作转换为不透明的 KV 单元原语。 |
-| `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace) | - | 等待所有已配置后端就绪,然后将领域形态发布为一个受生命周期约束的服务,用于类型化持久状态。 |
+| `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace)、[`message-feedback`](../packages/feedback/message-feedback) | - | 等待所有已配置后端就绪,然后将领域形态发布为一个受生命周期约束的服务,用于类型化持久状态。 |
+| `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | 拥有本地逐 assistant 消息反馈、生命周期与目标校验、逐条目 compare-and-set 及 Host 一元 Remote 契约,且不进入 Session 历史或遥测。 |
| `ctx.workspace` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | 通过领域设施拥有带 WorkspaceId 品牌类型的记录;稳定的 sessionIds 账户驱动 Host RPC 与 GUI 投影。 |
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference)、[`tool-session-query`](../packages/session-query/tool-session-query) | - | 该接口提供精确读取、过滤和追踪;具体后端还提供全文协调、排序、摘要片段和游标世代,而模型消费方负责工作区权限与不含游标的渲染。 |
| `ctx.sessionReferences` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | 将当前表层中有界的对话快照投影为持久但不可信的消息上下文;Host 适配器负责提及语法。 |
diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index 889b2e24e7..75e5d33e09 100644
--- a/docs/config-catalog.i18n.yaml
+++ b/docs/config-catalog.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 docs/config-catalog.md
-config-catalog.md: 471680f92dc44f3dd4e98ba9e946525ec79f25b0
-config-catalog.zh.md: bf78766799b02a1f6f21f935723abace108bc306
+config-catalog.md: 1e2416d479daaef0f9b6b03846406fab4c30f21a
+config-catalog.zh.md: 5a9ecc5b30c55f70b2685fe9633430e68f30f6c8
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index 471680f92d..1e2416d479 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -259,7 +259,7 @@ export interface Config {
Depends on: [`ToolPresentationMode`](subsystems/tools.md)
-Source: [`packages/core/agent-tool-mode/src/index.ts:36`](../packages/core/agent-tool-mode/src/index.ts)
+Source: [`packages/core/agent-tool-mode/src/index.ts:38`](../packages/core/agent-tool-mode/src/index.ts)
## `@deepseek-ai/dsh-attachment-local`
@@ -533,7 +533,7 @@ export interface Config {
}
```
-Source: [`packages/fs/fs-local/src/index.ts:40`](../packages/fs/fs-local/src/index.ts)
+Source: [`packages/fs/fs-local/src/index.ts:41`](../packages/fs/fs-local/src/index.ts)
## `@deepseek-ai/dsh-fs-sandbox`
@@ -572,7 +572,7 @@ Source: [`packages/goal/goal/src/index.ts:116`](../packages/goal/goal/src/index.
Requires: `agentDefaultModel` · `agents` · `sessions`
```ts config-catalog
-/** Plugin config: the task, patched in by the launcher. */
+/** Plugin config: the task resolved from this app's injected provider service. */
export interface Config {
/** The prompt text for the single run. */
task: string
@@ -986,6 +986,8 @@ export interface ReplayModelConfig {
description?: string
/** Optional positive integer context capacity published by the replay adapter. */
contextWindow?: number
+ /** Optional declared input modalities, so a scenario can exercise capability gates (e.g. image-capable `read_image`). */
+ inputModalities?: readonly ModelModality[]
/**
* Optional per-request output cap the replay route materializes when callers
* omit one, so replay reconstructs the request header a live catalog produced.
@@ -1001,9 +1003,9 @@ export interface ReplayModelConfig {
}
```
-Depends on: [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
+Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
-Source: [`packages/support/llm-replay/src/index.ts:769`](../packages/support/llm-replay/src/index.ts)
+Source: [`packages/support/llm-replay/src/index.ts:776`](../packages/support/llm-replay/src/index.ts)
## `@deepseek-ai/dsh-llm-retry`
@@ -1086,6 +1088,8 @@ export interface StdioConfig {
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
+ /** Automatic reconnect policy after a lost connection; omission uses the defaults. */
+ reconnect?: ReconnectConfig
}
/** Config for connecting to an MCP server over Streamable HTTP (SSE). */
@@ -1106,10 +1110,38 @@ export interface StreamableHttpConfig {
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
+ /** Automatic reconnect policy after a lost connection; omission uses the defaults. */
+ reconnect?: ReconnectConfig
+}
+
+/** Automatic reconnect policy for one MCP server connection. */
+export interface ReconnectConfig {
+ /** Reconnect automatically after a lost connection (default true). */
+ enabled?: boolean
+ /** First reconnect delay in milliseconds; doubles per consecutive failed attempt (default 500). */
+ initialDelayMs?: number
+ /** Backoff ceiling in milliseconds; also the uptime after which the attempt budget resets (default 30000). */
+ maxDelayMs?: number
+ /** Consecutive failed attempts per outage before giving up for good (default 10). */
+ maxAttempts?: number
}
```
-Source: [`packages/mcp/mcp-client/src/index.ts:100`](../packages/mcp/mcp-client/src/index.ts)
+Source: [`packages/mcp/mcp-client/src/index.ts:98`](../packages/mcp/mcp-client/src/index.ts)
+
+## `@deepseek-ai/dsh-message-feedback`
+
+Requires: `storageDomain` · `sessionPersistence` · `sessions`
+
+```ts config-catalog
+/** Required deployment policy for optional notes. */
+export interface Config {
+ /** Maximum UTF-8 byte length accepted for one note. */
+ readonly maxNoteBytes: number
+}
+```
+
+Source: [`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts)
## `@deepseek-ai/dsh-permission`
@@ -1161,6 +1193,8 @@ export interface Config {
* variables. Empty text drops the section at render, matching the registry.
*/
text: string
+ /** Make this persona the complete system prompt, suppressing every other section. */
+ complete?: boolean
}
```
@@ -1306,22 +1340,6 @@ export interface Config {
Source: [`packages/guard/repeat-tool-guard/src/index.ts:28`](../packages/guard/repeat-tool-guard/src/index.ts)
-## `@deepseek-ai/dsh-repository-plugin`
-
-Requires: `loader`
-
-```ts config-catalog
-/** Repository Plugin runtime and source-list configuration. */
-export interface Config {
- /** GitHub repository sources with explicit refs and optional `.dsh-plugin` subpaths. */
- repositories?: string[]
- /** Persistent generation cache; defaults to `$DSH_HOME/cache/repository-plugins`. */
- cacheDir?: string
-}
-```
-
-Source: [`packages/self-modification/repository-plugin/src/index.ts:44`](../packages/self-modification/repository-plugin/src/index.ts)
-
## `@deepseek-ai/dsh-sandbox-local`
```ts config-catalog
@@ -1350,7 +1368,7 @@ export interface Config {
}
```
-Source: [`packages/sandbox/sandbox-local/src/index.ts:43`](../packages/sandbox/sandbox-local/src/index.ts)
+Source: [`packages/sandbox/sandbox-local/src/index.ts:44`](../packages/sandbox/sandbox-local/src/index.ts)
## `@deepseek-ai/dsh-sandbox-policy`
@@ -1412,7 +1430,7 @@ export interface Config {
export type JsonlCompression = 'zstd' | 'none'
```
-Source: [`packages/session/session-persistence-jsonl/src/index.ts:59`](../packages/session/session-persistence-jsonl/src/index.ts)
+Source: [`packages/session/session-persistence-jsonl/src/index.ts:60`](../packages/session/session-persistence-jsonl/src/index.ts)
## `@deepseek-ai/dsh-session-persistence-sqlite`
@@ -1455,7 +1473,7 @@ export interface Config {
export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
```
-Source: [`packages/session/session-persistence-sqlite/src/index.ts:67`](../packages/session/session-persistence-sqlite/src/index.ts)
+Source: [`packages/session/session-persistence-sqlite/src/index.ts:70`](../packages/session/session-persistence-sqlite/src/index.ts)
## `@deepseek-ai/dsh-session-projection-cache`
@@ -1576,7 +1594,7 @@ export enum TelemetryMode {
Depends on: `BatchLogRecordProcessorOptions` (`@opentelemetry/sdk-logs`) · `OTLPExporterNodeConfigBase` (`@opentelemetry/otlp-exporter-base`)
-Source: [`packages/session/session-telemetry-otel/src/index.ts:79`](../packages/session/session-telemetry-otel/src/index.ts)
+Source: [`packages/session/session-telemetry-otel/src/index.ts:91`](../packages/session/session-telemetry-otel/src/index.ts)
## `@deepseek-ai/dsh-session-title`
@@ -2003,7 +2021,7 @@ export interface Config {
}
```
-Source: [`packages/core/system-prompt/src/index.ts:177`](../packages/core/system-prompt/src/index.ts)
+Source: [`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
## `@deepseek-ai/dsh-time-context`
@@ -2114,7 +2132,7 @@ export interface Config {
}
```
-Source: [`packages/fs/tool-fs/src/index.ts:24`](../packages/fs/tool-fs/src/index.ts)
+Source: [`packages/fs/tool-fs/src/index.ts:25`](../packages/fs/tool-fs/src/index.ts)
## `@deepseek-ai/dsh-tool-fs-search`
@@ -2534,33 +2552,28 @@ Source: [`packages/web/web/src/index.ts:55`](../packages/web/web/src/index.ts)
Requires: `httpServer`
```ts config-catalog
-/** Plugin config: the surface facts the launcher patches over this bundle's defaults. */
+/** Plugin config: composed deployment settings plus per-invocation command-line values. */
export interface Config {
/** Whether this process mounted the client-plugin HMR receiver (`dsh web --dev`). */
mode: WebMode
- /** Print the URL line on activation; a headless layer over this bundle turns it off. */
+ /** Print the URL line on activation; a non-interactive layer can turn it off. */
printUrl: boolean
/**
* Register the model-visible surface context (the `app:web-surface` prompt
* section and the `DSH_WEB_URL`/`DSH_WEB_MODE` bash variables). A one-shot
- * layer turns it off: its user is not interacting through the GUI, so the
+ * non-interactive layer can turn it off when its user is not in the GUI, so the
* orientation text would be false.
*/
surfaceContext: boolean
- /**
- * LAN IPv4 addresses sampled once by the launcher when the effective bind
- * is all-interfaces — the exact snapshot the /api trust fence was
- * configured with, so the printed LAN URL can never name an address the
- * fence rejects. Empty on a loopback bind.
- */
- lanAddresses: string[]
+ /** Explicit `--trusted-host` authorities from this invocation. */
+ trustedHosts: string[]
}
/** Web runtime mode: production, or development when the client-plugin HMR receiver is active. */
export type WebMode = 'production' | 'development'
```
-Source: [`packages/bundle/web-app/src/index.ts:32`](../packages/bundle/web-app/src/index.ts)
+Source: [`packages/bundle/web-app/src/index.ts:43`](../packages/bundle/web-app/src/index.ts)
## `@deepseek-ai/dsh-web-fetch-local`
@@ -2739,6 +2752,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-client-ui-skill` ([`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts))
- `@deepseek-ai/dsh-client-ui-slash` ([`packages/client/ui-slash/src/index.ts`](../packages/client/ui-slash/src/index.ts))
- `@deepseek-ai/dsh-client-ui-subagent` ([`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-task` ([`packages/client/ui-task/src/index.ts`](../packages/client/ui-task/src/index.ts))
- `@deepseek-ai/dsh-client-ui-theme` ([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts))
- `@deepseek-ai/dsh-client-ui-tool` ([`packages/client/ui-tool/src/index.ts`](../packages/client/ui-tool/src/index.ts))
- `@deepseek-ai/dsh-client-ui-trajectory` ([`packages/client/ui-trajectory/src/index.ts`](../packages/client/ui-trajectory/src/index.ts))
@@ -2806,6 +2820,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/dsh-client-ui-slots` ([`packages/client/ui-slots/src/index.ts`](../packages/client/ui-slots/src/index.ts))
- `@deepseek-ai/dsh-client-web` ([`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts))
- `@deepseek-ai/dsh-client-web-react` ([`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts))
+- `@deepseek-ai/dsh-cmdline` ([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts))
- `@deepseek-ai/dsh-environment` ([`packages/util/environment/src/index.ts`](../packages/util/environment/src/index.ts))
- `@deepseek-ai/dsh-helper` ([`packages/scaffold/helper/src/index.ts`](../packages/scaffold/helper/src/index.ts))
- `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index bf78766799..5a9ecc5b30 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -261,7 +261,7 @@ export interface Config {
依赖:[`ToolPresentationMode`](subsystems/tools.md)
-来源:[`packages/core/agent-tool-mode/src/index.ts:36`](../packages/core/agent-tool-mode/src/index.ts)
+来源:[`packages/core/agent-tool-mode/src/index.ts:38`](../packages/core/agent-tool-mode/src/index.ts)
## `@deepseek-ai/dsh-attachment-local`
@@ -574,7 +574,7 @@ export interface Config {
需要:`agentDefaultModel` · `agents` · `sessions`
```ts config-catalog
-/** Plugin config: the task, patched in by the launcher. */
+/** Plugin config: the task resolved from this app's injected provider service. */
export interface Config {
/** The prompt text for the single run. */
task: string
@@ -988,6 +988,8 @@ export interface ReplayModelConfig {
description?: string
/** Optional positive integer context capacity published by the replay adapter. */
contextWindow?: number
+ /** Optional declared input modalities, so a scenario can exercise capability gates (e.g. image-capable `read_image`). */
+ inputModalities?: readonly ModelModality[]
/**
* Optional per-request output cap the replay route materializes when callers
* omit one, so replay reconstructs the request header a live catalog produced.
@@ -1003,9 +1005,9 @@ export interface ReplayModelConfig {
}
```
-依赖:[`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
+依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
-来源:[`packages/support/llm-replay/src/index.ts:769`](../packages/support/llm-replay/src/index.ts)
+来源:[`packages/support/llm-replay/src/index.ts:776`](../packages/support/llm-replay/src/index.ts)
## `@deepseek-ai/dsh-llm-retry`
@@ -1088,6 +1090,8 @@ export interface StdioConfig {
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
+ /** Automatic reconnect policy after a lost connection; omission uses the defaults. */
+ reconnect?: ReconnectConfig
}
/** Config for connecting to an MCP server over Streamable HTTP (SSE). */
@@ -1108,10 +1112,38 @@ export interface StreamableHttpConfig {
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
+ /** Automatic reconnect policy after a lost connection; omission uses the defaults. */
+ reconnect?: ReconnectConfig
+}
+
+/** Automatic reconnect policy for one MCP server connection. */
+export interface ReconnectConfig {
+ /** Reconnect automatically after a lost connection (default true). */
+ enabled?: boolean
+ /** First reconnect delay in milliseconds; doubles per consecutive failed attempt (default 500). */
+ initialDelayMs?: number
+ /** Backoff ceiling in milliseconds; also the uptime after which the attempt budget resets (default 30000). */
+ maxDelayMs?: number
+ /** Consecutive failed attempts per outage before giving up for good (default 10). */
+ maxAttempts?: number
}
```
-来源:[`packages/mcp/mcp-client/src/index.ts:100`](../packages/mcp/mcp-client/src/index.ts)
+来源:[`packages/mcp/mcp-client/src/index.ts:94`](../packages/mcp/mcp-client/src/index.ts)
+
+## `@deepseek-ai/dsh-message-feedback`
+
+需要:`storageDomain` · `sessionPersistence` · `sessions`
+
+```ts config-catalog
+/** Required deployment policy for optional notes. */
+export interface Config {
+ /** Maximum UTF-8 byte length accepted for one note. */
+ readonly maxNoteBytes: number
+}
+```
+
+来源:[`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts)
## `@deepseek-ai/dsh-permission`
@@ -1163,6 +1195,8 @@ export interface Config {
* variables. Empty text drops the section at render, matching the registry.
*/
text: string
+ /** Make this persona the complete system prompt, suppressing every other section. */
+ complete?: boolean
}
```
@@ -1308,22 +1342,6 @@ export interface Config {
来源:[`packages/guard/repeat-tool-guard/src/index.ts:28`](../packages/guard/repeat-tool-guard/src/index.ts)
-## `@deepseek-ai/dsh-repository-plugin`
-
-需要:`loader`
-
-```ts config-catalog
-/** Repository Plugin runtime and source-list configuration. */
-export interface Config {
- /** GitHub repository sources with explicit refs and optional `.dsh-plugin` subpaths. */
- repositories?: string[]
- /** Persistent generation cache; defaults to `$DSH_HOME/cache/repository-plugins`. */
- cacheDir?: string
-}
-```
-
-来源:[`packages/self-modification/repository-plugin/src/index.ts:44`](../packages/self-modification/repository-plugin/src/index.ts)
-
## `@deepseek-ai/dsh-sandbox-local`
```ts config-catalog
@@ -1352,7 +1370,7 @@ export interface Config {
}
```
-来源:[`packages/sandbox/sandbox-local/src/index.ts:43`](../packages/sandbox/sandbox-local/src/index.ts)
+来源:[`packages/sandbox/sandbox-local/src/index.ts:44`](../packages/sandbox/sandbox-local/src/index.ts)
## `@deepseek-ai/dsh-sandbox-policy`
@@ -1457,7 +1475,7 @@ export interface Config {
export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
```
-来源:[`packages/session/session-persistence-sqlite/src/index.ts:67`](../packages/session/session-persistence-sqlite/src/index.ts)
+来源:[`packages/session/session-persistence-sqlite/src/index.ts:70`](../packages/session/session-persistence-sqlite/src/index.ts)
## `@deepseek-ai/dsh-session-projection-cache`
@@ -2005,7 +2023,7 @@ export interface Config {
}
```
-来源:[`packages/core/system-prompt/src/index.ts:177`](../packages/core/system-prompt/src/index.ts)
+来源:[`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
## `@deepseek-ai/dsh-time-context`
@@ -2535,33 +2553,28 @@ export interface WebServiceConfig {
需要:`httpServer`
```ts config-catalog
-/** Plugin config: the surface facts the launcher patches over this bundle's defaults. */
+/** Plugin config: composed deployment settings plus per-invocation command-line values. */
export interface Config {
/** Whether this process mounted the client-plugin HMR receiver (`dsh web --dev`). */
mode: WebMode
- /** Print the URL line on activation; a headless layer over this bundle turns it off. */
+ /** Print the URL line on activation; a non-interactive layer can turn it off. */
printUrl: boolean
/**
* Register the model-visible surface context (the `app:web-surface` prompt
* section and the `DSH_WEB_URL`/`DSH_WEB_MODE` bash variables). A one-shot
- * layer turns it off: its user is not interacting through the GUI, so the
+ * non-interactive layer can turn it off when its user is not in the GUI, so the
* orientation text would be false.
*/
surfaceContext: boolean
- /**
- * LAN IPv4 addresses sampled once by the launcher when the effective bind
- * is all-interfaces — the exact snapshot the /api trust fence was
- * configured with, so the printed LAN URL can never name an address the
- * fence rejects. Empty on a loopback bind.
- */
- lanAddresses: string[]
+ /** Explicit `--trusted-host` authorities from this invocation. */
+ trustedHosts: string[]
}
/** Web runtime mode: production, or development when the client-plugin HMR receiver is active. */
export type WebMode = 'production' | 'development'
```
-来源:[`packages/bundle/web-app/src/index.ts:32`](../packages/bundle/web-app/src/index.ts)
+来源:[`packages/bundle/web-app/src/index.ts:43`](../packages/bundle/web-app/src/index.ts)
## `@deepseek-ai/dsh-web-fetch-local`
@@ -2740,6 +2753,7 @@ export interface Config {
- `@deepseek-ai/dsh-client-ui-skill`([`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts))
- `@deepseek-ai/dsh-client-ui-slash`([`packages/client/ui-slash/src/index.ts`](../packages/client/ui-slash/src/index.ts))
- `@deepseek-ai/dsh-client-ui-subagent`([`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-task`([`packages/client/ui-task/src/index.ts`](../packages/client/ui-task/src/index.ts))
- `@deepseek-ai/dsh-client-ui-theme`([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts))
- `@deepseek-ai/dsh-client-ui-tool`([`packages/client/ui-tool/src/index.ts`](../packages/client/ui-tool/src/index.ts))
- `@deepseek-ai/dsh-client-ui-trajectory`([`packages/client/ui-trajectory/src/index.ts`](../packages/client/ui-trajectory/src/index.ts))
@@ -2806,6 +2820,7 @@ export interface Config {
- `@deepseek-ai/dsh-client-ui-slots`([`packages/client/ui-slots/src/index.ts`](../packages/client/ui-slots/src/index.ts))
- `@deepseek-ai/dsh-client-web`([`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts))
- `@deepseek-ai/dsh-client-web-react`([`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts))
+- `@deepseek-ai/dsh-cmdline`([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts))
- `@deepseek-ai/dsh-environment`([`packages/util/environment/src/index.ts`](../packages/util/environment/src/index.ts))
- `@deepseek-ai/dsh-helper`([`packages/scaffold/helper/src/index.ts`](../packages/scaffold/helper/src/index.ts))
- `@deepseek-ai/dsh-hook-protocol`([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
diff --git a/docs/cookbook/adding-a-conversation-node.i18n.yaml b/docs/cookbook/adding-a-conversation-node.i18n.yaml
index aa268e9461..52234b5562 100644
--- a/docs/cookbook/adding-a-conversation-node.i18n.yaml
+++ b/docs/cookbook/adding-a-conversation-node.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 docs/cookbook/adding-a-conversation-node.md
-adding-a-conversation-node.md: ea4ec73eb109af6b0e4c7cf50fc8692942c75dd4
-adding-a-conversation-node.zh.md: 4b9a8049e2f1d060ec4bc3334036559b989ea562
+adding-a-conversation-node.md: c1965dc8a3081eebb8c1026ac53d2f7b8964edb7
+adding-a-conversation-node.zh.md: 92445e1432369a4e42cc372b5d5869c3cdeada4a
diff --git a/docs/cookbook/adding-a-conversation-node.md b/docs/cookbook/adding-a-conversation-node.md
index ea4ec73eb1..c1965dc8a3 100644
--- a/docs/cookbook/adding-a-conversation-node.md
+++ b/docs/cookbook/adding-a-conversation-node.md
@@ -120,6 +120,7 @@ function viewData(state: ReviewState): ReviewChatData {
const reviewDefinition: ConversationNodeDefinition = {
kind: 'review-job',
+ target: 'chat',
match: (event) => {
if (event.type === 'review/start') {
return { id: String(event.data.reviewId), role: 'start' }
@@ -161,8 +162,8 @@ const reviewDefinition: ConversationNodeDefinition = {
value: viewData(context.state),
}
},
- buildViewNode: (context, target) => {
- if (target !== 'chat' || context.state === undefined) return null
+ buildViewNode: (context) => {
+ if (context.state === undefined) return null
return {
key: context.key,
kind: 'review-job',
@@ -196,7 +197,7 @@ export function apply(ctx: ClientContext): void {
`buildLocationData(context, scope)` optionally publishes Definition-owned data onto an engine-owned Turn or Step. Use declaration merging to give each key a precise value type. Another Node in the same Location can consume that value through its constrained slot hook, such as `useTurnData(key)`, without receiving the Session or scanning `snapshot.chat.nodes`.
-`buildViewNode(context, target)` materializes the final target-specific Node. Preserve `context.key` as the React-facing identity, choose `anchorSeq` from durable ordering evidence, and return only renderer-ready data. Once a target Node has been published, keep returning the same key; use `visibility: 'hidden'` when it must temporarily leave the visible flow rather than withdrawing it with `null`.
+`target` and `buildViewNode(context)` declare one target-owned rendering contribution and must appear together. Preserve `context.key` as the React-facing identity, choose `anchorSeq` from durable ordering evidence, and return only renderer-ready data. Once a target Node has been published, keep returning the same key; use `visibility: 'hidden'` when it must temporarily leave the visible flow rather than withdrawing it with `null`.
## 3. Query an earlier business Context only at start
diff --git a/docs/cookbook/adding-a-conversation-node.zh.md b/docs/cookbook/adding-a-conversation-node.zh.md
index 4b9a8049e2..92445e1432 100644
--- a/docs/cookbook/adding-a-conversation-node.zh.md
+++ b/docs/cookbook/adding-a-conversation-node.zh.md
@@ -120,6 +120,7 @@ function viewData(state: ReviewState): ReviewChatData {
const reviewDefinition: ConversationNodeDefinition = {
kind: 'review-job',
+ target: 'chat',
match: (event) => {
if (event.type === 'review/start') {
return { id: String(event.data.reviewId), role: 'start' }
@@ -161,8 +162,8 @@ const reviewDefinition: ConversationNodeDefinition = {
value: viewData(context.state),
}
},
- buildViewNode: (context, target) => {
- if (target !== 'chat' || context.state === undefined) return null
+ buildViewNode: (context) => {
+ if (context.state === undefined) return null
return {
key: context.key,
kind: 'review-job',
@@ -196,7 +197,7 @@ export function apply(ctx: ClientContext): void {
`buildLocationData(context, scope)` 可以把 Definition 拥有的数据发布到引擎拥有的 Turn 或 Step 上。通过 declaration merging 为每个 key 指定精确 value 类型。同一 Location 内的另一个 Node 可以使用受限 slot hook(例如 `useTurnData(key)`)读取该值,无须取得 Session,也无须扫描 `snapshot.chat.nodes`。
-`buildViewNode(context, target)` 物化最终的目标专用 Node。把 `context.key` 保留为 React 侧身份,根据持久排序证据选择 `anchorSeq`,并且只返回 renderer 可以直接使用的数据。某个 target Node 一旦发布,就要继续返回同一个 key;需要暂时离开可见流时使用 `visibility: 'hidden'`,不要改为返回 `null` 撤回它。
+`target` 与 `buildViewNode(context)` 必须同时声明一项由 target 拥有的渲染贡献。把 `context.key` 保留为 React 侧身份,根据持久排序证据选择 `anchorSeq`,并且只返回 renderer 可以直接使用的数据。某个 target Node 一旦发布,就要继续返回同一个 key;需要暂时离开可见流时使用 `visibility: 'hidden'`,不要改为返回 `null` 撤回它。
## 3. 只在 start 时查询更早的业务 Context
diff --git a/docs/cookbook/adding-a-package.i18n.yaml b/docs/cookbook/adding-a-package.i18n.yaml
index 5bdff5df21..0dcfdafacd 100644
--- a/docs/cookbook/adding-a-package.i18n.yaml
+++ b/docs/cookbook/adding-a-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 docs/cookbook/adding-a-package.md
-adding-a-package.md: dcd5fa66f3616c2c22930babd09cb3edab38e182
-adding-a-package.zh.md: c8769197e0b1db31348b7f2442dbcd636bf43cb2
+adding-a-package.md: e108b9e88e0f0e470f96173306af79c69ff695fb
+adding-a-package.zh.md: 97b4df150fda7010fba048d7acb5e23a87519911
diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md
index dcd5fa66f3..e108b9e88e 100644
--- a/docs/cookbook/adding-a-package.md
+++ b/docs/cookbook/adding-a-package.md
@@ -22,7 +22,7 @@ packages///
Choose an existing group when one matches the package's role (`core`, `llm`, `bash`, `compact`, `subagent`, `todo`, `session-persistence`, `ui`, `util`, or `support`). A new group is allowed, but it is a pure container: no `package.json`, no source files, and packages still sit exactly one level below it.
-package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, a `version` matching the root `package.json`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list contains exactly `lib/index.js`, `lib/invariant.js`, `lib/types/**/*.d.ts`, and package-specific runtime artifacts recognized by the gate; a package whose runtime export points into the emitted tree also includes `lib/types/**/*.js`. Do not publish `src`, declaration maps, JS maps, or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`.
+package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, a `version` matching the root `package.json`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `@deepseek-ai/cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `@deepseek-ai/schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list contains exactly `lib/index.js`, `lib/invariant.js`, `lib/types/**/*.d.ts`, and package-specific runtime artifacts recognized by the gate; a package whose runtime export points into the emitted tree also includes `lib/types/**/*.js`. Do not publish `src`, declaration maps, JS maps, or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`.
In-package relative imports use explicit `.ts` specifiers in source (for example, `export * from './types.ts'`). The compiler rewrites those to `.js` in emitted JS and leaves explicit `.ts` specifiers in declarations, which standard NodeNext/Node16 TypeScript consumers resolve to the sibling `.d.ts` files.
diff --git a/docs/cookbook/adding-a-package.zh.md b/docs/cookbook/adding-a-package.zh.md
index c8769197e0..97b4df150f 100644
--- a/docs/cookbook/adding-a-package.zh.md
+++ b/docs/cookbook/adding-a-package.zh.md
@@ -22,7 +22,7 @@ packages///
当已有分组与包的角色匹配时,选择该分组(`core`、`llm`、`bash`、`compact`、`subagent`、`todo`、`session-persistence`、`ui`、`util` 或 `support`)。允许新建分组,但分组只是纯容器:没有 `package.json`,没有源文件,包仍然恰好位于其下一层。
-package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-constraints.ts` 强制执行):`private: true`,`version` 与根 `package.json` 一致,`type: module`,`main: "lib/index.js"`,`types: "lib/types/index.d.ts"`,`exports["."].types: "./lib/types/index.d.ts"`,`exports["."].default: "./lib/index.js"`,`cordis` 同时出现在 peerDependencies 和 devDependencies 中(相同范围)。每个 dsh 对等依赖(peer dependency)都要在 devDependencies 中镜像。`schemastery` 放在 `dependencies` 中(它是运行时校验器),与 agent-loop 保持一致。`files` 列表精确包含 `lib/index.js`、`lib/invariant.js`、`lib/types/**/*.d.ts` 以及门禁认可的包专用运行时产物;如果包的运行时 export 指向输出树,还要包含 `lib/types/**/*.js`。不要发布 `src`、声明映射、JS map 或陈旧的根声明文件。带有 `bin` 的 CLI 应用包在 `files` 中将 `lib/bin.js` 紧跟在 `lib/index.js` 之后。
+package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-constraints.ts` 强制执行):`private: true`,`version` 与根 `package.json` 一致,`type: module`,`main: "lib/index.js"`,`types: "lib/types/index.d.ts"`,`exports["."].types: "./lib/types/index.d.ts"`,`exports["."].default: "./lib/index.js"`,`@deepseek-ai/cordis` 同时出现在 peerDependencies 和 devDependencies 中(相同范围)。每个 dsh 对等依赖(peer dependency)都要在 devDependencies 中镜像。`@deepseek-ai/schemastery` 放在 `dependencies` 中(它是运行时校验器),与 agent-loop 保持一致。`files` 列表精确包含 `lib/index.js`、`lib/invariant.js`、`lib/types/**/*.d.ts` 以及门禁认可的包专用运行时产物;如果包的运行时 export 指向输出树,还要包含 `lib/types/**/*.js`。不要发布 `src`、声明映射、JS map 或陈旧的根声明文件。带有 `bin` 的 CLI 应用包在 `files` 中将 `lib/bin.js` 紧跟在 `lib/index.js` 之后。
包内的相对导入在源码中使用显式 `.ts` 后缀(例如 `export * from './types.ts'`)。编译器在输出的 JS 中将其重写为 `.js`,在声明文件中保留显式 `.ts` 后缀;标准的 NodeNext/Node16 TypeScript 消费方会将其解析到同目录的 `.d.ts` 文件。
diff --git a/docs/cookbook/adding-a-tool.i18n.yaml b/docs/cookbook/adding-a-tool.i18n.yaml
index e232ad4302..5b47beab61 100644
--- a/docs/cookbook/adding-a-tool.i18n.yaml
+++ b/docs/cookbook/adding-a-tool.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 docs/cookbook/adding-a-tool.md
-adding-a-tool.md: b030d3c3a6b7dd66b6594779345a96af3a895bd8
-adding-a-tool.zh.md: 4272a7a4571782bc213ca29de1a57d51fbd24075
+adding-a-tool.md: fa39c4b97f3c0eb739ea34d1b43ef46d11285bbf
+adding-a-tool.zh.md: ab32e90fac5539ee403a36b2dd52e60db3ad603c
diff --git a/docs/cookbook/adding-a-tool.md b/docs/cookbook/adding-a-tool.md
index b030d3c3a6..fa39c4b97f 100644
--- a/docs/cookbook/adding-a-tool.md
+++ b/docs/cookbook/adding-a-tool.md
@@ -8,7 +8,7 @@ Reference for the contracts a model-facing tool must satisfy. For an ordered fir
```ts
import { readFile } from 'node:fs/promises'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-tool'
diff --git a/docs/cookbook/adding-a-tool.zh.md b/docs/cookbook/adding-a-tool.zh.md
index 4272a7a457..ab32e90fac 100644
--- a/docs/cookbook/adding-a-tool.zh.md
+++ b/docs/cookbook/adding-a-tool.zh.md
@@ -8,7 +8,7 @@
```ts
import { readFile } from 'node:fs/promises'
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-tool'
diff --git a/docs/cookbook/adding-a-vendored-package.i18n.yaml b/docs/cookbook/adding-a-vendored-package.i18n.yaml
index 4f5b8c3c49..b17f3390cf 100644
--- a/docs/cookbook/adding-a-vendored-package.i18n.yaml
+++ b/docs/cookbook/adding-a-vendored-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 docs/cookbook/adding-a-vendored-package.md
-adding-a-vendored-package.md: 724d89c1c7cd728f7123a6975b5500cd40815851
-adding-a-vendored-package.zh.md: d16ec1056431a4ac1c02d50a5ef0f0a64b67ca6d
+adding-a-vendored-package.md: 239ac27565204332559038014fabae83fc2d1057
+adding-a-vendored-package.zh.md: 2bf5c7eeaffec594dbc4d13e0aa33b12a9eccf4f
diff --git a/docs/cookbook/adding-a-vendored-package.md b/docs/cookbook/adding-a-vendored-package.md
index 724d89c1c7..239ac27565 100644
--- a/docs/cookbook/adding-a-vendored-package.md
+++ b/docs/cookbook/adding-a-vendored-package.md
@@ -8,7 +8,7 @@ When the harness needs another upstream Cordis package (e.g. `@cordisjs/plugin-h
```
vendor//
- package.json # from upstream; set "private": true, keep name/exports/type
+ package.json # from upstream; set "private": true, rescope the name, keep exports/type
tsconfig.json # extends ../../tsconfig.base.json (see configuration below)
src/ # the upstream src/ verbatim
README.md LICENSE # if upstream ships them
@@ -29,7 +29,7 @@ vendor//
}
```
-`package.json` invariants: `"private": true` (vendored packages are never published), keep upstream's `name`/`version`/`exports`/`type`, point declaration metadata at `lib/types`, publish `.d.ts` and `.d.ts.map` declaration outputs, and list its cordis deps in `peerDependencies` (matching the upstream manifest). Transitive upstream deps must themselves be vendored or already present — vendoring one package often means vendoring its dependency tree (e.g. `@cordisjs/plugin-http` pulls `@cordisjs/fetch-file`).
+`package.json` invariants: `"private": true` (vendored packages are never published), rescope the `name` ([mapping](../rescope.md)) while keeping upstream's `version`/`exports`/`type`, point declaration metadata at `lib/types`, publish `.d.ts` and `.d.ts.map` declaration outputs, and list its cordis deps in `peerDependencies` (matching the upstream manifest). Transitive upstream deps must themselves be vendored or already present — vendoring one package often means vendoring its dependency tree (e.g. `@cordisjs/plugin-http` pulls `@cordisjs/fetch-file`).
Local relative imports/exports in vendored TypeScript source use explicit `.ts` specifiers after copying. This is a repo-local build difference from upstream: `rewriteRelativeImportExtensions` emits `.js` runtime imports while declarations keep explicit `.ts` specifiers that NodeNext/Node16 TypeScript consumers can resolve.
diff --git a/docs/cookbook/adding-a-vendored-package.zh.md b/docs/cookbook/adding-a-vendored-package.zh.md
index d16ec10564..2bf5c7eeaf 100644
--- a/docs/cookbook/adding-a-vendored-package.zh.md
+++ b/docs/cookbook/adding-a-vendored-package.zh.md
@@ -8,7 +8,7 @@
```
vendor//
- package.json # from upstream; set "private": true, keep name/exports/type
+ package.json # from upstream; set "private": true, rescope the name, keep exports/type
tsconfig.json # extends ../../tsconfig.base.json (see configuration below)
src/ # the upstream src/ verbatim
README.md LICENSE # if upstream ships them
@@ -29,7 +29,7 @@ vendor//
}
```
-`package.json` 的不变式:`"private": true`(vendored 包永不发布);保留上游的 `name`/`version`/`exports`/`type`;声明元数据指向 `lib/types`;发布 `.d.ts` 与 `.d.ts.map` 声明输出;在 `peerDependencies` 中列出其 Cordis 依赖(与上游 manifest(元数据清单)一致)。传递性上游依赖本身也必须被 vendor 或已存在于仓库中——vendor 一个包往往意味着 vendor 其整条依赖树(如 `@cordisjs/plugin-http` 会拉入 `@cordisjs/fetch-file`)。
+`package.json` 的不变式:`"private": true`(vendored 包永不发布);改写 `name` 的 scope([映射](../rescope.md)),保留上游的 `version`/`exports`/`type`;声明元数据指向 `lib/types`;发布 `.d.ts` 与 `.d.ts.map` 声明输出;在 `peerDependencies` 中列出其 Cordis 依赖(与上游 manifest(元数据清单)一致)。传递性上游依赖本身也必须被 vendor 或已存在于仓库中——vendor 一个包往往意味着 vendor 其整条依赖树(如 `@cordisjs/plugin-http` 会拉入 `@cordisjs/fetch-file`)。
vendored TypeScript 源码中的本地相对导入/导出在复制后使用显式 `.ts` 后缀。这是仓库本地构建与上游的差异:`rewriteRelativeImportExtensions` 输出 `.js` 运行时导入,而声明文件保留显式 `.ts` 后缀,使 NodeNext/Node16 的 TypeScript 消费方能够解析。
diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml
index ac600e8c6d..3a98437564 100644
--- a/docs/cookbook/extension-cookbook.i18n.yaml
+++ b/docs/cookbook/extension-cookbook.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 docs/cookbook/extension-cookbook.md
-extension-cookbook.md: 95ba269a5d62e14cfde487d5a3aaca5db493657e
-extension-cookbook.zh.md: e3fbe09f1ec09568e3b259aee361d33ba3e62140
+extension-cookbook.md: f292075dfdad5016d81521318b38594e3d7ee8b4
+extension-cookbook.zh.md: 0623c49d9d7075b3823c1fd340b36a5fba21f31e
diff --git a/docs/cookbook/extension-cookbook.md b/docs/cookbook/extension-cookbook.md
index 95ba269a5d..f292075dfd 100644
--- a/docs/cookbook/extension-cookbook.md
+++ b/docs/cookbook/extension-cookbook.md
@@ -13,7 +13,7 @@ A tool registers on `ctx.tools`. The annotated `defineTool` example (typed `exec
This permission gate is one example of a hook plugin. It returns a typed decision from the `tools/pre-execute` gate to allow or deny a call; sandbox, permission, and plan-mode plugins can use this extension point. Hook plugins can intercept other extension points and are not inherently permission gates. A "native hook" is an ordinary Cordis plugin on an interception point; it needs no external protocol.
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
declare function isAllowed(exec: ToolExecution): Promise
@@ -37,7 +37,7 @@ This waterfall is the reorderable policy layer. Use `ctx.tools.guard()` when an
A UI plugin renders from the `session/event` feed (the assistant token stream as `assistant/chunk`, plus turn/step boundaries and tool activity), and drives input back in via `agent.followup()` / `agent.steer()`. A browser plugin contributing a business row to the built-in Web Client instead registers a `ConversationNodeDefinition` and keyed Chat renderer; follow the [Conversation Node guide](adding-a-conversation-node.md).
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
@@ -67,7 +67,7 @@ A *protocol driver* adapts a wire peer to `ctx.agents`; it may serve a UI or an
[`packages/acp/acp`](../../packages/acp/acp) is the automation-only worked example: it exposes fresh text sessions over Agent Client Protocol JSON-RPC stdio, emits committed assistant text, and registers a one-shot machine permission answerer for agents it owns. Its [README](../../packages/acp/acp/README.md) defines the exact methods, event order, and lifecycle contract.
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-protocol-bridge'
export const inject = ['agents', 'sessions', 'sessionPersistence']
diff --git a/docs/cookbook/extension-cookbook.zh.md b/docs/cookbook/extension-cookbook.zh.md
index e3fbe09f1e..0623c49d9d 100644
--- a/docs/cookbook/extension-cookbook.zh.md
+++ b/docs/cookbook/extension-cookbook.zh.md
@@ -13,7 +13,7 @@ harness 扩展的参考模式。代码片段省略了 import 和辅助实现,
这个权限门禁是钩子插件的一个示例。它从 `tools/pre-execute` 门禁返回一个类型化的决策,用于允许或拒绝一次调用;沙箱、权限和 plan-mode 插件都可以使用该扩展点。钩子插件也可以拦截其他扩展点,本身并不等同于权限门禁。「原生钩子」是在拦截点上运行的普通 Cordis 插件,不需要外部协议。
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
declare function isAllowed(exec: ToolExecution): Promise
@@ -37,7 +37,7 @@ export function apply(ctx: Context) {
UI 插件从 `session/event` 事件流渲染(助手 token 流以 `assistant/chunk` 形式到达,加上轮次/步骤边界与工具活动),并通过 `agent.followup()` / `agent.steer()` 将输入驱动回去。如果浏览器插件要向内建 Web Client 贡献业务行,则应注册 `ConversationNodeDefinition` 与 keyed Chat renderer;具体步骤见 [Conversation Node 指南](adding-a-conversation-node.md)。
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
@@ -67,7 +67,7 @@ export function apply(ctx: Context) {
[`packages/acp/acp`](../../packages/acp/acp) 是仅面向自动化的完整示例:它通过 ACP(Agent Client Protocol)JSON-RPC stdio 提供全新文本会话,发出已提交的助手文本,并为其拥有的 agent 注册一次性机器权限应答器。其 [README](../../packages/acp/acp/README.md) 定义确切的方法、事件顺序和生命周期约定。
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-protocol-bridge'
export const inject = ['agents', 'sessions', 'sessionPersistence']
diff --git a/docs/cordis-api/fiber.i18n.yaml b/docs/cordis-api/fiber.i18n.yaml
index 6c01366dc1..537be01dbc 100644
--- a/docs/cordis-api/fiber.i18n.yaml
+++ b/docs/cordis-api/fiber.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 docs/cordis-api/fiber.md
-fiber.md: 36d2861ac6a53e8186a92d86c65ba228d4b59ee5
-fiber.zh.md: fafa559ca911677c43893862190009d82c39c56b
+fiber.md: 182b77390b29b8a90504437d0ccc2dfeba23921a
+fiber.zh.md: 9ed3e52618586dc3815b9d913439d11a227fb64b
diff --git a/docs/cordis-api/fiber.md b/docs/cordis-api/fiber.md
index 36d2861ac6..182b77390b 100644
--- a/docs/cordis-api/fiber.md
+++ b/docs/cordis-api/fiber.md
@@ -34,7 +34,7 @@ Register a cleanup-aware effect on this fiber.
**Returns** a disposer that tears the effect down and settles once done.
-[Source](../../vendor/cordis/src/fiber.ts#L420)
+[Source](../../vendor/cordis/src/fiber.ts#L415)
### ctx.fiber
@@ -97,7 +97,7 @@ public state
Current lifecycle state; transitions emit `internal/status`.
-[Source](../../vendor/cordis/src/fiber.ts#L192)
+[Source](../../vendor/cordis/src/fiber.ts#L194)
### fiber.dispose
@@ -108,7 +108,7 @@ public readonly dispose: () => Promise
Dispose this fiber: unload the plugin, then settle once cleanup finished.
-[Source](../../vendor/cordis/src/fiber.ts#L194)
+[Source](../../vendor/cordis/src/fiber.ts#L196)
### fiber.store
@@ -119,7 +119,7 @@ public store: Dict | undefined
Snapshot of required service implementations while loaded; `undefined` otherwise.
-[Source](../../vendor/cordis/src/fiber.ts#L196)
+[Source](../../vendor/cordis/src/fiber.ts#L198)
### fiber.inertia
@@ -130,7 +130,7 @@ public inertia: Promise | undefined
The in-flight load/unload transition, if one is currently running.
-[Source](../../vendor/cordis/src/fiber.ts#L198)
+[Source](../../vendor/cordis/src/fiber.ts#L200)
### fiber.name
@@ -141,7 +141,7 @@ get name()
The plugin's display name, inherited from the nearest named ancestor, else `'root'`.
-[Source](../../vendor/cordis/src/fiber.ts#L341)
+[Source](../../vendor/cordis/src/fiber.ts#L336)
### fiber.assertActive()
@@ -159,7 +159,7 @@ Throw if the fiber has already been disposed.
**Returns** nothing when the fiber is still active.
-[Source](../../vendor/cordis/src/fiber.ts#L356)
+[Source](../../vendor/cordis/src/fiber.ts#L351)
### fiber.effect(execute, label?)
@@ -190,7 +190,7 @@ Register a cleanup-aware effect on this fiber.
**Returns** a disposer that tears the effect down and settles once done.
-[Source](../../vendor/cordis/src/fiber.ts#L420)
+[Source](../../vendor/cordis/src/fiber.ts#L415)
### fiber.getEffects()
@@ -207,7 +207,7 @@ Return metadata for currently registered effects.
**Returns** one `EffectMeta` tree per labeled live effect.
-[Source](../../vendor/cordis/src/fiber.ts#L573)
+[Source](../../vendor/cordis/src/fiber.ts#L568)
### fiber.await()
@@ -225,7 +225,7 @@ Wait for current lifecycle work and rethrow startup errors.
**Returns** this fiber, once it has settled into a stable state.
-[Source](../../vendor/cordis/src/fiber.ts#L702)
+[Source](../../vendor/cordis/src/fiber.ts#L704)
### fiber.restart()
@@ -243,7 +243,7 @@ Dispose and immediately reload this plugin with its current config.
**Returns** a promise resolving once the reload settled.
-[Source](../../vendor/cordis/src/fiber.ts#L716)
+[Source](../../vendor/cordis/src/fiber.ts#L718)
### fiber.update(config, noSave?)
@@ -271,7 +271,7 @@ Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto o
**Returns** the update waterfall result; the default restart returns a promise.
-[Source](../../vendor/cordis/src/fiber.ts#L734)
+[Source](../../vendor/cordis/src/fiber.ts#L736)
## Effect
diff --git a/docs/cordis-api/fiber.zh.md b/docs/cordis-api/fiber.zh.md
index fafa559ca9..9ed3e52618 100644
--- a/docs/cordis-api/fiber.zh.md
+++ b/docs/cordis-api/fiber.zh.md
@@ -36,7 +36,7 @@ effect(execute: () => Effect, label?: string): AsyncDisposable>
**返回**一个用于撤销该作用的清理函数,并在清理完成后结算。
-[源码](../../vendor/cordis/src/fiber.ts#L420)
+[源码](../../vendor/cordis/src/fiber.ts#L415)
### ctx.fiber
@@ -99,7 +99,7 @@ public state
当前生命周期状态;状态转换会发出 `internal/status`。
-[源码](../../vendor/cordis/src/fiber.ts#L192)
+[源码](../../vendor/cordis/src/fiber.ts#L194)
### fiber.dispose
@@ -110,7 +110,7 @@ public readonly dispose: () => Promise
dispose 此 fiber:卸载插件,并在清理完成后结算。
-[源码](../../vendor/cordis/src/fiber.ts#L194)
+[源码](../../vendor/cordis/src/fiber.ts#L196)
### fiber.store
@@ -121,7 +121,7 @@ public store: Dict | undefined
加载期间所需服务实现的快照;其他情况下为 `undefined`。
-[源码](../../vendor/cordis/src/fiber.ts#L196)
+[源码](../../vendor/cordis/src/fiber.ts#L198)
### fiber.inertia
@@ -132,7 +132,7 @@ public inertia: Promise | undefined
当前正在进行的加载或卸载转换;如果没有此类转换,则为 undefined。
-[源码](../../vendor/cordis/src/fiber.ts#L198)
+[源码](../../vendor/cordis/src/fiber.ts#L200)
### fiber.name
@@ -143,7 +143,7 @@ get name()
插件的显示名称,继承自最近的具名祖先;如果不存在,则为 `'root'`。
-[源码](../../vendor/cordis/src/fiber.ts#L341)
+[源码](../../vendor/cordis/src/fiber.ts#L336)
### fiber.assertActive()
@@ -161,7 +161,7 @@ assertActive()
**返回**:fiber 仍处于活动状态时不返回任何内容。
-[源码](../../vendor/cordis/src/fiber.ts#L356)
+[源码](../../vendor/cordis/src/fiber.ts#L351)
### fiber.effect(execute, label?)
@@ -192,7 +192,7 @@ effect(execute: () => Effect, label?: string): AsyncDisposable>
**返回**一个用于撤销该作用的清理函数,并在清理完成后结算。
-[源码](../../vendor/cordis/src/fiber.ts#L420)
+[源码](../../vendor/cordis/src/fiber.ts#L415)
### fiber.getEffects()
@@ -209,7 +209,7 @@ getEffects()
**返回**:每个带标签的活动作用对应一棵 `EffectMeta` 树。
-[源码](../../vendor/cordis/src/fiber.ts#L573)
+[源码](../../vendor/cordis/src/fiber.ts#L568)
### fiber.await()
@@ -227,7 +227,7 @@ async await()
**返回**:进入稳定状态后的此 fiber。
-[源码](../../vendor/cordis/src/fiber.ts#L702)
+[源码](../../vendor/cordis/src/fiber.ts#L704)
### fiber.restart()
@@ -245,7 +245,7 @@ dispose 此插件,并立即使用其当前配置重新加载。
**返回**一个在重新加载完成后兑现的 promise。
-[源码](../../vendor/cordis/src/fiber.ts#L716)
+[源码](../../vendor/cordis/src/fiber.ts#L718)
### fiber.update(config, noSave?)
@@ -273,7 +273,7 @@ update(config: any, noSave = false)
**返回**更新 waterfall 的结果;默认的重新启动操作返回一个 promise。
-[源码](../../vendor/cordis/src/fiber.ts#L734)
+[源码](../../vendor/cordis/src/fiber.ts#L736)
## Effect
diff --git a/docs/cordis-primer.i18n.yaml b/docs/cordis-primer.i18n.yaml
index 12177e3d35..180ba85c01 100644
--- a/docs/cordis-primer.i18n.yaml
+++ b/docs/cordis-primer.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 docs/cordis-primer.md
-cordis-primer.md: c95909a4a1deab9407efedbb990ef13be6e43a16
-cordis-primer.zh.md: a18b8b37af19a610b71babbe5e67f96bb09e81b1
+cordis-primer.md: d1e7c5fd8eaaa89fe448d238359389d945cd6346
+cordis-primer.zh.md: d6ce0f2024f65b006c9505daffaa06a08bb56875
diff --git a/docs/cordis-primer.md b/docs/cordis-primer.md
index c95909a4a1..d1e7c5fd8e 100644
--- a/docs/cordis-primer.md
+++ b/docs/cordis-primer.md
@@ -35,7 +35,7 @@ For single-decision events, short-circuiting is the design. A policy listener ca
## Loader Configuration
-`@cordisjs/plugin-include` parses `!!js` into expression nodes, but the Loader interpolates only an entry's `config` before mounting the plugin. Entry metadata (`id`, `name`, `group`, `disabled`, `inject`, `intercept`, and `isolate`) remains literal; `disabled: !!js ...` is therefore a truthy object that always disables the entry. Use explicit config overlays when environment selection changes which plugins are mounted.
+`@deepseek-ai/cordis-plugin-include` parses `!!js` into expression nodes. Loader interpolates only an entry's `config`, after declared injections activate, against that plugin context (`ctx.serviceName`); Include preserves nested row expressions until target activation. Entry metadata (`id`, `name`, `group`, `disabled`, `inject`, `intercept`, `isolate`) stays literal, so `disabled: !!js ...` always disables the entry. Use overlays when the environment selects plugins.
## Practical Rules
diff --git a/docs/cordis-primer.zh.md b/docs/cordis-primer.zh.md
index a18b8b37af..d6ce0f2024 100644
--- a/docs/cordis-primer.zh.md
+++ b/docs/cordis-primer.zh.md
@@ -39,7 +39,7 @@ Cordis 是 DeepSeek Harness SDK 底层以 vendor 方式引入的插件框架。
## Loader 配置
-`@cordisjs/plugin-include` 将 `!!js` 解析为表达式节点,但 Loader 仅在挂载插件前对条目的 `config` 做插值。条目元数据(`id`、`name`、`group`、`disabled`、`inject`、`intercept` 和 `isolate`)保持字面值;因此 `disabled: !!js ...` 是一个 truthy 对象,会始终禁用该条目。需要根据环境选择挂载哪些插件时,请使用显式的配置覆盖层。
+`@deepseek-ai/cordis-plugin-include` 将 `!!js` 解析为表达式节点。Loader 只在声明的注入激活后,基于该插件上下文(`ctx.serviceName`)插值条目的 `config`;Include 会保留嵌套行表达式,直到目标行激活。条目元数据(`id`、`name`、`group`、`disabled`、`inject`、`intercept`、`isolate`)保持字面值,因此 `disabled: !!js ...` 始终禁用该条目。由环境选择插件时,请使用 overlay。
## 实践规则
diff --git a/docs/cordis-tutorial/01-first-plugin.i18n.yaml b/docs/cordis-tutorial/01-first-plugin.i18n.yaml
index 9bf649ab29..cbb4aee438 100644
--- a/docs/cordis-tutorial/01-first-plugin.i18n.yaml
+++ b/docs/cordis-tutorial/01-first-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 docs/cordis-tutorial/01-first-plugin.md
-01-first-plugin.md: 260026329443f9a5b8860d11a6527dbd687eb44c
-01-first-plugin.zh.md: 69dedb898c7ea29f99233f07126cd413fa0ddbe2
+01-first-plugin.md: 79df2f42df1f34a7ef32cc81607e1b926a1854ae
+01-first-plugin.zh.md: 87cd7a96843d69d68fb95662e59bbfb9ebfc2a37
diff --git a/docs/cordis-tutorial/01-first-plugin.md b/docs/cordis-tutorial/01-first-plugin.md
index 2600263294..79df2f42df 100644
--- a/docs/cordis-tutorial/01-first-plugin.md
+++ b/docs/cordis-tutorial/01-first-plugin.md
@@ -9,7 +9,7 @@ In the loader configuration used here, a Cordis plugin module named-exports an `
In your `tmp/cordis-tutorial` directory (see [setup](index.md#setup)), create `hello.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello'
@@ -55,7 +55,7 @@ There is no framework bootstrap code in your file: a plugin describes what it co
A function is the most common form, but Cordis accepts three:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
// 1. Function plugin (what you just wrote).
export function apply(ctx: Context) {}
@@ -92,4 +92,4 @@ One caveat worth knowing early: a config entry whose module cannot be **resolved
Next: [Lifecycle and effects](02-lifecycle-and-effects.md) — what happens when a plugin unloads.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/01-first-plugin.zh.md b/docs/cordis-tutorial/01-first-plugin.zh.md
index 69dedb898c..87cd7a9684 100644
--- a/docs/cordis-tutorial/01-first-plugin.zh.md
+++ b/docs/cordis-tutorial/01-first-plugin.zh.md
@@ -9,7 +9,7 @@
在 `tmp/cordis-tutorial` 目录中(参见[环境设置](index.md#setup))创建 `hello.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello'
@@ -55,7 +55,7 @@ hello from my first plugin
函数是最常见的形式,但 Cordis 接受三种形式:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
// 1. Function plugin (what you just wrote).
export function apply(ctx: Context) {}
@@ -92,4 +92,4 @@ export function apply(ctx: Context) {
下一章:[生命周期与 effect](02-lifecycle-and-effects.md):插件卸载时会发生什么。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/02-lifecycle-and-effects.i18n.yaml b/docs/cordis-tutorial/02-lifecycle-and-effects.i18n.yaml
index 12793267e2..26ffcac268 100644
--- a/docs/cordis-tutorial/02-lifecycle-and-effects.i18n.yaml
+++ b/docs/cordis-tutorial/02-lifecycle-and-effects.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 docs/cordis-tutorial/02-lifecycle-and-effects.md
-02-lifecycle-and-effects.md: 7b195b63a1e8730f27b9dd9af8af6a68a588cee9
-02-lifecycle-and-effects.zh.md: 4a3f83dedd5c95c7fcb5c1aebbbb8cb2e849b9cf
+02-lifecycle-and-effects.md: 8e75708eb0cba1aceb7fa3dae5ae334995eb113f
+02-lifecycle-and-effects.zh.md: bc4489e702ab58b5d1efd8a72fc693ee48af2624
diff --git a/docs/cordis-tutorial/02-lifecycle-and-effects.md b/docs/cordis-tutorial/02-lifecycle-and-effects.md
index 7b195b63a1..8e75708eb0 100644
--- a/docs/cordis-tutorial/02-lifecycle-and-effects.md
+++ b/docs/cordis-tutorial/02-lifecycle-and-effects.md
@@ -11,7 +11,7 @@ For a resource Cordis does not already manage — a timer, a connection, a watch
Create `lifecycle.ts` in `tmp/cordis-tutorial`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'lifecycle-demo'
@@ -95,4 +95,4 @@ One ordering caveat: disposers start in reverse registration order, but multiple
Next: [Services](03-services.md) — how plugins share capabilities.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md b/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md
index 4a3f83dedd..bc4489e702 100644
--- a/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md
+++ b/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md
@@ -11,7 +11,7 @@ Cordis 插件可能因修改配置、热重载、显式资源释放或所需服
创建 `lifecycle.ts`,将它放在 `tmp/cordis-tutorial` 中:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'lifecycle-demo'
@@ -95,4 +95,4 @@ PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
下一章:[服务](03-services.md):插件如何共享功能。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/03-services.i18n.yaml b/docs/cordis-tutorial/03-services.i18n.yaml
index bdb7e19387..372e028a47 100644
--- a/docs/cordis-tutorial/03-services.i18n.yaml
+++ b/docs/cordis-tutorial/03-services.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 docs/cordis-tutorial/03-services.md
-03-services.md: 82b08b7b8a2ec8a6b340dd1fdc7fa3de98cedff9
-03-services.zh.md: ba4152454eb79a21b183b867c0ba2ef32cd43923
+03-services.md: 32007284be99ef46b4621089c9b3a80317e77189
+03-services.zh.md: d82be29aa69686b8dc10cc6a45a658683c017cbd
diff --git a/docs/cordis-tutorial/03-services.md b/docs/cordis-tutorial/03-services.md
index 82b08b7b8a..32007284be 100644
--- a/docs/cordis-tutorial/03-services.md
+++ b/docs/cordis-tutorial/03-services.md
@@ -9,9 +9,9 @@ A **service** is a named capability one plugin provides and other plugins consum
Create `greeter.ts` in `tmp/cordis-tutorial`:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
greeter: GreeterService
}
@@ -37,7 +37,7 @@ export function apply(ctx: Context) {
Two pieces work together:
- **Runtime**: `super(ctx, 'greeter')` registers the instance under the name `greeter`. From then on, any plugin can reach it as `ctx.greeter`. The registration is an effect — unloading the provider removes the service.
-- **Compile time**: the `declare module 'cordis'` block is TypeScript declaration merging. It adds `greeter` to the `Context` interface so `ctx.greeter` typechecks everywhere. It generates no code; without it the service still works at runtime, but consumers lose type safety.
+- **Compile time**: the `declare module '@deepseek-ai/cordis'` block is TypeScript declaration merging. It adds `greeter` to the `Context` interface so `ctx.greeter` typechecks everywhere. It generates no code; without it the service still works at runtime, but consumers lose type safety.
A `Service` subclass is itself a plugin (the class form from chapter 1), so `ctx.plugin(GreeterService)` mounts it like any other.
@@ -46,7 +46,7 @@ A `Service` subclass is itself a plugin (the class form from chapter 1), so `ctx
Create `consumer.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'consumer'
export const inject = ['greeter']
@@ -95,4 +95,4 @@ Service names live in one flat namespace per application. Prefix or namespace yo
Next: [Events](04-events.md) — communication without a shared service.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/03-services.zh.md b/docs/cordis-tutorial/03-services.zh.md
index ba4152454e..d82be29aa6 100644
--- a/docs/cordis-tutorial/03-services.zh.md
+++ b/docs/cordis-tutorial/03-services.zh.md
@@ -9,9 +9,9 @@
创建 `greeter.ts`,将它放在 `tmp/cordis-tutorial` 中:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
greeter: GreeterService
}
@@ -37,7 +37,7 @@ export function apply(ctx: Context) {
两部分协同工作:
- **运行时**:`super(ctx, 'greeter')` 以名称 `greeter` 注册该实例。此后,任何插件都可以通过 `ctx.greeter` 访问它。注册属于 effect,卸载提供方时会移除该服务。
-- **编译时**:`declare module 'cordis'` 块使用 TypeScript 声明合并,把 `greeter` 加入 `Context` 接口,使 `ctx.greeter` 在各处都能通过类型检查。它不会生成代码;没有该声明时,服务在运行时仍能工作,但消费方会失去类型安全。
+- **编译时**:`declare module '@deepseek-ai/cordis'` 块使用 TypeScript 声明合并,把 `greeter` 加入 `Context` 接口,使 `ctx.greeter` 在各处都能通过类型检查。它不会生成代码;没有该声明时,服务在运行时仍能工作,但消费方会失去类型安全。
`Service` 子类本身就是插件(第 1 章介绍的类形态),因此 `ctx.plugin(GreeterService)` 会像挂载其他插件一样挂载它。
@@ -46,7 +46,7 @@ export function apply(ctx: Context) {
创建 `consumer.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'consumer'
export const inject = ['greeter']
@@ -95,4 +95,4 @@ export function apply(ctx: Context) {
下一章:[事件](04-events.md):无需共享服务即可通信。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/04-events.i18n.yaml b/docs/cordis-tutorial/04-events.i18n.yaml
index b453ffb1f6..4ce3fb9a65 100644
--- a/docs/cordis-tutorial/04-events.i18n.yaml
+++ b/docs/cordis-tutorial/04-events.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 docs/cordis-tutorial/04-events.md
-04-events.md: e77641dffcb82fcb50a24ca2e3e764d152218094
-04-events.zh.md: 00cce854e9a54fddb594ffa8e306f60a725ac012
+04-events.md: db911b9a06d7304d73030a2020de3d465fde2f9c
+04-events.zh.md: e0357b2c04785d52a54f839d1b8ff3ecccb027ed
diff --git a/docs/cordis-tutorial/04-events.md b/docs/cordis-tutorial/04-events.md
index e77641dffc..db911b9a06 100644
--- a/docs/cordis-tutorial/04-events.md
+++ b/docs/cordis-tutorial/04-events.md
@@ -9,9 +9,9 @@ Services support direct calls; **events** let a plugin announce something withou
Create `stats.ts` in `tmp/cordis-tutorial` — a service that counts things and announces each change:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
stats: StatsService
}
@@ -46,7 +46,7 @@ The `interface Events` merge is the event-system twin of the `interface Context`
Create `reporter.ts`:
```ts ignore-check
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from './stats.ts'
export const name = 'reporter'
@@ -96,9 +96,9 @@ Every harness event documents its mode in the generated reference on its owning
Waterfall is the mode that powers interception. Each listener receives the arguments plus a `next()` continuation; it can transform what `next()` returns, or return without calling `next()` and short-circuit the rest of the chain — what the Cordis docs call the veto. Create `waterfall-demo.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Events {
'demo/transform'(input: string, next: () => Promise): Promise
}
@@ -141,4 +141,4 @@ The harness uses waterfalls for decisions that cooperating plugins may wrap or a
Next: [Configuration](05-config.md) — plugin options from `cordis.yml`.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/04-events.zh.md b/docs/cordis-tutorial/04-events.zh.md
index 00cce854e9..e0357b2c04 100644
--- a/docs/cordis-tutorial/04-events.zh.md
+++ b/docs/cordis-tutorial/04-events.zh.md
@@ -9,9 +9,9 @@
创建 `stats.ts`,将它放在 `tmp/cordis-tutorial` 中。它是一项负责计数并在每次变化时发出通知的服务:
```ts
-import { Service, type Context } from 'cordis'
+import { Service, type Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Context {
stats: StatsService
}
@@ -46,7 +46,7 @@ export function apply(ctx: Context) {
创建 `reporter.ts`:
```ts ignore-check
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from './stats.ts'
export const name = 'reporter'
@@ -96,9 +96,9 @@ export function apply(ctx: Context) {
waterfall 是实现拦截的模式。每个监听器都会收到参数和一个 `next()` continuation;它可以转换 `next()` 的返回值,也可以不调用 `next()` 就直接返回,从而短路链条的其余部分。Cordis 文档把后一种行为称为否决。创建 `waterfall-demo.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
-declare module 'cordis' {
+declare module '@deepseek-ai/cordis' {
interface Events {
'demo/transform'(input: string, next: () => Promise): Promise
}
@@ -141,4 +141,4 @@ harness 使用 waterfall 处理协作插件可以包装或回答的决策:[`ag
下一章:[配置](05-config.md):来自 `cordis.yml` 的插件选项。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/05-config.i18n.yaml b/docs/cordis-tutorial/05-config.i18n.yaml
index 7db45165c4..4e953918dd 100644
--- a/docs/cordis-tutorial/05-config.i18n.yaml
+++ b/docs/cordis-tutorial/05-config.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 docs/cordis-tutorial/05-config.md
-05-config.md: 834bb140cc1ff976acc8f21c8f54a7fb02636eac
-05-config.zh.md: f5cc6ac1ca4fa02eba6a1b015b9f6ae3b1a925fc
+05-config.md: 2357f663135d6fc78a65f9d0952e0bc3f5eefae4
+05-config.zh.md: fbd94d179494ad0b6f73baff2ca525c786cc9e33
diff --git a/docs/cordis-tutorial/05-config.md b/docs/cordis-tutorial/05-config.md
index 834bb140cc..2357f66313 100644
--- a/docs/cordis-tutorial/05-config.md
+++ b/docs/cordis-tutorial/05-config.md
@@ -9,8 +9,8 @@ Each `cordis.yml` entry can carry a `config` block, and the plugin declares a sc
Create `config-demo.ts` in `tmp/cordis-tutorial`:
```ts
-import type { Context } from 'cordis'
-import Schema from 'schemastery'
+import type { Context } from '@deepseek-ai/cordis'
+import Schema from '@deepseek-ai/schemastery'
export const name = 'config-demo'
@@ -81,4 +81,4 @@ The loader used in this repo supports a `!!js` tag for config values that must b
Next: [Composition and HMR](06-composition-and-hmr.md) — treating `cordis.yml` as the application.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/05-config.zh.md b/docs/cordis-tutorial/05-config.zh.md
index f5cc6ac1ca..fbd94d1794 100644
--- a/docs/cordis-tutorial/05-config.zh.md
+++ b/docs/cordis-tutorial/05-config.zh.md
@@ -9,8 +9,8 @@
创建 `config-demo.ts`,并将其放在 `tmp/cordis-tutorial` 中:
```ts
-import type { Context } from 'cordis'
-import Schema from 'schemastery'
+import type { Context } from '@deepseek-ai/cordis'
+import Schema from '@deepseek-ai/schemastery'
export const name = 'config-demo'
@@ -81,4 +81,4 @@ ValidationError: invalid config:
下一章:[组合与 HMR(热模块替换)](06-composition-and-hmr.md):将 `cordis.yml` 视为应用。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml b/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml
index 3732651e58..6a79850803 100644
--- a/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml
+++ b/docs/cordis-tutorial/06-composition-and-hmr.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 docs/cordis-tutorial/06-composition-and-hmr.md
-06-composition-and-hmr.md: a169d7a164be63c939e352e4e5b0bf9bce43da29
-06-composition-and-hmr.zh.md: 07ae46555c390d625a4397933e2ec5ac059bd270
+06-composition-and-hmr.md: 87ea26014657ae8c8199e1ebb486556c827d96ca
+06-composition-and-hmr.zh.md: 830f55de7c1be351fe701cb068197543602619a7
diff --git a/docs/cordis-tutorial/06-composition-and-hmr.md b/docs/cordis-tutorial/06-composition-and-hmr.md
index a169d7a164..87ea260146 100644
--- a/docs/cordis-tutorial/06-composition-and-hmr.md
+++ b/docs/cordis-tutorial/06-composition-and-hmr.md
@@ -22,24 +22,24 @@ Groups nest a sub-list of entries that load and unload as one unit, and `isolate
## Hot module replacement
-Because unloading releases effects ([chapter 2](02-lifecycle-and-effects.md)) and loading follows dependencies ([chapter 3](03-services.md)), HMR can replace a running plugin by unloading and loading it. The `@cordisjs/plugin-hmr` plugin watches your files and does exactly that on save.
+Because unloading releases effects ([chapter 2](02-lifecycle-and-effects.md)) and loading follows dependencies ([chapter 3](03-services.md)), HMR can replace a running plugin by unloading and loading it. The `@deepseek-ai/cordis-plugin-hmr` plugin watches your files and does exactly that on save.
In `tmp/cordis-tutorial`, write `cordis.yml`:
```yaml
- id: logger
- name: '@cordisjs/plugin-logger-console'
+ name: '@deepseek-ai/cordis-plugin-logger-console'
- id: timer
- name: '@cordisjs/plugin-timer'
+ name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
- name: '@cordisjs/plugin-hmr'
+ name: '@deepseek-ai/cordis-plugin-hmr'
config:
root: ['.']
- id: hello
name: './hello.ts'
```
-Two support plugins joined the list: HMR logs through the Cordis logger service, so without a console exporter you would not see its messages, and it `inject`s the `timer` service for debouncing — without `@cordisjs/plugin-timer` it sits in PENDING forever, silently. That silence is the subject of the next section.
+Two support plugins joined the list: HMR logs through the Cordis logger service, so without a console exporter you would not see its messages, and it `inject`s the `timer` service for debouncing — without `@deepseek-ai/cordis-plugin-timer` it sits in PENDING forever, silently. That silence is the subject of the next section.
HMR reads Node's loader internals through the Loader's native helper. Run Cordis under tsx:
@@ -65,7 +65,7 @@ The flip side of dependency-driven loading: a plugin whose `inject` names a serv
You can see the states directly. Every context can enumerate the plugin registry; create `diagnose.ts`:
```ts
-import { FiberState, type Context } from 'cordis'
+import { FiberState, type Context } from '@deepseek-ai/cordis'
export const name = 'diagnose'
@@ -85,7 +85,7 @@ export function apply(ctx: Context) {
And a plugin with an unsatisfiable dependency, `needs-timer.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'needs-timer'
export const inject = ['timer']
@@ -106,8 +106,8 @@ Run it (plain `node --import tsx ../../vendor/cordis/bin.js`; stop with Ctrl-C):
needs-timer is PENDING — a required service is missing
```
-`inject: ['timer']` has no provider. Add `- name: '@cordisjs/plugin-timer'` to the list and the plugin loads. When a plugin does nothing and reports nothing, inspect its fiber state. Iterating without the PENDING filter also shows the loader's own plugins (Loader, Include) as ACTIVE fibers because plugins mount the config file itself.
+`inject: ['timer']` has no provider. Add `- name: '@deepseek-ai/cordis-plugin-timer'` to the list and the plugin loads. When a plugin does nothing and reports nothing, inspect its fiber state. Iterating without the PENDING filter also shows the loader's own plugins (Loader, Include) as ACTIVE fibers because plugins mount the config file itself.
Next: [Into the harness](07-into-the-harness.md) — the same patterns against real harness services.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/06-composition-and-hmr.zh.md b/docs/cordis-tutorial/06-composition-and-hmr.zh.md
index 07ae46555c..830f55de7c 100644
--- a/docs/cordis-tutorial/06-composition-and-hmr.zh.md
+++ b/docs/cordis-tutorial/06-composition-and-hmr.zh.md
@@ -22,24 +22,24 @@ Cordis 配置项除了 `name` 和 `config`,还接受其他元数据:
## 热模块替换
-卸载会释放 effect([第 2 章](02-lifecycle-and-effects.md)),加载则遵循依赖关系([第 3 章](03-services.md)),因此 HMR 可以先卸载、再加载,以替换正在运行的插件。`@cordisjs/plugin-hmr` 插件会监视文件,并在保存时执行这一过程。
+卸载会释放 effect([第 2 章](02-lifecycle-and-effects.md)),加载则遵循依赖关系([第 3 章](03-services.md)),因此 HMR 可以先卸载、再加载,以替换正在运行的插件。`@deepseek-ai/cordis-plugin-hmr` 插件会监视文件,并在保存时执行这一过程。
在 `tmp/cordis-tutorial` 中编写 `cordis.yml`:
```yaml
- id: logger
- name: '@cordisjs/plugin-logger-console'
+ name: '@deepseek-ai/cordis-plugin-logger-console'
- id: timer
- name: '@cordisjs/plugin-timer'
+ name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
- name: '@cordisjs/plugin-hmr'
+ name: '@deepseek-ai/cordis-plugin-hmr'
config:
root: ['.']
- id: hello
name: './hello.ts'
```
-列表中增加了两个辅助插件:HMR 通过 Cordis logger 服务记录日志,因此没有控制台导出器时看不到其消息;它还会 `inject` `timer` 服务来实现去抖,如果没有 `@cordisjs/plugin-timer`,它就会永远停在 PENDING,而且不发出任何提示。下一节就讨论这种静默状态。
+列表中增加了两个辅助插件:HMR 通过 Cordis logger 服务记录日志,因此没有控制台导出器时看不到其消息;它还会 `inject` `timer` 服务来实现去抖,如果没有 `@deepseek-ai/cordis-plugin-timer`,它就会永远停在 PENDING,而且不发出任何提示。下一节就讨论这种静默状态。
HMR 通过 Loader 的原生辅助工具读取 Node 的 loader 内部结构。请在 tsx 下运行 Cordis:
@@ -65,7 +65,7 @@ hello from my EDITED plugin
你可以直接查看这些状态。每个上下文都能枚举插件注册表;创建 `diagnose.ts`:
```ts
-import { FiberState, type Context } from 'cordis'
+import { FiberState, type Context } from '@deepseek-ai/cordis'
export const name = 'diagnose'
@@ -85,7 +85,7 @@ export function apply(ctx: Context) {
再创建一个依赖无法满足的插件 `needs-timer.ts`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
export const name = 'needs-timer'
export const inject = ['timer']
@@ -106,8 +106,8 @@ export function apply(ctx: Context) {
needs-timer is PENDING — a required service is missing
```
-`inject: ['timer']` 没有提供方。向列表添加 `- name: '@cordisjs/plugin-timer'` 后,插件就会加载。如果插件既不执行任何操作,也不报告任何内容,请检查其 fiber 状态。不加 PENDING 过滤条件进行迭代时,还会看到 loader 自身的插件(Loader、Include)处于 ACTIVE,因为配置文件本身也是通过插件挂载的。
+`inject: ['timer']` 没有提供方。向列表添加 `- name: '@deepseek-ai/cordis-plugin-timer'` 后,插件就会加载。如果插件既不执行任何操作,也不报告任何内容,请检查其 fiber 状态。不加 PENDING 过滤条件进行迭代时,还会看到 loader 自身的插件(Loader、Include)处于 ACTIVE,因为配置文件本身也是通过插件挂载的。
下一章:[进入 harness](07-into-the-harness.md):把相同模式用于真实的 harness 服务。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/07-into-the-harness.i18n.yaml b/docs/cordis-tutorial/07-into-the-harness.i18n.yaml
index 8fb1893fca..fbc6ef1cde 100644
--- a/docs/cordis-tutorial/07-into-the-harness.i18n.yaml
+++ b/docs/cordis-tutorial/07-into-the-harness.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 docs/cordis-tutorial/07-into-the-harness.md
-07-into-the-harness.md: 69133786f58541b015aed080f4ac8fb2a7e488c0
-07-into-the-harness.zh.md: bc9c61da984e3eb691eb6bfbe59ae556823e82de
+07-into-the-harness.md: 2d3c23f9f7f7fc6bd6cabd4e7e68ebfc46e20665
+07-into-the-harness.zh.md: 45dc0ee6f07ab3b0275499cd4ab0c436eebdddef
diff --git a/docs/cordis-tutorial/07-into-the-harness.md b/docs/cordis-tutorial/07-into-the-harness.md
index 69133786f5..2d3c23f9f7 100644
--- a/docs/cordis-tutorial/07-into-the-harness.md
+++ b/docs/cordis-tutorial/07-into-the-harness.md
@@ -9,7 +9,7 @@ This chapter registers a model-callable tool with the harness's `tools` service,
Create `greet-tool.ts` in `tmp/cordis-tutorial`:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { CallId } from '@deepseek-ai/dsh-llm'
@@ -53,7 +53,7 @@ Every pattern here is from the earlier chapters: `inject: ['tools']` ([chapter 3
Create `tool-logger.ts` — a separate plugin that watches every tool call in the app through the harness's `tools/result` event:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
@@ -104,4 +104,4 @@ Where to go next:
- The generated `cordis-surface` regions on the [subsystem pages](../subsystems/core.md) — everything you can inject and listen to, each on its owning page.
- [Architecture](../architecture.md) — the system map these plugins live in.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/07-into-the-harness.zh.md b/docs/cordis-tutorial/07-into-the-harness.zh.md
index bc9c61da98..45dc0ee6f0 100644
--- a/docs/cordis-tutorial/07-into-the-harness.zh.md
+++ b/docs/cordis-tutorial/07-into-the-harness.zh.md
@@ -9,7 +9,7 @@
创建 `greet-tool.ts`,将它放在 `tmp/cordis-tutorial` 中:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { CallId } from '@deepseek-ai/dsh-llm'
@@ -53,7 +53,7 @@ export function apply(ctx: Context) {
创建 `tool-logger.ts`。这是一个独立插件,通过 harness 的 `tools/result` 事件观察应用中的每次工具调用:
```ts
-import type { Context } from 'cordis'
+import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
@@ -104,4 +104,4 @@ logger 会先触发:`tools/result` 在结果物化过程中发出,发生在
- [子系统页面](../subsystems/core.md)上生成的 `cordis-surface` 区块:可以注入和监听的所有内容,各在其所属页面上。
- [架构](../architecture.md):这些插件所处的系统地图。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/index.i18n.yaml b/docs/cordis-tutorial/index.i18n.yaml
index 719e949ffe..06857ab177 100644
--- a/docs/cordis-tutorial/index.i18n.yaml
+++ b/docs/cordis-tutorial/index.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 docs/cordis-tutorial/index.md
-index.md: 307c12854b3075cfd4dd5ea8a19806c58b4e998d
-index.zh.md: a0107b7d15272e6ef8d526b9c0e03a99275644d6
+index.md: fb700344e6d07d3864655009d2edac15ee9eede8
+index.zh.md: a68e931d81e745164d8f9a5dc7ec9aec4cd0e590
diff --git a/docs/cordis-tutorial/index.md b/docs/cordis-tutorial/index.md
index 307c12854b..fb700344e6 100644
--- a/docs/cordis-tutorial/index.md
+++ b/docs/cordis-tutorial/index.md
@@ -13,7 +13,7 @@ If you want the condensed concept reference instead of a walkthrough, read the [
You need a clone of this repository with dependencies installed — the [quick start](../user/guide/quickstart.md) covers prerequisites. No API key is needed for this tutorial; every example runs keylessly.
```sh
-git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
+git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
```
@@ -50,9 +50,9 @@ That one-file launcher (see [vendor/cordis/bin.js](../../vendor/cordis/bin.js))
The examples use three TypeScript features beyond ordinary modern JavaScript:
- **Type annotations** describe values without changing runtime behavior: `ctx: Context` says that `ctx` has the Cordis context API, `who: string` accepts text, and `string[]` means an array of strings.
-- **`import type { Context } from 'cordis'`** imports only type information. It vanishes at runtime, so a plugin file that needs `Context` solely for annotations adds no runtime dependency.
-- **Declaration merging** (`declare module 'cordis' { ... }`) adds your entries to interfaces that Cordis already declares — for example the type of a new `ctx.greeter` property or event name. It generates no runtime wiring; the plugin separately provides the service or emits the event. Chapter 3 shows the pattern in full.
+- **`import type { Context } from '@deepseek-ai/cordis'`** imports only type information. It vanishes at runtime, so a plugin file that needs `Context` solely for annotations adds no runtime dependency.
+- **Declaration merging** (`declare module '@deepseek-ai/cordis' { ... }`) adds your entries to interfaces that Cordis already declares — for example the type of a new `ctx.greeter` property or event name. It generates no runtime wiring; the plugin separately provides the service or emits the event. Chapter 3 shows the pattern in full.
Chapter 5 also uses an `interface` to describe a configuration object's fields and a generic type such as `Schema` to say which object fields a schema validates. You can copy those declarations as shown; the surrounding text explains what each one connects.
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/cordis-tutorial/index.zh.md b/docs/cordis-tutorial/index.zh.md
index a0107b7d15..a68e931d81 100644
--- a/docs/cordis-tutorial/index.zh.md
+++ b/docs/cordis-tutorial/index.zh.md
@@ -13,7 +13,7 @@ Cordis 是 DeepSeek Harness SDK 底层的插件框架:它是一个小型运行
你需要克隆本仓库并安装依赖,具体前置条件见[快速入门](../user/guide/quickstart.md)。本教程不需要 API 密钥;所有示例均可在无密钥环境中运行。
```sh
-git clone https://github.com/deepseek-ai/deepseek-harness-sdk.git
+git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
```
@@ -50,9 +50,9 @@ node --import tsx ../../vendor/cordis/bin.js
这些示例使用了普通现代 JavaScript 之外的三项 TypeScript 功能:
- **类型注解** 描述值,但不会改变运行时行为:`ctx: Context` 表示 `ctx` 具备 Cordis 上下文 API,`who: string` 接受文本,而 `string[]` 表示字符串数组。
-- **`import type { Context } from 'cordis'`** 只导入类型信息。它在运行时会消失,因此仅为类型注解使用 `Context` 的插件文件不会增加运行时依赖。
-- **声明合并**(`declare module 'cordis' { ... }`)会为 Cordis 已经声明的接口添加你的条目,例如新 `ctx.greeter` 属性的类型或事件名称。它不会生成任何运行时接线;插件必须另行提供服务或发出事件。第 3 章会完整展示该模式。
+- **`import type { Context } from '@deepseek-ai/cordis'`** 只导入类型信息。它在运行时会消失,因此仅为类型注解使用 `Context` 的插件文件不会增加运行时依赖。
+- **声明合并**(`declare module '@deepseek-ai/cordis' { ... }`)会为 Cordis 已经声明的接口添加你的条目,例如新 `ctx.greeter` 属性的类型或事件名称。它不会生成任何运行时接线;插件必须另行提供服务或发出事件。第 3 章会完整展示该模式。
第 5 章还会使用 `interface` 描述配置对象的字段,并使用 `Schema` 这类泛型表示 schema 校验哪些对象字段。你可以直接照写这些声明;周围的正文会解释每项声明连接了什么。
-[](https://github.com/deepseek-ai/deepseek-harness-sdk)
+[](https://github.com/deepseek-ai/deepseek-harness)
diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml
index da29debe08..9d8d8fba47 100644
--- a/docs/development.i18n.yaml
+++ b/docs/development.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 docs/development.md
-development.md: 8e565f21c6e2ede7dab7dbda3c4b18b77ce0920f
-development.zh.md: d9c0fbfbb663334b8f7e2ca11d4e8d9a0652c22e
+development.md: 7d6ca74df1311560b0d61444c92e74927639ca31
+development.zh.md: 1461717cf496b1863f2f0c47ad4b42990dc789e9
diff --git a/docs/development.md b/docs/development.md
index 8e565f21c6..7d6ca74df1 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -129,7 +129,7 @@ The root [contributor instructions](../AGENTS.md#commands) summarize common comm
The one-shot Headless coding agent needs `DEEPSEEK_API_KEY` in the environment or repo-root `.env`:
```sh
-pnpm run demo:headless "summarize this workspace"
+pnpm dsh --profile headless "summarize this workspace"
```
The self-referential cordis demo can inspect and modify its live plugin runtime and needs the same credentials (`web` by default, or `acp`):
diff --git a/docs/development.zh.md b/docs/development.zh.md
index d9c0fbfbb6..1461717cf4 100644
--- a/docs/development.zh.md
+++ b/docs/development.zh.md
@@ -129,7 +129,7 @@ keyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若
单次运行的 Headless coding agent 需要环境变量或仓库根目录 `.env` 中的 `DEEPSEEK_API_KEY`:
```sh
-pnpm run demo:headless "summarize this workspace"
+pnpm dsh --profile headless "summarize this workspace"
```
自指的 cordis 演示可以检查并修改其实时插件运行时,并需要相同的凭证(默认 `web`,也可用 `acp`):
diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml
index 268fa1c8d0..2d2341925b 100644
--- a/docs/event-producer-consumer.i18n.yaml
+++ b/docs/event-producer-consumer.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 docs/event-producer-consumer.md
-event-producer-consumer.md: d2e5cd66c2406cf84320fc1c0537d580214dd93d
-event-producer-consumer.zh.md: 12ac194d68ed6121cc0b2c5e9bf2c7f77944f57b
+event-producer-consumer.md: 55a57480e0311aa047e9b5f0f90b6457fc9a007f
+event-producer-consumer.zh.md: c84c621befefdab7e668d64e90dcb14e28fd74ea
diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md
index d2e5cd66c2..55a57480e0 100644
--- a/docs/event-producer-consumer.md
+++ b/docs/event-producer-consumer.md
@@ -8,7 +8,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:182`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session) |
+| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-session`](../packages/goal/goal-session) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) |
@@ -30,19 +30,19 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
-| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
-| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
-| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
-| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
+| `session/created` | `emit` | [`packages/core/session/src/index.ts:75`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
+| `session/event` | `emit` | [`packages/core/session/src/index.ts:97`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
+| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:106`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
-| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:162`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
-| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:136`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
-| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:29`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) |
-| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:35`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
+| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:165`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
+| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:139`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:145`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:156`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
+| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`system-prompt`](../packages/core/system-prompt) |
+| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:193`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
| `tools/code-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:175`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md
index 12ac194d68..c84c621bef 100644
--- a/docs/event-producer-consumer.zh.md
+++ b/docs/event-producer-consumer.zh.md
@@ -10,7 +10,7 @@
| 事件 | 模式 | 声明位置 | 派发方 | 监听方 |
| --- | --- | --- | --- | --- |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:182`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session) |
+| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`goal-session`](../packages/goal/goal-session) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-session`](../packages/goal/goal-session), [`subagent`](../packages/subagent/subagent) |
@@ -32,19 +32,19 @@
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | `apiproxy`, [`llm`](../packages/llm/llm) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:64`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
-| `session/created` | `emit` | [`packages/core/session/src/index.ts:74`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
-| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
-| `session/event` | `emit` | [`packages/core/session/src/index.ts:96`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
-| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:105`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
+| `session/created` | `emit` | [`packages/core/session/src/index.ts:75`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
+| `session/event` | `emit` | [`packages/core/session/src/index.ts:97`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) |
+| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:106`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
-| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:162`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
-| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:136`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
-| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:29`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) |
-| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:35`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
+| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:165`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) |
+| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:139`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:145`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:156`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
+| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`system-prompt`](../packages/core/system-prompt) |
+| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:193`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
| `tools/code-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:175`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml
index 77fcfca867..bfcfb4f693 100644
--- a/docs/module-graph.i18n.yaml
+++ b/docs/module-graph.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 docs/module-graph.md
-module-graph.md: b596cb564943e321ff42cd59855bd7dc10981319
-module-graph.zh.md: b64f30c2df1eedb1a1fa106b79710558b765fdde
+module-graph.md: 84fcd63100fb16a0ac26bc8f2f33fdd65bcf4e91
+module-graph.zh.md: 7aa6a57be53fbc74ed3017dd9d06400f176b99a6
diff --git a/docs/module-graph.md b/docs/module-graph.md
index b596cb5649..84fcd63100 100644
--- a/docs/module-graph.md
+++ b/docs/module-graph.md
@@ -133,6 +133,7 @@ flowchart TD
end
subgraph group_boot["packages/boot"]
pkg_app_boot["app-boot"]
+ pkg_cmdline["cmdline"]
end
subgraph group_bundle["packages/bundle"]
pkg_base["base"]
@@ -166,6 +167,7 @@ flowchart TD
pkg_client_ui_slash["client-ui-slash"]
pkg_client_ui_slots["client-ui-slots"]
pkg_client_ui_subagent["client-ui-subagent"]
+ pkg_client_ui_task["client-ui-task"]
pkg_client_ui_theme["client-ui-theme"]
pkg_client_ui_tool["client-ui-tool"]
pkg_client_ui_trajectory["client-ui-trajectory"]
@@ -199,6 +201,7 @@ flowchart TD
end
subgraph group_feedback["packages/feedback"]
pkg_command_feedback["command-feedback"]
+ pkg_message_feedback["message-feedback"]
end
subgraph group_guard["packages/guard"]
pkg_repeat_tool_guard["repeat-tool-guard"]
@@ -253,7 +256,6 @@ flowchart TD
pkg_telemetry["telemetry"]
end
subgraph group_self_modification["packages/self-modification"]
- pkg_repository_plugin["repository-plugin"]
pkg_tool_cordis["tool-cordis"]
end
subgraph group_session["packages/session"]
@@ -314,6 +316,7 @@ flowchart TD
pkg_timeout --> pkg_invariants
pkg_scope --> pkg_invariants
pkg_llm_mock_server --> pkg_invariants
+ pkg_cmdline --> pkg_invariants
pkg_base --> pkg_invariants
pkg_client_modules --> pkg_invariants
pkg_client_schema_form --> pkg_invariants
@@ -398,9 +401,6 @@ flowchart TD
pkg_client_ui_settings --> pkg_client_ui_primitives
pkg_client_ui_settings --> pkg_client_ui_slots
pkg_client_ui_settings --> pkg_invariants
- pkg_client_ui_trajectory --> pkg_client_runtime
- pkg_client_ui_trajectory --> pkg_client_ui_primitives
- pkg_client_ui_trajectory --> pkg_invariants
pkg_credentials_local --> pkg_atomic_write
pkg_credentials_local --> pkg_credentials
pkg_credentials_local --> pkg_environment
@@ -515,12 +515,6 @@ flowchart TD
pkg_host_directory_picker_native --> pkg_client_ui_slots
pkg_host_directory_picker_native --> pkg_client_ui_workspace
pkg_host_directory_picker_native --> pkg_invariants
- pkg_agent_presets --> pkg_atomic_write
- pkg_agent_presets --> pkg_invariants
- pkg_agent_presets --> pkg_paths
- pkg_agent_presets --> pkg_scope
- pkg_agent_presets --> pkg_session
- pkg_agent_presets --> pkg_settings
pkg_persona --> pkg_invariants
pkg_persona --> pkg_system_prompt
pkg_sandbox --> pkg_invariants
@@ -572,8 +566,13 @@ flowchart TD
pkg_time_context --> pkg_agent
pkg_time_context --> pkg_invariants
pkg_time_context --> pkg_session
- pkg_host_apiproxy --> pkg_agent_presets
- pkg_host_apiproxy --> pkg_invariants
+ pkg_message_feedback --> pkg_brand
+ pkg_message_feedback --> pkg_invariants
+ pkg_message_feedback --> pkg_llm
+ pkg_message_feedback --> pkg_session
+ pkg_message_feedback --> pkg_session_persistence
+ pkg_message_feedback --> pkg_storage_domain
+ pkg_message_feedback --> pkg_type_meta
pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
pkg_host_directory_picker_auto --> pkg_host_webserver
@@ -593,6 +592,14 @@ flowchart TD
pkg_user_interaction --> pkg_agent
pkg_user_interaction --> pkg_invariants
pkg_user_interaction --> pkg_llm
+ pkg_agent_presets --> pkg_agent
+ pkg_agent_presets --> pkg_atomic_write
+ pkg_agent_presets --> pkg_invariants
+ pkg_agent_presets --> pkg_paths
+ pkg_agent_presets --> pkg_scope
+ pkg_agent_presets --> pkg_session
+ pkg_agent_presets --> pkg_settings
+ pkg_agent_presets --> pkg_system_prompt
pkg_pty --> pkg_agent
pkg_pty --> pkg_brand
pkg_pty --> pkg_invariants
@@ -702,11 +709,6 @@ flowchart TD
pkg_headless --> pkg_invariants
pkg_headless --> pkg_llm
pkg_headless --> pkg_session
- pkg_client_test_runtime --> pkg_client_runtime
- pkg_client_test_runtime --> pkg_client_ui_slots
- pkg_client_test_runtime --> pkg_client_web_react
- pkg_client_test_runtime --> pkg_host_apiproxy
- pkg_client_test_runtime --> pkg_invariants
pkg_tmux_context --> pkg_agent
pkg_tmux_context --> pkg_bash
pkg_tmux_context --> pkg_invariants
@@ -717,7 +719,10 @@ flowchart TD
pkg_command_feedback --> pkg_commands
pkg_command_feedback --> pkg_invariants
pkg_command_feedback --> pkg_session
+ pkg_command_feedback --> pkg_session_telemetry
pkg_command_feedback --> pkg_user_id
+ pkg_host_apiproxy --> pkg_agent_presets
+ pkg_host_apiproxy --> pkg_invariants
pkg_permission --> pkg_bash
pkg_permission --> pkg_commands
pkg_permission --> pkg_invariants
@@ -793,6 +798,7 @@ flowchart TD
pkg_fs_sandbox --> pkg_invariants
pkg_fs_sandbox --> pkg_sandbox
pkg_fs_sandbox --> pkg_sandbox_policy
+ pkg_tool_fs --> pkg_attachment
pkg_tool_fs --> pkg_fs
pkg_tool_fs --> pkg_invariants
pkg_tool_fs --> pkg_llm
@@ -829,6 +835,8 @@ flowchart TD
pkg_subagent --> pkg_brand
pkg_subagent --> pkg_invariants
pkg_subagent --> pkg_llm
+ pkg_subagent --> pkg_sandbox
+ pkg_subagent --> pkg_sandbox_policy
pkg_subagent --> pkg_scope
pkg_subagent --> pkg_session
pkg_subagent --> pkg_session_persistence
@@ -836,6 +844,7 @@ flowchart TD
pkg_subagent --> pkg_session_projection_cache
pkg_subagent --> pkg_tasks
pkg_subagent --> pkg_tools
+ pkg_subagent --> pkg_user_approval
pkg_tool_web --> pkg_invariants
pkg_tool_web --> pkg_llm
pkg_tool_web --> pkg_system_prompt
@@ -889,6 +898,18 @@ flowchart TD
pkg_llm_replay --> pkg_invariants
pkg_llm_replay --> pkg_llm
pkg_llm_replay --> pkg_session
+ pkg_client_test_runtime --> pkg_client_runtime
+ pkg_client_test_runtime --> pkg_client_ui_slots
+ pkg_client_test_runtime --> pkg_client_web_react
+ pkg_client_test_runtime --> pkg_host_apiproxy
+ pkg_client_test_runtime --> pkg_invariants
+ pkg_client_ui_trajectory --> pkg_agent
+ pkg_client_ui_trajectory --> pkg_client_locale
+ pkg_client_ui_trajectory --> pkg_client_runtime
+ pkg_client_ui_trajectory --> pkg_client_ui_primitives
+ pkg_client_ui_trajectory --> pkg_compact
+ pkg_client_ui_trajectory --> pkg_invariants
+ pkg_client_ui_trajectory --> pkg_tools
pkg_session_reference --> pkg_agent
pkg_session_reference --> pkg_compact
pkg_session_reference --> pkg_invariants
@@ -923,6 +944,7 @@ flowchart TD
pkg_mcp_client --> pkg_invariants
pkg_mcp_client --> pkg_llm
pkg_mcp_client --> pkg_subprocess
+ pkg_mcp_client --> pkg_timeout
pkg_mcp_client --> pkg_tools
pkg_tool_bash_persistent --> pkg_agent
pkg_tool_bash_persistent --> pkg_invariants
@@ -1018,12 +1040,10 @@ flowchart TD
pkg_subagent_inprocess --> pkg_agent
pkg_subagent_inprocess --> pkg_invariants
pkg_subagent_inprocess --> pkg_llm
- pkg_subagent_inprocess --> pkg_sandbox_policy
pkg_subagent_inprocess --> pkg_session
pkg_subagent_inprocess --> pkg_subagent
pkg_subagent_inprocess --> pkg_system_prompt
pkg_subagent_inprocess --> pkg_tools
- pkg_subagent_inprocess --> pkg_user_approval
pkg_tool_subagent --> pkg_agent
pkg_tool_subagent --> pkg_invariants
pkg_tool_subagent --> pkg_llm
@@ -1069,10 +1089,6 @@ flowchart TD
pkg_sdk_protocol --> pkg_llm
pkg_sdk_protocol --> pkg_session
pkg_sdk_protocol --> pkg_subagent
- pkg_repository_plugin --> pkg_invariants
- pkg_repository_plugin --> pkg_mcp_client
- pkg_repository_plugin --> pkg_paths
- pkg_repository_plugin --> pkg_skill_local
pkg_tool_ralph --> pkg_agent
pkg_tool_ralph --> pkg_invariants
pkg_tool_ralph --> pkg_llm
@@ -1158,6 +1174,12 @@ flowchart TD
pkg_client_ui_subagent --> pkg_invariants
pkg_client_ui_subagent --> pkg_subagent
pkg_client_ui_subagent --> pkg_token_meter
+ pkg_client_ui_task --> pkg_client_locale
+ pkg_client_ui_task --> pkg_client_runtime
+ pkg_client_ui_task --> pkg_client_ui_conversation
+ pkg_client_ui_task --> pkg_client_ui_primitives
+ pkg_client_ui_task --> pkg_client_ui_slots
+ pkg_client_ui_task --> pkg_invariants
pkg_client_ui_tool --> pkg_client_locale
pkg_client_ui_tool --> pkg_client_runtime
pkg_client_ui_tool --> pkg_client_ui_conversation
@@ -1256,6 +1278,7 @@ flowchart TD
| [`timeout`](../packages/util/timeout) | `util` | [`invariants`](../packages/support/invariants) |
| [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/support/invariants) |
| [`llm-mock-server`](../packages/support/llm-mock-server) | `support` | [`invariants`](../packages/support/invariants) |
+| [`cmdline`](../packages/boot/cmdline) | `boot` | [`invariants`](../packages/support/invariants) |
| [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/support/invariants) |
| [`client-modules`](../packages/client/modules) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-schema-form`](../packages/client/schema-form) | `client` | [`invariants`](../packages/support/invariants) |
@@ -1296,7 +1319,6 @@ flowchart TD
| [`client-locale`](../packages/client/locale) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`invariants`](../packages/support/invariants) |
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
| [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) |
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
@@ -1324,7 +1346,6 @@ flowchart TD
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
-| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings) |
| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
@@ -1338,11 +1359,12 @@ flowchart TD
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/support/invariants), [`spill`](../packages/spill/spill) |
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
-| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
+| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`type-meta`](../packages/typert/type-meta) |
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`user-interaction`](../packages/interaction/user-interaction) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
+| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt) |
| [`pty`](../packages/pty/pty) | `pty` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
@@ -1369,10 +1391,10 @@ flowchart TD
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/interaction/user-approval) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-registry`](../packages/typert/registry) |
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) |
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
-| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-id`](../packages/session/user-id) |
+| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry), [`user-id`](../packages/session/user-id) |
+| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
| [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) |
| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) |
@@ -1386,12 +1408,12 @@ flowchart TD
| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`pwsh-sandbox`](../packages/bash/pwsh-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`pwsh-local`](../packages/bash/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
-| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`command-compact`](../packages/compact/command-compact) | `compact` | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants) |
-| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
+| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
@@ -1401,13 +1423,15 @@ flowchart TD
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`llm-replay`](../packages/support/llm-replay) | `support` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
+| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) |
+| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
| [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-ask-user`](../packages/interaction/tool-ask-user) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools), [`user-interaction`](../packages/interaction/user-interaction) |
| [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) |
+| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-bash-persistent`](../packages/pty/tool-bash-persistent) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-pty`](../packages/pty/tool-pty) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`pty`](../packages/pty/pty), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-cordis`](../packages/self-modification/tool-cordis) | `self-modification` | [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) |
@@ -1422,7 +1446,7 @@ flowchart TD
| [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
@@ -1430,7 +1454,6 @@ flowchart TD
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
| [`sdk-protocol`](../packages/scaffold/protocol) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
-| [`repository-plugin`](../packages/self-modification/repository-plugin) | `self-modification` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
@@ -1443,6 +1466,7 @@ flowchart TD
| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) |
| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`plan-mode`](../packages/plan/plan-mode) |
| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) |
+| [`client-ui-task`](../packages/client/ui-task) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`bash-env`](../packages/bash/bash-env), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks-local`](../packages/tasks/tasks-local), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) |
| [`jsonrpc`](../packages/scaffold/server) | `scaffold` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md
index b64f30c2df..7aa6a57be5 100644
--- a/docs/module-graph.zh.md
+++ b/docs/module-graph.zh.md
@@ -135,6 +135,7 @@ flowchart TD
end
subgraph group_boot["packages/boot"]
pkg_app_boot["app-boot"]
+ pkg_cmdline["cmdline"]
end
subgraph group_bundle["packages/bundle"]
pkg_base["base"]
@@ -168,6 +169,7 @@ flowchart TD
pkg_client_ui_slash["client-ui-slash"]
pkg_client_ui_slots["client-ui-slots"]
pkg_client_ui_subagent["client-ui-subagent"]
+ pkg_client_ui_task["client-ui-task"]
pkg_client_ui_theme["client-ui-theme"]
pkg_client_ui_tool["client-ui-tool"]
pkg_client_ui_trajectory["client-ui-trajectory"]
@@ -201,6 +203,7 @@ flowchart TD
end
subgraph group_feedback["packages/feedback"]
pkg_command_feedback["command-feedback"]
+ pkg_message_feedback["message-feedback"]
end
subgraph group_guard["packages/guard"]
pkg_repeat_tool_guard["repeat-tool-guard"]
@@ -255,7 +258,6 @@ flowchart TD
pkg_telemetry["telemetry"]
end
subgraph group_self_modification["packages/self-modification"]
- pkg_repository_plugin["repository-plugin"]
pkg_tool_cordis["tool-cordis"]
end
subgraph group_session["packages/session"]
@@ -316,6 +318,7 @@ flowchart TD
pkg_timeout --> pkg_invariants
pkg_scope --> pkg_invariants
pkg_llm_mock_server --> pkg_invariants
+ pkg_cmdline --> pkg_invariants
pkg_base --> pkg_invariants
pkg_client_modules --> pkg_invariants
pkg_client_schema_form --> pkg_invariants
@@ -400,9 +403,6 @@ flowchart TD
pkg_client_ui_settings --> pkg_client_ui_primitives
pkg_client_ui_settings --> pkg_client_ui_slots
pkg_client_ui_settings --> pkg_invariants
- pkg_client_ui_trajectory --> pkg_client_runtime
- pkg_client_ui_trajectory --> pkg_client_ui_primitives
- pkg_client_ui_trajectory --> pkg_invariants
pkg_credentials_local --> pkg_atomic_write
pkg_credentials_local --> pkg_credentials
pkg_credentials_local --> pkg_environment
@@ -517,12 +517,6 @@ flowchart TD
pkg_host_directory_picker_native --> pkg_client_ui_slots
pkg_host_directory_picker_native --> pkg_client_ui_workspace
pkg_host_directory_picker_native --> pkg_invariants
- pkg_agent_presets --> pkg_atomic_write
- pkg_agent_presets --> pkg_invariants
- pkg_agent_presets --> pkg_paths
- pkg_agent_presets --> pkg_scope
- pkg_agent_presets --> pkg_session
- pkg_agent_presets --> pkg_settings
pkg_persona --> pkg_invariants
pkg_persona --> pkg_system_prompt
pkg_sandbox --> pkg_invariants
@@ -574,8 +568,13 @@ flowchart TD
pkg_time_context --> pkg_agent
pkg_time_context --> pkg_invariants
pkg_time_context --> pkg_session
- pkg_host_apiproxy --> pkg_agent_presets
- pkg_host_apiproxy --> pkg_invariants
+ pkg_message_feedback --> pkg_brand
+ pkg_message_feedback --> pkg_invariants
+ pkg_message_feedback --> pkg_llm
+ pkg_message_feedback --> pkg_session
+ pkg_message_feedback --> pkg_session_persistence
+ pkg_message_feedback --> pkg_storage_domain
+ pkg_message_feedback --> pkg_type_meta
pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
pkg_host_directory_picker_auto --> pkg_host_webserver
@@ -595,6 +594,14 @@ flowchart TD
pkg_user_interaction --> pkg_agent
pkg_user_interaction --> pkg_invariants
pkg_user_interaction --> pkg_llm
+ pkg_agent_presets --> pkg_agent
+ pkg_agent_presets --> pkg_atomic_write
+ pkg_agent_presets --> pkg_invariants
+ pkg_agent_presets --> pkg_paths
+ pkg_agent_presets --> pkg_scope
+ pkg_agent_presets --> pkg_session
+ pkg_agent_presets --> pkg_settings
+ pkg_agent_presets --> pkg_system_prompt
pkg_pty --> pkg_agent
pkg_pty --> pkg_brand
pkg_pty --> pkg_invariants
@@ -704,11 +711,6 @@ flowchart TD
pkg_headless --> pkg_invariants
pkg_headless --> pkg_llm
pkg_headless --> pkg_session
- pkg_client_test_runtime --> pkg_client_runtime
- pkg_client_test_runtime --> pkg_client_ui_slots
- pkg_client_test_runtime --> pkg_client_web_react
- pkg_client_test_runtime --> pkg_host_apiproxy
- pkg_client_test_runtime --> pkg_invariants
pkg_tmux_context --> pkg_agent
pkg_tmux_context --> pkg_bash
pkg_tmux_context --> pkg_invariants
@@ -719,7 +721,10 @@ flowchart TD
pkg_command_feedback --> pkg_commands
pkg_command_feedback --> pkg_invariants
pkg_command_feedback --> pkg_session
+ pkg_command_feedback --> pkg_session_telemetry
pkg_command_feedback --> pkg_user_id
+ pkg_host_apiproxy --> pkg_agent_presets
+ pkg_host_apiproxy --> pkg_invariants
pkg_permission --> pkg_bash
pkg_permission --> pkg_commands
pkg_permission --> pkg_invariants
@@ -795,6 +800,7 @@ flowchart TD
pkg_fs_sandbox --> pkg_invariants
pkg_fs_sandbox --> pkg_sandbox
pkg_fs_sandbox --> pkg_sandbox_policy
+ pkg_tool_fs --> pkg_attachment
pkg_tool_fs --> pkg_fs
pkg_tool_fs --> pkg_invariants
pkg_tool_fs --> pkg_llm
@@ -831,6 +837,8 @@ flowchart TD
pkg_subagent --> pkg_brand
pkg_subagent --> pkg_invariants
pkg_subagent --> pkg_llm
+ pkg_subagent --> pkg_sandbox
+ pkg_subagent --> pkg_sandbox_policy
pkg_subagent --> pkg_scope
pkg_subagent --> pkg_session
pkg_subagent --> pkg_session_persistence
@@ -838,6 +846,7 @@ flowchart TD
pkg_subagent --> pkg_session_projection_cache
pkg_subagent --> pkg_tasks
pkg_subagent --> pkg_tools
+ pkg_subagent --> pkg_user_approval
pkg_tool_web --> pkg_invariants
pkg_tool_web --> pkg_llm
pkg_tool_web --> pkg_system_prompt
@@ -891,6 +900,18 @@ flowchart TD
pkg_llm_replay --> pkg_invariants
pkg_llm_replay --> pkg_llm
pkg_llm_replay --> pkg_session
+ pkg_client_test_runtime --> pkg_client_runtime
+ pkg_client_test_runtime --> pkg_client_ui_slots
+ pkg_client_test_runtime --> pkg_client_web_react
+ pkg_client_test_runtime --> pkg_host_apiproxy
+ pkg_client_test_runtime --> pkg_invariants
+ pkg_client_ui_trajectory --> pkg_agent
+ pkg_client_ui_trajectory --> pkg_client_locale
+ pkg_client_ui_trajectory --> pkg_client_runtime
+ pkg_client_ui_trajectory --> pkg_client_ui_primitives
+ pkg_client_ui_trajectory --> pkg_compact
+ pkg_client_ui_trajectory --> pkg_invariants
+ pkg_client_ui_trajectory --> pkg_tools
pkg_session_reference --> pkg_agent
pkg_session_reference --> pkg_compact
pkg_session_reference --> pkg_invariants
@@ -925,6 +946,7 @@ flowchart TD
pkg_mcp_client --> pkg_invariants
pkg_mcp_client --> pkg_llm
pkg_mcp_client --> pkg_subprocess
+ pkg_mcp_client --> pkg_timeout
pkg_mcp_client --> pkg_tools
pkg_tool_bash_persistent --> pkg_agent
pkg_tool_bash_persistent --> pkg_invariants
@@ -1020,12 +1042,10 @@ flowchart TD
pkg_subagent_inprocess --> pkg_agent
pkg_subagent_inprocess --> pkg_invariants
pkg_subagent_inprocess --> pkg_llm
- pkg_subagent_inprocess --> pkg_sandbox_policy
pkg_subagent_inprocess --> pkg_session
pkg_subagent_inprocess --> pkg_subagent
pkg_subagent_inprocess --> pkg_system_prompt
pkg_subagent_inprocess --> pkg_tools
- pkg_subagent_inprocess --> pkg_user_approval
pkg_tool_subagent --> pkg_agent
pkg_tool_subagent --> pkg_invariants
pkg_tool_subagent --> pkg_llm
@@ -1071,10 +1091,6 @@ flowchart TD
pkg_sdk_protocol --> pkg_llm
pkg_sdk_protocol --> pkg_session
pkg_sdk_protocol --> pkg_subagent
- pkg_repository_plugin --> pkg_invariants
- pkg_repository_plugin --> pkg_mcp_client
- pkg_repository_plugin --> pkg_paths
- pkg_repository_plugin --> pkg_skill_local
pkg_tool_ralph --> pkg_agent
pkg_tool_ralph --> pkg_invariants
pkg_tool_ralph --> pkg_llm
@@ -1160,6 +1176,12 @@ flowchart TD
pkg_client_ui_subagent --> pkg_invariants
pkg_client_ui_subagent --> pkg_subagent
pkg_client_ui_subagent --> pkg_token_meter
+ pkg_client_ui_task --> pkg_client_locale
+ pkg_client_ui_task --> pkg_client_runtime
+ pkg_client_ui_task --> pkg_client_ui_conversation
+ pkg_client_ui_task --> pkg_client_ui_primitives
+ pkg_client_ui_task --> pkg_client_ui_slots
+ pkg_client_ui_task --> pkg_invariants
pkg_client_ui_tool --> pkg_client_locale
pkg_client_ui_tool --> pkg_client_runtime
pkg_client_ui_tool --> pkg_client_ui_conversation
@@ -1246,7 +1268,7 @@ flowchart TD
pkg_acp_demo --> pkg_workspace_context
```
-| 包 | 分组 | 依赖项 |
+| Package | Group | Depends on |
| --- | --- | --- |
| [`invariants`](../packages/support/invariants) | `support` | — |
| [`atomic-write`](../packages/util/atomic-write) | `util` | [`invariants`](../packages/support/invariants) |
@@ -1258,6 +1280,7 @@ flowchart TD
| [`timeout`](../packages/util/timeout) | `util` | [`invariants`](../packages/support/invariants) |
| [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/support/invariants) |
| [`llm-mock-server`](../packages/support/llm-mock-server) | `support` | [`invariants`](../packages/support/invariants) |
+| [`cmdline`](../packages/boot/cmdline) | `boot` | [`invariants`](../packages/support/invariants) |
| [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/support/invariants) |
| [`client-modules`](../packages/client/modules) | `client` | [`invariants`](../packages/support/invariants) |
| [`client-schema-form`](../packages/client/schema-form) | `client` | [`invariants`](../packages/support/invariants) |
@@ -1298,7 +1321,6 @@ flowchart TD
| [`client-locale`](../packages/client/locale) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
-| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`invariants`](../packages/support/invariants) |
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
| [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) |
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
@@ -1326,7 +1348,6 @@ flowchart TD
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
-| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings) |
| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
@@ -1340,11 +1361,12 @@ flowchart TD
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/support/invariants), [`spill`](../packages/spill/spill) |
| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
-| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
+| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`type-meta`](../packages/typert/type-meta) |
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`user-interaction`](../packages/interaction/user-interaction) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
+| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt) |
| [`pty`](../packages/pty/pty) | `pty` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
@@ -1371,10 +1393,10 @@ flowchart TD
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/interaction/user-approval) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-registry`](../packages/typert/registry) |
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
-| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) |
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
-| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-id`](../packages/session/user-id) |
+| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry), [`user-id`](../packages/session/user-id) |
+| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/support/invariants) |
| [`permission`](../packages/interaction/permission) | `interaction` | [`bash`](../packages/bash/bash), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`user-approval`](../packages/interaction/user-approval) |
| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) |
@@ -1388,12 +1410,12 @@ flowchart TD
| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`pwsh-sandbox`](../packages/bash/pwsh-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`pwsh-local`](../packages/bash/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
-| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`command-compact`](../packages/compact/command-compact) | `compact` | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants) |
-| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
+| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
@@ -1403,13 +1425,15 @@ flowchart TD
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`llm-replay`](../packages/support/llm-replay) | `support` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
+| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) |
+| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
| [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
| [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) |
| [`timeout-policy`](../packages/guard/timeout-policy) | `guard` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-ask-user`](../packages/interaction/tool-ask-user) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools), [`user-interaction`](../packages/interaction/user-interaction) |
| [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) |
+| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-bash-persistent`](../packages/pty/tool-bash-persistent) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-pty`](../packages/pty/tool-pty) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`pty`](../packages/pty/pty), [`retention`](../packages/util/retention), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-cordis`](../packages/self-modification/tool-cordis) | `self-modification` | [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) |
@@ -1424,7 +1448,7 @@ flowchart TD
| [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
+| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
@@ -1432,7 +1456,6 @@ flowchart TD
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
| [`sdk-protocol`](../packages/scaffold/protocol) | `scaffold` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
-| [`repository-plugin`](../packages/self-modification/repository-plugin) | `self-modification` | [`invariants`](../packages/support/invariants), [`mcp-client`](../packages/mcp/mcp-client), [`paths`](../packages/util/paths), [`skill-local`](../packages/skill/skill-local) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
@@ -1445,6 +1468,7 @@ flowchart TD
| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) |
| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`plan-mode`](../packages/plan/plan-mode) |
| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) |
+| [`client-ui-task`](../packages/client/ui-task) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`bash-env`](../packages/bash/bash-env), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks-local`](../packages/tasks/tasks-local), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) |
| [`jsonrpc`](../packages/scaffold/server) | `scaffold` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/scaffold/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml
index ee0b5cbdd6..fb2d7d3d3a 100644
--- a/docs/persistence-catalog.i18n.yaml
+++ b/docs/persistence-catalog.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 docs/persistence-catalog.md
-persistence-catalog.md: f44569d3bacec0a832f4b4bca6acf4abb0846a0d
-persistence-catalog.zh.md: 21ed29a3da2587a604ec90d201030fd644fc5bd4
+persistence-catalog.md: 88d8f833ce3e6c51692db74519279a5354a1759b
+persistence-catalog.zh.md: 5ab0fa0c6ccb099ba10b9021625f20486a02d94c
diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md
index f44569d3ba..88d8f833ce 100644
--- a/docs/persistence-catalog.md
+++ b/docs/persistence-catalog.md
@@ -7,7 +7,7 @@ Every event type that can appear in a session's durable event log: the complete
This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md).
-The envelope declarations below compose each event's `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.
+The envelope declarations below compose each event's `type`, monotonic `seq`, epoch-ms `time`, `data`, the optional `ignorable` unknown-type skip marker, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.
## Event envelope
@@ -63,6 +63,17 @@ export type SessionEvent = {
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
+ /**
+ * Marks an event a reader may safely skip when it does not recognize
+ * `type`. Absent means required: a reader meeting an unrecognized type
+ * without this marker MUST refuse to reconstruct the session instead of
+ * silently dropping the event, because an unrecognized required event may
+ * change how the rest of the log is interpreted. A writer sets `true` only
+ * on purely informational records whose loss cannot affect reconstruction;
+ * defaulting to required means a forgotten marker over-refuses (an
+ * inconvenience) rather than silently resuming a gutted session.
+ */
+ ignorable?: true
} & (K extends SurfaceEventType ? {
/**
* Seq numbers of earlier events that this event cites as sources
@@ -79,7 +90,7 @@ export type SessionEvent = {
}[T]
```
-Sources: [`packages/core/session/src/types.ts:316`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:323`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:352`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:384`](../packages/core/session/src/types.ts)
+Sources: [`packages/core/session/src/types.ts:331`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:338`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:367`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:399`](../packages/core/session/src/types.ts)
## Events
@@ -192,7 +203,7 @@ Source: [`packages/interaction/user-approval/src/index.ts:67`](../packages/inter
Types: [StreamChunk](subsystems/llm-streaming.md)
-Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:261`](../packages/core/session/src/types.ts)
#### `assistant/message` — surface
@@ -208,7 +219,7 @@ Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/
Types: [TokenUsage](subsystems/llm-streaming.md)
-Source: [`packages/core/session/src/types.ts:253`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:268`](../packages/core/session/src/types.ts)
### `command/*`
@@ -364,7 +375,7 @@ Source: [`packages/compact/compact/src/types.ts:33`](../packages/compact/compact
'feedback/record': { text: string }
```
-Source: [`packages/feedback/command-feedback/src/index.ts:25`](../packages/feedback/command-feedback/src/index.ts)
+Source: [`packages/feedback/command-feedback/src/index.ts:62`](../packages/feedback/command-feedback/src/index.ts)
### `goal/*`
@@ -488,7 +499,7 @@ Source: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/s
'request/context': RequestContext
```
-Source: [`packages/core/session/src/types.ts:289`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:304`](../packages/core/session/src/types.ts)
#### `request/header` — log-only
@@ -500,7 +511,7 @@ Source: [`packages/core/session/src/types.ts:289`](../packages/core/session/src/
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
```
-Source: [`packages/core/session/src/types.ts:284`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts)
### `sandbox/*`
@@ -553,7 +564,7 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:33`](../packages/s
'session/end-seed': Record
```
-Source: [`packages/core/session/src/types.ts:312`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:327`](../packages/core/session/src/types.ts)
#### `session/title` — log-only
@@ -589,7 +600,7 @@ Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/sessi
'step/end': { turn: number; step: number }
```
-Source: [`packages/core/session/src/types.ts:236`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/types.ts)
#### `step/start` — log-only
@@ -598,7 +609,7 @@ Source: [`packages/core/session/src/types.ts:236`](../packages/core/session/src/
'step/start': { turn: number; step: number }
```
-Source: [`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:249`](../packages/core/session/src/types.ts)
### `subagent/*`
@@ -628,7 +639,7 @@ Source: [`packages/subagent/subagent/src/descriptor.ts:37`](../packages/subagent
Types: [TodoItem](subsystems/session.md)
-Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:294`](../packages/core/session/src/types.ts)
### `tool/*`
@@ -645,7 +656,7 @@ Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/
Types: [CallId](subsystems/core.md)
-Source: [`packages/core/session/src/types.ts:259`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:274`](../packages/core/session/src/types.ts)
#### `tool/code-dispatch` — log-only
@@ -714,7 +725,7 @@ Source: [`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types
}
```
-Source: [`packages/core/session/src/types.ts:271`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:286`](../packages/core/session/src/types.ts)
### `turn/*`
@@ -734,7 +745,7 @@ Source: [`packages/core/session/src/types.ts:271`](../packages/core/session/src/
Types: [TurnEndReason](subsystems/session.md)
-Source: [`packages/core/session/src/types.ts:232`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:247`](../packages/core/session/src/types.ts)
#### `turn/start` — log-only
@@ -748,7 +759,7 @@ Source: [`packages/core/session/src/types.ts:232`](../packages/core/session/src/
'turn/start': { turn: number }
```
-Source: [`packages/core/session/src/types.ts:223`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:238`](../packages/core/session/src/types.ts)
### `user/*`
@@ -765,7 +776,7 @@ Source: [`packages/core/session/src/types.ts:223`](../packages/core/session/src/
'user/message': UserMessage
```
-Source: [`packages/core/session/src/types.ts:244`](../packages/core/session/src/types.ts)
+Source: [`packages/core/session/src/types.ts:259`](../packages/core/session/src/types.ts)
### `web/*`
diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md
index 21ed29a3da..5ab0fa0c6c 100644
--- a/docs/persistence-catalog.zh.md
+++ b/docs/persistence-catalog.zh.md
@@ -9,7 +9,7 @@
英文源文件根据源码生成(`scripts/gen-persistence-catalog.ts`),并由 `pnpm run verify-persistence-catalog`(`doc-sync`(文档同步门禁)的一部分)验证新鲜度;本中文文件作为经评审对侧通过双语配对维护。声明块保留源码声明和嵌套属性的 JSDoc,只移除其所在接口/模块带来的缩进,并使用 `ts persistence-catalog` 围栏(doc-typecheck 会跳过这些围栏,因为声明引用了其所属模块中的类型)。payload 中的类型名称会链接到记录该类型的页面。参见 [persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md)。
-以下信封声明组合了每个事件的 `type`、单调递增的 `seq`、以 epoch 毫秒表示的 `time`、`data`,以及条件字段 `surfaceOp`/`sourceEventSeqs`。**surface** 表示 `SurfaceEventType` 成员:它会生成一条 LLM(大语言模型)消息,并声明该事件如何加入 surface 列表。**log-only** 表示其他所有事件:这类记录可持久化、可回放,但不参与派生历史。每个 payload 均可进行 JSON 序列化(在 `Session.append` 处强制执行),整个格式固定为 `SESSION_FORMAT_VERSION = 0`:这是预发布格式,不暗示任何兼容性(参见[版本立场](subsystems/persistence.md))。范围仅限本仓库中的包;下游插件可以继续合并其他事件类型,而这些类型按设计不属于本目录。
+以下信封声明组合了每个事件的 `type`、单调递增的 `seq`、以 epoch 毫秒表示的 `time`、`data`、可选的未知类型跳过标记 `ignorable`,以及条件字段 `surfaceOp`/`sourceEventSeqs`。**surface** 表示 `SurfaceEventType` 成员:它会生成一条 LLM(大语言模型)消息,并声明该事件如何加入 surface 列表。**log-only** 表示其他所有事件:这类记录可持久化、可回放,但不参与派生历史。每个 payload 均可进行 JSON 序列化(在 `Session.append` 处强制执行),整个格式固定为 `SESSION_FORMAT_VERSION = 0`:这是预发布格式,不暗示任何兼容性(参见[版本立场](subsystems/persistence.md))。范围仅限本仓库中的包;下游插件可以继续合并其他事件类型,而这些类型按设计不属于本目录。
## 事件信封
@@ -65,6 +65,17 @@ export type SessionEvent = {
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
+ /**
+ * Marks an event a reader may safely skip when it does not recognize
+ * `type`. Absent means required: a reader meeting an unrecognized type
+ * without this marker MUST refuse to reconstruct the session instead of
+ * silently dropping the event, because an unrecognized required event may
+ * change how the rest of the log is interpreted. A writer sets `true` only
+ * on purely informational records whose loss cannot affect reconstruction;
+ * defaulting to required means a forgotten marker over-refuses (an
+ * inconvenience) rather than silently resuming a gutted session.
+ */
+ ignorable?: true
} & (K extends SurfaceEventType ? {
/**
* Seq numbers of earlier events that this event cites as sources
diff --git a/docs/rescope.i18n.yaml b/docs/rescope.i18n.yaml
new file mode 100644
index 0000000000..2959c354da
--- /dev/null
+++ b/docs/rescope.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 docs/rescope.md
+rescope.md: 3dde39875021e7a4161e1ae66550e9dedf5eb4fa
+rescope.zh.md: a7f355cf651fb063bf2d4c3cefe18babd4a57401
diff --git a/docs/rescope.md b/docs/rescope.md
new file mode 100644
index 0000000000..3dde398750
--- /dev/null
+++ b/docs/rescope.md
@@ -0,0 +1,53 @@
+# Vendored package rescope
+
+English | [中文](rescope.zh.md)
+
+The Cordis framework and its foundation libraries are vendored under [`vendor/`](../vendor/README.md) and published under the `@deepseek-ai` scope, because every harness package declares the framework as a peer dependency: publishing the harness publishes this layer with it, and under the upstream names that publication would squat them on the registry. This page is the name mapping; the decision and its consequences live in the [rescope Agent Note](../.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md), and the upstream commits in [`vendor/README.md`](../vendor/README.md).
+
+## Name mapping
+
+| Directory | Upstream name | Published name | Version | Role |
+|---|---|---|---|---|
+| `vendor/cordis/` | `cordis` | `@deepseek-ai/cordis` | 4.0.0-rc.7 | Framework core: `Context`, `Service`, `Fiber`, events |
+| `vendor/cosmokit/` | `cosmokit` | `@deepseek-ai/cosmokit` | 1.8.1 | Shared utilities the framework and Schemastery build on |
+| `vendor/schemastery/` | `schemastery` | `@deepseek-ai/schemastery` | 3.18.0 | Config schemas (`Schema`) behind every plugin's `Config` |
+| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | `cordis.yml` loading, plugin resolution, repository cache |
+| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.0.4 | Config includes and patch overlays |
+| `vendor/group/` | `@cordisjs/plugin-group` | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | Nested plugin groups |
+| `vendor/timer/` | `@cordisjs/plugin-timer` | `@deepseek-ai/cordis-plugin-timer` | 1.1.2 | Disposal-aware timers on `ctx` |
+| `vendor/hmr/` | `@cordisjs/plugin-hmr` | `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | Hot module replacement for plugins and config |
+| `vendor/logger-console/` | `@cordisjs/plugin-logger-console` | `@deepseek-ai/cordis-plugin-logger-console` | 1.0.0 | Console logger exporter |
+
+Subpath exports keep their path: `@cordisjs/plugin-loader/repository` becomes `@deepseek-ai/cordis-plugin-loader/repository`.
+
+## What the rename does not touch
+
+- **Directory names and versions.** `vendor/hmr/` stays `vendor/hmr/`, and every package keeps the upstream version its manifest table row records, so the vendored tree still reads as an upstream snapshot.
+- **Dependency ranges.** A dependency entry changes its key, never its range: `"cordis": "^4.0.0-rc.7"` becomes `"@deepseek-ai/cordis": "^4.0.0-rc.7"`. `linkWorkspacePackages` resolves those preserved ranges to the pinned workspaces.
+- **The Loader's `cordis:` builtin prefix.** `cordis:include` and `cordis:group` are a protocol prefix, not a package name.
+- **The `cordis.yml` configuration family**, including `*.cordis.yml`, `*.cordis.snapshot.yml`, and `cordis.patch.yml`.
+- **Harness packages whose own names contain the word**, such as `@deepseek-ai/dsh-tool-cordis`.
+- **Upstream runtime identifiers**, such as Schemastery's `Symbol.for('schemastery')` and its `vendor:` metadata field.
+- **Prose outside `docs/`.** `vendor/*/README.md`, package READMEs, and Agent Notes keep the names they were written with; a bare `cordis` there can also be the Python SDK's option name or an agent-preset id. Inside `docs/`, prose and every Markdown fence follow the rename.
+
+## What your code has to change
+
+| Site | Before | After |
+|---|---|---|
+| Module import | `import { Context } from 'cordis'` | `import { Context } from '@deepseek-ai/cordis'` |
+| Typed-event merge | `declare module 'cordis'` | `declare module '@deepseek-ai/cordis'` |
+| `package.json` dependency key | `"@cordisjs/plugin-hmr": "^1.0.15"` | `"@deepseek-ai/cordis-plugin-hmr": "^1.0.15"` |
+| `cordis.yml` plugin entry | `name: '@cordisjs/plugin-include'` | `name: '@deepseek-ai/cordis-plugin-include'` |
+
+## Applying, verifying, and reverting
+
+[`scripts/rescope-vendor.ts`](../scripts/rescope-vendor.ts) owns the mapping above and performs the rename, so no reference is renamed by hand:
+
+```sh
+pnpm run rescope-vendor # report what would change
+pnpm run rescope-vendor --apply # rewrite every reference
+pnpm run rescope-vendor:check # assert the post-state; runs in the hygiene gate
+pnpm run rescope-vendor --apply --reverse # return to the upstream names
+```
+
+Re-apply it after an upstream sync ([procedure](../vendor/README.md)), and follow it with the regeneration it prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`, and `pnpm run verify-translation-pairing --write` for the bilingual pairs it touched.
diff --git a/docs/rescope.zh.md b/docs/rescope.zh.md
new file mode 100644
index 0000000000..a7f355cf65
--- /dev/null
+++ b/docs/rescope.zh.md
@@ -0,0 +1,53 @@
+# Vendored 包改名
+
+[English](rescope.md) | 中文
+
+Cordis 框架及其基础库以源码形式 vendored 在 [`vendor/`](../vendor/README.md) 下,并以 `@deepseek-ai` scope 发布:每个 harness 包都把框架声明为 peer dependency,发布 harness 就会连带发布这一层,用上游名发布等于在 registry 上占用别人的名字。本页是名字映射表;决策与影响见 [改名 Agent Note](../.agents/notes/implemented/process/2026-08-10-vendor-package-rescope.md),上游 commit 见 [`vendor/README.md`](../vendor/README.md)。
+
+## 名字映射
+
+| 目录 | 上游名 | 发布名 | 版本 | 角色 |
+|---|---|---|---|---|
+| `vendor/cordis/` | `cordis` | `@deepseek-ai/cordis` | 4.0.0-rc.7 | 框架核心:`Context`、`Service`、`Fiber`、事件 |
+| `vendor/cosmokit/` | `cosmokit` | `@deepseek-ai/cosmokit` | 1.8.1 | 框架与 Schemastery 共用的基础工具 |
+| `vendor/schemastery/` | `schemastery` | `@deepseek-ai/schemastery` | 3.18.0 | 配置 schema(`Schema`),每个插件的 `Config` 都基于它 |
+| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | `cordis.yml` 装载、插件解析、repository 缓存 |
+| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.0.4 | 配置包含与 patch 叠加 |
+| `vendor/group/` | `@cordisjs/plugin-group` | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | 嵌套插件分组 |
+| `vendor/timer/` | `@cordisjs/plugin-timer` | `@deepseek-ai/cordis-plugin-timer` | 1.1.2 | `ctx` 上随 disposal 回收的定时器 |
+| `vendor/hmr/` | `@cordisjs/plugin-hmr` | `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | 插件与配置的热替换 |
+| `vendor/logger-console/` | `@cordisjs/plugin-logger-console` | `@deepseek-ai/cordis-plugin-logger-console` | 1.0.0 | 控制台日志导出 |
+
+子路径导出保持原路径:`@cordisjs/plugin-loader/repository` 变成 `@deepseek-ai/cordis-plugin-loader/repository`。
+
+## 改名不碰什么
+
+- **目录名与版本号。** `vendor/hmr/` 仍是 `vendor/hmr/`,每个包保留清单表那行记录的上游版本,所以 vendored 树依旧读作一份上游快照。
+- **依赖 range。** 依赖条目只换键、不换范围:`"cordis": "^4.0.0-rc.7"` 变成 `"@deepseek-ai/cordis": "^4.0.0-rc.7"`;`linkWorkspacePackages` 靠这些保留下来的范围把它们解析到固定的 workspace。
+- **Loader 的 `cordis:` 内建前缀。** `cordis:include`、`cordis:group` 是协议前缀,不是包名。
+- **`cordis.yml` 配置文件家族**,包括 `*.cordis.yml`、`*.cordis.snapshot.yml`、`cordis.patch.yml`。
+- **名字里带这个词的 harness 包**,例如 `@deepseek-ai/dsh-tool-cordis`。
+- **上游运行时标识符**,例如 Schemastery 的 `Symbol.for('schemastery')` 及其 `vendor:` 元数据字段。
+- **`docs/` 之外的散文。** `vendor/*/README.md`、各包 README 与 Agent Note 保留写作当时的名字;那里的裸 `cordis` 也可能是 Python SDK 的选项名或某个 agent-preset 的 id。`docs/` 之内,散文与所有 Markdown 围栏都跟着改。
+
+## 你的代码要改什么
+
+| 位置 | 改前 | 改后 |
+|---|---|---|
+| 模块 import | `import { Context } from 'cordis'` | `import { Context } from '@deepseek-ai/cordis'` |
+| 类型事件声明合并 | `declare module 'cordis'` | `declare module '@deepseek-ai/cordis'` |
+| `package.json` 依赖键 | `"@cordisjs/plugin-hmr": "^1.0.15"` | `"@deepseek-ai/cordis-plugin-hmr": "^1.0.15"` |
+| `cordis.yml` 插件条目 | `name: '@cordisjs/plugin-include'` | `name: '@deepseek-ai/cordis-plugin-include'` |
+
+## 施加、核验与回退
+
+上面这份映射由 [`scripts/rescope-vendor.ts`](../scripts/rescope-vendor.ts) 承载并执行改名,任何引用都不靠手改:
+
+```sh
+pnpm run rescope-vendor # report what would change
+pnpm run rescope-vendor --apply # rewrite every reference
+pnpm run rescope-vendor:check # assert the post-state; runs in the hygiene gate
+pnpm run rescope-vendor --apply --reverse # return to the upstream names
+```
+
+上游 sync 之后重跑它([流程](../vendor/README.md)),并接上它打印的重生成:`pnpm install` 重生成 lockfile、`pnpm run gen-third-party-notices`、以及对它触及的双语对跑 `pnpm run verify-translation-pairing --write`。
diff --git a/docs/subsystems/README.i18n.yaml b/docs/subsystems/README.i18n.yaml
index 9e578f7886..ebdccb923a 100644
--- a/docs/subsystems/README.i18n.yaml
+++ b/docs/subsystems/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/README.md
-README.md: fddbf460c8e9e7c6f9ed1d3375bdabe65947661f
-README.zh.md: febc5a97426fef5ec4b2b80d9677957369994a3b
+README.md: 560851eeda607b762456fe20c874b4497704709f
+README.zh.md: 4acba4372e995b54a3bb326bec0cf9606f997773
diff --git a/docs/subsystems/README.md b/docs/subsystems/README.md
index fddbf460c8..560851eeda 100644
--- a/docs/subsystems/README.md
+++ b/docs/subsystems/README.md
@@ -18,6 +18,7 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
| [settings.md](settings.md) | the user-settings seam: `SettingsNamespace` registration, layered resolution (defaults → composition `base` → user document), owner scopes, hot commits |
| [credentials.md](credentials.md) | the credential seam: `CredentialRef` references (never values) in configuration, per-operation resolution, UI-safe `CredentialInfo`, provider source layers |
| [session-query.md](session-query.md) | logical records, bounded exact-event reads, relationship traces, semantic filters/documents, and full-text result pages |
+| [feedback.md](feedback.md) | lifecycle-bound per-message feedback records, optimistic versions, sidecar persistence, and the Host Remote contract |
| [session-title.md](session-title.md) | durable title snapshots, cited source-message seqs, and the asynchronous provider contract |
| [session-reference.md](session-reference.md) | structured cross-session references: `SessionReferenceInput`/`Candidate`, prepared message contexts, the stable error taxonomy |
| [system-prompt.md](system-prompt.md) | per-assembly context, tool-provider results, prompt sections, and cooperative assembly |
diff --git a/docs/subsystems/README.zh.md b/docs/subsystems/README.zh.md
index febc5a9742..4acba4372e 100644
--- a/docs/subsystems/README.zh.md
+++ b/docs/subsystems/README.zh.md
@@ -18,6 +18,7 @@
| [settings.md](settings.md) | 用户设置 seam:`SettingsNamespace` 注册、分层解析(默认值 → 组合 `base` → 用户文档)、owner scope、热提交 |
| [credentials.md](credentials.md) | 凭据 seam:配置中的 `CredentialRef` 引用(绝不含值)、按操作解析、对 UI 安全的 `CredentialInfo`、provider 来源层 |
| [session-query.md](session-query.md) | 逻辑记录、有界精确事件读取、关系追踪、语义筛选器/文档与全文检索结果页 |
+| [feedback.md](feedback.md) | 绑定生命周期的逐消息反馈记录、乐观版本、伴随记录持久化与 Host Remote 契约 |
| [session-title.md](session-title.md) | 持久标题快照、被引用的来源消息 seq 与异步提供方约定 |
| [session-reference.md](session-reference.md) | 结构化跨会话引用:`SessionReferenceInput`/`Candidate`、prepared 消息上下文、稳定错误分类 |
| [system-prompt.md](system-prompt.md) | 逐次组装的上下文、工具提供方结果、提示词段落与协作式组装 |
diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml
index a7d26cee1b..0123946525 100644
--- a/docs/subsystems/core.i18n.yaml
+++ b/docs/subsystems/core.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 docs/subsystems/core.md
-core.md: ad00c4da7d77b0e1ab4728173b202ebc17fb56a0
-core.zh.md: 9c606023c85369643e7148f829526b1f75ea3631
+core.md: 96655026f5affda6fed080496d975e2366f0356f
+core.zh.md: e2cde8845ddf6b78f64d062fd8860c0c88b7ce11
diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md
index ad00c4da7d..96655026f5 100644
--- a/docs/subsystems/core.md
+++ b/docs/subsystems/core.md
@@ -546,7 +546,7 @@ async standingKeyFor(id?: string): Promise
Types: [ScopeKey](scope.md)
-Source: [`packages/preset/agent-presets/src/index.ts:78`](../../packages/preset/agent-presets/src/index.ts)
+Source: [`packages/preset/agent-presets/src/index.ts:80`](../../packages/preset/agent-presets/src/index.ts)
diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md
index 9c606023c8..e2cde8845d 100644
--- a/docs/subsystems/core.zh.md
+++ b/docs/subsystems/core.zh.md
@@ -554,7 +554,7 @@ async standingKeyFor(id?: string): Promise
Types: [ScopeKey](scope.md)
-Source: [`packages/preset/agent-presets/src/index.ts:78`](../../packages/preset/agent-presets/src/index.ts)
+Source: [`packages/preset/agent-presets/src/index.ts:80`](../../packages/preset/agent-presets/src/index.ts)
diff --git a/docs/subsystems/feedback.i18n.yaml b/docs/subsystems/feedback.i18n.yaml
new file mode 100644
index 0000000000..f6de2debe9
--- /dev/null
+++ b/docs/subsystems/feedback.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 docs/subsystems/feedback.md
+feedback.md: 76a29f7d6ba604fa07ed56429c9b066e22639671
+feedback.zh.md: 5a409832de68b6d0bc9688a907c0f22edd3b0a43
diff --git a/docs/subsystems/feedback.md b/docs/subsystems/feedback.md
new file mode 100644
index 0000000000..76a29f7d6b
--- /dev/null
+++ b/docs/subsystems/feedback.md
@@ -0,0 +1,256 @@
+# Message Feedback
+
+English | [中文](feedback.zh.md)
+
+[`@deepseek-ai/dsh-message-feedback`](../../packages/feedback/message-feedback) owns editable feedback for individual assistant messages. It is deliberately separate from the immutable Session-level `feedback/record` event: message feedback is a local storage-domain sidecar, not Session-log content or a projection, and it performs no telemetry handoff.
+
+Source: [`packages/feedback/message-feedback/src/types.ts`](../../packages/feedback/message-feedback/src/types.ts)
+
+## Public types
+
+```ts type-equiv
+/** Opaque compare-and-set token for one exact feedback item revision. */
+type MessageFeedbackVersion = Branded<'MessageFeedbackVersion'>
+```
+
+```ts type-equiv
+/** The human's overall judgment of one assistant message. */
+type MessageFeedbackRating = 'positive' | 'negative'
+```
+
+```ts type-equiv
+/** One current feedback value and its opaque mutation token. */
+interface MessageFeedbackItem {
+ /** Stable identity of the assistant message inside the owning Session. */
+ readonly messageId: MessageId
+ /** Overall positive or negative judgment. */
+ readonly rating: MessageFeedbackRating
+ /** Optional explanation, preserved verbatim after validation. */
+ readonly note?: string
+ /** Equality-only token replaced by every material create or update. */
+ readonly version: MessageFeedbackVersion
+ /** Host-assigned creation time in Unix epoch milliseconds. */
+ readonly createdAt: number
+ /** Host-assigned time of the most recent material update. */
+ readonly updatedAt: number
+}
+```
+
+```ts type-equiv
+/** Read all message feedback belonging to one persisted Session lifecycle. */
+interface MessageFeedbackListRequest {
+ /** Persisted Session whose sidecar should be read. */
+ readonly sessionId: SessionId
+}
+```
+
+```ts type-equiv
+/** Current feedback values for one Session, in first-creation order. */
+interface MessageFeedbackListValue {
+ /** Fresh immutable item snapshots. */
+ readonly items: readonly MessageFeedbackItem[]
+}
+```
+
+```ts type-equiv
+/** Create or replace feedback for one assistant message. */
+interface MessageFeedbackPutRequest {
+ /** Persisted Session that owns the target message. */
+ readonly sessionId: SessionId
+ /** Target assistant-message identity. */
+ readonly messageId: MessageId
+ /** Desired overall judgment. */
+ readonly rating: MessageFeedbackRating
+ /** Optional non-blank explanation. */
+ readonly note?: string
+ /** Observed item version, or `null` to require that no item exists. */
+ readonly ifVersion: MessageFeedbackVersion | null
+}
+```
+
+```ts type-equiv
+/** Delete feedback for one message after observing its current version. */
+interface MessageFeedbackDeleteRequest {
+ /** Persisted Session that owns the sidecar. */
+ readonly sessionId: SessionId
+ /** Message whose feedback should be absent after this operation. */
+ readonly messageId: MessageId
+ /** Observed item version; ignored when the item is already absent. */
+ readonly ifVersion: MessageFeedbackVersion
+}
+```
+
+```ts type-equiv
+/** Idempotent deletion acknowledgement. */
+interface MessageFeedbackDeleteValue {
+ /** Stable postcondition shared by the first deletion and every retry. */
+ readonly absent: true
+}
+```
+
+```ts type-equiv
+/** No persisted Session header exists for the requested id. */
+interface MessageFeedbackSessionNotFound {
+ readonly code: 'session-not-found'
+ readonly sessionId: SessionId
+}
+```
+
+```ts type-equiv
+/** The id does not name a derived, append-origin assistant message. */
+interface MessageFeedbackTargetNotFound {
+ readonly code: 'target-not-found'
+ readonly sessionId: SessionId
+ readonly messageId: MessageId
+}
+```
+
+```ts type-equiv
+/** A material mutation did not match the addressed item's current version. */
+interface MessageFeedbackVersionConflict {
+ readonly code: 'version-conflict'
+ /** Authoritative current item, or `null` when it does not exist. */
+ readonly current: MessageFeedbackItem | null
+}
+```
+
+```ts type-equiv
+/** A supplied note contains no non-whitespace character. */
+interface MessageFeedbackNoteBlank {
+ readonly code: 'note-blank'
+}
+```
+
+```ts type-equiv
+/** A supplied note exceeds the configured UTF-8 byte limit. */
+interface MessageFeedbackNoteTooLarge {
+ readonly code: 'note-too-large'
+ readonly maxBytes: number
+ readonly actualBytes: number
+}
+```
+
+```ts type-equiv
+/** Failures shared by the public message-feedback operations. */
+type MessageFeedbackFailure =
+ | MessageFeedbackSessionNotFound
+ | MessageFeedbackTargetNotFound
+ | MessageFeedbackVersionConflict
+ | MessageFeedbackNoteBlank
+ | MessageFeedbackNoteTooLarge
+```
+
+```ts type-equiv
+/** Successful public operation result. */
+interface MessageFeedbackSuccess {
+ readonly ok: true
+ readonly value: T
+}
+```
+
+```ts type-equiv
+/** Rejected public operation result with a stable business failure. */
+interface MessageFeedbackRejected {
+ readonly ok: false
+ readonly error: E
+}
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `list` operation. */
+type MessageFeedbackListResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `put` operation. */
+type MessageFeedbackPutResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected<
+ | MessageFeedbackSessionNotFound
+ | MessageFeedbackTargetNotFound
+ | MessageFeedbackVersionConflict
+ | MessageFeedbackNoteBlank
+ | MessageFeedbackNoteTooLarge
+ >
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `delete` operation. */
+type MessageFeedbackDeleteResult =
+ | MessageFeedbackSuccess
+ | MessageFeedbackRejected
+```
+
+## Data and concurrency
+
+One Session sidecar row contains its header identity `{createdAt, cwd}` and feedback items keyed by `MessageId`. Each item carries a positive or negative rating, an optional note, Host-assigned `createdAt`/`updatedAt` timestamps, and its own opaque version. Versions are compared only for equality and only against the addressed message; callers do not order or synthesize them.
+
+`put` uses strict optimistic concurrency: every request for an existing item must match its current `ifVersion`, including a no-op. A conflict returns the authoritative current item (or `null`), so a caller can reconcile a lost response or a concurrent edit without another read. Deleting an already absent item succeeds. A per-Session queue encloses inspection, read, conflict evaluation, and whole-row write, so these guarantees cover concurrent calls in one Host process.
+
+## Target and lifecycle authority
+
+`SessionPersistence.inspect()` supplies the target Session observation without publishing or resuming an Agent and without committing cold repair. A cold `listSnapshots()` preflight classifies definite absence; inspection failure for a catalogued Session propagates as infrastructure failure. `put` accepts only a non-empty, append-origin `assistant/message` with the requested `MessageId`; replacement-origin, usage-only empty, and non-assistant records are not feedback targets.
+
+The stored `{createdAt, cwd}` identity must match the inspected header. A mismatch is treated as absence: `list` returns no items, while `put` may replace the stale row with one bound to the current header identity. Forks use a new Session identity and receive no sidecar copy even when their seed contains the same messages.
+
+## Persistence and Remote contract
+
+The service stores whole Session rows in the `message_feedback` storage domain through `ctx.storageDomain`. Before `put` commits a row that references a target message, a matching live target passes through the canonical `ctx.sessions.flush` checkpoint; both live and cold paths are then physically read from sequence zero through `SessionPersistence.readFrom`. The resulting observation is revalidated before the sidecar write, so the durable target log always precedes its sidecar commit. `maxNoteBytes` is required and bounds note text by UTF-8 bytes; the Web Host composition sets `8192`. The package publishes the Host `messageFeedback.list`, `messageFeedback.put`, and `messageFeedback.delete` unary Remote contract through `GatewayService` and `@Remote`; the generated Cordis surface below is the method-level authority.
+
+Plugin disposal closes mutation admission, drains accepted per-Session queue work, and then closes the storage domain.
+
+## Boundaries and limitations
+
+- The client Remote aggregate mount and UI consumer are separately owned and deferred.
+- The mutation queue is process-local. Storage-domain has no cross-process conditional write, so multiple Host writers to one storage root have no compare-and-swap or lost-update guarantee.
+- Session persistence has no durable deletion surface. The service does not treat `session/disposed` or `host/session-removed` as deletion and therefore performs no fake cascade; orphan sidecar rows may remain after out-of-band log removal.
+- A request in the narrow interval after live detach but before the persistence catalog materializes the header can receive `session-not-found`; callers retry after retirement materialization.
+- Cold requests scan the complete Session snapshot catalog because persistence has no lookup-by-id metadata operation. One Session row also has no item-count or aggregate-byte cap; `maxNoteBytes` bounds only each note until a concrete consumer owns a row policy.
+- Header identity detects a reused id only when `{createdAt, cwd}` differs; a cloned log retaining the same header identity is indistinguishable by this contract.
+- The Host contract records no authenticated actor or audit identity and therefore assumes a trusted caller boundary.
+
+
+
+
+
+## Cordis surface
+
+Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+
+
+
+### `ctx.messageFeedback` — `MessageFeedbackService`
+
+Storage-domain sidecar service. It inspects persisted Session history and never creates or resumes an Agent or Session.
+
+```ts cordis-catalog
+/**
+ * Read feedback belonging to the current persisted Session lifecycle.
+ * A stale row from a reused Session id is invisible.
+ * @param request - Session identity to inspect and list.
+ * @returns current immutable items or `session-not-found`.
+ */
+@Remote('list') async list(request: MessageFeedbackListRequest): Promise
+
+/**
+ * Create or replace feedback for one derived append-origin assistant
+ * message. Every request must match the addressed item's current version;
+ * a matching no-op returns the stored item without changing its revision.
+ * @param request - target, desired value, and observed item version.
+ * @returns the committed item or an explicit business failure.
+ */
+@Remote('put') put(request: MessageFeedbackPutRequest): Promise
+
+/**
+ * Delete one feedback item. Absence is successful regardless of the
+ * supplied version; an existing item requires an exact version match.
+ * @param request - Session, message, and observed item version.
+ * @returns the stable absent postcondition, or an explicit failure.
+ */
+@Remote('delete') delete(request: MessageFeedbackDeleteRequest): Promise
+```
+
+Source: [`packages/feedback/message-feedback/src/index.ts:150`](../../packages/feedback/message-feedback/src/index.ts)
+
diff --git a/docs/subsystems/feedback.zh.md b/docs/subsystems/feedback.zh.md
new file mode 100644
index 0000000000..5a409832de
--- /dev/null
+++ b/docs/subsystems/feedback.zh.md
@@ -0,0 +1,256 @@
+# 消息反馈
+
+[English](feedback.md) | 中文
+
+[`@deepseek-ai/dsh-message-feedback`](../../packages/feedback/message-feedback)拥有针对单条 assistant 消息的可编辑反馈。它刻意与不可变的 Session 级 `feedback/record` 事件分离:message feedback 是本地 storage-domain 伴随记录(sidecar),不是 Session 日志内容或投影,也不执行遥测交接。
+
+来源:[`packages/feedback/message-feedback/src/types.ts`](../../packages/feedback/message-feedback/src/types.ts)
+
+## 公开类型
+
+```ts type-equiv
+/** Opaque compare-and-set token for one exact feedback item revision. */
+type MessageFeedbackVersion = Branded<'MessageFeedbackVersion'>
+```
+
+```ts type-equiv
+/** The human's overall judgment of one assistant message. */
+type MessageFeedbackRating = 'positive' | 'negative'
+```
+
+```ts type-equiv
+/** One current feedback value and its opaque mutation token. */
+interface MessageFeedbackItem {
+ /** Stable identity of the assistant message inside the owning Session. */
+ readonly messageId: MessageId
+ /** Overall positive or negative judgment. */
+ readonly rating: MessageFeedbackRating
+ /** Optional explanation, preserved verbatim after validation. */
+ readonly note?: string
+ /** Equality-only token replaced by every material create or update. */
+ readonly version: MessageFeedbackVersion
+ /** Host-assigned creation time in Unix epoch milliseconds. */
+ readonly createdAt: number
+ /** Host-assigned time of the most recent material update. */
+ readonly updatedAt: number
+}
+```
+
+```ts type-equiv
+/** Read all message feedback belonging to one persisted Session lifecycle. */
+interface MessageFeedbackListRequest {
+ /** Persisted Session whose sidecar should be read. */
+ readonly sessionId: SessionId
+}
+```
+
+```ts type-equiv
+/** Current feedback values for one Session, in first-creation order. */
+interface MessageFeedbackListValue {
+ /** Fresh immutable item snapshots. */
+ readonly items: readonly MessageFeedbackItem[]
+}
+```
+
+```ts type-equiv
+/** Create or replace feedback for one assistant message. */
+interface MessageFeedbackPutRequest {
+ /** Persisted Session that owns the target message. */
+ readonly sessionId: SessionId
+ /** Target assistant-message identity. */
+ readonly messageId: MessageId
+ /** Desired overall judgment. */
+ readonly rating: MessageFeedbackRating
+ /** Optional non-blank explanation. */
+ readonly note?: string
+ /** Observed item version, or `null` to require that no item exists. */
+ readonly ifVersion: MessageFeedbackVersion | null
+}
+```
+
+```ts type-equiv
+/** Delete feedback for one message after observing its current version. */
+interface MessageFeedbackDeleteRequest {
+ /** Persisted Session that owns the sidecar. */
+ readonly sessionId: SessionId
+ /** Message whose feedback should be absent after this operation. */
+ readonly messageId: MessageId
+ /** Observed item version; ignored when the item is already absent. */
+ readonly ifVersion: MessageFeedbackVersion
+}
+```
+
+```ts type-equiv
+/** Idempotent deletion acknowledgement. */
+interface MessageFeedbackDeleteValue {
+ /** Stable postcondition shared by the first deletion and every retry. */
+ readonly absent: true
+}
+```
+
+```ts type-equiv
+/** No persisted Session header exists for the requested id. */
+interface MessageFeedbackSessionNotFound {
+ readonly code: 'session-not-found'
+ readonly sessionId: SessionId
+}
+```
+
+```ts type-equiv
+/** The id does not name a derived, append-origin assistant message. */
+interface MessageFeedbackTargetNotFound {
+ readonly code: 'target-not-found'
+ readonly sessionId: SessionId
+ readonly messageId: MessageId
+}
+```
+
+```ts type-equiv
+/** A material mutation did not match the addressed item's current version. */
+interface MessageFeedbackVersionConflict {
+ readonly code: 'version-conflict'
+ /** Authoritative current item, or `null` when it does not exist. */
+ readonly current: MessageFeedbackItem | null
+}
+```
+
+```ts type-equiv
+/** A supplied note contains no non-whitespace character. */
+interface MessageFeedbackNoteBlank {
+ readonly code: 'note-blank'
+}
+```
+
+```ts type-equiv
+/** A supplied note exceeds the configured UTF-8 byte limit. */
+interface MessageFeedbackNoteTooLarge {
+ readonly code: 'note-too-large'
+ readonly maxBytes: number
+ readonly actualBytes: number
+}
+```
+
+```ts type-equiv
+/** Failures shared by the public message-feedback operations. */
+type MessageFeedbackFailure =
+ | MessageFeedbackSessionNotFound
+ | MessageFeedbackTargetNotFound
+ | MessageFeedbackVersionConflict
+ | MessageFeedbackNoteBlank
+ | MessageFeedbackNoteTooLarge
+```
+
+```ts type-equiv
+/** Successful public operation result. */
+interface MessageFeedbackSuccess {
+ readonly ok: true
+ readonly value: T
+}
+```
+
+```ts type-equiv
+/** Rejected public operation result with a stable business failure. */
+interface MessageFeedbackRejected {
+ readonly ok: false
+ readonly error: E
+}
+```
+
+```ts type-equiv
+/** Result returned by the message-feedback `list` operation. */
+type MessageFeedbackListResult =
+ | MessageFeedbackSuccess